Merge remote-tracking branch 'origin/GP-5830_ghidra1_ProjectDataTableUpdates--SQUASHED'

This commit is contained in:
Ryan Kurtz 2025-07-17 11:07:15 -04:00
commit 1a1cdefc14
8 changed files with 362 additions and 27 deletions

View file

@ -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));

View file

@ -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<String, String> 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<String, String> 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<String> linkErrors = null;

View file

@ -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<DomainFileType>, DisplayString
return result;
}
public String getContentType() {
return contentType;
}
public Icon getIcon() {
return icon;
}

View file

@ -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<DomainFileInfo, ProjectData> {
private ProjectData projectData;
private volatile int modCount;
private boolean editingOn;
private boolean loadWasCancelled;
@ -50,6 +51,7 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
protected void doLoad(Accumulator<DomainFileInfo> 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<DomainFileInfo, Pr
DomainFile[] files = folder.getFiles();
for (DomainFile domainFile : files) {
monitor.checkCancelled();
accumulator.add(new DomainFileInfo(domainFile));
accumulator.add(new DomainFileInfo(domainFile, this));
}
DomainFolder[] folders = folder.getFolders();
for (DomainFolder domainFolder : folders) {
@ -113,13 +115,16 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
@Override
public void refresh() {
List<DomainFileInfo> 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<DomainFileInfo, Pr
//==================================================================================================
private class DomainFileTypeColumn
extends AbstractDynamicTableColumn<DomainFileInfo, DomainFileType, ProjectData> {
extends AbstractDynamicTableColumn<DomainFileInfo, DomainFileType, ProjectData> {
@Override
public String getColumnName() {
@ -182,7 +187,7 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
}
private class DomainFileNameColumn
extends AbstractDynamicTableColumn<DomainFileInfo, String, ProjectData> {
extends AbstractDynamicTableColumn<DomainFileInfo, String, ProjectData> {
@Override
public String getColumnName() {
@ -203,7 +208,7 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
}
private class ModificationDateColumn
extends AbstractDynamicTableColumn<DomainFileInfo, Date, ProjectData> {
extends AbstractDynamicTableColumn<DomainFileInfo, Date, ProjectData> {
@Override
public String getColumnName() {
@ -224,7 +229,7 @@ public class ProjectDataTableModel extends ThreadedTableModel<DomainFileInfo, Pr
}
private class DomainFilePathColumn
extends AbstractDynamicTableColumn<DomainFileInfo, String, ProjectData> {
extends AbstractDynamicTableColumn<DomainFileInfo, String, ProjectData> {
@Override
public String getColumnName() {

View file

@ -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

View file

@ -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
}
}

View file

@ -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);
}

View file

@ -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;
}
}