getMetadata() {
- DomainFile df = getLinkedFileNoError();
- return df != null ? df.getMetadata() : Map.of();
+ return realDomainFile.getMetadata();
}
@Override
public long length() throws IOException {
- DomainFile df = getLinkedFileNoError();
- return df != null ? df.length() : 0;
+ return realDomainFile.length();
}
@Override
public boolean isLink() {
- DomainFile df = getLinkedFileNoError();
- return df != null ? df.isLink() : false;
+ return linkInfo != null;
}
@Override
public LinkFileInfo getLinkInfo() {
- DomainFile df = getLinkedFileNoError();
- return df != null ? df.getLinkInfo() : null;
+ return linkInfo;
+ }
+
+ private class LinkedFileLinkInfo implements LinkFileInfo {
+
+ @Override
+ public DomainFile getFile() {
+ return LinkedGhidraFile.this;
+ }
+
+ @Override
+ public LinkedGhidraFolder getLinkedFolder() {
+ try {
+ return FolderLinkContentHandler.getLinkedFolder(LinkedGhidraFile.this);
+ }
+ catch (IOException e) {
+ // Ignore
+ }
+ return null;
+ }
+
+ @Override
+ public String getLinkPath() {
+ return realDomainFile.getLinkInfo().getLinkPath();
+ }
+
+ @Override
+ public String getAbsoluteLinkPath() throws IOException {
+ return realDomainFile.getLinkInfo().getAbsoluteLinkPath();
+ }
+
}
@Override
@@ -470,12 +472,7 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override
public String toString() {
- String str = parent.toString();
- if (!str.endsWith("/")) {
- str += "/";
- }
- str += getName();
- return str;
+ return getPathname() + "->" + realDomainFile.getPathname();
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java
index d99d53830d..955d49b11a 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java
@@ -57,7 +57,8 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
* since an error will occur if there is no active project at the time the link is followed.
*
* @param folderLinkFile link-file which corresponds to a linked-folder
- * (see {@link LinkFileInfo#isFolderLink()}).
+ * (see {@link LinkFileInfo#isFolderLink()}). This file should used to establish the parent
+ * of this linked-folder.
* @param linkedFolderUrl linked folder URL
*/
LinkedGhidraFolder(DomainFile folderLinkFile, URL linkedFolderUrl) {
@@ -87,7 +88,8 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
* {@link #getProjectData() project data} instance.
*
* @param folderLinkFile link-file which corresponds to a linked-folder
- * (see {@link LinkFileInfo#isFolderLink()}).
+ * (see {@link LinkFileInfo#isFolderLink()}). This file should used to establish the parent
+ * of this linked-folder.
* @param linkedFolder locally-linked folder within same project
*/
LinkedGhidraFolder(DomainFile folderLinkFile, DomainFolder linkedFolder) {
@@ -114,7 +116,7 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
if (!(obj instanceof LinkedGhidraFolder other)) {
return false;
}
- return linkedPathname.equals(other.linkedPathname) &&
+ return getPathname().equals(other.getPathname()) &&
folderLinkFile.equals(other.folderLinkFile);
}
@@ -238,9 +240,9 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
@Override
public String toString() {
if (linkedFolder != null) {
- return "->" + getLinkedPathname();
+ return getPathname() + "->" + getLinkedPathname();
}
- return "->" + linkedFolderUrl.toString();
+ return getPathname() + "->" + linkedFolderUrl.toString();
}
@Override
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java
index 24d5813916..d2274b35ac 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java
@@ -274,7 +274,7 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
DomainFile[] files = linkedFolder.getFiles();
LinkedGhidraFile[] linkedSubFolders = new LinkedGhidraFile[files.length];
for (int i = 0; i < files.length; i++) {
- linkedSubFolders[i] = new LinkedGhidraFile(this, files[i].getName());
+ linkedSubFolders[i] = new LinkedGhidraFile(this, files[i]);
}
return linkedSubFolders;
}
@@ -286,6 +286,10 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
/**
* Get the true file within this linked folder.
+ *
+ * NOTE: The returned file is the "real" file and would be the have the equivalence:
+ * {@code folder.getLinkedFileNoError("X") == folder.getFile("X").getRealFile() }.
+ *
* @param name file name
* @return file or null if not found or error occurs
*/
@@ -300,6 +304,16 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
return null;
}
+ /**
+ * Get the true file within this linked folder.
+ *
+ * NOTE: The returned file is the "real" file and would be the have the equivalence:
+ * {@code folder.getLinkedFile("X") == folder.getFile("X").getRealFile() }.
+ *
+ * @param name file name
+ * @return file or null if not found or error occurs
+ * @throws IOException if an IO error ocurs such as FileNotFoundException
+ */
DomainFile getLinkedFile(String name) throws IOException {
DomainFolder linkedFolder = getRealFolder();
DomainFile df = linkedFolder.getFile(name);
@@ -311,8 +325,8 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
@Override
public DomainFile getFile(String name) {
- DomainFile f = getLinkedFileNoError(name);
- return f != null ? new LinkedGhidraFile(this, name) : null;
+ DomainFile file = getLinkedFileNoError(name);
+ return file != null ? new LinkedGhidraFile(this, file) : null;
}
@Override
@@ -333,36 +347,40 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
DomainFolder linkedFolder = getRealFolder();
- return linkedFolder.createFile(name, obj, monitor);
+ DomainFile file = linkedFolder.createFile(name, obj, monitor);
+ return getFile(file.getName());
}
@Override
public DomainFile createFile(String name, File packFile, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
DomainFolder linkedFolder = getRealFolder();
- return linkedFolder.createFile(name, packFile, monitor);
+ DomainFile file = linkedFolder.createFile(name, packFile, monitor);
+ return getFile(file.getName());
}
@Override
public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname,
boolean makeRelative, String linkFilename, LinkHandler> lh) throws IOException {
DomainFolder linkedFolder = getRealFolder();
- return linkedFolder.createLinkFile(sourceProjectData, pathname, makeRelative, linkFilename,
- lh);
+ DomainFile file = linkedFolder.createLinkFile(sourceProjectData, pathname, makeRelative,
+ linkFilename, lh);
+ return getFile(file.getName());
}
@Override
public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler> lh)
throws IOException {
DomainFolder linkedFolder = getRealFolder();
- return linkedFolder.createLinkFile(ghidraUrl, linkFilename, lh);
+ DomainFile file = linkedFolder.createLinkFile(ghidraUrl, linkFilename, lh);
+ return getFile(file.getName());
}
@Override
public DomainFolder createFolder(String name) throws InvalidNameException, IOException {
DomainFolder linkedFolder = getRealFolder();
- DomainFolder child = linkedFolder.createFolder(name);
- return new LinkedGhidraSubFolder(parent, child.getName());
+ DomainFolder folder = linkedFolder.createFolder(name);
+ return getFolder(folder.getName());
}
@Override
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/RootGhidraFolderData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/RootGhidraFolderData.java
index dab2dafcdc..3542151fae 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/RootGhidraFolderData.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/RootGhidraFolderData.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,11 +15,26 @@
*/
package ghidra.framework.data;
+import java.io.IOException;
+import java.util.HashMap;
+
import ghidra.framework.model.DomainFolderChangeListener;
import ghidra.framework.store.FileSystem;
+import ghidra.util.task.TaskMonitor;
public class RootGhidraFolderData extends GhidraFolderData {
+ // Folder path reference counts, associated with discovered file-links and folder-links,
+ // are tracked to ensure that such folders are visited immediately or upon their
+ // creation to ensure that the folder change listener is properly notified of all changes
+ // related to the folder paths contained within this map.
+ private HashMap folderReferenceCounts = new HashMap<>();
+
+ /**
+ * Constructor for project data root folder.
+ * @param projectData project data
+ * @param listener folder change listener
+ */
RootGhidraFolderData(DefaultProjectData projectData, DomainFolderChangeListener listener) {
super(projectData, listener);
}
@@ -50,4 +65,97 @@ public class RootGhidraFolderData extends GhidraFolderData {
return true;
}
+ /**
+ * Determine if the specified folder path must be visited due to
+ * possible link references to the folder or one of its children.
+ * @param folderPathname folder pathname (not ending with '/')
+ * @return true if folder should be visited to ensure that changes are properly tracked
+ * with proper change notifications sent.
+ */
+ public boolean mustVisit(String folderPathname) {
+ return folderReferenceCounts.containsKey(folderPathname);
+ }
+
+ /**
+ * Register internal file/folder-link to ensure we do not ignore change events which affect
+ * the referenced file/folder.
+ * @param absoluteLinkPath absolute internal path referenced by a link-file
+ */
+ void registerInternalLinkPath(String absoluteLinkPath) {
+ if (!absoluteLinkPath.startsWith(FileSystem.SEPARATOR)) {
+ throw new IllegalArgumentException();
+ }
+
+ // Register path elements upto parent of absoluteLinkPath
+ String[] pathSplit = absoluteLinkPath.split(FileSystem.SEPARATOR);
+ int folderElementCount = pathSplit.length - 1;
+
+ // Start at 1 since element 0 corresponds to root and will be empty
+ GhidraFolderData folderData = this;
+ StringBuilder pathBuilder = new StringBuilder();
+ for (int i = 1; i < folderElementCount; i++) {
+ String folderName = pathSplit[i];
+ if (folderName.length() == 0) {
+ // ignore blank names
+ continue;
+ }
+ if (folderData != null) {
+ folderData = folderData.getFolderData(folderName, false);
+ if (folderData != null && !folderData.visited()) {
+ try {
+ folderData.refresh(false, true, TaskMonitor.DUMMY);
+ }
+ catch (IOException e) {
+ // ignore - things may get out-of-sync
+ folderData = null;
+ }
+ }
+ }
+
+ // Increment folder reference count for all folders leading up to referenced folder
+ pathBuilder.append(FileSystem.SEPARATOR);
+ pathBuilder.append(folderName);
+ folderReferenceCounts.compute(pathBuilder.toString(),
+ (path, count) -> (count == null) ? 1 : ++count);
+ }
+ }
+
+ /**
+ * Unregister internal file/folder-link to ensure we do not ignore change events which affect
+ * the referenced file/folder.
+ * @param absoluteLinkPath absolute internal path referenced by a link-file
+ */
+ void unregisterInternalLinkPath(String absoluteLinkPath) {
+ if (!absoluteLinkPath.startsWith(FileSystem.SEPARATOR)) {
+ throw new IllegalArgumentException();
+ }
+
+ // Register path elements upto parent of absoluteLinkPath
+ String[] pathSplit = absoluteLinkPath.split(FileSystem.SEPARATOR);
+ int folderElementCount = pathSplit.length - 1;
+
+ // Start at 1 since element 0 corresponds to root and will be empty
+ StringBuilder pathBuilder = new StringBuilder();
+ for (int i = 1; i < folderElementCount; i++) {
+ String folderName = pathSplit[i];
+ if (folderName.length() == 0) {
+ // ignore blank names
+ continue;
+ }
+ // Increment folder reference count for all folders leading up to referenced folder
+ pathBuilder.append(FileSystem.SEPARATOR);
+ pathBuilder.append(folderName);
+ String path = pathBuilder.toString();
+ Integer count = folderReferenceCounts.get(path);
+ if (count != null) {
+ if (count == 1) {
+ folderReferenceCounts.remove(path);
+ }
+ else {
+ folderReferenceCounts.put(path, count - 1);
+ }
+ }
+ }
+ }
+
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java
index 0048dd7c8a..1a2ccb7c6c 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java
@@ -54,6 +54,7 @@ import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.preferences.Preferences;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.remote.User;
+import ghidra.framework.store.FileSystem;
import ghidra.util.*;
import ghidra.util.filechooser.GhidraFileChooserModel;
import ghidra.util.filechooser.GhidraFileFilter;
@@ -137,6 +138,7 @@ public class FrontEndPlugin extends Plugin
private ProjectDataRenameAction renameAction;
private ProjectDataOpenDefaultToolAction openAction;
private ProjectDataFollowLinkAction followLinkAction;
+ private ProjectDataSelectRealFileOrFolderAction selectRealFileOrFolderAction;
private ProjectDataExpandAction expandAction;
private ProjectDataCollapseAction collapseAction;
private ProjectDataSelectAction selectAction;
@@ -221,6 +223,7 @@ public class FrontEndPlugin extends Plugin
// Top of popup menu actions - no group
openAction = new ProjectDataOpenDefaultToolAction(owner, null);
followLinkAction = new ProjectDataFollowLinkAction(this, null);
+ selectRealFileOrFolderAction = new ProjectDataSelectRealFileOrFolderAction(this, null);
String groupName = "Cut/copy/paste/new1";
newFolderAction = new FrontEndProjectDataNewFolderAction(owner, groupName);
@@ -258,6 +261,7 @@ public class FrontEndPlugin extends Plugin
tool.addAction(deleteAction);
tool.addAction(openAction);
tool.addAction(followLinkAction);
+ tool.addAction(selectRealFileOrFolderAction);
tool.addAction(renameAction);
tool.addAction(expandAction);
tool.addAction(collapseAction);
@@ -1117,9 +1121,15 @@ public class FrontEndPlugin extends Plugin
showInViewedProject(LinkHandler.getLinkURL(lastLink), true);
}
else if (!dataTreePanel.isShowing()) {
- // Filter table on absolute link path
String linkPath = LinkHandler.getAbsoluteLinkPath(domainFile);
- dataTablePanel.setFilter(linkPath);
+ if (linkPath.startsWith(FileSystem.SEPARATOR) && linkPath.length() > 1) {
+ // Filter table on absolute internal link path
+ if (linkPath.endsWith(FileSystem.SEPARATOR)) {
+ // Remove trailing '/' from path to ensure we match
+ linkPath = linkPath.substring(0, linkPath.length() - 1);
+ }
+ dataTablePanel.setFilter(linkPath);
+ }
}
}
catch (IOException e) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataSelectRealFileOrFolderAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataSelectRealFileOrFolderAction.java
new file mode 100644
index 0000000000..86c7d93ef1
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataSelectRealFileOrFolderAction.java
@@ -0,0 +1,103 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.framework.main;
+
+import java.io.IOException;
+
+import docking.action.MenuData;
+import ghidra.framework.main.datatable.FrontendProjectTreeAction;
+import ghidra.framework.main.datatable.ProjectDataContext;
+import ghidra.framework.main.datatree.DataTree;
+import ghidra.framework.model.*;
+import ghidra.util.HelpLocation;
+import ghidra.util.Msg;
+
+public class ProjectDataSelectRealFileOrFolderAction extends FrontendProjectTreeAction {
+
+ private FrontEndPlugin plugin;
+
+ public ProjectDataSelectRealFileOrFolderAction(FrontEndPlugin plugin, String group) {
+ super("Select Real File or Folder", plugin.getName());
+ this.plugin = plugin;
+ setPopupMenuData(new MenuData(new String[] { "Select Real File" }, group));
+ setHelpLocation(new HelpLocation("FrontEndPlugin", "Select_Real_File_or_Folder"));
+ }
+
+ @Override
+ protected void actionPerformed(ProjectDataContext context) {
+
+ boolean isFolder = false;
+ String pathname;
+
+ try {
+ if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
+ DomainFolder folder = context.getSelectedFolders().get(0);
+ if (!(folder instanceof LinkedDomainFolder linkedFolder)) {
+ return;
+ }
+ isFolder = true;
+ pathname = linkedFolder.getRealFolder().getPathname();
+ }
+ else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
+ DomainFile file = context.getSelectedFiles().get(0);
+ if (!(file instanceof LinkedDomainFile linkedFile)) {
+ return;
+ }
+ isFolder = false;
+ pathname = linkedFile.getRealFile().getPathname();
+ }
+ else {
+ return;
+ }
+
+ // Path is local to its project data tree
+ plugin.showInProjectTree(context.getProjectData(), pathname, isFolder);
+ }
+ catch (IOException e) {
+ Msg.showError(this, null, "Linked Content Error",
+ "Failed to resolve linked " + (isFolder ? "folder" : "file"), e);
+ return;
+ }
+
+ }
+
+ @Override
+ protected boolean isEnabledForContext(ProjectDataContext context) {
+ boolean enabled = false;
+ String contentType = "Content";
+ if (context.getComponent() instanceof DataTree) {
+ if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
+ DomainFolder folder = context.getSelectedFolders().get(0);
+ if (folder instanceof LinkedDomainFolder) {
+ contentType = "Folder";
+ enabled = true;
+ }
+ }
+ else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
+ DomainFile file = context.getSelectedFiles().get(0);
+ if (file instanceof LinkedDomainFile) {
+ contentType = "File";
+ enabled = true;
+ }
+ }
+ }
+ if (enabled) {
+ setPopupMenuData(new MenuData(new String[] { "Select Real " + contentType },
+ getPopupMenuData().getMenuGroup()));
+ }
+ return enabled;
+ }
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java
index f3b5199ecd..60825733d1 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java
@@ -27,6 +27,9 @@ import docking.widgets.tree.GTreeNode;
import ghidra.framework.data.LinkHandler;
import ghidra.framework.main.datatree.DataTreeNode.NodeType;
import ghidra.framework.model.*;
+import ghidra.framework.store.local.LocalFileSystem;
+import ghidra.util.Msg;
+import ghidra.util.Swing;
/**
* Class to handle changes when a domain folder changes; updates the
@@ -49,6 +52,10 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
private boolean skipLinkUpdate = false; // updates within Swing event dispatch thread only
+ // The refreshedTrackingSet is used to track recursive path refreshes to avoid infinite
+ // recursion. See updateLinkedContent and LinkedTreeNode.refreshLinks methods.
+ private HashSet refreshedTrackingSet;
+
ChangeManager(ProjectDataTreePanel treePanel) {
this.treePanel = treePanel;
projectData = treePanel.getProjectData();
@@ -75,11 +82,13 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override
public void domainFileAdded(DomainFile file) {
+
boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
String fileName = file.getName();
DomainFolder parentFolder = file.getParent();
- updateLinkedContent(parentFolder, p -> addFileNode(p, fileName, isFolderLink),
+ updateLinkedContent(parentFolder.getPathname(), p -> addFileNode(p, fileName, isFolderLink),
ltn -> ltn.refreshLinks(fileName));
+
DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
if (folderNode != null && folderNode.isLoaded()) {
addFileNode(folderNode, fileName, isFolderLink);
@@ -88,7 +97,9 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override
public void domainFileRemoved(DomainFolder parent, String name, String fileID) {
- updateLinkedContent(parent, null, ltn -> ltn.refreshLinks(name));
+
+ updateLinkedContent(parent.getPathname(), null, ltn -> ltn.refreshLinks(name));
+
DomainFolderNode folderNode = findDomainFolderNode(parent, true);
if (folderNode != null) {
updateChildren(folderNode);
@@ -97,14 +108,16 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override
public void domainFileRenamed(DomainFile file, String oldName) {
+
boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
- updateLinkedContent(file.getParent(), p -> {
+ updateLinkedContent(file.getParent().getPathname(), p -> {
updateChildren(p);
addFileNode(p, file.getName(), isFolderLink);
}, ltn -> {
ltn.refreshLinks(oldName);
ltn.refreshLinks(file.getName());
});
+
DomainFolder parent = file.getParent();
skipLinkUpdate = true;
try {
@@ -124,10 +137,22 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override
public void domainFileStatusChanged(DomainFile file, boolean fileIDset) {
+
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ boolean isFolderLink = linkInfo != null && linkInfo.isFolderLink();
+
DomainFolder parentFolder = file.getParent();
- updateLinkedContent(parentFolder, fn -> {
- /* No folder update required */
+ updateLinkedContent(parentFolder.getPathname(), fn -> {
+ // Refresh any linked folder content containing file
+ if (fn.isLoaded()) {
+ NodeType type = isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE;
+ DomainFileNode fileNode = (DomainFileNode) fn.getChild(file.getName(), type);
+ if (fileNode != null) {
+ fileNode.refresh();
+ }
+ }
}, ltn -> ltn.refreshLinks(file.getName()));
+
DomainFileNode fileNode = findDomainFileNode(file, true);
if (fileNode != null) {
fileNode.refresh();
@@ -141,10 +166,12 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override
public void domainFolderAdded(DomainFolder folder) {
+
String folderName = folder.getName();
DomainFolder parentFolder = folder.getParent();
- updateLinkedContent(parentFolder, p -> addFolderNode(p, folderName),
+ updateLinkedContent(parentFolder.getPathname(), p -> addFolderNode(p, folderName),
ltn -> ltn.refreshLinks(folderName));
+
DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
if (folderNode != null && folderNode.isLoaded()) {
addFolderNode(folderNode, folderName);
@@ -153,7 +180,9 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override
public void domainFolderRemoved(DomainFolder parent, String name) {
- updateLinkedContent(parent, null, ltn -> ltn.refreshLinks(name));
+
+ updateLinkedContent(parent.getPathname(), null, ltn -> ltn.refreshLinks(name));
+
DomainFolderNode folderNode = findDomainFolderNode(parent, true);
if (folderNode != null) {
updateChildren(folderNode);
@@ -162,17 +191,12 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override
public void domainFolderRenamed(DomainFolder folder, String oldName) {
- updateLinkedContent(folder.getParent(), p -> {
- updateChildren(p);
- addFolderNode(p, folder.getName());
- }, ltn -> {
- ltn.refreshLinks(oldName);
- ltn.refreshLinks(folder.getName());
- });
- DomainFolder parent = folder.getParent();
+
+ domainFolderMoved(folder.getParent().getPathname(), oldName, folder);
+
skipLinkUpdate = true;
try {
- domainFolderRemoved(parent, oldName);
+ domainFolderRemoved(folder.getParent(), oldName);
domainFolderAdded(folder);
}
finally {
@@ -182,8 +206,17 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override
public void domainFolderMoved(DomainFolder folder, DomainFolder oldParent) {
- domainFolderRemoved(oldParent, folder.getName());
- domainFolderAdded(folder);
+
+ domainFolderMoved(oldParent.getPathname(), folder.getName(), folder);
+
+ skipLinkUpdate = true;
+ try {
+ domainFolderRemoved(oldParent, folder.getName());
+ domainFolderAdded(folder);
+ }
+ finally {
+ skipLinkUpdate = false;
+ }
}
@Override
@@ -194,6 +227,36 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
}
}
+ /**
+ * Following a folder move or rename where only a single notification is provided this
+ * method should be used to propogate link related updates which may refer to the affected
+ * folder or its children. This method is invoked recursively for all child folders.
+ * @param oldParentPath folder's old parent path
+ * @param oldName folder's previous name
+ * @param folder folder instance following rename
+ */
+ private void domainFolderMoved(String oldParentPath, String oldName, DomainFolder folder) {
+
+ String oldFolderPathname = LocalFileSystem.getPath(oldParentPath, oldName);
+
+ // Recurse over all child folders.
+ for (DomainFolder childFolder : folder.getFolders()) {
+ domainFolderMoved(oldFolderPathname, childFolder.getName(), childFolder);
+ }
+
+ // Refresh links to old placement
+ updateLinkedContent(oldParentPath, null, ltn -> {
+ ltn.refreshLinks(oldName);
+ });
+
+ // Refresh links to new placement
+ String newName = folder.getName();
+ updateLinkedContent(folder.getParent().getPathname(), p -> addFolderNode(p, newName),
+ ltn -> {
+ ltn.refreshLinks(newName);
+ });
+ }
+
//
// Helper methods
//
@@ -210,9 +273,13 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
}
private void addFileNode(DataTreeNode node, String fileName, boolean isFolderLink) {
+
+ Msg.debug(this, "addFileNode: " + node.getPathname() + " " + fileName + " " +
+ Boolean.toString(isFolderLink));
if (node.isLeaf() || !node.isLoaded()) {
return;
}
+
// Check for existance of file by that name
DomainFileNode fileNode = (DomainFileNode) node.getChild(fileName,
isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
@@ -234,6 +301,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
if (node.isLeaf() || !node.isLoaded()) {
return;
}
+
// Check for existance of folder by that name
if (node.getChild(folderName, NodeType.FOLDER) != null) {
return;
@@ -262,6 +330,20 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
return findDomainFolderNode(folderPath, lazy);
}
+// private List getPathAsList(String pathname) {
+// ArrayList folderPath = new ArrayList();
+// String[] pathSplit = pathname.split(FileSystem.SEPARATOR);
+// for (int i = 1; i < pathSplit.length; i++) {
+// folderPath.add(pathSplit[i]);
+// }
+// return folderPath;
+// }
+//
+// private DomainFolderNode findDomainFolderNode(String pathname, boolean lazy) {
+// List folderPath = getPathAsList(pathname);
+// return findDomainFolderNode(folderPath, lazy);
+// }
+
private DomainFolderNode findDomainFolderNode(List folderPath, boolean lazy) {
DomainFolderNode folderNode = root;
for (String name : folderPath) {
@@ -284,6 +366,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
if (lazy && !folderNode.isLoaded()) {
return null; // not visited
}
+
boolean isFolderLink = domainFile.isLink() && domainFile.getLinkInfo().isFolderLink();
return (DomainFileNode) folderNode.getChild(domainFile.getName(),
isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
@@ -334,40 +417,51 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
@Override
public void treeStructureChanged(TreeModelEvent e) {
- // This is used when an existing node is loaded to register all of its link-file children
- // since the occurance of treeNodesChanged cannot be relied upon for notification of
- // these existing children.
+ // NOTE: We have seen getTreePath return null in the test environment
+ // immediately before ChangeManager disposal
TreePath treePath = e.getTreePath();
- if (treePath == null) {
- return;
+
+ Object[] changedChildren = e.getChildren();
+ if (changedChildren != null) {
+ for (Object child : changedChildren) {
+ treeNodeChanged(child, true);
+ }
}
- Object treeNode = treePath.getLastPathComponent();
+ else if (treePath != null) {
+ treeNodeChanged(treePath.getLastPathComponent(), true);
+ }
+ }
+
+ private void treeNodeChanged(Object treeNode, boolean processLoadedChildren) {
+
if (!(treeNode instanceof DataTreeNode dataTreeNode)) {
return;
}
- if (!dataTreeNode.isLoaded()) {
- return;
- }
- // Register all visible link-file nodes
- for (GTreeNode child : dataTreeNode.getChildren()) {
- if (child instanceof DomainFileNode fileNode) {
- if (fileNode.getDomainFile().isLink()) {
- addLinkFile(fileNode);
- }
- }
+
+ if (treeNode instanceof DomainFileNode fileNode) {
+ addLinkFile(fileNode);
}
+
+ // TODO: Not sure we need the following code
+// if (processLoadedChildren && dataTreeNode.isLoaded()) {
+// for (GTreeNode node : dataTreeNode.getChildren()) {
+// treeNodeChanged(node, true);
+// }
+// }
}
@Override
public void treeNodesChanged(TreeModelEvent e) {
- // This is used to register link-file nodes which may be added to the tree as a result
- // of changes to the associated project data.
-
- Object treeNode = e.getTreePath().getLastPathComponent();
- if (treeNode instanceof DomainFileNode fileNode) {
- addLinkFile(fileNode);
+ Object[] changedChildren = e.getChildren();
+ if (changedChildren != null) {
+ for (Object child : changedChildren) {
+ treeNodeChanged(child, false);
+ }
+ }
+ else {
+ treeNodeChanged(e.getTreePath().getLastPathComponent(), false);
}
}
@@ -385,6 +479,19 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
// Link tracking tree update support
//
+ private void addLoadedChildren(DataTreeNode node) {
+
+ if (!node.isLoaded()) {
+ return;
+ }
+
+ for (GTreeNode child : node.getChildren()) {
+ if (child instanceof DomainFileNode fileNode) {
+ addLinkFile(fileNode);
+ }
+ }
+ }
+
/**
* Update link tree if the specified {@code domainFileNode} corresponds to an link-file
* which has an internal link-path which links to either a file or folder within the same
@@ -403,6 +510,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
}
try {
+
String linkPath = LinkHandler.getAbsoluteLinkPath(file);
if (linkPath == null) {
return;
@@ -420,6 +528,7 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
if (isFolderLink) {
folderLinkNode.addLinkedFolder(domainFileNode);
+ addLoadedChildren(domainFileNode);
}
else {
folderLinkNode.addLinkedFile(pathElements[lastFolderIndex + 1], domainFileNode);
@@ -439,39 +548,63 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
* once if a {@code LinkedTreeNode} is found which corresponds to the specified
* {@code parentFolder}. This allows targeted refresh of link-files.
*
- * @param parentFolder a parent folder which relates to a change
+ * @param parentFolderPath the parent folder path which relates to a change
* @param folderNodeConsumer optional consumer which will be invoked for each loaded parent
* tree node which is a linked-reflection of the specified {@code parentFolder}. If null is
* specified for this consumer a general update will be performed to remove any missing nodes.
* @param linkNodeConsumer optional consumer which will be invoked once if a {@code LinkedTreeNode}
* is found which corresponds to the specified {@code parentFolder}.
*/
- void updateLinkedContent(DomainFolder parentFolder, Consumer folderNodeConsumer,
- Consumer linkNodeConsumer) {
+ private void updateLinkedContent(String parentFolderPath,
+ Consumer folderNodeConsumer, Consumer linkNodeConsumer) {
+
+ if (!Swing.isSwingThread()) {
+ throw new RuntimeException(
+ "Listener and all node updates must operate in Swing thread");
+ }
+
if (skipLinkUpdate) {
return;
}
- String pathname = parentFolder.getPathname();
- String[] pathElements = pathname.split("/");
- LinkedTreeNode folderLinkNode = linkTreeRoot;
- folderLinkNode.updateLinkedContent(pathElements, 1, folderNodeConsumer);
- for (int i = 1; i < pathElements.length; i++) {
- folderLinkNode = folderLinkNode.folderMap.get(pathElements[i]);
- if (folderLinkNode == null) {
- return; // requested folder not contained within link-tree
- }
- folderLinkNode.updateLinkedContent(pathElements, i + 1, folderNodeConsumer);
+
+ // NOTE: This method must track those paths which have been refreshed to avoid the
+ // possibility of infinite recursion when circular links exist.
+ boolean clearRefreshedTrackingSet = false;
+ if (refreshedTrackingSet == null) {
+ refreshedTrackingSet = new HashSet<>();
+ clearRefreshedTrackingSet = true;
}
- // Requested folder was found in link-tree - invoke consumer to perform
- // selective refresh
- if (linkNodeConsumer != null) {
- linkNodeConsumer.accept(folderLinkNode);
+ try {
+ String[] pathElements = parentFolderPath.split("/");
+ LinkedTreeNode folderLinkNode = linkTreeRoot;
+ folderLinkNode.updateLinkedContent(pathElements, 1, folderNodeConsumer);
+ for (int i = 1; i < pathElements.length; i++) {
+ folderLinkNode = folderLinkNode.folderMap.get(pathElements[i]);
+ if (folderLinkNode == null) {
+ return; // requested folder not contained within link-tree
+ }
+ folderLinkNode.updateLinkedContent(pathElements, i + 1, folderNodeConsumer);
+ }
+
+ // Requested folder was found in link-tree - invoke consumer to perform
+ // selective refresh
+ if (linkNodeConsumer != null) {
+ linkNodeConsumer.accept(folderLinkNode);
+ }
+ }
+ finally {
+ if (clearRefreshedTrackingSet) {
+ refreshedTrackingSet = null;
+ }
}
}
private class LinkedTreeNode {
+ // NOTE: The use of HashSet to track LinkedTreeNodes relies on identity hashcode and
+ // same instance for equality.
+
private final LinkedTreeNode parent;
private final String name;
@@ -491,7 +624,14 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
boolean updateThisNode = subFolderPathIndex >= pathElements.length;
- for (DomainFileNode folderLink : folderLinks) {
+ Iterator folderLinkIter = folderLinks.iterator();
+ while (folderLinkIter.hasNext()) {
+
+ DomainFileNode folderLink = folderLinkIter.next();
+ if (folderLink.getParent() == null) {
+ // Remove disposed link node
+ folderLinkIter.remove();
+ }
if (!folderLink.isLoaded()) {
continue;
@@ -529,6 +669,25 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
}
private void refreshLinks(String childName) {
+
+ String childPathName = LocalFileSystem.getPath(getPathname(), childName);
+ if (!refreshedTrackingSet.add(childPathName)) {
+ return;
+ }
+
+ // If links are defined be sure to visit DomainFolder so that we pickup on change
+ // events even if not visible within tree.
+// TODO: Should no longer be needed after changes were made to force domain folder events
+// which would affect discovered link-files
+// if (!folderMap.isEmpty()) {
+// String path = LocalFileSystem.getPath(getPathname(), childName);
+// DomainFolder folder =
+// projectData.getFolder(path, DomainFolderFilter.ALL_INTERNAL_FOLDERS_FILTER);
+// if (folder != null) {
+// folder.getFolders(); // forced visit to folder
+// }
+// }
+
// We are forced to refresh file-links and folder-links since a folder-link may be
// referencing another folder-link file and not the final referenced folder.
if (refreshFileLinks(childName) || refreshFolderLinks(childName)) {
@@ -537,10 +696,28 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
}
private boolean refreshFolderLinks(String folderName) {
+
LinkedTreeNode linkedTreeNode = folderMap.get(folderName);
if (linkedTreeNode != null) {
+
refresh(linkedTreeNode.folderLinks);
- return linkedTreeNode.folderLinks.isEmpty();
+ boolean removed = linkedTreeNode.folderLinks.isEmpty();
+
+ // Refresh all file links refering to files within this folder
+ Collection> linkedFileSets =
+ linkedTreeNode.linkedFilesMap.values();
+ if (!linkedFileSets.isEmpty()) {
+ Iterator> iterator = linkedFileSets.iterator();
+ while (iterator.hasNext()) {
+ Set linkFileSet = iterator.next();
+ refresh(linkFileSet);
+ if (linkFileSet.isEmpty()) {
+ iterator.remove();
+ removed = true;
+ }
+ }
+ }
+ return removed;
}
return false;
}
@@ -557,12 +734,22 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
return false;
}
+ /**
+ * Add or get existing named child folder node for this folder node
+ * @param folderName child folder node
+ * @return new or existing named child folder node
+ */
private LinkedTreeNode addFolder(String folderName) {
return folderMap.computeIfAbsent(folderName, n -> new LinkedTreeNode(this, n));
}
- private void addLinkedFolder(DomainFileNode folderLink) {
- folderLinks.add(folderLink);
+ /**
+ * Add a folder-link which references this folder node
+ * @param folderLink link which references this folder node
+ * @return true if the set did not already contain the specified folderLink
+ */
+ private boolean addLinkedFolder(DomainFileNode folderLink) {
+ return folderLinks.add(folderLink);
}
private void addLinkedFile(String fileName, DomainFileNode fileLink) {
@@ -579,24 +766,28 @@ class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
}
}
- private static void refresh(Set linkFiles) {
- List purgeList = null;
- for (DomainFileNode fileLink : linkFiles) {
- DomainFile file = fileLink.getDomainFile();
- // Perform lazy purge of missing link files
- if (!file.isLink()) {
- if (purgeList == null) {
- purgeList = new ArrayList<>();
- }
- purgeList.add(fileLink);
+ private void refresh(Set linkFiles) {
+ Iterator linkFileIter = linkFiles.iterator();
+ while (linkFileIter.hasNext()) {
+ DomainFileNode fileLink = linkFileIter.next();
+ if (fileLink.getParent() == null || !fileLink.getDomainFile().isLink()) {
+ linkFileIter.remove();
}
else {
fileLink.refresh();
+
+ GTreeNode linkParent = fileLink.getParent();
+ if (linkParent instanceof DomainFolderNode linkParentNode) {
+
+ // TODO: What about LinkedDomainFolders?
+ ChangeManager.this.updateLinkedContent(linkParentNode.getPathname(), fn -> {
+ /* do nothing */ }, ltn -> {
+ ltn.refreshLinks(fileLink.getName());
+ });
+ }
+
}
}
- if (purgeList != null) {
- linkFiles.removeAll(purgeList);
- }
}
private String getPathname() {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java
index c4cf844fab..809447c23e 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java
@@ -142,19 +142,7 @@ public class DataTree extends GTree {
}
else if (node instanceof DomainFileNode fileNode) {
if (fileNode.isFolderLink()) {
- // Handle case where file node corresponds to a folder-link.
- // Folder-Link status needs to be checked to ensure it corresponds to a folder
- // internal to the same project.
- LinkFileInfo linkInfo = fileNode.getDomainFile().getLinkInfo();
- if (linkInfo == null) {
- return null; // unexpected
- }
- LinkStatus linkStatus = linkInfo.getLinkStatus(null);
- if (linkStatus != LinkStatus.INTERNAL) {
- return null;
- }
- // Get linked folder - status check ensures null will not be returned
- folder = linkInfo.getLinkedFolder();
+ folder = getLinkedFolder(fileNode);
}
else {
// Handle normal file cases where we return node's parent folder
@@ -162,8 +150,12 @@ public class DataTree extends GTree {
if (parent instanceof DomainFolderNode folderNode) {
folder = folderNode.getDomainFolder();
}
+ else if (parent instanceof DomainFileNode parentFileNode) {
+ folder = getLinkedFolder(parentFileNode);
+ }
}
}
+
if (folder instanceof LinkedDomainFolder linkedFolder) {
// Resolve linked internal folder to its real folder
try {
@@ -175,4 +167,20 @@ public class DataTree extends GTree {
}
return folder;
}
+
+ private static DomainFolder getLinkedFolder(DomainFileNode fileNode) {
+ // Handle case where file node corresponds to a folder-link.
+ // Folder-Link status needs to be checked to ensure it corresponds to a folder
+ // internal to the same project.
+ LinkFileInfo linkInfo = fileNode.getDomainFile().getLinkInfo();
+ if (linkInfo == null) {
+ return null; // unexpected
+ }
+ LinkStatus linkStatus = linkInfo.getLinkStatus(null);
+ if (linkStatus != LinkStatus.INTERNAL) {
+ return null;
+ }
+ // Get linked folder - status check ensures null will not be returned
+ return linkInfo.getLinkedFolder();
+ }
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java
index 6e3798f48c..455a3bb32c 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java
@@ -174,7 +174,9 @@ public class DataTreeDragNDropHandler implements GTreeDragNDropHandler {
private List getDomainParentNodes(List nodeList) {
List parentList = new ArrayList<>();
for (GTreeNode node : nodeList) {
- if (!node.isLeaf()) {
+ if (node instanceof DomainFolderNode) {
+ // We want to ensure we treat link-file node as not being a parent
+ // for this operation.
parentList.add(node);
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java
index a1537eae5f..5413f340b4 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java
@@ -94,6 +94,11 @@ public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cutta
*/
public abstract ProjectData getProjectData();
+ /**
+ * @returns domain folder/file pathname within project
+ */
+ public abstract String getPathname();
+
@Override
public abstract int compareTo(GTreeNode node);
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java
index 77fa76968f..0b1f285cd4 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java
@@ -46,7 +46,12 @@ public class DomainFileNode extends DataTreeNode {
private static final Icon UNKNOWN_FILE_ICON = new GIcon("icon.datatree.node.domain.file");
private static final String RIGHT_ARROW = "\u2192";
+ // NOTE: We must ensure anything used by sort comparator remains fixed
private final DomainFile domainFile;
+ private final boolean isFolderLink;
+
+ private LinkFileInfo linkInfo;
+ private boolean isLeaf;
private volatile String displayName; // name displayed in the tree
private volatile Icon icon = UNKNOWN_FILE_ICON;
@@ -54,15 +59,14 @@ public class DomainFileNode extends DataTreeNode {
private volatile String toolTipText;
private AtomicInteger refreshCount = new AtomicInteger();
- private boolean isLeaf = true;
- private LinkFileInfo linkInfo;
private DomainFileFilter filter; // relavent when expand folder-link which is a file
private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa");
DomainFileNode(DomainFile domainFile, DomainFileFilter filter) {
this.domainFile = domainFile;
- this.linkInfo = domainFile.getLinkInfo();
+ linkInfo = domainFile.getLinkInfo();
+ isFolderLink = linkInfo != null && linkInfo.isFolderLink();
this.filter = filter != null ? filter : DomainFileFilter.ALL_FILES_FILTER;
displayName = domainFile.getName();
refresh();
@@ -84,6 +88,11 @@ public class DomainFileNode extends DataTreeNode {
return domainFile;
}
+ @Override
+ public String getPathname() {
+ return domainFile.getPathname();
+ }
+
@Override
public boolean isLeaf() {
return isLeaf;
@@ -103,15 +112,11 @@ public class DomainFileNode extends DataTreeNode {
* @return true if file is a folder-link
*/
public boolean isFolderLink() {
- if (linkInfo != null) {
- return linkInfo.isFolderLink();
- }
- return false;
+ return isFolderLink;
}
/**
- * Get linked folder which corresponds to this folder-link
- * (see {@link #isFolderLink()}).
+ * Get linked folder which corresponds to this folder-link (see {@link #isFolderLink()}).
* @return linked folder or null if this is not a folder-link
*/
LinkedDomainFolder getLinkedFolder() {
@@ -214,33 +219,42 @@ public class DomainFileNode extends DataTreeNode {
private void doRefresh() {
isLeaf = true;
- linkInfo = null;
+ LinkFileInfo updatedLinkInfo = domainFile.getLinkInfo();
boolean brokenLink = false;
List linkErrors = null;
- if (domainFile.isLink()) {
- linkInfo = domainFile.getLinkInfo();
- List errors = new ArrayList<>();
- LinkStatus linkStatus =
- LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg));
- brokenLink = (linkStatus == LinkStatus.BROKEN);
- if (brokenLink) {
- linkErrors = errors;
- }
- else if (isFolderLink()) {
- if (linkStatus == LinkStatus.INTERNAL) {
- isLeaf = false;
+
+ if (isFolderLink != (updatedLinkInfo != null && updatedLinkInfo.isFolderLink())) {
+ // Linked-folder node state changed. Since this alters sort order we can't handle this.
+ // Such a DomainFile state change must be handled by the ChangeManager
+ brokenLink = true;
+ linkErrors = List.of("Unsupported folder-link transition");
+ }
+ else {
+ linkInfo = updatedLinkInfo;
+ if (linkInfo != null) {
+ List errors = new ArrayList<>();
+ LinkStatus linkStatus =
+ LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg));
+ brokenLink = (linkStatus == LinkStatus.BROKEN);
+ if (brokenLink) {
+ linkErrors = errors;
}
- else if (linkStatus == LinkStatus.EXTERNAL &&
- filter.followExternallyLinkedFolders()) {
- isLeaf = false;
+ else if (isFolderLink) {
+ if (linkStatus == LinkStatus.INTERNAL) {
+ isLeaf = false;
+ }
+ else if (linkStatus == LinkStatus.EXTERNAL &&
+ filter.followExternallyLinkedFolders()) {
+ isLeaf = false;
+ }
}
}
}
- if (isLeaf) {
- unloadChildren();
- }
+ // We must always unload any children since a leaf has no children and a folder-link
+ // may be transitioning to a state where its children may need to be re-loaded.
+ unloadChildren();
displayName = getFormattedDisplayName();
@@ -289,7 +303,25 @@ public class DomainFileNode extends DataTreeNode {
private String getFormattedLinkPath() {
- String linkPath = linkInfo != null ? linkInfo.getLinkPath() : null;
+ String linkPath = null;
+
+ // If link-file is a LinkedDomainFile we must always show an absolute link-path since
+ // relative paths are relative to the real file's location and it would be rather confusing
+ // to show as relative
+ if (domainFile instanceof LinkedDomainFile) {
+ try {
+ // will return GhidraURL or absolute internal path
+ linkPath = LinkHandler.getAbsoluteLinkPath(domainFile);
+ }
+ catch (IOException e) {
+ // attempt to use stored path, although it may fail as well
+ linkPath = linkInfo.getLinkPath();
+ }
+ }
+ else if (linkInfo != null) {
+ linkPath = linkInfo.getLinkPath();
+ }
+
if (GhidraURL.isGhidraURL(linkPath)) {
try {
URL url = new URL(linkPath);
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java
index c7f48fa5c5..05a662728d 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java
@@ -20,6 +20,7 @@ import java.util.List;
import javax.swing.Icon;
+import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode;
import ghidra.framework.model.*;
import ghidra.util.*;
@@ -48,6 +49,7 @@ public class DomainFolderNode extends DataTreeNode {
private boolean isEditable;
DomainFolderNode(DomainFolder domainFolder, DomainFileFilter filter) {
+
this.domainFolder = domainFolder;
this.filter = filter;
@@ -73,6 +75,11 @@ public class DomainFolderNode extends DataTreeNode {
return domainFolder;
}
+ @Override
+ public String getPathname() {
+ return domainFolder.getPathname();
+ }
+
/**
* Returns true if this node has no children.
*/
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java
index 37458a911c..510b479057 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java
@@ -112,23 +112,35 @@ public final class LocalTreeNodeHandler
}
private boolean isValidDrag(DomainFolder destFolder, GTreeNode draggedNode) {
- // NOTE: We may have issues since checks are not based on canonical paths
- if (draggedNode instanceof DomainFolderNode folderNode) {
- // This also checks cases where src/dest projects are using the same repository.
- // Unfortunately, it will also prevent cases where shared-project folder
- // does not contain versioned content and could actually be allowed.
- DomainFolder folder = folderNode.getDomainFolder();
- return !folder.isSameOrAncestor(destFolder);
- }
- if (draggedNode instanceof DomainFileNode fileNode) {
- DomainFolder folder = fileNode.getDomainFile().getParent();
- DomainFile file = fileNode.getDomainFile();
- if (file.isVersioned()) {
+ try {
+ // NOTE: destFolder should be real folder and not linked-folder
+ // NOTE: We may have issues since checks are not based on canonical paths
+ if (draggedNode instanceof DomainFolderNode folderNode) {
// This also checks cases where src/dest projects are using the same repository.
- return !folder.isSame(destFolder);
+ // Unfortunately, it will also prevent cases where shared-project folder
+ // does not contain versioned content and could actually be allowed.
+ DomainFolder folder = folderNode.getDomainFolder();
+ if (folder instanceof LinkedDomainFolder linkedFolder) {
+ folder = linkedFolder.getRealFolder();
+ }
+ return !folder.isSameOrAncestor(destFolder);
}
- DomainFile destFile = destFolder.getFile(file.getName());
- return destFile == null || !destFile.equals(file);
+ if (draggedNode instanceof DomainFileNode fileNode) {
+ DomainFile file = fileNode.getDomainFile();
+ if (file instanceof LinkedDomainFile linkedFile) {
+ file = linkedFile.getRealFile();
+ }
+ DomainFolder folder = file.getParent();
+ if (file.isVersioned()) {
+ // This also checks cases where src/dest projects are using the same repository.
+ return !folder.isSame(destFolder);
+ }
+ DomainFile destFile = destFolder.getFile(file.getName());
+ return destFile == null || !destFile.equals(file);
+ }
+ }
+ catch (IOException e) {
+ // ignore
}
return false;
}
@@ -165,6 +177,9 @@ public final class LocalTreeNodeHandler
}
try {
+ if (file instanceof LinkedDomainFile linkedFile) {
+ file = linkedFile.getRealFile();
+ }
file.moveTo(destFolder);
}
catch (IOException e) {
@@ -186,6 +201,9 @@ public final class LocalTreeNodeHandler
}
try {
+ if (sourceFolder instanceof LinkedDomainFolder linkedFolder) {
+ sourceFolder = linkedFolder.getRealFolder();
+ }
sourceFolder.moveTo(destFolder);
}
catch (DuplicateFileException dfe) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java
index 61ba49e411..a82cc4e01e 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java
@@ -17,8 +17,7 @@ package ghidra.framework.main.projectdata.actions;
import java.awt.Component;
import java.io.IOException;
-import java.util.Objects;
-import java.util.Set;
+import java.util.*;
import docking.widgets.OptionDialog;
import docking.widgets.OptionDialogBuilder;
@@ -65,10 +64,21 @@ public class DeleteProjectFilesTask extends Task {
initializeMonitor(monitor);
- deleteFiles(selectedFiles, monitor);
+ Set resolvedFiles = resolveLinkedFiles(selectedFiles);
- deleteFolders(selectedFolders, monitor);
+ Set resolvedFolders = resolveLinkedFolders(selectedFolders);
+ try {
+ deleteFiles(resolvedFiles, monitor);
+ deleteFolders(resolvedFolders, monitor);
+ }
+ catch (CancelledException e) {
+ // ignore
+ }
+
+ }
+
+ public void showReport() {
statistics.showReport(parent);
}
@@ -77,60 +87,105 @@ public class DeleteProjectFilesTask extends Task {
monitor.initialize(statistics.getFileCount());
}
- private void deleteFiles(Set files, TaskMonitor monitor) {
- try {
- for (DomainFile file : files) {
- monitor.checkCancelled();
- deleteFile(file);
- monitor.incrementProgress(1);
+ /**
+ * Domain file comparator for use in establishing order of file removal.
+ * All real files (i.e., non-link-files) must be removed before link-files
+ * afterwhich depth-first applies.
+ */
+ private static final Comparator FILE_PATH_COMPARATOR =
+ new Comparator() {
+
+ @Override
+ public int compare(DomainFile o1, DomainFile o2) {
+
+ // Ensure that non-link-files occur first in sorted list
+ boolean isLink1 = o1.isLink();
+ boolean isLink2 = o2.isLink();
+ if (isLink1) {
+ if (!isLink2) {
+ return 1;
+ }
+ }
+ else if (isLink2) {
+ return -1;
+ }
+
+ // Compare paths to ensure deeper paths occur first in sorted list
+ String path1 = o1.getPathname();
+ String path2 = o2.getPathname();
+ return path2.compareTo(path1);
}
+ };
+
+ private Set resolveLinkedFiles(Set files) {
+ Set resolvedFiles = new HashSet<>();
+ for (DomainFile file : files) {
+ // If file is contained within a linked-folder (LinkedDomainFile) we need to
+ // use the actual linked file. Since we should be dealing with internally
+ // linked content IOExceptions are unexpected.
+ if (file instanceof LinkedDomainFile linkedFile) {
+ try {
+ file = linkedFile.getRealFile();
+ }
+ catch (IOException e) {
+ continue; // Skip file if unable to resolve
+ }
+ }
+ resolvedFiles.add(file);
}
- catch (CancelledException e) {
- // just return so that statistics for what completed can be displayed
+ return resolvedFiles;
+ }
+
+ private Set resolveLinkedFolders(Set folders) {
+ Set resolvedFolders = new HashSet<>();
+ for (DomainFolder folder : folders) {
+ // If folder is a linked-folder (LinkedDomainFolder) we need to
+ // use the actual linked folder. Since we should be dealing with internally
+ // linked content IOExceptions are unexpected.
+ if (folder instanceof LinkedDomainFolder linkedFolder) {
+ try {
+ folder = linkedFolder.getRealFolder();
+ }
+ catch (IOException e) {
+ continue; // skip file if unable to resolve
+ }
+ }
+ resolvedFolders.add(folder);
+ }
+ return resolvedFolders;
+ }
+
+ private void deleteFiles(Set files, TaskMonitor monitor) throws CancelledException {
+
+ ArrayList sortedFiles = new ArrayList<>(files);
+ Collections.sort(sortedFiles, FILE_PATH_COMPARATOR);
+
+ for (DomainFile file : sortedFiles) {
+ monitor.checkCancelled();
+ deleteFile(file, monitor);
}
}
- private void deleteFolders(Set folders, TaskMonitor monitor) {
+ private void deleteFolders(Set folders, TaskMonitor monitor)
+ throws CancelledException {
- try {
- for (DomainFolder folder : folders) {
- deleteFolder(folder, monitor);
- }
- }
- catch (CancelledException e) {
- // just return so that statistics for what completed can be displayed
+ for (DomainFolder folder : folders) {
+ monitor.checkCancelled();
+ deleteFolder(folder, monitor);
}
}
private void deleteFolder(DomainFolder folder, TaskMonitor monitor) throws CancelledException {
- while (folder instanceof LinkedDomainFolder linkedFolder) {
- if (linkedFolder.isLinked()) {
- throw new IllegalArgumentException(
- "Linked-folder's originating file-link should have been removed instead: " +
- linkedFolder.getPathname());
- }
- try {
- folder = linkedFolder.getRealFolder();
- }
- catch (IOException e) {
- Msg.error(this, "Error following linked-folder: " + e.getMessage() + "\n" +
- folder.getPathname());
- return;
- }
- }
-
for (DomainFolder subFolder : folder.getFolders()) {
monitor.checkCancelled();
- if (!selectedFolders.contains(subFolder)) {
- deleteFolder(subFolder, monitor);
- }
+ deleteFolder(subFolder, monitor);
}
for (DomainFile file : folder.getFiles()) {
monitor.checkCancelled();
if (!selectedFiles.contains(file)) {
- deleteFile(file);
+ deleteFile(file, monitor);
monitor.incrementProgress(1);
}
}
@@ -149,42 +204,47 @@ public class DeleteProjectFilesTask extends Task {
}
}
- private void deleteFile(DomainFile file) throws CancelledException {
- if (file.isOpen()) {
- statistics.incrementFileInUse();
- showFileInUseDialog(file);
+ private void deleteFile(DomainFile file, TaskMonitor monitor) throws CancelledException {
+
+ if (!file.exists()) {
return;
}
- if (file.isVersioned() && file.isCheckedOut()) {
- showCheckedOutVersionedDialog(file);
- statistics.incrementCheckedOutVersioned();
- return;
- }
-
- if (file.isReadOnly()) {
- int result = showConfirmReadOnlyDialog(file);
- if (result == OptionDialog.CANCEL_OPTION) {
- throw new CancelledException();
- }
- if (result != OptionDialog.YES_OPTION) {
- statistics.incrementReadOnly();
- return;
- }
- }
-
- if (file.isVersioned()) {
- int result = showConfirmDeleteVersionedDialog(file);
- if (result == OptionDialog.CANCEL_OPTION) {
- throw new CancelledException();
- }
- if (result != OptionDialog.YES_OPTION) {
- statistics.incrementVersioned();
- return;
- }
- }
-
try {
+ if (file.isOpen()) {
+ statistics.incrementFileInUse();
+ showFileInUseDialog(file);
+ return;
+ }
+
+ if (file.isVersioned() && file.isCheckedOut()) {
+ showCheckedOutVersionedDialog(file);
+ statistics.incrementCheckedOutVersioned();
+ return;
+ }
+
+ if (file.isReadOnly()) {
+ int result = showConfirmReadOnlyDialog(file);
+ if (result == OptionDialog.CANCEL_OPTION) {
+ throw new CancelledException();
+ }
+ if (result != OptionDialog.YES_OPTION) {
+ statistics.incrementReadOnly();
+ return;
+ }
+ }
+
+ if (file.isVersioned()) {
+ int result = showConfirmDeleteVersionedDialog(file);
+ if (result == OptionDialog.CANCEL_OPTION) {
+ throw new CancelledException();
+ }
+ if (result != OptionDialog.YES_OPTION) {
+ statistics.incrementVersioned();
+ return;
+ }
+ }
+
file.delete();
statistics.incrementDeleted();
}
@@ -198,6 +258,9 @@ public class DeleteProjectFilesTask extends Task {
throw new CancelledException();
}
}
+ finally {
+ monitor.increment();
+ }
}
private int showConfirmDeleteVersionedDialog(DomainFile file) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FileCountStatistics.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FileCountStatistics.java
index b5a3c8d87b..049ae45cd6 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FileCountStatistics.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FileCountStatistics.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -31,6 +31,8 @@ class FileCountStatistics {
private int deleted;
FileCountStatistics(int fileCount) {
+ // NOTE: Do the possibility of file duplication through the selection of linked-folder
+ // content, this count is an estimate only
this.fileCount = fileCount;
}
@@ -72,9 +74,10 @@ class FileCountStatistics {
public void showReport(Component parent) {
// don't show results if only one file processed.
- if (getTotalProcessed() == 1) {
+ if (fileCount == 1 && getTotalProcessed() == 1) {
return;
}
+
// don't show results if all selected files deleted
if (deleted == fileCount) {
return;
@@ -97,20 +100,24 @@ class FileCountStatistics {
builder.append("In Use: | ").append(fileInUse).append(" |
");
}
if (versionedDeclined > 0) {
- builder.append(" Versioned: | ").append(versionedDeclined).append(
- " |
");
+ builder.append(" Versioned: | ")
+ .append(versionedDeclined)
+ .append(" |
");
}
if (checkedOutVersioned > 0) {
- builder.append("Checked-out: | ").append(checkedOutVersioned).append(
- " |
");
+ builder.append("Checked-out: | ")
+ .append(checkedOutVersioned)
+ .append(" |
");
}
if (readOnlySkipped > 0) {
- builder.append("Read only: | ").append(readOnlySkipped).append(
- " |
");
+ builder.append("Read only: | ")
+ .append(readOnlySkipped)
+ .append(" |
");
}
if (generalFailure > 0) {
- builder.append("Other: | ").append(generalFailure).append(
- " |
");
+ builder.append("Other: | ")
+ .append(generalFailure)
+ .append(" |
");
}
builder.append("");
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java
index 4aa8698fbd..6f87539e01 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java
@@ -58,23 +58,28 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
// Confirm the delete *without* using a task so that do not have 2 dialogs showing
int fileCount = countTask.getFileCount();
- if (!confirmDelete(fileCount, files, context.getComponent())) {
+ if (!confirmDelete(fileCount, files, folders, context.getComponent())) {
return;
}
// Task 2 - perform the delete--this could take a while
DeleteProjectFilesTask deleteTask = createDeleteTask(context, files, folders, fileCount);
TaskLauncher.launch(deleteTask);
- }
+ if (!deleteTask.isCancelled()) {
+ deleteTask.showReport();
+ }
+ }
+
DeleteProjectFilesTask createDeleteTask(ProjectDataContext context, Set files,
Set folders, int fileCount) {
return new DeleteProjectFilesTask(folders, files, fileCount, context.getComponent());
}
- private boolean confirmDelete(int fileCount, Set files, Component parent) {
+ private boolean confirmDelete(int fileCount, Set files, Set folders,
+ Component parent) {
- String message = getMessage(fileCount, files);
+ String message = getMessage(fileCount, files, folders);
OptionDialogBuilder builder = new OptionDialogBuilder("Confirm Delete", message);
int choice = builder.addOption("OK")
.addCancel()
@@ -83,28 +88,25 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
return choice != OptionDialog.CANCEL_OPTION;
}
- private String getMessage(int fileCount, Set selectedFiles) {
+ private String getMessage(int fileCount, Set files, Set folders) {
if (fileCount == 0) {
return "Are you sure you want to delete the selected empty folder(s)?";
}
- if (fileCount == 1) {
- if (!selectedFiles.isEmpty()) {
- DomainFile file = CollectionUtils.any(selectedFiles);
- String type = file.isLink() ? "link" : "file";
+ if (folders.isEmpty()) {
+ if (fileCount == 1) {
+ DomainFile file = CollectionUtils.any(files);
+ String type = file.getContentType();
return "Are you sure you want to permanently delete " + type +
" \"" + HTMLUtilities.escapeHTML(file.getName()) + "\"?";
}
-
- // only folders are selected, but they contain files
return "Are you sure you want to permanently delete the " +
- " selected files and folders?";
+ " selected files?";
}
// multiple files selected
- return "Are you sure you want to permanently delete the " + fileCount +
- " selected files?";
+ return "Are you sure you want to permanently delete the selected folder(s) and file(s)?";
}
@Override
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java
index 17e3b4bf7a..d75fe1627e 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java
@@ -25,8 +25,7 @@ import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
import ghidra.framework.main.datatable.ProjectTreeContext;
import ghidra.framework.main.datatree.*;
-import ghidra.framework.model.DomainFile;
-import ghidra.framework.model.DomainFolder;
+import ghidra.framework.model.*;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.AssertException;
@@ -48,26 +47,32 @@ public class ProjectDataNewFolderAction
@Override
protected boolean isEnabledForContext(T context) {
- return getFolder(context).isInWritableProject();
+ try {
+ return getFolder(context).isInWritableProject();
+ }
+ catch (IOException e) {
+ return false;
+ }
}
private void createNewFolder(T context) {
-
- DomainFolder parentFolder = getFolder(context);
- DomainFolder newFolder = createNewFolderWithDefaultName(parentFolder);
+ DomainFolder newFolder = createNewFolderWithDefaultName(context);
GTreeNode parent = getParentNode(context);
DataTree tree = context.getTree();
tree.setEditable(true);
tree.startEditing(parent, newFolder.getName());
}
- private DomainFolder createNewFolderWithDefaultName(DomainFolder parentFolder) {
- String name = getNewFolderName(parentFolder);
+ private DomainFolder createNewFolderWithDefaultName(T context) {
+ String errName = "";
try {
+ DomainFolder parentFolder = getFolder(context);
+ String name = getNewFolderName(parentFolder);
+ errName = ": " + name;
return parentFolder.createFolder(name);
}
catch (InvalidNameException | IOException e) {
- throw new AssertException("Unexpected Error creating new folder: " + name, e);
+ throw new AssertException("Unexpected Error creating new folder" + errName, e);
}
}
@@ -82,18 +87,33 @@ public class ProjectDataNewFolderAction
return name;
}
- private DomainFolder getFolder(T context) {
- // the following code relies on the isAddToPopup to ensure that there is exactly one
+ private DomainFolder getFolder(T context) throws IOException {
+ // the following code relied upon by the isAddToPopup to ensure that there is exactly one
// file or folder selected
+ DomainFolder folder = null;
if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
- return context.getSelectedFolders().get(0);
+ folder = context.getSelectedFolders().get(0);
}
- if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
+ else if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
DomainFile file = context.getSelectedFiles().get(0);
- return file.getParent();
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ if (linkInfo != null && linkInfo.isFolderLink()) {
+ folder = linkInfo.getLinkedFolder();
+ }
+ else {
+ folder = file.getParent();
+ }
}
- DomainFolderNode rootNode = (DomainFolderNode) context.getTree().getModelRoot();
- return rootNode.getDomainFolder();
+ if (folder instanceof LinkedDomainFolder linkedFolder) {
+ // Use real folder associated with linked file/folder selection
+ folder = linkedFolder.getRealFolder();
+ }
+ if (folder == null) {
+ // Use root folder if valid selection not found
+ DomainFolderNode rootNode = (DomainFolderNode) context.getTree().getModelRoot();
+ folder = rootNode.getDomainFolder();
+ }
+ return folder;
}
private GTreeNode getParentNode(T context) {
@@ -104,8 +124,10 @@ public class ProjectDataNewFolderAction
return context.getTree().getModelRoot();
}
- if (node instanceof DomainFileNode) {
- return node.getParent();
+ if (node instanceof DomainFileNode fileNode) {
+ if (!fileNode.isFolderLink()) {
+ return node.getParent();
+ }
}
return node;
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolderChangeListener.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolderChangeListener.java
index 92afd300bc..e87be81326 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolderChangeListener.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolderChangeListener.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -59,6 +59,11 @@ public interface DomainFolderChangeListener {
/**
* Notify listeners when a domain folder is renamed.
+ *
+ * NOTE: Only a single event will be sent for the specific folder renamed and not its children.
+ * If the listener cares about the impact of this event on the folder's children it will need
+ * to process accordingly.
+ *
* @param folder folder that was renamed
* @param oldName old name of folder
*/
@@ -77,6 +82,11 @@ public interface DomainFolderChangeListener {
/**
* Notification that the domain folder was moved.
+ *
+ * NOTE: Only a single event will be sent for the specific folder moved and not its children.
+ * If the listener cares about the impact of this event on the folder's children it will need
+ * to process accordingly.
+ *
* @param folder the folder (after move)
* @param oldParent original parent folder
*/
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkFileInfo.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkFileInfo.java
index 22c401096a..3a9326aa9d 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkFileInfo.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkFileInfo.java
@@ -59,8 +59,11 @@ public interface LinkFileInfo {
* method on an {@link #isExternalLink() external-link} will cause the associated
* project or repository to be opened and associated with the active project as a
* a viewed-project. The resulting folder instance will return true to the method
- * {@link DomainFolder#isLinked()}. This method will recurse all internal folder-links
- * which may be chained together.
+ * {@link DomainFolder#isLinked()}.
+ *
+ * NOTE: This method will recurse all internal folder-links which may be chained together
+ * and relies on link status checks to prevent possible recursion (See
+ * {@link LinkHandler#getLinkFileStatus(DomainFile, Consumer)}).
*
* @return a linked domain folder or null if not a valid folder-link.
*/
@@ -71,7 +74,10 @@ public interface LinkFileInfo {
* link-file's project or a Ghidra URL.
*
* If you want to ensure that a project path returned is absolute and normalized, then
- * {@link #getAbsoluteLinkPath()} may be used.
+ * {@link #getAbsoluteLinkPath()} may be used. If this corresponds to a link-file that
+ * implements {@link LinkedDomainFile} the absolute path form must be used to avoid treating
+ * as relative to the incorrect parent folder. A {@link LinkedDomainFile} can occur when
+ * the link-file exists within a linked-folder or subfolder.
*
* @return associated link path
*/
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java
index bfc8df833c..69f8927536 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java
@@ -35,6 +35,6 @@ public interface LinkedDomainFile extends DomainFile {
* @return domain file
* @throws IOException if IO error occurs or file not found
*/
- public DomainFile getLinkedFile() throws IOException;
+ public DomainFile getRealFile() throws IOException;
}
diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/data/RelativePathTest.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/data/RelativePathTest.java
new file mode 100644
index 0000000000..c97519a427
--- /dev/null
+++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/data/RelativePathTest.java
@@ -0,0 +1,47 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.framework.data;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+import generic.test.AbstractGenericTest;
+
+public class RelativePathTest extends AbstractGenericTest {
+
+ @Test
+ public void testGetRelativePath() {
+
+ // File links
+ assertEquals("../b", GhidraFolderData.getRelativePath("/a/b/../b", "/a/b", false));
+ assertEquals("../b", GhidraFolderData.getRelativePath("/a/b", "/a/b", false));
+ assertEquals("c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b", false));
+ assertEquals("../c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b/d", false));
+
+ // Folder links
+ assertEquals(".", GhidraFolderData.getRelativePath("/a/b/../b", "/a/b", true));
+ assertEquals(".", GhidraFolderData.getRelativePath("/a/b", "/a/b", true));
+ assertEquals("c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b", true));
+ assertEquals("../c", GhidraFolderData.getRelativePath("/a/b/c", "/a/b/d", true));
+ assertEquals(".", GhidraFolderData.getRelativePath("/a/b/../b/", "/a/b", true)); // See Note-1
+ assertEquals(".", GhidraFolderData.getRelativePath("/a/b/", "/a/b", true));
+ assertEquals("c/", GhidraFolderData.getRelativePath("/a/b/c/", "/a/b", true));
+ assertEquals("../c/", GhidraFolderData.getRelativePath("/a/b/c/", "/a/b/d", true));
+
+ }
+
+}
diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTaskTest.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTaskTest.java
index 42b72f195d..5b59223aa5 100644
--- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTaskTest.java
+++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTaskTest.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -57,6 +57,8 @@ public class DeleteProjectFilesTaskTest extends AbstractDockingTest {
@Before
public void setUp() throws Exception {
+ // NOTE: TestDummyDomainFolder is inadequate to test against linked-folder cases
+ // which requires real project data support.
root = new TestDummyDomainFolder(null, "root");
a = root.createFolder("a");
b = root.createFolder("b");
@@ -488,9 +490,9 @@ public class DeleteProjectFilesTaskTest extends AbstractDockingTest {
private void runAction() {
- ActionContext context = new ProjectDataContext(/*provider*/null, /*project data*/null,
- /*context object*/ null, CollectionUtils.asList(folders), CollectionUtils.asList(files),
- null, true);
+ ActionContext context =
+ new ProjectDataContext(/*provider*/null, /*project data*/null, /*context object*/ null,
+ CollectionUtils.asList(folders), CollectionUtils.asList(files), null, true);
performAction(deleteAction, context, false);
waitForSwing();
}
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteFromRepositoryTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteFromRepositoryTest.java
index 6cd77609b6..ab22099d50 100644
--- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteFromRepositoryTest.java
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteFromRepositoryTest.java
@@ -526,7 +526,6 @@ public class ProjectCopyPasteFromRepositoryTest extends AbstractGhidraHeadedInte
// of folder or another folder-link-file at the referenced location
//
String urlPath = sharedFolderURL.toExternalForm(); // will end with '/'
- urlPath = urlPath.substring(0, urlPath.length() - 1); // strip trailing '/'
assertEquals(urlPath, linkInfo.getLinkPath());
@@ -593,7 +592,7 @@ public class ProjectCopyPasteFromRepositoryTest extends AbstractGhidraHeadedInte
viewTreeHelper.getDomainFileActionContext(f1LinkFile);
URL sharedFolderURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
- "Test", "/f1Link", null);
+ "Test", "/f1Link/", null);
DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
assertNotNull("Copy action not found", copyAction);
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectDataTreeTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectDataTreeTest.java
new file mode 100644
index 0000000000..5d3c80c9ed
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectDataTreeTest.java
@@ -0,0 +1,420 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.framework.main.datatree;
+
+import static org.junit.Assert.*;
+
+import java.util.function.BooleanSupplier;
+
+import org.junit.*;
+
+import docking.widgets.tree.GTree;
+import docking.widgets.tree.GTreeNode;
+import ghidra.framework.data.FolderLinkContentHandler;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.DomainFolder;
+import ghidra.program.database.ProgramLinkContentHandler;
+import ghidra.program.model.listing.Program;
+import ghidra.test.*;
+import ghidra.util.Swing;
+import ghidra.util.task.TaskMonitor;
+
+public class ProjectDataTreeTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private FrontEndTestEnv env;
+
+ private DomainFile programAFile;
+
+ private Program program;
+
+ @Before
+ public void setUp() throws Exception {
+ env = new FrontEndTestEnv();
+ program = ToyProgramBuilder.buildSimpleProgram("foo", this);
+
+ DomainFolder rootFolder = env.getRootFolder();
+ programAFile = rootFolder.getFile("Program_A");
+ assertNotNull(programAFile);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (program != null) {
+ program.release(this);
+ }
+ env.dispose();
+ }
+
+ @Test
+ public void testLinkFileUpdate() throws Exception {
+
+ GTree tree = env.getTree();
+ GTreeNode modelRoot = tree.getModelRoot();
+ DomainFolder rootFolder = env.getRootFolder();
+
+ DomainFolder aFolder = rootFolder.createFolder("A");
+
+ // file link created before referenced file
+ aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
+ ProgramLinkContentHandler.INSTANCE);
+
+ rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
+ FolderLinkContentHandler.INSTANCE);
+
+ env.waitForTree();
+
+ Swing.runNow(() -> tree.expandPath(modelRoot.getChild("A")));
+ env.waitForTree();
+
+ Swing.runNow(() -> tree.expandPath(modelRoot.getChild("B")));
+ env.waitForTree();
+
+ // Add file 'x' while folder A and linked-folder B are both expanded
+ aFolder.createFile("x", program, TaskMonitor.DUMMY);
+ program.release(this);
+ program = null;
+
+ env.waitForTree();
+
+ //
+ // Verify good state after everything created
+ //
+ // /A
+ // x
+ // y -> x
+ // /B -> /A (linked-folder)
+ // x
+ // y -> x
+ //
+
+ DomainFolderNode aFolderNode = (DomainFolderNode) modelRoot.getChild("A");
+ DomainFileNode xNode = (DomainFileNode) aFolderNode.getChild("x");
+ assertNotNull(xNode);
+
+ DomainFileNode yNode = (DomainFileNode) aFolderNode.getChild("y");
+ assertNotNull(yNode);
+ waitForRefresh(yNode);
+
+ String tip = yNode.getToolTip();
+ assertFalse(tip.contains("Broken"));
+
+ xNode = (DomainFileNode) aFolderNode.getChild("x");
+ assertNotNull(xNode);
+
+ DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
+ yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
+ assertNotNull(yNode);
+ waitForRefresh(yNode);
+
+ tip = yNode.getToolTip();
+ assertFalse(tip.contains("Broken"));
+
+ // Remove 'x' file and verify broken links are reflected
+
+ xNode = (DomainFileNode) aFolderNode.getChild("x");
+ xNode.getDomainFile().delete();
+
+ env.waitForTree();
+
+ assertNull(aFolderNode.getChild("x"));
+
+ yNode = (DomainFileNode) aFolderNode.getChild("y");
+ assertNotNull(yNode);
+ waitForRefresh(yNode);
+
+ tip = yNode.getToolTip();
+ assertTrue(tip.contains("Broken"));
+
+ xNode = (DomainFileNode) aFolderNode.getChild("x");
+ assertNull(xNode);
+
+ yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
+ assertNotNull(yNode);
+ waitForRefresh(yNode);
+
+ tip = yNode.getToolTip();
+ assertTrue(tip.contains("Broken"));
+
+ }
+
+ @Test
+ public void testLinkFileUpdate1() throws Exception {
+
+ GTree tree = env.getTree();
+ GTreeNode modelRoot = tree.getModelRoot();
+ DomainFolder rootFolder = env.getRootFolder();
+
+ DomainFolder aFolder = rootFolder.createFolder("A");
+
+ // file link created before referenced file
+ aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
+ ProgramLinkContentHandler.INSTANCE);
+
+ env.waitForTree();
+
+ Swing.runNow(() -> tree.expandPath(modelRoot.getChild("A")));
+ env.waitForTree();
+
+ // Add file 'x' before folder A and is expanded and linked-folder B is not
+ aFolder.createFile("x", program, TaskMonitor.DUMMY);
+ program.release(this);
+ program = null;
+
+ env.waitForTree();
+
+ rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
+ FolderLinkContentHandler.INSTANCE);
+ env.waitForTree();
+
+ DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
+ Swing.runNow(() -> tree.expandPath(bFolderLinkNode));
+ env.waitForTree();
+
+ //
+ // Verify good state after everything created
+ //
+ // /A
+ // x
+ // y -> x
+ // /B -> /A (linked-folder)
+ // x
+ // y -> x
+ //
+
+ DomainFolderNode aFolderNode = (DomainFolderNode) modelRoot.getChild("A");
+ DomainFileNode xNode = (DomainFileNode) aFolderNode.getChild("x");
+ assertNotNull(xNode);
+
+ DomainFileNode yNode = (DomainFileNode) aFolderNode.getChild("y");
+ assertNotNull(yNode);
+ waitForRefresh(yNode);
+
+ String tip = yNode.getToolTip();
+ assertFalse(tip.contains("Broken"));
+
+ xNode = (DomainFileNode) aFolderNode.getChild("x");
+ assertNotNull(xNode);
+
+ yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
+ assertNotNull(yNode);
+ waitForRefresh(yNode);
+
+ tip = yNode.getToolTip();
+ assertFalse(tip.contains("Broken"));
+
+ // Remove 'x' file and verify broken links are reflected
+
+ xNode = (DomainFileNode) aFolderNode.getChild("x");
+ assertNotNull(xNode);
+
+ xNode.getDomainFile().delete();
+
+ env.waitForTree();
+
+ assertNull(aFolderNode.getChild("x"));
+
+ yNode = (DomainFileNode) aFolderNode.getChild("y");
+ assertNotNull(yNode);
+ waitForRefresh(yNode);
+
+ tip = yNode.getToolTip();
+ assertTrue(tip.contains("Broken"));
+
+ xNode = (DomainFileNode) aFolderNode.getChild("x");
+ assertNull(xNode);
+
+ yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
+ assertNotNull(yNode);
+ waitForRefresh(yNode);
+
+ tip = yNode.getToolTip();
+ assertTrue(tip.contains("Broken"));
+
+ }
+
+ @Test
+ public void testLinkFileUpdate2() throws Exception {
+
+ GTree tree = env.getTree();
+ GTreeNode modelRoot = tree.getModelRoot();
+ DomainFolder rootFolder = env.getRootFolder();
+
+ DomainFolder aFolder = rootFolder.createFolder("A");
+
+ // file link created before referenced file
+ aFolder.createLinkFile(rootFolder.getProjectData(), "/A/x", true, "y",
+ ProgramLinkContentHandler.INSTANCE);
+
+ rootFolder.createLinkFile(rootFolder.getProjectData(), "/A", false, "B",
+ FolderLinkContentHandler.INSTANCE);
+ env.waitForTree();
+
+ DomainFileNode bFolderLinkNode = (DomainFileNode) modelRoot.getChild("B");
+ Swing.runNow(() -> tree.expandPath(bFolderLinkNode));
+ env.waitForTree();
+
+ // Add file 'x' while linked-folder B is expanded and folder A is not
+ DomainFile xFile = aFolder.createFile("x", program, TaskMonitor.DUMMY);
+ program.release(this);
+ program = null;
+ env.waitForTree();
+
+ //// Verify good state after everything created (leave A collapsed)
+
+ DomainFileNode xNode = (DomainFileNode) bFolderLinkNode.getChild("x");
+ assertNotNull(xNode);
+
+ DomainFileNode yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
+ assertNotNull(yNode);
+ waitForRefresh(yNode);
+
+ String tip = yNode.getToolTip();
+ assertFalse(tip.contains("Broken"));
+
+ //// Remove 'x' file
+
+ xFile.delete();
+
+ env.waitForTree();
+
+ assertNull(bFolderLinkNode.getChild("x"));
+
+ yNode = (DomainFileNode) bFolderLinkNode.getChild("y");
+ assertNotNull(yNode);
+ waitForRefresh(yNode);
+
+ tip = yNode.getToolTip();
+ assertTrue(tip.contains("Broken"));
+
+ }
+
+ @Test
+ public void testLinkFileUpdate3() throws Exception {
+
+ GTree tree = env.getTree();
+ GTreeNode modelRoot = tree.getModelRoot();
+ DomainFolder rootFolder = env.getRootFolder();
+
+ rootFolder.createLinkFile(rootFolder.getProjectData(), "/usr/bin", false, "bin",
+ FolderLinkContentHandler.INSTANCE);
+
+ DomainFolder usrBinFolder = rootFolder.createFolder("usr").createFolder("bin");
+
+ env.waitForTree();
+
+ Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr")));
+ env.waitForTree();
+
+ Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr").getChild("bin")));
+ env.waitForTree();
+
+ Swing.runNow(() -> tree.expandPath(modelRoot.getChild("bin")));
+ env.waitForTree();
+
+ // Add file 'bash'
+ DomainFile bashFile = usrBinFolder.createFile("bash", program, TaskMonitor.DUMMY);
+ program.release(this);
+ program = null;
+ env.waitForTree();
+
+ DomainFileNode binFolderLinkNode = (DomainFileNode) modelRoot.getChild("bin");
+ assertNotNull(binFolderLinkNode.getChild("bash"));
+
+ //
+ // /bin -> /usr/bin (linked folder)
+ // bash
+ // /usr
+ // /bin
+ // bash
+ //
+
+ // Delete real folders and content
+ bashFile.delete();
+ usrBinFolder.delete(); // /usr/bin
+ rootFolder.getFolder("usr").delete();
+
+ env.waitForTree();
+
+ assertNull(binFolderLinkNode.getChild("bash"));
+
+ waitForRefresh(binFolderLinkNode);
+ env.waitForTree();
+
+ String tip = binFolderLinkNode.getToolTip();
+ assertTrue(tip.contains("Broken"));
+
+// binLinkFile.delete();
+ env.waitForTree();
+
+ // Re-create content
+
+ rootFolder.createLinkFile(rootFolder.getProjectData(), "/usr/bin", false, "bin",
+ FolderLinkContentHandler.INSTANCE);
+
+ usrBinFolder = rootFolder.createFolder("usr").createFolder("bin");
+
+ env.waitForTree();
+
+ Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr")));
+ env.waitForTree();
+
+ Swing.runNow(() -> tree.expandPath(modelRoot.getChild("usr").getChild("bin")));
+ env.waitForTree();
+
+ Swing.runNow(() -> tree.expandPath(modelRoot.getChild("bin")));
+ env.waitForTree();
+
+ program = (Program) programAFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ assertNotNull(program);
+ usrBinFolder.createFile("bash", program, TaskMonitor.DUMMY);
+ program.release(this);
+ program = null;
+
+ env.waitForTree();
+
+ DomainFileNode xLinkedFileNode = (DomainFileNode) binFolderLinkNode.getChild("bash");
+ assertNotNull(xLinkedFileNode);
+
+ tip = binFolderLinkNode.getToolTip();
+ assertFalse(tip.contains("Broken"));
+
+ // Repeat removal of folder A and its contents
+ bashFile = usrBinFolder.getFile("bash");
+ assertNotNull(bashFile);
+ bashFile.delete();
+ usrBinFolder.delete();
+ rootFolder.getFolder("usr").delete();
+
+ env.waitForTree();
+
+ assertNull(binFolderLinkNode.getChild("bash"));
+
+ waitForRefresh(binFolderLinkNode);
+ env.waitForTree();
+
+ tip = binFolderLinkNode.getToolTip();
+ assertTrue(tip.contains("Broken"));
+ }
+
+ private void waitForRefresh(DomainFileNode fileNode) {
+ waitFor(new BooleanSupplier() {
+ @Override
+ public boolean getAsBoolean() {
+ return !fileNode.hasPendingRefresh();
+ }
+ });
+ }
+}
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTest.java
index e1b7fe1c02..95fa029c86 100644
--- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTest.java
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTest.java
@@ -32,6 +32,7 @@ import ghidra.program.database.ProgramLinkContentHandler;
import ghidra.program.model.listing.Program;
import ghidra.server.remote.ServerTestUtil;
import ghidra.test.*;
+import ghidra.util.Swing;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.task.TaskMonitor;
@@ -50,13 +51,16 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
/**
/abc/ (folder)
- abc -> /xyz/abc (circular)
+ abc -> /xyz/abc (circular folder allowed as internal)
foo (program file)
/xyz/
abc -> /abc (folder link)
- abc -> (circular)
+ abc -> /xyz/abc (circular folder allowed as internal)
foo
foo -> /abc/foo (program link)
+ /e -> f (circular folder link path)
+ /f -> g (circular folder link path)
+ /g -> e (circular folder link path)
**/
DomainFolder rootFolder = env.getRootFolder();
@@ -72,6 +76,18 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
programFile.copyToAsLink(xyzFolder, false);
+ // Circular folder-link path without real folder
+ rootFolder.createLinkFile(rootFolder.getProjectData(), "/f", true, "e",
+ FolderLinkContentHandler.INSTANCE);
+ rootFolder.createLinkFile(rootFolder.getProjectData(), "/g", true, "f",
+ FolderLinkContentHandler.INSTANCE);
+ rootFolder.createLinkFile(rootFolder.getProjectData(), "/e", true, "g",
+ FolderLinkContentHandler.INSTANCE);
+
+ rootFolder.createLinkFile(rootFolder.getProjectData(),
+ "/home/tsharr2/Examples/linktest/usr/lib64/../lib64", true, "nested2lib64",
+ FolderLinkContentHandler.INSTANCE);
+
env.waitForTree();
}
@@ -257,23 +273,154 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
}
@Test
- public void testBrokenFolderLink() throws Exception {
+ public void testCircularFolderLink1() throws Exception {
//
- // Verify broken folder-link status for /abc/abc which has circular reference
+ // Verify broken folder-link status for /e which has circular reference
+ //
+ DomainFileNode eLinkNode = waitForFileNode("/e");
+ assertTrue(eLinkNode.isFolderLink());
+ String displayName = runSwing(() -> eLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(eLinkNode.getDomainFile(), null));
+ String tooltip = eLinkNode.getToolTip().replace(" ", " ");
+ assertTrue(tooltip.contains("circular"));
+
+ //
+ // Verify broken folder-link status for /g which has circular reference
+ //
+ DomainFileNode gLinkNode = waitForFileNode("/g");
+ assertTrue(gLinkNode.isFolderLink());
+ displayName = runSwing(() -> gLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
+ tooltip = gLinkNode.getToolTip().replace(" ", " ");
+ assertTrue(tooltip.contains("circular"));
+
+ //
+ // Verify broken folder-link status for /f which has circular reference
+ //
+ DomainFileNode fLinkNode = waitForFileNode("/f");
+ assertTrue(fLinkNode.isFolderLink());
+ displayName = runSwing(() -> fLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
+ tooltip = fLinkNode.getToolTip().replace(" ", " ");
+ assertTrue(tooltip.contains("circular"));
+
+ //
+ // Rename folder /e to /ABC causing folder-links to have broken path
+ //
+ Swing.runNow(() -> eLinkNode.setName("ABC"));
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ // Verify /e node not found
+ assertNull(env.getRootNode().getChild("e"));
+
+ //
+ // Verify broken folder-link status for /ABC (final folder /e not found)
+ //
+ DomainFileNode abcLinkNode = waitForFileNode("/ABC");
+ assertTrue(abcLinkNode.isFolderLink());
+ displayName = runSwing(() -> abcLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(abcLinkNode.getDomainFile(), null));
+ tooltip = abcLinkNode.getToolTip().replace(" ", " ");
+ assertTrue(tooltip.contains("folder not found: /e"));
+
+ //
+ // Verify broken folder-link status for /g (final folder /e not found)
+ //
+ waitForFileNode("/g");
+ assertTrue(gLinkNode.isFolderLink());
+ displayName = runSwing(() -> gLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
+ tooltip = gLinkNode.getToolTip().replace(" ", " ");
+ assertTrue(tooltip.contains("folder not found: /e"));
+
+ //
+ // Verify broken folder-link status for /f (final folder /e not found)
+ //
+ waitForFileNode("/f");
+ assertTrue(fLinkNode.isFolderLink());
+ displayName = runSwing(() -> fLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
+ tooltip = fLinkNode.getToolTip().replace(" ", " ");
+ assertTrue(tooltip.contains("folder not found: /e"));
+
+ //
+ // Create folder /e
+ //
+ DomainFolder rootFolder = env.getRootFolder();
+ rootFolder.createFolder("e");
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ //
+ // Verify good folder-link status for /ABC
+ //
+ waitForFileNode("/ABC");
+ assertTrue(abcLinkNode.isFolderLink());
+ displayName = runSwing(() -> abcLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" f"));
+ assertEquals(LinkStatus.INTERNAL,
+ LinkHandler.getLinkFileStatus(abcLinkNode.getDomainFile(), null));
+ tooltip = gLinkNode.getToolTip().replace(" ", " ");
+ assertFalse(tooltip.contains("folder not found"));
+ assertFalse(tooltip.contains("circular"));
+
+ //
+ // Verify good folder-link status for /g
+ //
+ waitForFileNode("/g");
+ assertTrue(gLinkNode.isFolderLink());
+ displayName = runSwing(() -> gLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" e"));
+ assertEquals(LinkStatus.INTERNAL,
+ LinkHandler.getLinkFileStatus(gLinkNode.getDomainFile(), null));
+ tooltip = gLinkNode.getToolTip().replace(" ", " ");
+ assertFalse(tooltip.contains("folder not found"));
+ assertFalse(tooltip.contains("circular"));
+
+ //
+ // Verify good folder-link status for /f
+ //
+ waitForFileNode("/f");
+ assertTrue(fLinkNode.isFolderLink());
+ displayName = runSwing(() -> fLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" g"));
+ assertEquals(LinkStatus.INTERNAL,
+ LinkHandler.getLinkFileStatus(fLinkNode.getDomainFile(), null));
+ tooltip = fLinkNode.getToolTip().replace(" ", " ");
+ assertFalse(tooltip.contains("folder not found"));
+ assertFalse(tooltip.contains("circular"));
+ }
+
+ @Test
+ public void testCircularFolderLink2() throws Exception {
+
+ //
+ // Verify good folder-link internal status for /abc/abc which has allowed circular reference
//
DomainFileNode abcAbcLinkNode = waitForFileNode("/abc/abc");
assertTrue(abcAbcLinkNode.isFolderLink());
String displayName = runSwing(() -> abcAbcLinkNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName,
displayName.endsWith(" /xyz/abc"));
- assertEquals(LinkStatus.BROKEN,
+ assertEquals(LinkStatus.INTERNAL,
LinkHandler.getLinkFileStatus(abcAbcLinkNode.getDomainFile(), null));
- String tooltip = abcAbcLinkNode.getToolTip().replace(" ", " ");
- assertTrue(tooltip.contains("circular"));
//
- // Verify good folder-link internal status for /xyz/abc which has circular reference
+ // Verify good folder-link internal status for /xyz/abc which has allowed circular reference
//
DomainFileNode xyzAbcLinkNode = waitForFileNode("/xyz/abc");
assertTrue(xyzAbcLinkNode.isFolderLink());
@@ -283,17 +430,15 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
LinkHandler.getLinkFileStatus(xyzAbcLinkNode.getDomainFile(), null));
//
- // Verify broken folder-link status for /xyz/abc/abc which has circular reference
+ // Verify good folder-link internal status for /xyz/abc/abc which has allowed circular reference
//
DomainFileNode abcLinkedNode = waitForFileNode("/xyz/abc/abc");
assertTrue(abcLinkedNode.isFolderLink());
displayName = runSwing(() -> abcLinkedNode.getDisplayText());
assertTrue("Unexpected node display name: " + displayName,
displayName.endsWith(" /xyz/abc"));
- assertEquals(LinkStatus.BROKEN,
+ assertEquals(LinkStatus.INTERNAL,
LinkHandler.getLinkFileStatus(abcLinkedNode.getDomainFile(), null));
- tooltip = abcLinkedNode.getToolTip().replace(" ", " ");
- assertTrue(tooltip.contains("circular"));
//
// Rename folder /abc to /ABC causing folder-link /xyz/abc to become broken
@@ -315,7 +460,7 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe
displayName.endsWith(" /xyz/abc"));
assertEquals(LinkStatus.BROKEN,
LinkHandler.getLinkFileStatus(ABCAbcLinkNode.getDomainFile(), null));
- tooltip = ABCAbcLinkNode.getToolTip().replace(" ", " ");
+ String tooltip = ABCAbcLinkNode.getToolTip().replace(" ", " ");
assertTrue(tooltip.contains("folder not found: /abc"));
env.waitForTree(); // give time for ChangeManager to update