GP-5908 Corrected project tree change listener, domain folder events and various data tree actions to resolve issues when link-files are used.

This commit is contained in:
ghidra1
2025-09-12 15:28:28 -04:00
parent e90c852353
commit 8d31fa97bb
38 changed files with 1988 additions and 515 deletions
@@ -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);
@@ -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();
}
});
}
}
@@ -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