GP-4433 Revised opening/viewing project folder/file links

This commit is contained in:
ghidra1 2024-04-19 13:55:00 -04:00
parent 146c177343
commit 38a01c0434
9 changed files with 176 additions and 31 deletions

View file

@ -24,6 +24,7 @@ import ghidra.framework.main.AppInfo;
import ghidra.framework.model.*;
import ghidra.framework.store.FileSystem;
import ghidra.util.InvalidNameException;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -45,8 +46,7 @@ public class FolderLinkContentHandler extends LinkHandler<NullFolderDomainObject
if (!(obj instanceof URLLinkObject)) {
throw new IOException("Unsupported domain object: " + obj.getClass().getName());
}
return createFile((URLLinkObject) obj, FOLDER_LINK_CONTENT_TYPE, fs, path, name,
monitor);
return createFile((URLLinkObject) obj, FOLDER_LINK_CONTENT_TYPE, fs, path, name, monitor);
}
@Override
@ -91,6 +91,11 @@ public class FolderLinkContentHandler extends LinkHandler<NullFolderDomainObject
URL url = getURL(folderLinkFile);
Project activeProject = AppInfo.getActiveProject();
if (activeProject == null) {
Msg.error(FolderLinkContentHandler.class,
"Use of Linked Folders requires active project.");
return null;
}
GhidraFolder parent = ((GhidraFile) folderLinkFile).getParent();
return new LinkedGhidraFolder(activeProject, parent, folderLinkFile.getName(), url);
}

View file

@ -1067,10 +1067,14 @@ public class GhidraFileData {
if (folderItem.isCheckedOut() || versionedFolderItem != null) {
throw new IOException("File already versioned");
}
if (isLinkFile() && !GhidraURL.isServerRepositoryURL(LinkHandler.getURL(folderItem))) {
throw new IOException("Local project link-file may not be versioned");
ContentHandler<?> contentHandler = getContentHandler();
if (contentHandler instanceof LinkHandler linkHandler) {
// must check local vs remote URL
if (!GhidraURL.isServerRepositoryURL(LinkHandler.getURL(folderItem))) {
throw new IOException("Local project link-file may not be versioned");
}
}
if (getContentHandler().isPrivateContentType()) {
else if (contentHandler.isPrivateContentType()) {
throw new IOException("Content may not be versioned: " + getContentType());
}
}

View file

@ -165,8 +165,7 @@ public abstract class LinkHandler<T extends DomainObjectAdapterDB> extends DBCon
@Override
public final boolean isPrivateContentType() {
// NOTE: URL must be checked - only repository-based links may be versioned
return true;
throw new UnsupportedOperationException("Link file requires checking server vs local URL");
}
/**

View file

@ -34,8 +34,7 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
public static Icon FOLDER_LINK_CLOSED_ICON =
new GIcon("icon.content.handler.linked.folder.closed");
public static Icon FOLDER_LINK_OPEN_ICON =
new GIcon("icon.content.handler.linked.folder.open");
public static Icon FOLDER_LINK_OPEN_ICON = new GIcon("icon.content.handler.linked.folder.open");
private final Project activeProject;
private final DomainFolder localParent;
@ -73,8 +72,8 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
}
/**
* Get the Ghidra URL associated with this linked folder's project or repository
* @return Ghidra URL associated with this linked folder's project or repository
* Get the Ghidra URL of the project/repository folder referenced by this object
* @return Ghidra URL of the project/repository folder referenced by this object
*/
public URL getProjectURL() {
if (projectUrl == null) {
@ -83,6 +82,7 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
return projectUrl;
}
@Override
LinkedGhidraFolder getLinkedRootFolder() {
return this;
}
@ -108,12 +108,12 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
@Override
public ProjectLocator getProjectLocator() {
return activeProject.getProjectLocator();
return localParent.getProjectLocator();
}
@Override
public ProjectData getProjectData() {
return activeProject.getProjectData();
return localParent.getProjectData();
}
@Override

View file

@ -28,6 +28,10 @@ import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* {@code LinkedGhidraSubFolder} corresponds to a {@link DomainFolder} contained within a
* {@link LinkedGhidraFolder} or another {@code LinkedGhidraSubFolder}.
*/
class LinkedGhidraSubFolder implements LinkedDomainFolder {
private final LinkedGhidraFolder linkedRootFolder;

View file

@ -15,15 +15,15 @@
*/
package ghidra.framework.main;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Set;
import ghidra.framework.data.FolderLinkContentHandler;
import ghidra.framework.main.datatree.ProjectDataTreePanel;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURLQueryTask;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.task.TaskMonitor;
@ -36,33 +36,87 @@ public class AcceptUrlContentTask extends GhidraURLQueryTask {
this.plugin = plugin;
}
private boolean isSameLocalProject(ProjectLocator projectLoc1, ProjectLocator projectLoc2) {
if (projectLoc1.isTransient() || projectLoc2.isTransient()) {
return false;
}
if (!projectLoc1.getName().equals(projectLoc2.getName())) {
return false;
}
try {
File proj1Dir = projectLoc1.getProjectDir().getCanonicalFile();
File proj2Dir = projectLoc2.getProjectDir().getCanonicalFile();
return proj1Dir.equals(proj2Dir);
}
catch (IOException e) {
return false;
}
}
@Override
public void processResult(DomainFile domainFile, URL url, TaskMonitor monitor)
throws IOException {
Swing.runNow(() -> {
Project activeProject = AppInfo.getActiveProject();
if (activeProject == null) {
Msg.showError(this, null, "Ghidra Error",
"Unable to accept URL without active project open");
return;
}
Swing.runNow(() -> {
if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE
.equals(domainFile.getContentType())) {
plugin.showLinkedFolder(domainFile);
return;
// Simply select folder link-file within project - do not follow - let user do that.
if (isSameLocalProject(activeProject.getProjectLocator(),
domainFile.getProjectLocator())) {
// Select file within active project
DomainFile df =
activeProject.getProjectData().getFile(domainFile.getPathname());
if (df == null) {
return; // unexpected race condition
}
plugin.selectFiles(Set.of(df));
}
else {
// Select file within read-only viewed project
plugin.showInViewedProject(url, false);
}
}
else {
AppInfo.getFrontEndTool().getToolServices().launchDefaultToolWithURL(url);
}
AppInfo.getFrontEndTool().getToolServices().launchDefaultToolWithURL(url);
});
}
@Override
public void processResult(DomainFolder domainFolder, URL url, TaskMonitor monitor)
throws IOException {
ProjectDataPanel projectDataPanel = plugin.getProjectDataPanel();
Project activeProject = AppInfo.getActiveProject();
if (activeProject == null) {
Msg.showError(this, null, "Ghidra Error",
"Unable to accept URL without active project open");
return;
}
Swing.runNow(() -> {
ProjectDataTreePanel dtp = projectDataPanel.openView(GhidraURL.getProjectURL(url));
if (dtp == null) {
return;
if (isSameLocalProject(activeProject.getProjectLocator(),
domainFolder.getProjectLocator())) {
// Select folder within active project
DomainFolder df =
activeProject.getProjectData().getFolder(domainFolder.getPathname());
if (df == null) {
return; // unexpected race condition
}
plugin.selectFolder(df);
}
else {
// Select folder within read-only viewed project
plugin.showInViewedProject(url, true);
}
dtp.selectDomainFolder(domainFolder);
});
}
}

View file

@ -51,6 +51,7 @@ import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.preferences.Preferences;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.remote.User;
import ghidra.util.*;
import ghidra.util.filechooser.GhidraFileChooserModel;
@ -539,8 +540,22 @@ public class FrontEndPlugin extends Plugin
SwingUtilities.invokeLater(() -> {
// there was a delete bug; make the set unmodifiable to catch this earlier
Set<DomainFile> unmodifiableFiles = Collections.unmodifiableSet(files);
if (dataTablePanel.isCapacityExceeded()) {
projectDataPanel.showTree();
}
else {
dataTablePanel.setSelectedDomainFiles(unmodifiableFiles);
}
dataTreePanel.selectDomainFiles(unmodifiableFiles);
dataTablePanel.setSelectedDomainFiles(unmodifiableFiles);
});
}
void selectFolder(final DomainFolder folder) {
// Do this later in case any of the given files are newly created, which means that the
// GUIs may have not yet been notified.
SwingUtilities.invokeLater(() -> {
projectDataPanel.showTree();
dataTreePanel.selectDomainFolder(folder);
});
}
@ -1081,7 +1096,7 @@ public class FrontEndPlugin extends Plugin
public void openDomainFile(DomainFile domainFile) {
if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(domainFile.getContentType())) {
showLinkedFolder(domainFile);
showLinkedFolderInViewedProject(domainFile);
return;
}
@ -1109,11 +1124,11 @@ public class FrontEndPlugin extends Plugin
"opens this type of file");
}
void showLinkedFolder(DomainFile domainFile) {
private void showLinkedFolderInViewedProject(DomainFile domainFile) {
try {
LinkedGhidraFolder linkedFolder =
FolderLinkContentHandler.getReadOnlyLinkedFolder(domainFile);
if (linkedFolder == null) {
return; // unsupported use
}
@ -1123,7 +1138,12 @@ public class FrontEndPlugin extends Plugin
return;
}
DomainFolder domainFolder = linkedFolder.getLinkedFolder();
// Do not hang onto domainFile, linkedFolder or their underlying project data
ProjectData viewedProjectData = dtp.getProjectData();
DomainFolder domainFolder =
viewedProjectData.getFolder(linkedFolder.getLinkedPathname());
if (domainFolder != null) {
// delayed to ensure tree is displayed
Swing.runLater(() -> dtp.selectDomainFolder(domainFolder));
@ -1133,6 +1153,53 @@ public class FrontEndPlugin extends Plugin
Msg.showError(this, projectDataPanel, "Linked-folder failure: " + domainFile.getName(),
e);
}
}
void showInViewedProject(URL ghidraURL, boolean isFolder) {
ProjectDataTreePanel dtp = projectDataPanel.openView(GhidraURL.getProjectURL(ghidraURL));
if (dtp == null) {
return;
}
Swing.runLater(() -> {
// delayed to ensure tree is displayed
ProjectData viewedProjectData = dtp.getProjectData();
String path = GhidraURL.getProjectPathname(ghidraURL);
if (isFolder) {
DomainFolder viewedProjectFolder = getViewProjectFolder(viewedProjectData, path);
if (viewedProjectFolder != null) {
dtp.selectDomainFolder(viewedProjectFolder);
}
}
else {
DomainFile viewedProjectFile = getViewProjectFile(viewedProjectData, path);
if (viewedProjectFile != null) {
dtp.selectDomainFile(viewedProjectFile);
}
}
});
}
private DomainFile getViewProjectFile(ProjectData viewedProjectData, String path) {
if (path == null || path.endsWith(DomainFolder.SEPARATOR)) {
return null;
}
return viewedProjectData.getFile(path);
}
private DomainFolder getViewProjectFolder(ProjectData viewedProjectData, String path) {
if (path == null || path.equals(DomainFolder.SEPARATOR)) {
return viewedProjectData.getRootFolder();
}
if (path.endsWith(DomainFolder.SEPARATOR)) {
path = path.substring(0, path.length() - 1); // remove trailing separator
}
return viewedProjectData.getFolder(path);
}
private class MyToolChestChangeListener implements ToolChestChangeListener {

View file

@ -406,6 +406,10 @@ class ProjectDataPanel extends JSplitPane implements ProjectViewListener {
}
}
void showTree() {
projectTab.setSelectedIndex(0);
}
private void showTable() {
projectTab.setSelectedIndex(1);
}

View file

@ -103,6 +103,14 @@ public class ProjectDataTablePanel extends JPanel {
new ProjectDataTableDnDHandler(gTable, model);
}
/**
* Determine if table capacity has been exceeded and files are not shown
* @return true if files are not shown in project data table, else false
*/
public boolean isCapacityExceeded() {
return capacityExceeded;
}
public void dispose() {
table.dispose(); // this will dispose the gTable as well
}