diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java index 5eb3f06970..5ea27dbdd4 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java @@ -71,6 +71,7 @@ class ProjectDataPanel extends JSplitPane implements ProjectViewListener { readOnlyViews = new HashMap<>(TYPICAL_NUM_VIEWS); projectTab = new JTabbedPane(SwingConstants.BOTTOM); + projectTab.setName("PROJECT_TABBED_PANE"); projectTab.setBorder(BorderFactory.createTitledBorder(BORDER_PREFIX)); projectTab.addChangeListener(e -> frontEndPlugin.getTool().contextChanged(null)); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java index 16bc739fa5..c1813cad42 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java @@ -28,6 +28,8 @@ import ghidra.framework.model.DomainFile; public class DomainFileInfo { private DomainFile domainFile; + private ProjectDataTableModel model; + private int modCount; private String name; private String path; private Map metadata; @@ -36,9 +38,9 @@ public class DomainFileInfo { private Boolean isBrokenLink; private String toolTipText; - public DomainFileInfo(DomainFile domainFile) { + DomainFileInfo(DomainFile domainFile, ProjectDataTableModel model) { this.domainFile = domainFile; - this.path = domainFile.getParent().getPathname(); + this.model = model; } private String computeName() { @@ -74,6 +76,7 @@ public class DomainFileInfo { } public synchronized String getDisplayName() { + checkModelModCount(); if (name == null) { name = computeName(); } @@ -81,6 +84,7 @@ public class DomainFileInfo { } public synchronized String getPath() { + checkModelModCount(); if (path == null) { path = domainFile.getParent().getPathname(); } @@ -88,6 +92,7 @@ public class DomainFileInfo { } public synchronized DomainFileType getDomainFileType() { + checkModelModCount(); if (domainFileType == null) { checkStatus(); String contentType = domainFile.getContentType(); @@ -102,7 +107,7 @@ public class DomainFileInfo { } public synchronized Date getModificationDate() { - + checkModelModCount(); if (modificationDate == null) { modificationDate = getLastModifiedTime(); } @@ -118,6 +123,7 @@ public class DomainFileInfo { } private synchronized Map getMetadata() { + checkModelModCount(); if (metadata == null) { metadata = domainFile.getMetadata(); if (metadata == null) { @@ -154,7 +160,16 @@ public class DomainFileInfo { return domainFile.getName(); } - private void checkStatus() { + private void checkModelModCount() { + int modelModCount = model.getModCount(); + if (modelModCount != modCount) { + refresh(); + modCount = modelModCount; + } + } + + private synchronized void checkStatus() { + checkModelModCount(); if (isBrokenLink == null) { isBrokenLink = false; List linkErrors = null; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileType.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileType.java index 8fe12aa333..6f2c66a189 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileType.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileType.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * 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. @@ -40,10 +39,6 @@ public class DomainFileType implements Comparable, DisplayString return result; } - public String getContentType() { - return contentType; - } - public Icon getIcon() { return icon; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTableModel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTableModel.java index ca077f4d8a..7e59f43e8e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTableModel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTableModel.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. @@ -34,6 +34,7 @@ import ghidra.util.task.TaskMonitor; public class ProjectDataTableModel extends ThreadedTableModel { private ProjectData projectData; + private volatile int modCount; private boolean editingOn; private boolean loadWasCancelled; @@ -50,6 +51,7 @@ public class ProjectDataTableModel extends ThreadedTableModel accumulator, TaskMonitor monitor) throws CancelledException { loadWasCancelled = false; + ++modCount; if (projectData != null) { loadWasCancelled = true; DomainFolder rootFolder = projectData.getRootFolder(); @@ -63,7 +65,7 @@ public class ProjectDataTableModel extends ThreadedTableModel modelData = getModelData(); - for (DomainFileInfo domainFileInfo : modelData) { - domainFileInfo.refresh(); - } + // The modCount allows DomainFileInfo to determine if its cached data is stale relative + // to this model + ++modCount; super.refresh(); } + int getModCount() { + return modCount; + } + public void setProjectData(ProjectData projectData) { this.projectData = projectData; reload(); @@ -162,7 +167,7 @@ public class ProjectDataTableModel extends ThreadedTableModel { + extends AbstractDynamicTableColumn { @Override public String getColumnName() { @@ -182,7 +187,7 @@ public class ProjectDataTableModel extends ThreadedTableModel { + extends AbstractDynamicTableColumn { @Override public String getColumnName() { @@ -203,7 +208,7 @@ public class ProjectDataTableModel extends ThreadedTableModel { + extends AbstractDynamicTableColumn { @Override public String getColumnName() { @@ -224,7 +229,7 @@ public class ProjectDataTableModel extends ThreadedTableModel { + extends AbstractDynamicTableColumn { @Override public String getColumnName() { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java index f3ca6b5efe..08968a0e11 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java @@ -373,7 +373,8 @@ public class ProjectDataTablePanel extends JPanel { } checkCapacity(); if (!capacityExceeded) { - model.addObject(new DomainFileInfo(file)); + model.addObject(new DomainFileInfo(file, model)); + model.refresh(); } } @@ -382,7 +383,7 @@ public class ProjectDataTablePanel extends JPanel { if (ignoreChanges()) { return; } - model.refresh(); + reload(); } @Override @@ -399,6 +400,7 @@ public class ProjectDataTablePanel extends JPanel { break; } } + model.refresh(); } @Override diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTableTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTableTest.java new file mode 100644 index 0000000000..76d0cfa78c --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTableTest.java @@ -0,0 +1,239 @@ +/* ### + * 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 org.junit.*; + +import ghidra.framework.data.FolderLinkContentHandler; +import ghidra.framework.data.LinkHandler.LinkStatus; +import ghidra.framework.main.datatable.DomainFileInfo; +import ghidra.framework.main.datatable.DomainFileType; +import ghidra.framework.model.*; +import ghidra.program.database.ProgramLinkContentHandler; +import ghidra.program.model.listing.Program; +import ghidra.test.*; +import ghidra.util.task.TaskMonitor; + +public class ProjectLinkFileStatusTableTest extends AbstractGhidraHeadedIntegrationTest { + + private FrontEndTestEnv env; + private FrontEndDataTableHelper tableHelper; + + private DomainFolder rootFolder; + private DomainFolder abcFolder; + private DomainFolder xyzFolder; + + @Before + public void setUp() throws Exception { + env = new FrontEndTestEnv(); + + /** + /abc/ (folder) + abc -> /xyz/abc (circular) + bar (program file) + /xyz/ (folder) + abc -> /abc (folder link) + abc -> (circular) + bar (program within linked-folder should not appear in table) + **/ + + rootFolder = env.getRootFolder(); + + abcFolder = rootFolder.createFolder("abc"); + xyzFolder = rootFolder.createFolder("xyz"); + DomainFile abcLinkFile = abcFolder.copyToAsLink(xyzFolder, false); + abcLinkFile.copyToAsLink(abcFolder, false); + + Program p = ToyProgramBuilder.buildSimpleProgram("bar", this); + abcFolder.createFile("bar", p, TaskMonitor.DUMMY); + p.release(this); + + tableHelper = new FrontEndDataTableHelper(env.getFrontEndTool()); + tableHelper.showTablePanel(); + tableHelper.waitForTable(); + } + + @After + public void tearDown() throws Exception { + env.dispose(); + } + + @Test + public void testFileWithinLinkedFolder() throws Exception { + + // + // Check program file + // + + DomainFileInfo fileInfo = tableHelper.getDomainFileInfoByPath("/abc/bar"); + assertNotNull(fileInfo); + + DomainFile df = fileInfo.getDomainFile(); + LinkFileInfo linkInfo = df.getLinkInfo(); + assertNull(linkInfo); + + DomainFileType domainFileType = fileInfo.getDomainFileType(); + assertEquals("Program", domainFileType.getDisplayString()); + assertTrue("Unexpected tooltip: " + fileInfo.getToolTip(), + fileInfo.getToolTip().startsWith("Last Modified")); + + // + // Verify program file reflected within linked-folder is not shown in table + // + + fileInfo = tableHelper.getDomainFileInfoByPath("/xyz/bar"); + assertNull(fileInfo); + + } + + @Test + public void testFileLink() throws Exception { + + // + // Create program link without referenced program in place + // + + xyzFolder.createLinkFile(rootFolder.getProjectData(), "/foo", false, "foo", + ProgramLinkContentHandler.INSTANCE); + + tableHelper.waitForTable(); + + // + // Check initial state of broken program link + // + + DomainFileInfo fileInfo = tableHelper.getDomainFileInfoByPath("/xyz/foo"); + assertNotNull(fileInfo); + + DomainFile df = fileInfo.getDomainFile(); + LinkFileInfo linkInfo = df.getLinkInfo(); + assertNotNull(linkInfo); + + LinkStatus linkStatus = linkInfo.getLinkStatus(null); + assertEquals(LinkStatus.BROKEN, linkStatus); + + DomainFileType domainFileType = fileInfo.getDomainFileType(); + assertEquals("ProgramLink", domainFileType.getDisplayString()); + assertTrue("Unexpected tooltip: " + fileInfo.getToolTip(), + fileInfo.getToolTip().startsWith("Broken ProgramLink - file not found")); + + // + // Add program file which should repair broken program link + // + + Program p = ToyProgramBuilder.buildSimpleProgram("foo", this); + DomainFile programFile = rootFolder.createFile("foo", p, TaskMonitor.DUMMY); + p.release(this); + + tableHelper.waitForTable(); + + // + // Check for new program file + // + + fileInfo = tableHelper.getDomainFileInfoByPath("/foo"); + assertNotNull(fileInfo); + + df = fileInfo.getDomainFile(); + linkInfo = df.getLinkInfo(); + assertNull(linkInfo); + + domainFileType = fileInfo.getDomainFileType(); + assertEquals("Program", domainFileType.getDisplayString()); + assertTrue("Unexpected tooltip: " + fileInfo.getToolTip(), + fileInfo.getToolTip().startsWith("Last Modified")); // no error + + // + // Check for repaired program link + // + + fileInfo = tableHelper.getDomainFileInfoByPath("/xyz/foo"); + assertNotNull(fileInfo); + + df = fileInfo.getDomainFile(); + linkInfo = df.getLinkInfo(); + assertNotNull(linkInfo); + + linkStatus = linkInfo.getLinkStatus(null); + assertEquals(LinkStatus.INTERNAL, linkStatus); + + domainFileType = fileInfo.getDomainFileType(); + assertEquals("ProgramLink", domainFileType.getDisplayString()); + assertTrue("Unexpected tooltip: " + fileInfo.getToolTip(), + fileInfo.getToolTip().startsWith("Last Modified")); // no error + + } + + @Test + public void testFolderLink() throws Exception { + + // Create folder link without referenced folder in place + xyzFolder.createLinkFile(rootFolder.getProjectData(), "/aaa", false, "aaa", + FolderLinkContentHandler.INSTANCE); + + tableHelper.waitForTable(); + + // + // Check initial state of broken folder link + // + + DomainFileInfo fileInfo = tableHelper.getDomainFileInfoByPath("/xyz/aaa"); + assertNotNull(fileInfo); + + DomainFile df = fileInfo.getDomainFile(); + LinkFileInfo linkInfo = df.getLinkInfo(); + assertNotNull(linkInfo); + + LinkStatus linkStatus = linkInfo.getLinkStatus(null); + assertEquals(LinkStatus.BROKEN, linkStatus); + + DomainFileType domainFileType = fileInfo.getDomainFileType(); + assertEquals("FolderLink", domainFileType.getDisplayString()); + assertTrue("Unexpected tooltip: " + fileInfo.getToolTip(), + fileInfo.getToolTip().startsWith("Broken FolderLink - folder not found")); + + // + // Add folder file which should repair broken folder link + // + + rootFolder.createFolder("aaa"); + + tableHelper.waitForTable(); + + // + // Check for repaired folder link + // + + fileInfo = tableHelper.getDomainFileInfoByPath("/xyz/aaa"); + assertNotNull(fileInfo); + + df = fileInfo.getDomainFile(); + linkInfo = df.getLinkInfo(); + assertNotNull(linkInfo); + + linkStatus = linkInfo.getLinkStatus(null); + assertEquals(LinkStatus.INTERNAL, linkStatus); + + domainFileType = fileInfo.getDomainFileType(); + assertEquals("FolderLink", domainFileType.getDisplayString()); + assertTrue("Unexpected tooltip: " + fileInfo.getToolTip(), + fileInfo.getToolTip().startsWith("Last Modified")); // no error + + } + +} 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 722997904f..e1b7fe1c02 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 @@ -77,9 +77,7 @@ public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTe @After public void tearDown() throws Exception { - if (env != null) { - env.dispose(); - } + env.dispose(); ClientUtil.clearRepositoryAdapter("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT); } diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndDataTableHelper.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndDataTableHelper.java new file mode 100644 index 0000000000..8591052ad0 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndDataTableHelper.java @@ -0,0 +1,80 @@ +/* ### + * 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.test; + +import static org.junit.Assert.*; + +import javax.swing.JTabbedPane; + +import docking.test.AbstractDockingTest; +import docking.widgets.table.GTable; +import generic.test.AbstractGuiTest; +import ghidra.framework.main.FrontEndTool; +import ghidra.framework.main.datatable.DomainFileInfo; +import ghidra.framework.main.datatable.ProjectDataTableModel; +import ghidra.framework.main.datatree.DataTree; +import ghidra.util.Swing; + +/** + * This class provides some convenience methods for interacting with a {@link DataTree}. + */ +public class FrontEndDataTableHelper { + + private FrontEndTool frontEndTool; + private GTable table; + private ProjectDataTableModel model; + + public FrontEndDataTableHelper(FrontEndTool frontEndTool) { + this.frontEndTool = frontEndTool; + table = AbstractGuiTest.findComponent(frontEndTool.getToolFrame(), GTable.class); + model = (ProjectDataTableModel) table.getModel(); + } + + public void showTablePanel() { + + JTabbedPane projectTabbedPane = (JTabbedPane) AbstractGuiTest + .findComponentByName(frontEndTool.getToolFrame(), "PROJECT_TABBED_PANE"); + assertNotNull("Project Data tabbed pane not found", projectTabbedPane); + + Swing.runNow(() -> { + for (int i = 0; i < projectTabbedPane.getTabCount(); i++) { + if (projectTabbedPane.getTitleAt(i).equals("Table View")) { + projectTabbedPane.setSelectedIndex(i); + break; + } + } + }); + } + + public void waitForTable() { + AbstractDockingTest.waitForTableModel(model); + } + + public GTable getTable() { + return table; + } + + public DomainFileInfo getDomainFileInfoByPath(String path) { + int rowCount = model.getRowCount(); + for (int row = 0; row < rowCount; ++row) { + DomainFileInfo fileInfo = model.getRowObject(row); + if (path.equals(fileInfo.getDomainFile().getPathname())) { + return fileInfo; + } + } + return null; + } +}