ref = new AtomicReference<>();
AbstractGenericTest.runSwing(() -> {
- boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
- AbstractDockingTest.setErrorGUIEnabled(false); // disable the error GUI while launching the tool
- FrontEndTool frontEndToolInstance = getFrontEndTool();
-
- Project project = frontEndToolInstance.getProject();
- ToolServices toolServices = project.getToolServices();
- PluginTool newTool = toolServices.launchTool(toolName, null);
- if (newTool == null) {
- // couldn't find the tool in the workspace...check the test area
- newTool = launchDefaultToolByName(toolName);
- }
-
+ PluginTool newTool = doLaunchTool(toolName);
ref.set(newTool);
-
- AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
- newTool.acceptDomainFiles(new DomainFile[] { domainFile });
+ if (newTool != null) {
+ newTool.acceptDomainFiles(new DomainFile[] { domainFile });
+ }
});
PluginTool launchedTool = ref.get();
@@ -906,6 +897,52 @@ public class TestEnv {
return launchedTool;
}
+ /**
+ * Launches a tool of the given name using the given Ghidra URL.
+ *
+ * Note: the tool returned will have auto save disabled by default.
+ *
+ * @param toolName the name of the tool to launch
+ * @param ghidraUrl The Ghidra URL to be opened in tool (see {@link GhidraURL})
+ * @return the tool that is launched
+ */
+ public PluginTool launchToolWithURL(String toolName, URL ghidraUrl) {
+ AtomicReference ref = new AtomicReference<>();
+
+ AbstractGenericTest.runSwing(() -> {
+ PluginTool newTool = doLaunchTool(toolName);
+ ref.set(newTool);
+ if (newTool != null) {
+ newTool.accept(ghidraUrl);
+ }
+ });
+
+ PluginTool launchedTool = ref.get();
+ if (launchedTool == null) {
+ throw new NullPointerException("Unable to launch the tool: " + toolName);
+ }
+
+ // this will make sure that our tool is closed during disposal
+ extraTools.add(launchedTool);
+ return launchedTool;
+ }
+
+ private PluginTool doLaunchTool(String toolName) {
+ boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
+ AbstractDockingTest.setErrorGUIEnabled(false); // disable the error GUI while launching the tool
+ FrontEndTool frontEndToolInstance = getFrontEndTool();
+
+ Project project = frontEndToolInstance.getProject();
+ ToolServices toolServices = project.getToolServices();
+ PluginTool newTool = toolServices.launchTool(toolName, null);
+ if (newTool == null) {
+ // couldn't find the tool in the workspace...check the test area
+ newTool = launchDefaultToolByName(toolName);
+ }
+ AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
+ return newTool;
+ }
+
/**
* Sets the auto-save feature for all tool instances running under the {@link FrontEndTool}
* created by this TestEnv instance. Auto-save is off by default when testing.
diff --git a/Ghidra/Features/Base/src/main/resources/images/link.png b/Ghidra/Features/Base/src/main/resources/images/link.png
deleted file mode 100644
index 1c654a0d61..0000000000
Binary files a/Ghidra/Features/Base/src/main/resources/images/link.png and /dev/null differ
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java
index 8bb7972354..b1d488d6bc 100644
--- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java
@@ -19,6 +19,8 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.awt.*;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -37,6 +39,8 @@ import ghidra.framework.model.*;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.framework.plugintool.TestDummyServiceProvider;
import ghidra.framework.project.ProjectDataService;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection;
+import ghidra.framework.store.FileSystem;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.database.ProgramDB;
import ghidra.program.model.address.Address;
@@ -620,6 +624,60 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
assertTrue(spyNavigatable.navigatedTo(otherProgramPath, address));
}
+ @Test
+ public void testGhidraLocalUrlAnnotation_Program_WithAddress() {
+
+ SpyNavigatable spyNavigatable = new SpyNavigatable();
+ SpyServiceProvider spyServiceProvider = new SpyServiceProvider();
+
+ String addresstring = "1001000";
+
+ String pathname = "/a/b/prog";
+ String url = "ghidra:/folder/project?" + pathname + "#" + addresstring;
+ String annotationText = "{@url \"" + url + "\"}";
+ String rawComment = "My comment - " + annotationText;
+ AttributedString prototype = prototype();
+ FieldElement element =
+ CommentUtils.parseTextForAnnotations(rawComment, program, prototype, 0);
+
+ String displayString = element.getText();
+ assertEquals("My comment - " + url, displayString);
+
+ AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element);
+ click(spyNavigatable, spyServiceProvider, annotatedElement);
+
+ assertTrue(spyServiceProvider.programOpened(pathname));
+
+ // Navigation performed by ProgramManager not tested due to use of spyServiceProvider
+ }
+
+ @Test
+ public void testGhidraServerUrlAnnotation_Program_WithAddress() {
+
+ SpyNavigatable spyNavigatable = new SpyNavigatable();
+ SpyServiceProvider spyServiceProvider = new SpyServiceProvider();
+
+ String addresstring = "1001000";
+
+ String pathname = "/a/b/prog";
+ String url = "ghidra://server/repo" + pathname + "#" + addresstring;
+ String annotationText = "{@url \"" + url + "\"}";
+ String rawComment = "My comment - " + annotationText;
+ AttributedString prototype = prototype();
+ FieldElement element =
+ CommentUtils.parseTextForAnnotations(rawComment, program, prototype, 0);
+
+ String displayString = element.getText();
+ assertEquals("My comment - " + url, displayString);
+
+ AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element);
+ click(spyNavigatable, spyServiceProvider, annotatedElement);
+
+ assertTrue(spyServiceProvider.programOpened(pathname));
+
+ // Navigation performed by ProgramManager not tested due to use of spyServiceProvider
+ }
+
@Test
public void testUnknownAnnotation() {
String rawComment = "This is a symbol {@syyyybol bob} annotation";
@@ -903,13 +961,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
private Set openedPrograms = new HashSet<>();
private Set closedPrograms = new HashSet<>();
- @Override
- public Program openProgram(DomainFile domainFile, int version, int state) {
- String name = domainFile.getName();
- String pathname = domainFile.getPathname();
-
- openedPrograms.add(name);
-
+ private Program generateProgram(String pathname, String name) {
try {
ProgramBuilder builder = new ProgramBuilder();
builder.setName(pathname);
@@ -923,6 +975,39 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
}
}
+ @Override
+ public Program openProgram(URL ghidraURL, int state) {
+ try {
+ GhidraURLConnection c = new GhidraURLConnection(ghidraURL);
+ String folderpath = c.getFolderPath();
+ String name = c.getFolderItemName();
+ String pathname = folderpath;
+ if (!pathname.endsWith(FileSystem.SEPARATOR)) {
+ pathname += FileSystem.SEPARATOR;
+ }
+ pathname += name;
+ openedPrograms.add(name);
+
+ Program p = generateProgram(pathname, name);
+
+ // NOTE: URL ref navigation not performed
+
+ return p;
+ }
+ catch (MalformedURLException e) {
+ failWithException("Bad URL", e);
+ }
+ return null;
+ }
+
+ @Override
+ public Program openProgram(DomainFile domainFile, int version, int state) {
+ String name = domainFile.getName();
+ String pathname = domainFile.getPathname();
+ openedPrograms.add(name);
+ return generateProgram(pathname, name);
+ }
+
@Override
public boolean closeProgram(Program p, boolean ignoreChanges) {
String name = FilenameUtils.getName(p.getName());
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/ProjectFileManagerTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/ProjectFileManagerTest.java
index 4a1a59efac..6512d66de2 100644
--- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/ProjectFileManagerTest.java
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/ProjectFileManagerTest.java
@@ -121,12 +121,7 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest
// If there are queued actions, then we have to kick the handling thread and
// let it finish running.
- try {
- assertTrue(eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS));
- }
- catch (InterruptedException e) {
- failWithException("Interrupted waiting for filesystem events", e);
- }
+ assertTrue(eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS));
}
private void deleteAll(File file) {
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java
index d0a768d832..19201b68cc 100644
--- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java
@@ -421,15 +421,10 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
assertNotNull(dialog);
}
- private DomainFileFilter createStartsWithFilter(String startsWith) {
- return (df) -> df.getName().startsWith(startsWith);
- }
-
- private void showFiltered(String startsWith) {
+ private void showFiltered(final String startsWith) {
SwingUtilities.invokeLater(() -> {
dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog",
- DataTreeDialog.OPEN, createStartsWithFilter(startsWith));
-
+ DataTreeDialog.OPEN, f -> f.getName().startsWith(startsWith));
dialog.showComponent();
});
waitForSwing();
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/AddViewToProjectTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/AddViewToProjectTest.java
index 35e42f2751..0b4cdd321a 100644
--- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/AddViewToProjectTest.java
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/AddViewToProjectTest.java
@@ -79,10 +79,10 @@ public class AddViewToProjectTest extends AbstractGhidraHeadlessIntegrationTest
try {
URL view = GhidraURL.makeURL(DIRECTORY_NAME, PROJECT_VIEW1);
- project.addProjectView(view);
+ project.addProjectView(view, true);
// add another view that will be removed to test the remove
- project.addProjectView(GhidraURL.makeURL(DIRECTORY_NAME, PROJECT_VIEW2));
+ project.addProjectView(GhidraURL.makeURL(DIRECTORY_NAME, PROJECT_VIEW2), true);
// validate the view was added to project
ProjectLocator[] projViews = project.getProjectViews();
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/CreateDomainObjectTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/CreateDomainObjectTest.java
index 8cf67aa389..e13c9446f8 100644
--- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/CreateDomainObjectTest.java
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/CreateDomainObjectTest.java
@@ -117,7 +117,7 @@ public class CreateDomainObjectTest extends AbstractGhidraHeadedIntegrationTest
project.close();
Project project2 = ProjectTestUtils.getProject(testDir, PROJECT_NAME2);
try {
- project2.addProjectView(GhidraURL.makeURL(testDir, PROJECT_NAME1));
+ project2.addProjectView(GhidraURL.makeURL(testDir, PROJECT_NAME1), true);
}
catch (Exception e) {
Assert.fail("View Not found");
diff --git a/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeRepository.java b/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeRepository.java
index 594df2de14..15d421ea3e 100644
--- a/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeRepository.java
+++ b/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeRepository.java
@@ -15,15 +15,18 @@
*/
package ghidra.base.project;
+import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
-import generic.test.TestUtils;
+import generic.test.AbstractGTest;
import ghidra.framework.model.DomainFile;
import ghidra.framework.remote.User;
+import ghidra.framework.store.local.IndexedV1LocalFileSystem;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.test.TestEnv;
+import utilities.util.FileUtilities;
/**
* This class represents the idea of a shared Ghidra repository. This class is meant to be
@@ -54,11 +57,34 @@ public class FakeRepository {
private Map usersByName = new HashMap<>();
private Map projectsByUser = new HashMap<>();
+ private File versionedFSDir;
private LocalFileSystem versionedFileSystem;
- public FakeRepository() {
+ public FakeRepository() throws IOException {
// validation must be enabled if both environments are utilized by a test
LocalFileSystem.setValidationRequired();
+
+ versionedFSDir =
+ new File(AbstractGTest.getTestDirectoryPath() + File.separator + "TestRepo.rep");
+ if (versionedFSDir.exists()) {
+ FileUtilities.deleteDir(versionedFSDir);
+ }
+ if (versionedFSDir.exists() || !FileUtilities.createDir(versionedFSDir)) {
+ throw new IOException("Failed to create clean repo dir: " + versionedFSDir);
+ }
+ versionedFileSystem = new MyVersionedFileSystem(versionedFSDir.getPath());
+ }
+
+ private static class MyVersionedFileSystem extends IndexedV1LocalFileSystem {
+ MyVersionedFileSystem(String rootPath) throws IOException {
+ super(rootPath, true, false, true, true);
+ }
+
+ @Override
+ public boolean isShared() {
+ // Enables use of asyncronous event dispatching thread
+ return true;
+ }
}
/**
@@ -109,11 +135,6 @@ public class FakeRepository {
FakeSharedProject project = new FakeSharedProject(this, user);
projectsByUser.put(user, project);
-
- if (versionedFileSystem == null) {
- versionedFileSystem = project.getVersionedFileSystem();
- TestUtils.setInstanceField("isShared", versionedFileSystem, Boolean.TRUE);
- }
return project;
}
@@ -139,6 +160,8 @@ public class FakeRepository {
*/
public void dispose() {
projectsByUser.values().forEach(p -> disposeProject(p));
+ versionedFileSystem.dispose();
+ FileUtilities.deleteDir(versionedFSDir);
}
private void disposeProject(FakeSharedProject p) {
diff --git a/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeSharedProject.java b/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeSharedProject.java
index 2378fc9db0..075b87a862 100644
--- a/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeSharedProject.java
+++ b/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeSharedProject.java
@@ -25,7 +25,7 @@ import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
-import generic.test.AbstractGenericTest;
+import generic.test.AbstractGTest;
import generic.test.TestUtils;
import ghidra.framework.data.*;
import ghidra.framework.model.*;
@@ -39,6 +39,7 @@ import ghidra.test.TestProgramManager;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import junit.framework.AssertionFailedError;
+import utilities.util.FileUtilities;
/**
* This class represents the idea of a shared Ghidra project. Each project is associated with
@@ -61,21 +62,18 @@ public class FakeSharedProject {
public FakeSharedProject(FakeRepository repo, User user) throws IOException {
this.repo = repo;
- String projectDirPath = AbstractGenericTest.getTestDirectoryPath();
+ String projectDirPath = AbstractGTest.getTestDirectoryPath();
gProject =
GhidraProject.createProject(projectDirPath, "TestProject_" + user.getName(), true);
gProject.setDeleteOnClose(true);
- LocalFileSystem fs = repo.getSharedFileSystem();
- if (fs != null) {
- // first project will keeps its versioned file system
- setVersionedFileSystem(fs);
- }
+ // use local shared fake repo versioned file system
+ setVersionedFileSystem(repo.getSharedFileSystem());
}
FakeSharedProject(User user) throws IOException {
- String projectDirPath = AbstractGenericTest.getTestDirectoryPath();
+ String projectDirPath = AbstractGTest.getTestDirectoryPath();
gProject =
GhidraProject.createProject(projectDirPath, "TestProject_" + user.getName(), true);
}
@@ -101,7 +99,7 @@ public class FakeSharedProject {
* @return the project file manager
*/
public ProjectFileManager getProjectFileManager() {
- return (ProjectFileManager) gProject.getProject().getProjectData();
+ return (ProjectFileManager) gProject.getProjectData();
}
/**
@@ -369,8 +367,11 @@ public class FakeSharedProject {
* @see FakeRepository#dispose()
*/
public void dispose() {
+ ProjectLocator projectLocator = getProjectFileManager().getProjectLocator();
programManager.disposeOpenPrograms();
gProject.close();
+ FileUtilities.deleteDir(projectLocator.getProjectDir());
+ projectLocator.getMarkerFile().delete();
}
@Override
@@ -400,12 +401,7 @@ public class FakeSharedProject {
(FileSystemEventManager) TestUtils.getInstanceField("eventManager",
versionedFileSystem);
- try {
- eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
- }
- catch (InterruptedException e) {
- failWithException("Interrupted waiting for filesystem events", e);
- }
+ eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
}
private DomainFolder getFolder(String path) throws Exception {
diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/CollectFailedRelocations.java b/Ghidra/Features/FunctionID/ghidra_scripts/CollectFailedRelocations.java
index da0da488a8..c72b79c3fc 100644
--- a/Ghidra/Features/FunctionID/ghidra_scripts/CollectFailedRelocations.java
+++ b/Ghidra/Features/FunctionID/ghidra_scripts/CollectFailedRelocations.java
@@ -99,6 +99,10 @@ public class CollectFailedRelocations extends GhidraScript {
if (monitor.isCancelled()) {
return;
}
+ // Do not follow folder-links or consider program links. Using content type
+ // to filter is best way to control this. If program links should be considered
+ // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
+ // should be used.
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile);
}
diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/CreateMultipleLibraries.java b/Ghidra/Features/FunctionID/ghidra_scripts/CreateMultipleLibraries.java
index 40fd19d123..9c3b8f3bfa 100644
--- a/Ghidra/Features/FunctionID/ghidra_scripts/CreateMultipleLibraries.java
+++ b/Ghidra/Features/FunctionID/ghidra_scripts/CreateMultipleLibraries.java
@@ -258,6 +258,10 @@ public class CreateMultipleLibraries extends GhidraScript {
DomainFile[] files = myFolder.getFiles();
for (DomainFile domainFile : files) {
monitor.checkCanceled();
+ // Do not follow folder-links or consider program links. Using content type
+ // to filter is best way to control this. If program links should be considered
+ // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
+ // should be used.
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile);
}
diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/FidStatistics.java b/Ghidra/Features/FunctionID/ghidra_scripts/FidStatistics.java
index 95b13d34d6..e1c6e5b688 100644
--- a/Ghidra/Features/FunctionID/ghidra_scripts/FidStatistics.java
+++ b/Ghidra/Features/FunctionID/ghidra_scripts/FidStatistics.java
@@ -372,6 +372,10 @@ public class FidStatistics extends GhidraScript {
DomainFile[] files = folder.getFiles();
for (DomainFile domainFile : files) {
monitor.checkCanceled();
+ // Do not follow folder-links or consider program links. Using content type
+ // to filter is best way to control this. If program links should be considered
+ // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
+ // should be used.
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile);
}
diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/FindErrors.java b/Ghidra/Features/FunctionID/ghidra_scripts/FindErrors.java
index 10a739e013..0f5e597a11 100644
--- a/Ghidra/Features/FunctionID/ghidra_scripts/FindErrors.java
+++ b/Ghidra/Features/FunctionID/ghidra_scripts/FindErrors.java
@@ -1,6 +1,5 @@
/* ###
* 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.
@@ -17,6 +16,9 @@
//Opens all programs under a chosen domain folder, grabs their error count,
//then sorts in increasing error order and prints them
//@category FunctionID
+import java.io.IOException;
+import java.util.*;
+
import generic.stl.Pair;
import ghidra.app.script.GhidraScript;
import ghidra.framework.model.DomainFile;
@@ -27,9 +29,6 @@ import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
-import java.io.IOException;
-import java.util.*;
-
public class FindErrors extends GhidraScript {
@Override
@@ -82,6 +81,10 @@ public class FindErrors extends GhidraScript {
if (monitor.isCancelled()) {
return;
}
+ // Do not follow folder-links or consider program links. Using content type
+ // to filter is best way to control this. If program links should be considered
+ // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
+ // should be used.
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile);
}
diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/FindFunctionByHash.java b/Ghidra/Features/FunctionID/ghidra_scripts/FindFunctionByHash.java
index 890204b81a..f9e7f0eb4c 100644
--- a/Ghidra/Features/FunctionID/ghidra_scripts/FindFunctionByHash.java
+++ b/Ghidra/Features/FunctionID/ghidra_scripts/FindFunctionByHash.java
@@ -100,6 +100,10 @@ public class FindFunctionByHash extends GhidraScript {
if (monitor.isCancelled()) {
return;
}
+ // Do not follow folder-links or consider program links. Using content type
+ // to filter is best way to control this. If program links should be considered
+ // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
+ // should be used.
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile);
}
diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/FindNamedFunction.java b/Ghidra/Features/FunctionID/ghidra_scripts/FindNamedFunction.java
index fb5ad98b2f..81acfa0be0 100644
--- a/Ghidra/Features/FunctionID/ghidra_scripts/FindNamedFunction.java
+++ b/Ghidra/Features/FunctionID/ghidra_scripts/FindNamedFunction.java
@@ -16,6 +16,9 @@
//Opens all programs under a chosen domain folder, scans them for functions
//that match a user supplied name, and prints info about the match.
//@category FunctionID
+import java.io.IOException;
+import java.util.ArrayList;
+
import ghidra.app.script.GhidraScript;
import ghidra.feature.fid.service.FidService;
import ghidra.framework.model.DomainFile;
@@ -26,9 +29,6 @@ import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
-import java.io.IOException;
-import java.util.ArrayList;
-
public class FindNamedFunction extends GhidraScript {
FidService service;
@@ -85,6 +85,10 @@ public class FindNamedFunction extends GhidraScript {
if (monitor.isCancelled()) {
return;
}
+ // Do not follow folder-links or consider program links. Using content type
+ // to filter is best way to control this. If program links should be considered
+ // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
+ // should be used.
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile);
}
diff --git a/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java b/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java
index 3b9bd1cd04..ae3e50ba69 100644
--- a/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java
+++ b/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java
@@ -153,6 +153,10 @@ public class IngestTask extends Task {
for (DomainFile domainFile : files) {
monitor.checkCanceled();
monitor.incrementProgress(1);
+ // Do not follow folder-links or consider program links. Using content type
+ // to filter is best way to control this. If program links should be considered
+ // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
+ // should be used.
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile);
}
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java
index 8c1278cb7b..3dc956ef27 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java
@@ -21,6 +21,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import db.*;
import ghidra.app.util.task.OpenProgramTask;
+import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator;
import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator;
import ghidra.feature.vt.api.impl.*;
@@ -309,7 +310,8 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession, VTC
TaskLauncher.launch(openTask);
- return openTask.getOpenProgram();
+ OpenProgramRequest openProgram = openTask.getOpenProgram();
+ return openProgram != null ? openProgram.getProgram() : null;
}
@Override
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java
index ef12f97735..4c8abe14f0 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java
@@ -24,7 +24,8 @@ import db.OpenMode;
import db.buffers.BufferFile;
import generic.theme.GIcon;
import ghidra.feature.vt.api.db.VTSessionDB;
-import ghidra.framework.data.*;
+import ghidra.framework.data.DBContentHandler;
+import ghidra.framework.data.DomainObjectMergeManager;
import ghidra.framework.model.ChangeSet;
import ghidra.framework.model.DomainObject;
import ghidra.framework.store.*;
@@ -34,7 +35,8 @@ import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor;
-public class VTSessionContentHandler extends DBContentHandler {
+public class VTSessionContentHandler extends DBContentHandler {
+
private static Icon ICON = new GIcon("icon.version.tracking.session.content.type");
public final static String CONTENT_TYPE = "VersionTracking";
@@ -49,7 +51,6 @@ public class VTSessionContentHandler extends DBContentHandler {
"Unsupported domain object: " + domainObject.getClass().getName());
}
return createFile((VTSessionDB) domainObject, CONTENT_TYPE, fs, path, name, monitor);
-
}
@Override
@@ -74,7 +75,7 @@ public class VTSessionContentHandler extends DBContentHandler {
}
@Override
- public DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
+ public VTSessionDB getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
throws IOException, CancelledException, VersionException {
@@ -119,7 +120,7 @@ public class VTSessionContentHandler extends DBContentHandler {
}
@Override
- public Class extends DomainObject> getDomainObjectClass() {
+ public Class getDomainObjectClass() {
return VTSessionDB.class;
}
@@ -129,7 +130,7 @@ public class VTSessionContentHandler extends DBContentHandler {
}
@Override
- public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version,
+ public VTSessionDB getImmutableObject(FolderItem item, Object consumer, int version,
int minChangeVersion, TaskMonitor monitor)
throws IOException, CancelledException, VersionException {
@@ -149,7 +150,7 @@ public class VTSessionContentHandler extends DBContentHandler {
}
@Override
- public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
+ public VTSessionDB getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java
index 4e176a6ee9..fe4ab3b24c 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java
@@ -15,6 +15,10 @@
*/
package ghidra.feature.vt.gui.actions;
+import docking.ActionContext;
+import docking.action.DockingAction;
+import docking.action.MenuData;
+import docking.tool.ToolConstants;
import ghidra.feature.vt.api.main.VTSession;
import ghidra.feature.vt.gui.plugin.VTController;
import ghidra.feature.vt.gui.plugin.VTPlugin;
@@ -23,10 +27,6 @@ import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFileFilter;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HelpLocation;
-import docking.ActionContext;
-import docking.action.DockingAction;
-import docking.action.MenuData;
-import docking.tool.ToolConstants;
public class OpenVersionTrackingSessionAction extends DockingAction {
@@ -60,5 +60,10 @@ public class OpenVersionTrackingSessionAction extends DockingAction {
Class> c = f.getDomainObjectClass();
return VTSession.class.isAssignableFrom(c);
}
+
+ @Override
+ public boolean followLinkedFolders() {
+ return false;
+ }
}
}
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java
index a0b69e6f8c..f93f0f527a 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java
@@ -29,6 +29,7 @@ import docking.widgets.label.GDLabel;
import docking.wizard.*;
import generic.theme.*;
import ghidra.app.util.task.OpenProgramTask;
+import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.framework.main.DataTreeDialog;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
@@ -383,8 +384,8 @@ public class NewSessionPanel extends AbstractMageJPanel {
OpenProgramTask openProgramTask = new OpenProgramTask(programInfo.getFile(), tool);
new TaskLauncher(openProgramTask, tool.getActiveWindow());
- Program program = openProgramTask.getOpenProgram();
- programInfo.setProgram(program);
+ OpenProgramRequest openProgram = openProgramTask.getOpenProgram();
+ programInfo.setProgram(openProgram != null ? openProgram.getProgram() : null);
}
@Override
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTWizardUtils.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTWizardUtils.java
index e338d08f75..d462548dd8 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTWizardUtils.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTWizardUtils.java
@@ -20,12 +20,12 @@ import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import docking.widgets.OptionDialog;
-import ghidra.feature.vt.api.impl.VTSessionContentHandler;
+import ghidra.feature.vt.api.main.VTSession;
import ghidra.feature.vt.gui.task.SaveTask;
import ghidra.framework.main.DataTreeDialog;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFileFilter;
-import ghidra.program.database.ProgramDB;
+import ghidra.program.model.listing.Program;
import ghidra.util.HTMLUtilities;
import ghidra.util.task.TaskLauncher;
@@ -36,23 +36,20 @@ public class VTWizardUtils {
}
public static final DomainFileFilter VT_SESSION_FILTER = new DomainFileFilter() {
+
@Override
public boolean accept(DomainFile df) {
- if (VTSessionContentHandler.CONTENT_TYPE.equals(df.getContentType())) {
- return true;
- }
+ return VTSession.class.isAssignableFrom(df.getDomainObjectClass());
+ }
+
+ @Override
+ public boolean followLinkedFolders() {
return false;
}
};
- public static final DomainFileFilter PROGRAM_FILTER = new DomainFileFilter() {
- @Override
- public boolean accept(DomainFile df) {
- if (ProgramDB.CONTENT_TYPE.equals(df.getContentType())) {
- return true;
- }
- return false;
- }
+ public static final DomainFileFilter PROGRAM_FILTER = f -> {
+ return Program.class.isAssignableFrom(f.getDomainObjectClass());
};
static DomainFile chooseDomainFile(Component parent, String domainIdentifier,
diff --git a/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java b/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java
index a544eb9025..a9f4633155 100644
--- a/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java
+++ b/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java
@@ -16,8 +16,7 @@
package db.buffers;
import java.io.IOException;
-import java.rmi.NoSuchObjectException;
-import java.rmi.Remote;
+import java.rmi.*;
import java.util.NoSuchElementException;
import ghidra.util.Msg;
@@ -104,8 +103,8 @@ public class BufferFileAdapter implements BufferFile {
bufferFileHandle.dispose();
}
catch (IOException e) {
- // handle may have already been disposed
- if (!(e instanceof NoSuchObjectException)) {
+ // handle may have already been disposed or disconnected
+ if (!(e instanceof NoSuchObjectException) && !(e instanceof ConnectException)) {
Msg.error(this, e);
}
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java
index 33e742c1ac..9e4b071559 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java
@@ -16,6 +16,7 @@
package docking.widgets.tree;
import java.util.*;
+import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.swing.Icon;
@@ -197,6 +198,22 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable filter) {
+ for (GTreeNode node : children()) {
+ if (name.equals(node.getName()) && filter.test(node)) {
+ return node;
+ }
+ }
+ return null;
+ }
+
/**
* Returns the child node at the given index. Returns null if the index is out of bounds.
*
@@ -488,6 +505,15 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable= MAX) {
return;
@@ -57,6 +67,9 @@ public class GTreeExpandAllTask extends GTreeTask {
if (parent.isLeaf()) {
return;
}
+ if (!force && !parent.isAutoExpandPermitted()) {
+ return;
+ }
monitor.checkCanceled();
List allChildren = parent.getChildren();
if (allChildren.size() == 0) {
@@ -68,9 +81,9 @@ public class GTreeExpandAllTask extends GTreeTask {
}
for (GTreeNode child : allChildren) {
monitor.checkCanceled();
- expandNode(child, monitor);
+ expandNode(child, false, monitor);
}
- monitor.incrementProgress(1);
+ monitor.incrementProgress(1); // TODO: total node count is unknown
}
private void expandPath(final TreePath treePath, final TaskMonitor monitor) {
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java
index ef42c7f013..7e3d9fccfe 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java
@@ -158,8 +158,10 @@ public class RepositoryAdapter implements RemoteAdapterListener {
/**
* Attempt to connect to the server.
+ * @throws RepositoryNotFoundException if named repository does not exist
+ * @throws IOException if IO error occurs
*/
- public void connect() throws IOException {
+ public void connect() throws RepositoryNotFoundException, IOException {
synchronized (serverAdapter) {
if (repository != null) {
try {
@@ -180,7 +182,7 @@ public class RepositoryAdapter implements RemoteAdapterListener {
unexpectedDisconnect = false;
if (repository == null) {
noSuchRepository = true;
- throw new IOException("Repository '" + name + "': not found");
+ throw new RepositoryNotFoundException("Repository '" + name + "': not found");
}
Msg.info(this, "Connected to repository '" + name + "'");
changeDispatcher.start();
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryNotFoundException.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryNotFoundException.java
new file mode 100644
index 0000000000..3b7903e8cc
--- /dev/null
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryNotFoundException.java
@@ -0,0 +1,31 @@
+/* ###
+ * 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.client;
+
+import java.io.IOException;
+
+/**
+ * {@code RepositoryNotFoundException} thrown when a failed connection occurs to a
+ * non-existing repository. A valid server connection is required to make this
+ * determination.
+ */
+public class RepositoryNotFoundException extends IOException {
+
+ public RepositoryNotFoundException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java
index a2532eceae..ff21415bab 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java
@@ -52,6 +52,9 @@ public class RepositoryServerAdapter {
// Keeps track of whether the connection attempt was cancelled by the user
private boolean connectCancelled = false;
+ // Keep track of last connect error
+ private Throwable lastConnectError;
+
private WeakSet listenerList =
WeakDataStructureFactory.createCopyOnWriteWeakSet();
@@ -105,6 +108,14 @@ public class RepositoryServerAdapter {
return connectCancelled;
}
+ /**
+ * Returns the last error associated with a failed connection attempt.
+ * @return last connect error or null
+ */
+ public Throwable getLastConnectError() {
+ return lastConnectError;
+ }
+
/**
* Notify listeners of a connection state change.
*/
@@ -135,7 +146,7 @@ public class RepositoryServerAdapter {
}
}
- Throwable cause = null;
+ lastConnectError = null;
try {
try {
serverHandle = ClientUtil.connect(server);
@@ -162,22 +173,22 @@ public class RepositoryServerAdapter {
catch (LoginException e) {
Msg.showError(this, null, "Server Error",
"Server access denied (" + serverInfoStr + ").");
- cause = e;
+ lastConnectError = e;
}
catch (GeneralSecurityException e) {
Msg.showError(this, null, "Server Error",
"Server access denied (" + serverInfoStr + "): " + e.getMessage());
- cause = e;
+ lastConnectError = e;
}
catch (SocketTimeoutException | java.net.ConnectException | java.rmi.ConnectException e) {
Msg.showError(this, null, "Server Error",
"Connection to server failed (" + server + ").");
- cause = e;
+ lastConnectError = e;
}
catch (java.net.UnknownHostException | java.rmi.UnknownHostException e) {
Msg.showError(this, null, "Server Error",
"Server Not Found (" + server.getServerName() + ").");
- cause = e;
+ lastConnectError = e;
}
catch (RemoteException e) {
String msg = e.getMessage();
@@ -185,7 +196,7 @@ public class RepositoryServerAdapter {
while ((t = t.getCause()) != null) {
String err = t.getMessage();
msg = err != null ? err : t.toString();
- cause = t;
+ lastConnectError = t;
}
Msg.showError(this, null, "Server Error",
"An error occurred on the server (" + serverInfoStr + ").\n" + msg, e);
@@ -200,7 +211,7 @@ public class RepositoryServerAdapter {
"An error occurred while connecting to the server (" + serverInfoStr + ").\n" + msg,
e);
}
- throw new NotConnectedException("Not connected to repository server", cause);
+ throw new NotConnectedException("Not connected to repository server", lastConnectError);
}
private void checkPasswordExpiration() {
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java
index c24dfbd022..f317923ae2 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java
@@ -32,7 +32,7 @@ public interface RepositoryHandle {
// TODO: NOTE! Debugging client or sever garbage collection delays could
// cause handle to be disposed prematurely.
- public final static int CLIENT_CHECK_PERIOD = SystemUtilities.isInTestingMode() ? 1000 : 30000;
+ public final static int CLIENT_CHECK_PERIOD = SystemUtilities.isInTestingMode() ? 2000 : 30000;
/**
* Returns the name of this repository.
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystemEventManager.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystemEventManager.java
index 6df9e92f98..9026b62212 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystemEventManager.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystemEventManager.java
@@ -22,33 +22,60 @@ import java.util.concurrent.*;
* FileSystemListenerList maintains a list of FileSystemListener's.
* This class, acting as a FileSystemListener, simply relays each callback to
* all FileSystemListener's within its list. Employs either a synchronous
- * and asynchronous notification mechanism.
+ * and asynchronous notification mechanism. Once disposed event dispatching will
+ * discontinue.
*/
public class FileSystemEventManager implements FileSystemListener {
+ private static enum ThreadState {
+ STOPPED, RUNNING, DISPOSED
+ }
+
private List listeners = new CopyOnWriteArrayList<>();
private BlockingQueue eventQueue = new LinkedBlockingQueue<>();
- private volatile boolean disposed = false;
+ private final boolean asyncDispatchEnabled;
+
+ private volatile ThreadState state = ThreadState.STOPPED;
private Thread thread;
/**
* Constructor
* @param enableAsynchronousDispatching if true a separate dispatch thread will be used
- * to notify listeners. If false, blocking notification will be performed.
+ * to notify listeners. If false, blocking notification will be performed. Events are
+ * immediately discarded in the absence of any listener(s).
*/
public FileSystemEventManager(boolean enableAsynchronousDispatching) {
+ asyncDispatchEnabled = enableAsynchronousDispatching;
+ }
- if (enableAsynchronousDispatching) {
- thread = new FileSystemEventProcessingThread();
- thread.start();
+ /**
+ * Return true if asynchornous event processing is enabled.
+ * @return true if asynchornous event processing is enabled, else false
+ */
+ public boolean isAsynchronous() {
+ return asyncDispatchEnabled;
+ }
+
+ /**
+ * Discontinue event dispatching and terminate dispatch thread if it exists.
+ */
+ public synchronized void dispose() {
+ state = ThreadState.DISPOSED;
+ if (asyncDispatchEnabled) {
+ if (thread != null && thread.isAlive()) {
+ thread.interrupt();
+ }
+ eventQueue.clear();
}
}
- public void dispose() {
- disposed = true;
- if (thread != null) {
- thread.interrupt();
+ private synchronized void startDispatchThread() {
+ if (asyncDispatchEnabled && state == ThreadState.STOPPED) {
+ // only starts when first listener is added
+ state = ThreadState.RUNNING;
+ thread = new FileSystemEventProcessingThread();
+ thread.start();
}
}
@@ -57,6 +84,7 @@ public class FileSystemEventManager implements FileSystemListener {
* @param listener the listener
*/
public void add(FileSystemListener listener) {
+ startDispatchThread(); // if asyncDispatchEnabled
listeners.add(listener);
}
@@ -116,30 +144,34 @@ public class FileSystemEventManager implements FileSystemListener {
@Override
public void syncronize() {
// Note: synchronize calls will only work when using a threaded event queue
- if (isAsynchronous()) {
- add(new SynchronizeEvent());
+ if (asyncDispatchEnabled) {
+ queueEvent(new SynchronizeEvent());
}
}
- private boolean isAsynchronous() {
- return thread != null;
- }
-
- private void add(FileSystemEvent ev) {
- if (!listeners.isEmpty()) {
- eventQueue.add(ev);
+ /**
+ * Queue specified event if listener thread is running
+ * @param ev filesystm event
+ * @return true if queued, else false if listener thread not running
+ */
+ private boolean queueEvent(FileSystemEvent ev) {
+ if (state == ThreadState.RUNNING) {
+ return eventQueue.add(ev);
}
+ return false;
}
private void handleEvent(FileSystemEvent e) {
- if (disposed) {
+ if (state == ThreadState.DISPOSED) {
return;
}
- if (isAsynchronous()) {
- add(e);
+ if (asyncDispatchEnabled) {
+ // if there are no listeners event will be discarded (i.e., listener thread not running)
+ queueEvent(e);
}
else {
+ // process in a synchronous fashion in current thread
e.process(listeners);
}
}
@@ -154,17 +186,26 @@ public class FileSystemEventManager implements FileSystemListener {
*
* @param timeout the maximum time to wait
* @param unit the time unit of the {@code time} argument
- * @return true if the events were processed in the given timeout
- * @throws InterruptedException if this waiting thread is interrupted
+ * @return true if the events were processed in the given timeout. A false value will be
+ * returned if either a timeout occured
*/
- public boolean flushEvents(long timeout, TimeUnit unit) throws InterruptedException {
- if (!isAsynchronous()) {
+ public boolean flushEvents(long timeout, TimeUnit unit) {
+ if (!asyncDispatchEnabled) {
return true; // each thread processes its own event
}
MarkerEvent event = new MarkerEvent();
- eventQueue.add(event);
- return event.waitForEvent(timeout, unit);
+ if (!queueEvent(event)) {
+ // events are not queuing since there are no listeners or dispose has occured
+ return true;
+ }
+ try {
+ return event.waitForEvent(timeout, unit);
+ }
+ catch (InterruptedException e) {
+ // ignore - listener thread stopped or disposed
+ return true;
+ }
}
//==================================================================================================
@@ -180,18 +221,14 @@ public class FileSystemEventManager implements FileSystemListener {
@Override
public void run() {
- while (!disposed) {
-
+ while (state == ThreadState.RUNNING) {
FileSystemEvent event;
try {
event = eventQueue.take();
event.process(listeners);
}
catch (InterruptedException e) {
- // interrupt has been cleared; if other threads rely on this interrupted state,
- // then mark the thread as interrupted again by calling:
- // Thread.currentThread().interrupt();
- // For now, this code relies on the 'alive' flag to know when to terminate
+ // ignore - interrupt has been cleared
}
}
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java
index a1dd2ffaf7..0222a5011c 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java
@@ -916,6 +916,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
@Override
public int getItemCount() throws IOException {
+ checkDisposed();
if (readOnly) {
refreshReadOnlyIndex();
}
@@ -930,11 +931,9 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
return count;
}
- /*
- * @see ghidra.framework.store.FileSystem#getFolders(java.lang.String)
- */
@Override
public synchronized String[] getFolderNames(String folderPath) throws IOException {
+ checkDisposed();
if (readOnly) {
refreshReadOnlyIndex();
}
@@ -948,13 +947,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
}
}
- /*
- * @see ghidra.framework.store.FileSystem#createFolder(java.lang.String, java.lang.String)
- */
@Override
public synchronized void createFolder(String parentPath, String folderName)
throws InvalidNameException, IOException {
+ checkDisposed();
+
if (readOnly) {
throw new ReadOnlyException();
}
@@ -987,12 +985,11 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
eventManager.folderCreated(parentPath, getName(path));
}
- /*
- * @see ghidra.framework.store.FileSystem#deleteFolder(java.lang.String)
- */
@Override
public synchronized void deleteFolder(String folderPath) throws IOException {
+ checkDisposed();
+
if (readOnly) {
throw new ReadOnlyException();
}
@@ -1059,13 +1056,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
getListener().itemCreated(destFolderPath, itemName);
}
- /*
- * @see ghidra.framework.store.FileSystem#moveItem(java.lang.String, java.lang.String, java.lang.String)
- */
@Override
public synchronized void moveItem(String folderPath, String name, String newFolderPath,
String newName) throws IOException, InvalidNameException {
+ checkDisposed();
+
if (readOnly) {
throw new ReadOnlyException();
}
@@ -1140,13 +1136,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
deleteEmptyVersionedFolders(folderPath);
}
- /*
- * @see ghidra.framework.store.FileSystem#moveFolder(java.lang.String, java.lang.String, java.lang.String)
- */
@Override
public synchronized void moveFolder(String parentPath, String folderName, String newParentPath)
throws InvalidNameException, IOException {
+ checkDisposed();
+
if (readOnly) {
throw new ReadOnlyException();
}
@@ -1206,13 +1201,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
}
}
- /*
- * @see ghidra.framework.store.FileSystem#renameFolder(java.lang.String, java.lang.String, java.lang.String)
- */
@Override
public synchronized void renameFolder(String parentPath, String folderName,
String newFolderName) throws InvalidNameException, IOException {
+ checkDisposed();
+
if (readOnly) {
throw new ReadOnlyException();
}
@@ -1247,16 +1241,14 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
eventManager.folderRenamed(parentPath, folderName, newFolderName);
}
- /*
- * @see ghidra.framework.store.FileSystem#folderExists(java.lang.String)
- */
@Override
public synchronized boolean folderExists(String folderPath) {
try {
+ checkDisposed();
getFolder(folderPath, GetFolderOption.READ_ONLY);
return true;
}
- catch (NotFoundException e) {
+ catch (IOException | NotFoundException e) {
return false;
}
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java
index 41bd6d3213..6c8cc9a542 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java
@@ -46,7 +46,7 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem {
* @throws FileNotFoundException if specified rootPath does not exist
* @throws IOException if error occurs while reading/writing index files
*/
- IndexedV1LocalFileSystem(String rootPath, boolean isVersioned, boolean readOnly,
+ protected IndexedV1LocalFileSystem(String rootPath, boolean isVersioned, boolean readOnly,
boolean enableAsyncronousDispatching, boolean create) throws IOException {
super(rootPath, isVersioned, readOnly, enableAsyncronousDispatching, create);
}
@@ -134,6 +134,7 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem {
@Override
public FolderItem getItem(String fileID) throws IOException, UnsupportedOperationException {
+ checkDisposed();
if (fileIdMap == null) {
return null;
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java
index b569e2b940..c014e43ac8 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java
@@ -71,6 +71,8 @@ public abstract class LocalFileSystem implements FileSystem {
private static boolean refreshRequired = false;
+ private boolean disposed = false;
+
protected final File root;
protected final boolean isVersioned;
protected final boolean readOnly;
@@ -78,9 +80,6 @@ public abstract class LocalFileSystem implements FileSystem {
private RepositoryLogger repositoryLogger;
- // Always false in production; can be manipulated by tests
- private boolean isShared;
-
/**
* Construct a local filesystem for existing data
* @param rootPath
@@ -285,25 +284,16 @@ public abstract class LocalFileSystem implements FileSystem {
return refreshRequired;
}
- /*
- * @see ghidra.framework.store.FileSystem#isVersioned()
- */
@Override
public boolean isVersioned() {
return isVersioned;
}
- /*
- * @see ghidra.framework.store.FileSystem#isOnline()
- */
@Override
public boolean isOnline() {
- return true;
+ return !disposed;
}
- /*
- * @see ghidra.framework.store.FileSystem#isReadOnly()
- */
@Override
public boolean isReadOnly() {
return readOnly;
@@ -388,9 +378,6 @@ public abstract class LocalFileSystem implements FileSystem {
return getItemNames(folderPath, false);
}
- /*
- * @see ghidra.framework.store.FileSystem#getItem(java.lang.String, java.lang.String)
- */
@Override
public synchronized LocalFolderItem getItem(String folderPath, String name) throws IOException {
try {
@@ -424,9 +411,6 @@ public abstract class LocalFileSystem implements FileSystem {
throw new UnsupportedOperationException("getItem by File-ID");
}
- /*
- * @see ghidra.framework.store.FileSystem#createDatabase(java.lang.String, java.lang.String, java.lang.String, db.buffers.BufferFile, java.lang.String, java.lang.String, boolean, ghidra.util.task.TaskMonitor, java.lang.String)
- */
@Override
public synchronized LocalDatabaseItem createDatabase(String parentPath, String name,
String fileID, BufferFile bufferFile, String comment, String contentType,
@@ -483,9 +467,6 @@ public abstract class LocalFileSystem implements FileSystem {
return item;
}
- /*
- * @see ghidra.framework.store.FileSystem#createDatabase(java.lang.String, java.lang.String, java.lang.String, int, java.lang.String)
- */
@Override
public LocalManagedBufferFile createDatabase(String parentPath, String name, String fileID,
String contentType, int bufferSize, String user, String projectPath)
@@ -513,9 +494,6 @@ public abstract class LocalFileSystem implements FileSystem {
return bufferFile;
}
- /*
- * @see ghidra.framework.store.FileSystem#createDataFile(java.lang.String, java.lang.String, java.io.InputStream, java.lang.String, java.lang.String, ghidra.util.task.TaskMonitor)
- */
@Override
public synchronized LocalDataFile createDataFile(String parentPath, String name,
InputStream istream, String comment, String contentType, TaskMonitor monitor)
@@ -546,9 +524,6 @@ public abstract class LocalFileSystem implements FileSystem {
return dataFile;
}
- /*
- * @see ghidra.framework.store.FileSystem#createFile(java.lang.String, java.lang.String, java.io.File, ghidra.util.task.TaskMonitor, java.lang.String)
- */
@Override
public LocalDatabaseItem createFile(String parentPath, String name, File packedFile,
TaskMonitor monitor, String user)
@@ -591,9 +566,6 @@ public abstract class LocalFileSystem implements FileSystem {
return item;
}
- /*
- * @see ghidra.framework.store.FileSystem#moveItem(java.lang.String, java.lang.String, java.lang.String)
- */
@Override
public synchronized void moveItem(String folderPath, String name, String newFolderPath,
String newName) throws IOException, InvalidNameException {
@@ -652,9 +624,6 @@ public abstract class LocalFileSystem implements FileSystem {
@Override
public abstract boolean folderExists(String folderPath);
- /*
- * @see ghidra.framework.store.FileSystem#fileExists(java.lang.String, java.lang.String)
- */
@Override
public boolean fileExists(String folderPath, String name) {
try {
@@ -669,9 +638,6 @@ public abstract class LocalFileSystem implements FileSystem {
}
}
- /*
- * @see ghidra.framework.store.FileSystem#addFileSystemListener(ghidra.framework.store.FileSystemListener)
- */
@Override
public void addFileSystemListener(FileSystemListener listener) {
if (eventManager != null) {
@@ -679,9 +645,6 @@ public abstract class LocalFileSystem implements FileSystem {
}
}
- /*
- * @see ghidra.framework.store.FileSystem#removeFileSystemListener(ghidra.framework.store.FileSystemListener)
- */
@Override
public void removeFileSystemListener(FileSystemListener listener) {
if (eventManager != null) {
@@ -829,8 +792,7 @@ public abstract class LocalFileSystem implements FileSystem {
@Override
public boolean isShared() {
- // Does not support direct sharing in production
- return isShared;
+ return false;
}
// static void testValidPathLength(File file) throws IOException {
@@ -846,6 +808,17 @@ public abstract class LocalFileSystem implements FileSystem {
if (eventManager != null) {
eventManager.dispose();
}
+ disposed = true;
+ }
+
+ /**
+ * Check to see if file-system has been disposed.
+ * @throws IOException if file-system has been disposed
+ */
+ protected void checkDisposed() throws IOException {
+ if (disposed) {
+ throw new IOException("File-system has been disposed");
+ }
}
public boolean migrationInProgress() {
diff --git a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java
index f74cfa8538..b65503a6ce 100644
--- a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java
+++ b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java
@@ -929,12 +929,7 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
FileSystemEventManager eventManager =
(FileSystemEventManager) TestUtils.getInstanceField("eventManager", fs);
- try {
- eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
- }
- catch (InterruptedException e) {
- failWithException("Interrupted waiting for filesystem events", e);
- }
+ eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java b/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java
index 5d052c3e20..d2d318f7e4 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java
@@ -507,6 +507,41 @@ public abstract class AbstractGenericTest extends AbstractGTest {
return null;
}
+ public static List findComponents(Container parent,
+ Class desiredClass) {
+ return findComponents(parent, desiredClass, false);
+ }
+
+ public static List findComponents(Container parent,
+ Class desiredClass, boolean checkOwnedWindows) {
+ Component[] comps = parent.getComponents();
+ List list = new ArrayList<>();
+ for (Component element : comps) {
+ if (element == null) {
+ continue;// this started happening in 1.6, not sure why
+ }
+ if (desiredClass.isAssignableFrom(element.getClass())) {
+ list.add(desiredClass.cast(element));
+ }
+ else if (element instanceof Container) {
+ T c = findComponent((Container) element, desiredClass, checkOwnedWindows);
+ if (c != null) {
+ list.add(desiredClass.cast(c));
+ }
+ }
+ }
+ if (checkOwnedWindows && (parent instanceof Window)) {
+ Window[] windows = ((Window) parent).getOwnedWindows();
+ for (int i = windows.length - 1; i >= 0; i--) {
+ Component c = findComponent(windows[i], desiredClass, checkOwnedWindows);
+ if (c != null) {
+ list.add(desiredClass.cast(c));
+ }
+ }
+ }
+ return list;
+ }
+
/**
* Get the first field object contained within object ownerInstance which
* has the type classType. This method is only really useful if it is known
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/util/FileChannelLock.java b/Ghidra/Framework/Generic/src/main/java/generic/util/FileChannelLock.java
index ccb2e52bdf..d80b7c2108 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/util/FileChannelLock.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/util/FileChannelLock.java
@@ -1,6 +1,5 @@
/* ###
* 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.
@@ -17,8 +16,7 @@
package generic.util;
import java.io.*;
-import java.nio.channels.FileChannel;
-import java.nio.channels.FileLock;
+import java.nio.channels.*;
public class FileChannelLock {
@@ -51,7 +49,7 @@ public class FileChannelLock {
return isLocked;
}
- catch (IOException e) {
+ catch (IOException | OverlappingFileLockException e) {
release();
}
return false;
diff --git a/Ghidra/Framework/Project/certification.manifest b/Ghidra/Framework/Project/certification.manifest
index ec700e3b9d..2a05b72413 100644
--- a/Ghidra/Framework/Project/certification.manifest
+++ b/Ghidra/Framework/Project/certification.manifest
@@ -36,6 +36,7 @@ src/main/resources/images/disconnected.gif||GHIDRA||reviewed||END|
src/main/resources/images/disk.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/face-glasses.png||Tango Icons - Public Domain|||tango icon set|END|
src/main/resources/images/folder_add.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
+src/main/resources/images/link.png||Crystal Clear Icons - LGPL 2.1||||END|
src/main/resources/images/lock.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/monitor.png||FAMFAMFAM Icons - CC 2.5|||silk|END|
src/main/resources/images/noneInTool.gif||GHIDRA||reviewed||END|
diff --git a/Ghidra/Framework/Project/data/project.icons.theme.properties b/Ghidra/Framework/Project/data/project.icons.theme.properties
index 5a8f6132d4..cf81d7ad06 100644
--- a/Ghidra/Framework/Project/data/project.icons.theme.properties
+++ b/Ghidra/Framework/Project/data/project.icons.theme.properties
@@ -6,10 +6,14 @@ icon.project.data.file.ghidra.unsupported = unknownFile.gif
icon.project.data.file.ghidra.checked.out = icon.check
icon.project.data.file.ghidra.checked.out.exclusive = checkex.png
icon.project.data.file.ghidra.hijacked = small_hijack.gif
-icon.project.data.file.ghidra.read.only = user-busy.png [size(10,10)]
+icon.project.data.file.ghidra.read.only = user-busy.png [size(8,8)]
icon.project.data.file.ghidra.not.latest = checkNotLatest.gif
+icon.content.handler.link = link.png
+icon.content.handler.link.overlay = EMPTY_ICON[size(16,16)]{icon.content.handler.link[move(0,8)]} // lower-left of 16x16 icon
+icon.content.handler.linked.folder.open = icon.datatree.node.domain.folder.open{icon.content.handler.link.overlay}
+icon.content.handler.linked.folder.closed = icon.datatree.node.domain.folder.closed{icon.content.handler.link.overlay}
[Dark Defaults]
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java
index 59754c8e4d..9c48a7d8c6 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java
@@ -19,9 +19,7 @@ import java.io.IOException;
import javax.swing.Icon;
-import db.DBHandle;
-import ghidra.framework.model.ChangeSet;
-import ghidra.framework.model.DomainObject;
+import ghidra.framework.model.*;
import ghidra.framework.store.FileSystem;
import ghidra.framework.store.FolderItem;
import ghidra.util.InvalidNameException;
@@ -31,15 +29,17 @@ import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor;
/**
- * NOTE: ALL ContentHandler CLASSES MUST END IN "ContentHandler". If not,
+ * NOTE: ALL ContentHandler implementations MUST END IN "ContentHandler". If not,
* the ClassSearcher will not find them.
*
* ContentHandler defines an application interface for converting
* between a specific domain object implementation and folder item storage.
* This interface also defines a method which provides an appropriate icon
* corresponding to the content.
+ *
+ * @param {@link DomainObjectAdapter} implementation class
*/
-public interface ContentHandler extends ExtensionPoint {
+public interface ContentHandler extends ExtensionPoint {
public static final String UNKNOWN_CONTENT = "Unknown-File";
public static final String MISSING_CONTENT = "Missing-File";
@@ -56,7 +56,8 @@ public interface ContentHandler extends ExtensionPoint {
* @param domainObject the domain object to store in the newly created folder item
* @param monitor the monitor that allows the user to cancel
* @return checkout ID for new item
- * @throws IOException if an i/o error occurs
+ * @throws IOException if an IO error occurs or an unsupported {@code domainObject}
+ * implementation is specified.
* @throws InvalidNameException if the specified name contains invalid characters
* @throws CancelledException if the user cancels
*/
@@ -77,12 +78,12 @@ public interface ContentHandler extends ExtensionPoint {
* set.
* @param monitor the monitor that allows the user to cancel
* @return immutable domain object
- * @throws IOException if a folder item access error occurs
+ * @throws IOException if an IO or folder item access error occurs
* @throws CancelledException if operation is cancelled by user
* @throws VersionException if unable to handle file content due to version
* difference which could not be handled.
*/
- DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version,
+ T getImmutableObject(FolderItem item, Object consumer, int version,
int minChangeVersion, TaskMonitor monitor)
throws IOException, CancelledException, VersionException;
@@ -98,12 +99,12 @@ public interface ContentHandler extends ExtensionPoint {
* @param consumer consumer of the returned object
* @param monitor the monitor that allows the user to cancel
* @return read-only domain object
- * @throws IOException if a folder item access error occurs
+ * @throws IOException if an IO or folder item access error occurs
* @throws CancelledException if operation is cancelled by user
* @throws VersionException if unable to handle file content due to version
* difference which could not be handled.
*/
- DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
+ T getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException;
@@ -121,12 +122,12 @@ public interface ContentHandler extends ExtensionPoint {
* @param consumer consumer of the returned object
* @param monitor cancelable task monitor
* @return updateable domain object
- * @throws IOException if a folder item access error occurs
+ * @throws IOException if an IO or folder item access error occurs
* @throws CancelledException if operation is cancelled by user
* @throws VersionException if unable to handle file content due to version
* difference which could not be handled.
*/
- DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
+ T getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
throws IOException, CancelledException, VersionException;
@@ -138,7 +139,7 @@ public interface ContentHandler extends ExtensionPoint {
* @param newerVersion the newer version number
* @return the set of changes that were made
* @throws VersionException if a database version change prevents reading of data.
- * @throws IOException if a folder item access error occurs or change set was
+ * @throws IOException if an IO or folder item access error occurs or change set was
* produced by newer version of software and can not be read
*/
ChangeSet getChangeSet(FolderItem versionedFolderItem, int olderVersion, int newerVersion)
@@ -161,55 +162,46 @@ public interface ContentHandler extends ExtensionPoint {
/**
* Returns true if the content type is always private
* (i.e., can not be added to the versioned filesystem).
+ * @return true if private content type, else false
*/
boolean isPrivateContentType();
/**
- * Returns list of unique content-types supported.
- * A minimum of one content-type will be returned. If more than one
- * is returned, these are considered equivalent aliases.
+ * Returns a unique content-type identifier
+ * @return content type identifier for associated domain object(s).
*/
String getContentType();
/**
* A string that is meant to be presented to the user.
+ * @return user friendly content type for associated domain object(s).
*/
String getContentTypeDisplayString();
/**
* Returns the Icon associated with this handlers content type.
+ * @return base icon to be used for a {@link DomainFile} with the associated content type.
*/
Icon getIcon();
/**
- * Returns the name of the default tool that should be used to open this content type
+ * Returns the name of the default tool that should be used to open this content type.
+ * @return associated default tool for this content type
*/
String getDefaultToolName();
/**
* Returns domain object implementation class supported.
+ * @return implementation class for the associated {@link DomainObjectAdapter} implementation.
*/
- Class extends DomainObject> getDomainObjectClass();
+ Class getDomainObjectClass();
/**
- * Create user data file associated with existing content.
- * This facilitates the lazy creation of the user data file.
- * @param associatedDomainObj associated domain object corresponding to this content handler
- * @param userDbh user data handle
- * @param userfs private user data filesystem
- * @param monitor task monitor
- * @throws IOException if an access error occurs
- * @throws CancelledException if operation is cancelled by user
+ * If linking is supported return an instanceof the appropriate {@link LinkHandler}.
+ * @return corresponding link handler or null if not supported.
*/
- void saveUserDataFile(DomainObject associatedDomainObj, DBHandle userDbh, FileSystem userfs,
- TaskMonitor monitor) throws CancelledException, IOException;
-
- /**
- * Remove user data file associated with an existing folder item.
- * @param item folder item
- * @param userFilesystem
- * @throws IOException if an access error occurs
- */
- void removeUserDataFile(FolderItem item, FileSystem userFilesystem) throws IOException;
+ default LinkHandler> getLinkHandler() {
+ return null;
+ }
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBContentHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBContentHandler.java
index b911cd998a..4b62da8926 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBContentHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBContentHandler.java
@@ -18,13 +18,10 @@ package ghidra.framework.data;
import java.io.IOException;
import db.DBHandle;
-import db.buffers.BufferFile;
import db.buffers.ManagedBufferFile;
-import ghidra.framework.model.DomainFile;
-import ghidra.framework.model.DomainObject;
import ghidra.framework.store.*;
-import ghidra.util.*;
-import ghidra.util.exception.AssertException;
+import ghidra.util.InvalidNameException;
+import ghidra.util.SystemUtilities;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@@ -32,8 +29,11 @@ import ghidra.util.task.TaskMonitor;
* DBContentHandler provides an abstract ContentHandler for
* domain object content which is stored within a database file.
* This class provides helper methods for working with database files.
+ *
+ * @param {@link DomainObjectAdapterDB} implementation class
*/
-public abstract class DBContentHandler implements ContentHandler {
+public abstract class DBContentHandler
+ implements ContentHandler {
/**
* Create a new database file from an open database handle.
@@ -70,6 +70,7 @@ public abstract class DBContentHandler implements ContentHandler {
bf.delete();
}
catch (IOException e) {
+ // ignore
}
abortCreate(fs, path, name, checkoutId);
}
@@ -77,7 +78,7 @@ public abstract class DBContentHandler implements ContentHandler {
return checkoutId;
}
- private void abortCreate(FileSystem fs, String path, String name, long checkoutId) {
+ protected void abortCreate(FileSystem fs, String path, String name, long checkoutId) {
try {
FolderItem item = fs.getItem(path, name);
if (item != null) {
@@ -92,98 +93,4 @@ public abstract class DBContentHandler implements ContentHandler {
}
}
- /**
- * Return user data content type corresponding to associatedContentType.
- */
- private static String getUserDataContentType(String associatedContentType) {
- return associatedContentType + "UserData";
- }
-
- /**
- * @see ghidra.framework.data.ContentHandler#saveUserDataFile(ghidra.framework.model.DomainObject, db.DBHandle, ghidra.framework.store.FileSystem, ghidra.util.task.TaskMonitor)
- */
- @Override
- public final void saveUserDataFile(DomainObject domainObj, DBHandle userDbh, FileSystem userfs,
- TaskMonitor monitor) throws CancelledException, IOException {
- if (userfs.isVersioned()) {
- throw new IllegalArgumentException("User data file-system may not be versioned");
- }
- String associatedContentType = getContentType();
- DomainFile associatedDf = domainObj.getDomainFile();
- if (associatedDf == null) {
- throw new IllegalStateException("associated " + associatedContentType +
- " file must be saved before user data can be saved");
- }
- String associatedFileID = associatedDf.getFileID();
- if (associatedFileID == null) {
- Msg.error(this, associatedContentType + " '" + associatedDf.getName() +
- "' has not been assigned a file ID, user settings can not be saved!");
- return;
- }
- String path = "/";
- String name = ProjectFileManager.getUserDataFilename(associatedFileID);
- BufferFile bf = null;
- boolean success = false;
- try {
- bf =
- userfs.createDatabase(path, name, FileIDFactory.createFileID(),
- getUserDataContentType(associatedContentType), userDbh.getBufferSize(),
- SystemUtilities.getUserName(), null);
- userDbh.saveAs(bf, true, monitor);
- success = true;
- }
- catch (InvalidNameException e) {
- throw new AssertException("Unexpected Error", e);
- }
- finally {
- if (bf != null && !success) {
- try {
- bf.delete();
- }
- catch (IOException e) {
- }
- abortCreate(userfs, path, name, FolderItem.DEFAULT_CHECKOUT_ID);
- }
- }
- }
-
- /**
- * @see ghidra.framework.data.ContentHandler#removeUserDataFile(ghidra.framework.store.FolderItem, ghidra.framework.store.FileSystem)
- */
- @Override
- public final void removeUserDataFile(FolderItem associatedItem, FileSystem userfs)
- throws IOException {
- String path = "/";
- String name = ProjectFileManager.getUserDataFilename(associatedItem.getFileID());
- FolderItem item = userfs.getItem(path, name);
- if (item != null) {
- item.delete(-1, null);
- }
- }
-
- /**
- * Open user data file associatedDbh
- * @param associatedFileID
- * @param associatedContentType
- * @param userfs
- * @param monitor
- * @return user data file database handle
- * @throws IOException
- * @throws CancelledException
- */
- protected final DBHandle openAssociatedUserFile(String associatedFileID,
- String associatedContentType, FileSystem userfs, TaskMonitor monitor)
- throws IOException, CancelledException {
- String path = "/";
- String name = ProjectFileManager.getUserDataFilename(associatedFileID);
- FolderItem item = userfs.getItem(path, name);
- if (item == null || !(item instanceof DatabaseItem) ||
- !getUserDataContentType(associatedContentType).equals(item.getContentType())) {
- return null;
- }
- DatabaseItem dbItem = (DatabaseItem) item;
- BufferFile bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID);
- return new DBHandle(bf, false, monitor);
- }
-
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBWithUserDataContentHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBWithUserDataContentHandler.java
new file mode 100644
index 0000000000..84738a3077
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBWithUserDataContentHandler.java
@@ -0,0 +1,144 @@
+/* ###
+ * 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 java.io.IOException;
+
+import db.DBHandle;
+import db.buffers.BufferFile;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.DomainObject;
+import ghidra.framework.store.*;
+import ghidra.util.*;
+import ghidra.util.exception.AssertException;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * DBContentHandler provides an abstract ContentHandler for
+ * domain object content which is stored within a database file.
+ * This class provides helper methods for working with database files.
+ *
+ * @param {@link DomainObjectAdapterDB} implementation class
+ */
+public abstract class DBWithUserDataContentHandler
+ extends DBContentHandler {
+
+ /**
+ * Return user data content type corresponding to associatedContentType.
+ */
+ private static String getUserDataContentType(String associatedContentType) {
+ return associatedContentType + "UserData";
+ }
+
+ /**
+ * Create user data file associated with existing content.
+ * This facilitates the lazy creation of the user data file.
+ * @param associatedDomainObj associated domain object corresponding to this content handler
+ * @param userDbh user data handle
+ * @param userfs private user data filesystem
+ * @param monitor task monitor
+ * @throws IOException if an IO or access error occurs
+ * @throws CancelledException if operation is cancelled by user
+ */
+ public final void saveUserDataFile(DomainObject associatedDomainObj, DBHandle userDbh,
+ FileSystem userfs,
+ TaskMonitor monitor) throws CancelledException, IOException {
+ if (userfs.isVersioned()) {
+ throw new IllegalArgumentException("User data file-system may not be versioned");
+ }
+ String associatedContentType = getContentType();
+ DomainFile associatedDf = associatedDomainObj.getDomainFile();
+ if (associatedDf == null) {
+ throw new IllegalStateException("associated " + associatedContentType +
+ " file must be saved before user data can be saved");
+ }
+ String associatedFileID = associatedDf.getFileID();
+ if (associatedFileID == null) {
+ Msg.error(this, associatedContentType + " '" + associatedDf.getName() +
+ "' has not been assigned a file ID, user settings can not be saved!");
+ return;
+ }
+ String path = "/";
+ String name = ProjectFileManager.getUserDataFilename(associatedFileID);
+ BufferFile bf = null;
+ boolean success = false;
+ try {
+ bf =
+ userfs.createDatabase(path, name, FileIDFactory.createFileID(),
+ getUserDataContentType(associatedContentType), userDbh.getBufferSize(),
+ SystemUtilities.getUserName(), null);
+ userDbh.saveAs(bf, true, monitor);
+ success = true;
+ }
+ catch (InvalidNameException e) {
+ throw new AssertException("Unexpected Error", e);
+ }
+ finally {
+ if (bf != null && !success) {
+ try {
+ bf.delete();
+ }
+ catch (IOException e) {
+ // ignore
+ }
+ abortCreate(userfs, path, name, FolderItem.DEFAULT_CHECKOUT_ID);
+ }
+ }
+ }
+
+ /**
+ * Remove user data file associated with an existing folder item.
+ * @param associatedItem associated folder item
+ * @param userFilesystem user data file system from which corresponding data should be removed.
+ * @throws IOException if an access error occurs
+ */
+ public final void removeUserDataFile(FolderItem associatedItem, FileSystem userFilesystem)
+ throws IOException {
+ String path = "/";
+ String name = ProjectFileManager.getUserDataFilename(associatedItem.getFileID());
+ FolderItem item = userFilesystem.getItem(path, name);
+ if (item != null) {
+ item.delete(-1, null);
+ }
+ }
+
+ /**
+ * Open user data file associatedDbh
+ * @param associatedFileID
+ * @param associatedContentType
+ * @param userfs
+ * @param monitor
+ * @return user data file database handle
+ * @throws IOException
+ * @throws CancelledException
+ */
+ protected final DBHandle openAssociatedUserFile(String associatedFileID,
+ String associatedContentType, FileSystem userfs, TaskMonitor monitor)
+ throws IOException, CancelledException {
+ String path = "/";
+ String name = ProjectFileManager.getUserDataFilename(associatedFileID);
+ FolderItem item = userfs.getItem(path, name);
+ if (item == null || !(item instanceof DatabaseItem) ||
+ !getUserDataContentType(associatedContentType).equals(item.getContentType())) {
+ return null;
+ }
+ DatabaseItem dbItem = (DatabaseItem) item;
+ BufferFile bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID);
+ return new DBHandle(bf, false, monitor);
+ }
+
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java
index dece247192..7a13516e27 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java
@@ -17,11 +17,14 @@ package ghidra.framework.data;
import java.io.File;
import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.util.*;
import javax.swing.Icon;
import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.framework.store.Version;
import ghidra.framework.store.db.PackedDatabase;
@@ -111,6 +114,29 @@ public class DomainFileProxy implements DomainFile {
return parentPath + DomainFolder.SEPARATOR + getName();
}
+ @Override
+ public URL getSharedProjectURL() {
+ if (projectLocation != null && version == DomainFile.DEFAULT_VERSION) {
+ URL projectURL = projectLocation.getURL();
+ if (GhidraURL.isServerRepositoryURL(projectURL)) {
+ try {
+ // Direct URL construction done so that ghidra protocol
+ // extension may be supported
+ String urlStr = projectURL.toExternalForm();
+ if (urlStr.endsWith("/")) {
+ urlStr = urlStr.substring(0, urlStr.length() - 1);
+ }
+ urlStr += getPathname();
+ return new URL(urlStr);
+ }
+ catch (MalformedURLException e) {
+ // ignore
+ }
+ }
+ }
+ return null;
+ }
+
@Override
public int compareTo(DomainFile df) {
return getName().compareToIgnoreCase(df.getName());
@@ -143,7 +169,7 @@ public class DomainFileProxy implements DomainFile {
DomainObjectAdapter dobj = getDomainObject();
if (dobj != null) {
try {
- ContentHandler ch = DomainObjectAdapter.getContentHandler(dobj);
+ ContentHandler> ch = DomainObjectAdapter.getContentHandler(dobj);
return ch.getContentType();
}
catch (IOException e) {
@@ -153,6 +179,27 @@ public class DomainFileProxy implements DomainFile {
return "Unknown File";
}
+ @Override
+ public boolean isLinkFile() {
+ DomainObjectAdapter dobj = getDomainObject();
+ if (dobj != null) {
+ ContentHandler> ch;
+ try {
+ ch = DomainObjectAdapter.getContentHandler(dobj);
+ return LinkHandler.class.isAssignableFrom(ch.getClass());
+ }
+ catch (IOException e) {
+ // ignore
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public DomainFolder followLink() {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public Class extends DomainObject> getDomainObjectClass() {
DomainObjectAdapter dobj = getDomainObject();
@@ -228,6 +275,7 @@ public class DomainFileProxy implements DomainFile {
dobj.release(consumer);
}
catch (IllegalArgumentException e) {
+ // ignore unknown consumer error
}
}
}
@@ -256,11 +304,6 @@ public class DomainFileProxy implements DomainFile {
throw new UnsupportedOperationException("Repository operations not supported");
}
- @Override
- public boolean isVersionControlSupported() {
- return false;
- }
-
@Override
public boolean canAddToRepository() {
return false;
@@ -303,6 +346,16 @@ public class DomainFileProxy implements DomainFile {
throw new UnsupportedOperationException("Repository operations not supported");
}
+ @Override
+ public boolean isLinkingSupported() {
+ return false;
+ }
+
+ @Override
+ public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
+ return null; // not supported by proxy file
+ }
+
@Override
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException {
@@ -318,11 +371,8 @@ public class DomainFileProxy implements DomainFile {
}
}
- /**
- * @see ghidra.framework.model.DomainFile#copyVersionTo(int, ghidra.framework.model.DomainFolder, ghidra.util.task.TaskMonitor)
- */
@Override
- public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
+ public DomainFile copyVersionTo(int ver, DomainFolder destFolder, TaskMonitor monitor)
throws IOException, CancelledException {
throw new UnsupportedOperationException("copyVersionTo unsupported for DomainFileProxy");
}
@@ -420,7 +470,7 @@ public class DomainFileProxy implements DomainFile {
throw new UnsupportedOperationException("packFile() only valid for Database files");
}
DomainObjectAdapterDB dbObj = (DomainObjectAdapterDB) domainObj;
- ContentHandler ch = DomainObjectAdapter.getContentHandler(domainObj);
+ ContentHandler> ch = DomainObjectAdapter.getContentHandler(domainObj);
PackedDatabase.packDatabase(dbObj.getDBHandle(), dbObj.getName(), ch.getContentType(), file,
monitor);
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java
index adc0c0d95c..58c161c7f8 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java
@@ -38,8 +38,8 @@ public abstract class DomainObjectAdapter implements DomainObject {
protected final static String DEFAULT_NAME = "untitled";
private static Class> defaultDomainObjClass; // Domain object implementation mapped to unknown content type
- private static HashMap contentHandlerTypeMap; // maps content-type string to handler
- private static HashMap, ContentHandler> contentHandlerClassMap; // maps domain object class to handler
+ private static HashMap> contentHandlerTypeMap; // maps content-type string to handler
+ private static HashMap, ContentHandler>> contentHandlerClassMap; // maps domain object class to handler
private static ChangeListener contentHandlerUpdateListener = new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
@@ -399,7 +399,7 @@ public abstract class DomainObjectAdapter implements DomainObject {
contentHandlerTypeMap.remove(null);
}
else {
- ContentHandler ch = contentHandlerClassMap.get(doClass);
+ ContentHandler> ch = contentHandlerClassMap.get(doClass);
if (ch != null) {
contentHandlerTypeMap.put(null, ch);
}
@@ -414,9 +414,9 @@ public abstract class DomainObjectAdapter implements DomainObject {
* @return content handler
* @throws IOException if no content handler can be found
*/
- static synchronized ContentHandler getContentHandler(String contentType) throws IOException {
+ static synchronized ContentHandler> getContentHandler(String contentType) throws IOException {
checkContentHandlerMaps();
- ContentHandler ch = contentHandlerTypeMap.get(contentType);
+ ContentHandler> ch = contentHandlerTypeMap.get(contentType);
if (ch == null) {
throw new IOException("Content handler not found for " + contentType);
}
@@ -430,10 +430,10 @@ public abstract class DomainObjectAdapter implements DomainObject {
* @return content handler
* @throws IOException if no content handler can be found
*/
- public static synchronized ContentHandler getContentHandler(DomainObject dobj)
+ public static synchronized ContentHandler> getContentHandler(DomainObject dobj)
throws IOException {
checkContentHandlerMaps();
- ContentHandler ch = contentHandlerClassMap.get(dobj.getClass());
+ ContentHandler> ch = contentHandlerClassMap.get(dobj.getClass());
if (ch == null) {
throw new IOException("Content handler not found for " + dobj.getClass().getName());
}
@@ -450,17 +450,15 @@ public abstract class DomainObjectAdapter implements DomainObject {
}
private synchronized static void getContentHandlers() {
- contentHandlerClassMap = new HashMap, ContentHandler>();
- contentHandlerTypeMap = new HashMap();
+ contentHandlerClassMap = new HashMap, ContentHandler>>();
+ contentHandlerTypeMap = new HashMap>();
+ @SuppressWarnings("rawtypes")
List handlers = ClassSearcher.getInstances(ContentHandler.class);
- for (ContentHandler ch : handlers) {
- String type = ch.getContentType();
- Class> DOClass = ch.getDomainObjectClass();
- if (type != null && DOClass != null) {
- contentHandlerClassMap.put(DOClass, ch);
- contentHandlerTypeMap.put(type, ch);
- continue;
+ for (ContentHandler> ch : handlers) {
+ contentHandlerTypeMap.put(ch.getContentType(), ch);
+ if (!(ch instanceof LinkHandler>)) {
+ contentHandlerClassMap.put(ch.getDomainObjectClass(), ch);
}
}
setDefaultContentClass(defaultDomainObjClass);
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java
new file mode 100644
index 0000000000..0dccc72cf9
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java
@@ -0,0 +1,119 @@
+/* ###
+ * 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 java.io.IOException;
+import java.net.URL;
+
+import javax.swing.Icon;
+
+import ghidra.framework.main.AppInfo;
+import ghidra.framework.model.*;
+import ghidra.framework.store.FileSystem;
+import ghidra.util.InvalidNameException;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * {@code FolderLinkContentHandler} provide folder-link support.
+ * Implementation relies on {@link AppInfo#getActiveProject()} to provide life-cycle
+ * management for related transient-projects opened while following folder-links.
+ */
+public class FolderLinkContentHandler extends LinkHandler {
+
+ public static FolderLinkContentHandler INSTANCE = new FolderLinkContentHandler();
+
+ public static final String FOLDER_LINK_CONTENT_TYPE = "FolderLink";
+
+ @Override
+ public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
+ DomainObject obj, TaskMonitor monitor)
+ throws IOException, InvalidNameException, CancelledException {
+ 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);
+ }
+
+ @Override
+ public String getContentType() {
+ return FOLDER_LINK_CONTENT_TYPE;
+ }
+
+ @Override
+ public String getContentTypeDisplayString() {
+ return FOLDER_LINK_CONTENT_TYPE;
+ }
+
+ @Override
+ public Class getDomainObjectClass() {
+ return NullFolderDomainObject.class; // special case since link corresponds to a Domain Folder
+ }
+
+ @Override
+ public Icon getIcon() {
+ return DomainFolder.CLOSED_FOLDER_ICON;
+ }
+
+ @Override
+ public String getDefaultToolName() {
+ return null;
+ }
+
+ /**
+ * Get linked domain folder
+ * @param folderLinkFile folder-link file.
+ * @return {@link LinkedGhidraFolder} referenced by specified folder-link file or null if
+ * folderLinkFile content type is not {@value #FOLDER_LINK_CONTENT_TYPE}.
+ * @throws IOException if an IO or folder item access error occurs
+ */
+ public static LinkedGhidraFolder getReadOnlyLinkedFolder(DomainFile folderLinkFile)
+ throws IOException {
+
+ if (!FOLDER_LINK_CONTENT_TYPE.equals(folderLinkFile.getContentType())) {
+ return null;
+ }
+
+ URL url = getURL(folderLinkFile);
+
+ Project activeProject = AppInfo.getActiveProject();
+ GhidraFolder parent = ((GhidraFile) folderLinkFile).getParent();
+ return new LinkedGhidraFolder(activeProject, parent, folderLinkFile.getName(), url);
+ }
+
+}
+
+/**
+ * Dummy domain object to satisfy {@link FolderLinkContentHandler#getDomainObjectClass()}
+ */
+final class NullFolderDomainObject extends DomainObjectAdapterDB {
+ private NullFolderDomainObject() {
+ // this object may not be instantiated
+ super(null, null, 0, NullFolderDomainObject.class);
+ throw new RuntimeException("Object may not be instantiated");
+ }
+
+ @Override
+ public boolean isChangeable() {
+ return false;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Dummy FolderLink Domain Object";
+ }
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java
index a2f6b78ea6..b784e944e2 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java
@@ -16,6 +16,7 @@
package ghidra.framework.data;
import java.io.*;
+import java.net.URL;
import java.util.*;
import javax.swing.Icon;
@@ -24,8 +25,7 @@ import ghidra.framework.model.*;
import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.framework.store.Version;
import ghidra.framework.store.local.LocalFileSystem;
-import ghidra.util.InvalidNameException;
-import ghidra.util.ReadOnlyException;
+import ghidra.util.*;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
@@ -123,6 +123,17 @@ public class GhidraFile implements DomainFile {
return fileManager.getProjectLocator();
}
+ @Override
+ public URL getSharedProjectURL() {
+ try {
+ return getFileData().getSharedProjectURL();
+ }
+ catch (IOException e) {
+ // ignore
+ }
+ return null;
+ }
+
@Override
public String getContentType() {
try {
@@ -134,6 +145,37 @@ public class GhidraFile implements DomainFile {
return ContentHandler.UNKNOWN_CONTENT;
}
+ @Override
+ public boolean isLinkFile() {
+ try {
+ return getFileData().isLinkFile();
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public DomainFolder followLink() {
+ try {
+ return FolderLinkContentHandler.getReadOnlyLinkedFolder(this);
+ }
+ catch (IOException e) {
+ Msg.error(this, "Failed to following folder-link: " + getPathname());
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isLinkingSupported() {
+ try {
+ return getFileData().isLinkingSupported();
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+
@Override
public Class extends DomainObject> getDomainObjectClass() {
try {
@@ -146,7 +188,7 @@ public class GhidraFile implements DomainFile {
}
@Override
- public DomainFolder getParent() {
+ public GhidraFolder getParent() {
return parent;
}
@@ -257,7 +299,7 @@ public class GhidraFile implements DomainFile {
catch (IOException e) {
fileError(e);
}
- return GhidraFileData.UNSUPPORTED_FILE_ICON;
+ return UNSUPPORTED_FILE_ICON;
}
@Override
@@ -353,17 +395,6 @@ public class GhidraFile implements DomainFile {
return true;
}
- @Override
- public boolean isVersionControlSupported() {
- try {
- return getFileData().isVersionControlSupported();
- }
- catch (IOException e) {
- fileError(e);
- }
- return false;
- }
-
@Override
public boolean isVersioned() {
try {
@@ -482,6 +513,9 @@ public class GhidraFile implements DomainFile {
@Override
public GhidraFile moveTo(DomainFolder newParent) throws IOException {
+ if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
+ throw new UnsupportedOperationException("newParent does not support moveTo");
+ }
GhidraFolder newGhidraParent = (GhidraFolder) newParent;
return getFileData().moveTo(newGhidraParent.getFolderData());
}
@@ -489,15 +523,30 @@ public class GhidraFile implements DomainFile {
@Override
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException,
CancelledException {
- GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation
+ if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
+ throw new UnsupportedOperationException("newParent does not support copyTo");
+ }
+ GhidraFolder newGhidraParent = (GhidraFolder) newParent;
return getFileData().copyTo(newGhidraParent.getFolderData(),
monitor != null ? monitor : TaskMonitor.DUMMY);
}
+ @Override
+ public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
+ if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
+ throw new UnsupportedOperationException("newParent does not support copyToAsLink");
+ }
+ GhidraFolder newGhidraParent = (GhidraFolder) newParent;
+ return getFileData().copyToAsLink(newGhidraParent.getFolderData());
+ }
+
@Override
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
throws IOException, CancelledException {
- GhidraFolder destGhidraFolder = (GhidraFolder) destFolder; // assumes single implementation
+ if (!GhidraFolder.class.isAssignableFrom(destFolder.getClass())) {
+ throw new UnsupportedOperationException("destFolder does not support copyVersionTo");
+ }
+ GhidraFolder destGhidraFolder = (GhidraFolder) destFolder;
return getFileData().copyVersionTo(version, destGhidraFolder.getFolderData(),
monitor != null ? monitor : TaskMonitor.DUMMY);
}
@@ -507,7 +556,7 @@ public class GhidraFile implements DomainFile {
* only when a non shared project is being converted to a shared project.
* @param monitor task monitor
* @throws IOException if an IO error occurs
- * @throws CancelledException if task cancelled
+ * @throws CancelledException if task is cancelled
*/
void convertToPrivateFile(TaskMonitor monitor) throws IOException, CancelledException {
getFileData().convertToPrivateFile(
@@ -596,6 +645,10 @@ public class GhidraFile implements DomainFile {
@Override
public String toString() {
+ ProjectLocator projectLocator = parent.getProjectData().getProjectLocator();
+ if (projectLocator.isTransient()) {
+ return fileManager.getProjectLocator().getName() + getPathname();
+ }
return fileManager.getProjectLocator().getName() + ":" + getPathname();
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java
index a0b0c7413c..e0a09f6be6 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java
@@ -17,6 +17,8 @@ package ghidra.framework.data;
import java.awt.*;
import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.util.HashMap;
import java.util.Map;
@@ -27,9 +29,9 @@ import db.Field;
import db.buffers.*;
import generic.theme.GColor;
import generic.theme.GIcon;
-import ghidra.framework.client.ClientUtil;
-import ghidra.framework.client.NotConnectedException;
+import ghidra.framework.client.*;
import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.*;
import ghidra.framework.store.FileSystem;
import ghidra.framework.store.local.LocalFileSystem;
@@ -37,12 +39,14 @@ import ghidra.framework.store.local.LocalFolderItem;
import ghidra.util.*;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
-import ghidra.util.task.TaskMonitorAdapter;
import resources.MultiIcon;
import resources.icons.TranslateIcon;
public class GhidraFileData {
+ static final int ICON_WIDTH = 18;
+ static final int ICON_HEIGHT = 17;
+
private static final boolean ALWAYS_MERGE = System.getProperty("ForceMerge") != null;
//@formatter:off
@@ -50,6 +54,7 @@ public class GhidraFileData {
public static final Icon CHECKED_OUT_ICON = new GIcon("icon.project.data.file.ghidra.checked.out");
public static final Icon CHECKED_OUT_EXCLUSIVE_ICON = new GIcon("icon.project.data.file.ghidra.checked.out.exclusive");
public static final Icon HIJACKED_ICON = new GIcon("icon.project.data.file.ghidra.hijacked");
+
public static final Icon VERSION_ICON = new VersionIcon();
public static final Icon READ_ONLY_ICON = new GIcon("icon.project.data.file.ghidra.read.only");
public static final Icon NOT_LATEST_CHECKED_OUT_ICON = new GIcon("icon.project.data.file.ghidra.not.latest");
@@ -202,9 +207,32 @@ public class GhidraFileData {
return new GhidraFile(parent.getDomainFolder(), name);
}
+ /**
+ * Get a remote Ghidra URL for this domain file if available within a remote repository.
+ * @return remote Ghidra URL for this file or null
+ */
+ URL getSharedProjectURL() {
+ synchronized (fileSystem) {
+ RepositoryAdapter repository = parent.getProjectFileManager().getRepository();
+ if (versionedFolderItem != null && repository != null) {
+ URL folderURL = parent.getDomainFolder().getSharedProjectURL();
+ try {
+ // Direct URL construction done so that ghidra protocol
+ // extension may be supported
+ return new URL(folderURL.toExternalForm() + name);
+ }
+ catch (MalformedURLException e) {
+ // ignore
+ }
+ }
+ return null;
+ }
+ }
+
/**
* Reassign a new file-ID to resolve file-ID conflict.
* Conflicts can occur as a result of a cancelled check-out.
+ * @throws IOException if an IO error occurs
*/
void resetFileID() throws IOException {
synchronized (fileSystem) {
@@ -262,6 +290,8 @@ public class GhidraFileData {
String getContentType() {
synchronized (fileSystem) {
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
+ // this can happen when we are trying to load a version file from
+ // a server to which we are not connected
if (item == null) {
return ContentHandler.MISSING_CONTENT;
}
@@ -270,14 +300,27 @@ public class GhidraFileData {
}
}
- Class extends DomainObject> getDomainObjectClass() {
+ /**
+ * Get content handler
+ * @return content handler
+ * @throws IOException if an IO error occurs, file not found, or unsupported content
+ */
+ ContentHandler> getContentHandler() throws IOException {
synchronized (fileSystem) {
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
+ // this can happen when we are trying to load a version file from
+ // a server to which we are not connected
+ if (item == null) {
+ throw new FileNotFoundException(name + " not found");
+ }
+ return DomainObjectAdapter.getContentHandler(item.getContentType());
+ }
+ }
+
+ Class extends DomainObject> getDomainObjectClass() {
+ synchronized (fileSystem) {
try {
- ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType());
- if (ch != null) {
- return ch.getDomainObjectClass();
- }
+ return getContentHandler().getDomainObjectClass();
}
catch (IOException e) {
// ignore missing content handler
@@ -289,8 +332,7 @@ public class GhidraFileData {
ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException {
synchronized (fileSystem) {
if (versionedFolderItem != null && folderItem != null && folderItem.isCheckedOut()) {
- ContentHandler ch =
- DomainObjectAdapter.getContentHandler(folderItem.getContentType());
+ ContentHandler> ch = getContentHandler();
return ch.getChangeSet(versionedFolderItem, folderItem.getCheckoutVersion(),
versionedFolderItem.getCurrentVersion());
}
@@ -305,10 +347,9 @@ public class GhidraFileData {
DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
TaskMonitor monitor) throws VersionException, IOException, CancelledException {
FolderItem myFolderItem;
- ContentHandler ch;
DomainObjectAdapter domainObj = null;
synchronized (fileSystem) {
- if (fileSystem.isReadOnly()) {
+ if (fileSystem.isReadOnly() || isLinkFile()) {
return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor);
}
domainObj = getOpenedDomainObject();
@@ -321,8 +362,8 @@ public class GhidraFileData {
return domainObj;
}
}
+ ContentHandler> ch = getContentHandler();
if (folderItem == null) {
- ch = DomainObjectAdapter.getContentHandler(versionedFolderItem.getContentType());
DomainObjectAdapter doa = ch.getReadOnlyObject(versionedFolderItem,
DomainFile.DEFAULT_VERSION, true, consumer, monitor);
doa.setChanged(false);
@@ -331,7 +372,6 @@ public class GhidraFileData {
proxy.setLastModified(getLastModifiedTime());
return doa;
}
- ch = DomainObjectAdapter.getContentHandler(folderItem.getContentType());
myFolderItem = folderItem;
domainObj = ch.getDomainObject(myFolderItem, parent.getUserFileSystem(),
@@ -368,14 +408,7 @@ public class GhidraFileData {
FolderItem item =
(folderItem != null && version == DomainFile.DEFAULT_VERSION) ? folderItem
: versionedFolderItem;
-
- // this can happen when we are trying to load a version file from
- // a server to which we are not connected
- if (item == null) {
- return null;
- }
-
- ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType());
+ ContentHandler> ch = getContentHandler();
DomainObjectAdapter doa = ch.getReadOnlyObject(item, version, true, consumer, monitor);
doa.setChanged(false);
@@ -390,15 +423,12 @@ public class GhidraFileData {
throws VersionException, IOException, CancelledException {
synchronized (fileSystem) {
DomainObjectAdapter obj = null;
+ ContentHandler> ch = getContentHandler();
if (versionedFolderItem == null ||
(version == DomainFile.DEFAULT_VERSION && folderItem != null) || isHijacked()) {
- ContentHandler ch =
- DomainObjectAdapter.getContentHandler(folderItem.getContentType());
obj = ch.getImmutableObject(folderItem, consumer, version, -1, monitor);
}
else {
- ContentHandler ch =
- DomainObjectAdapter.getContentHandler(versionedFolderItem.getContentType());
obj = ch.getImmutableObject(versionedFolderItem, consumer, version, -1, monitor);
}
DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), obj,
@@ -419,8 +449,11 @@ public class GhidraFileData {
}
boolean takeRecoverySnapshot() throws IOException {
+ if (fileSystem.isReadOnly()) {
+ return true;
+ }
DomainObjectAdapter dobj = fileManager.getOpenedDomainObject(getPathname());
- if (fileSystem.isReadOnly() || !(dobj instanceof DomainObjectAdapterDB) ||
+ if (!(dobj instanceof DomainObjectAdapterDB) ||
!dobj.isChanged()) {
return true;
}
@@ -481,13 +514,19 @@ public class GhidraFileData {
private Icon generateIcon(boolean disabled) {
if (parent == null) {
// instance has been disposed
- return UNSUPPORTED_FILE_ICON;
+ return DomainFile.UNSUPPORTED_FILE_ICON;
}
synchronized (fileSystem) {
+
+ boolean isLink = isLinkFile();
+
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
+
+ Icon baseIcon = new TranslateIcon(getBaseIcon(item), 1, 1);
+
if (versionedFolderItem != null) {
MultiIcon multiIcon = new MultiIcon(VERSION_ICON, disabled);
- multiIcon.addIcon(getBaseIcon(item));
+ multiIcon.addIcon(baseIcon);
if (isHijacked()) {
multiIcon.addIcon(HIJACKED_ICON);
}
@@ -504,12 +543,15 @@ public class GhidraFileData {
}
}
}
+ if (isLink) {
+ multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1));
+ }
return multiIcon;
}
else if (folderItem != null) {
- MultiIcon multiIcon = new MultiIcon(getBaseIcon(item), disabled);
+ MultiIcon multiIcon = new MultiIcon(baseIcon, disabled, ICON_WIDTH, ICON_HEIGHT);
if (isReadOnly() && !fileSystem.isReadOnly()) {
- multiIcon.addIcon(new TranslateIcon(READ_ONLY_ICON, 6, 6));
+ multiIcon.addIcon(new TranslateIcon(READ_ONLY_ICON, 8, 9));
}
if (isCheckedOut()) {
if (isCheckedOutExclusive()) {
@@ -519,23 +561,23 @@ public class GhidraFileData {
multiIcon.addIcon(CHECKED_OUT_ICON);
}
}
+ if (isLink) {
+ multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1));
+ }
return multiIcon;
}
}
- return UNSUPPORTED_FILE_ICON;
+ return DomainFile.UNSUPPORTED_FILE_ICON;
}
private Icon getBaseIcon(FolderItem item) {
try {
- ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType());
- if (ch != null) {
- return ch.getIcon();
- }
+ return getContentHandler().getIcon();
}
catch (IOException e) {
// ignore missing content handler
}
- return UNSUPPORTED_FILE_ICON;
+ return DomainFile.UNSUPPORTED_FILE_ICON;
}
boolean isChanged() {
@@ -593,9 +635,19 @@ public class GhidraFileData {
boolean canAddToRepository() {
synchronized (fileSystem) {
try {
- return (!fileSystem.isReadOnly() && !versionedFileSystem.isReadOnly() &&
- folderItem != null && versionedFolderItem == null &&
- !folderItem.isCheckedOut() && isVersionControlSupported());
+ if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) {
+ return false;
+ }
+ if (folderItem == null || versionedFolderItem != null) {
+ return false;
+ }
+ if (folderItem.isCheckedOut()) {
+ return false;
+ }
+ if (isLinkFile()) {
+ return GhidraURL.isServerRepositoryURL(LinkHandler.getURL(folderItem));
+ }
+ return !getContentHandler().isPrivateContentType();
}
catch (IOException e) {
return false;
@@ -606,8 +658,11 @@ public class GhidraFileData {
boolean canCheckout() {
synchronized (fileSystem) {
try {
- return folderItem == null && !fileSystem.isReadOnly() &&
- !versionedFileSystem.isReadOnly();
+ if (folderItem != null || fileSystem.isReadOnly() ||
+ versionedFileSystem.isReadOnly()) {
+ return false;
+ }
+ return !isLinkFile();
}
catch (IOException e) {
return false;
@@ -627,26 +682,6 @@ public class GhidraFileData {
}
}
- boolean isVersionControlSupported() {
- synchronized (fileSystem) {
- if (versionedFolderItem != null) {
- return true;
- }
- if (!(folderItem instanceof DatabaseItem)) {
- return false;
- }
- try {
- ContentHandler ch =
- DomainObjectAdapter.getContentHandler(folderItem.getContentType());
- return !ch.isPrivateContentType();
- }
- catch (IOException e) {
- // ignore missing content handler
- }
- return false;
- }
- }
-
int getVersion() {
synchronized (fileSystem) {
try {
@@ -714,18 +749,15 @@ public class GhidraFileData {
throws IOException, CancelledException {
DomainObjectAdapter oldDomainObj = null;
synchronized (fileSystem) {
- if (!isVersionControlSupported()) {
- throw new AssertException("file type does supported version control");
+ if (!canAddToRepository()) {
+ if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) {
+ throw new ReadOnlyException(
+ "addToVersionControl permitted within writeable project and repository only");
+ }
+ throw new IOException("addToVersionControl not allowed for file");
}
- if (versionedFolderItem != null) {
- throw new AssertException("file already versioned");
- }
- if (!versionedFileSystem.isOnline()) {
- throw new NotConnectedException("Not connected to repository server");
- }
- if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) {
- throw new ReadOnlyException(
- "addToVersionControl permitted within writeable project and repository only");
+ if (isLinkFile()) {
+ keepCheckedOut = false;
}
String parentPath = parent.getPathname();
String user = ClientUtil.getUserName();
@@ -832,6 +864,9 @@ public class GhidraFileData {
if (!versionedFileSystem.isOnline()) {
throw new NotConnectedException("Not connected to repository server");
}
+ if (isLinkFile()) {
+ return false;
+ }
String user = ClientUtil.getUserName();
ProjectLocator projectLocator = parent.getProjectLocator();
CheckoutType checkoutType;
@@ -934,8 +969,7 @@ public class GhidraFileData {
if (checkinHandler.createKeepFile()) {
DomainObject sourceObj = null;
try {
- ContentHandler ch =
- DomainObjectAdapter.getContentHandler(folderItem.getContentType());
+ ContentHandler> ch = getContentHandler();
sourceObj = ch.getImmutableObject(folderItem, this, DomainFile.DEFAULT_VERSION,
-1, monitor);
createKeepFile(sourceObj, monitor);
@@ -968,18 +1002,18 @@ public class GhidraFileData {
/**
* Verify that current user is the checkout user for this file
- * @param caseName name of user case (e.g., checkin)
- * @return true if server/repository will permit current user to checkin,
- * or update checkout version of current file. (i.e., server login matches
+ * @param operationName name of user case (e.g., checkin)
+ * @throws IOException if server/repository will not permit current user to checkin,
+ * or update checkout version of current file. (i.e., server login does not match
* user name used at time of initial checkout)
*/
- private void verifyRepoUser(String caseName) throws IOException {
+ private void verifyRepoUser(String operationName) throws IOException {
if (versionedFileSystem instanceof LocalFileSystem) {
return; // rely on local project ownership
}
String repoUserName = versionedFileSystem.getUserName();
if (repoUserName == null) {
- throw new IOException("File " + caseName + " not permitted (not connected)");
+ throw new IOException("File " + operationName + " not permitted (not connected)");
}
ItemCheckoutStatus checkoutStatus = getCheckoutStatus();
if (checkoutStatus == null) {
@@ -987,7 +1021,7 @@ public class GhidraFileData {
}
String checkoutUserName = checkoutStatus.getUser();
if (!repoUserName.equals(checkoutUserName)) {
- throw new IOException("File " + caseName + " not permitted - checkout user '" +
+ throw new IOException("File " + operationName + " not permitted - checkout user '" +
checkoutUserName + "' differs from repository user '" + repoUserName + "'");
}
}
@@ -1016,7 +1050,7 @@ public class GhidraFileData {
}
verifyRepoUser("checkin");
if (monitor == null) {
- monitor = TaskMonitorAdapter.DUMMY_MONITOR;
+ monitor = TaskMonitor.DUMMY;
}
synchronized (fileSystem) {
if (busy) {
@@ -1036,9 +1070,7 @@ public class GhidraFileData {
Msg.info(this, "Checkin with merge for " + name);
- ContentHandler ch =
- DomainObjectAdapter.getContentHandler(folderItem.getContentType());
-
+ ContentHandler> ch = getContentHandler();
DomainObjectAdapter checkinObj = ch.getDomainObject(versionedFolderItem, null,
folderItem.getCheckoutId(), okToUpgrade, false, this, monitor);
checkinObj.setDomainFile(new DomainFileProxy(name, getParent().getPathname(),
@@ -1366,9 +1398,12 @@ public class GhidraFileData {
private void removeAssociatedUserDataFile() {
try {
- FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
- ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType());
- ch.removeUserDataFile(item, parent.getUserFileSystem());
+ ContentHandler> ch = getContentHandler();
+ if (ch instanceof DBWithUserDataContentHandler) {
+ FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
+ ((DBWithUserDataContentHandler>) ch).removeUserDataFile(item,
+ parent.getUserFileSystem());
+ }
}
catch (Exception e) {
// ignore missing content handler
@@ -1403,7 +1438,7 @@ public class GhidraFileData {
}
verifyRepoUser("merge");
if (monitor == null) {
- monitor = TaskMonitorAdapter.DUMMY_MONITOR;
+ monitor = TaskMonitor.DUMMY;
}
synchronized (fileSystem) {
if (busy) {
@@ -1425,8 +1460,7 @@ public class GhidraFileData {
"Merge failed, file merge is not supported in headless mode");
}
- ContentHandler ch =
- DomainObjectAdapter.getContentHandler(folderItem.getContentType());
+ ContentHandler> ch = getContentHandler();
// Test versioned file for VersionException
int mergeVer = versionedFolderItem.getCurrentVersion();
@@ -1556,7 +1590,7 @@ public class GhidraFileData {
checkInUse();
GhidraFolderData oldParent = parent;
String oldName = name;
- String newName = getTargetName(name, newParent);
+ String newName = newParent.getTargetName(name);
try {
if (isHijacked()) {
fileSystem.moveItem(parent.getPathname(), name, newParent.getPathname(),
@@ -1595,15 +1629,49 @@ public class GhidraFileData {
}
}
- private String getTargetName(String preferredName, GhidraFolderData newParent)
- throws IOException {
- String newName = preferredName;
- int i = 1;
- while (newParent.getFileData(newName, false) != null) {
- newName = preferredName + "." + i;
- i++;
+ boolean isLinkFile() {
+ synchronized (fileSystem) {
+ try {
+ return LinkHandler.class.isAssignableFrom(getContentHandler().getClass());
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Get URL associated with a link-file
+ * @return link-file URL or null if not a link-file
+ * @throws IOException if an IO error occurs
+ */
+ URL getLinkFileURL() throws IOException {
+ if (!isLinkFile()) {
+ return null;
+ }
+ FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
+ return LinkHandler.getURL(item);
+ }
+
+ public boolean isLinkingSupported() {
+ synchronized (fileSystem) {
+ try {
+ return getContentHandler().getLinkHandler() != null;
+ }
+ catch (IOException e) {
+ return false; // ignore error
+ }
+ }
+ }
+
+ public DomainFile copyToAsLink(GhidraFolderData newParentData) throws IOException {
+ synchronized (fileSystem) {
+ LinkHandler> lh = getContentHandler().getLinkHandler();
+ if (lh == null) {
+ return null;
+ }
+ return newParentData.copyAsLink(fileManager, getPathname(), name, lh);
}
- return newName;
}
GhidraFile copyTo(GhidraFolderData newParentData, TaskMonitor monitor)
@@ -1615,7 +1683,7 @@ public class GhidraFileData {
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
String pathname = newParentData.getPathname();
String contentType = item.getContentType();
- String targetName = getTargetName(name, newParentData);
+ String targetName = newParentData.getTargetName(name);
String user = ClientUtil.getUserName();
try {
if (item instanceof DatabaseItem) {
@@ -1668,7 +1736,7 @@ public class GhidraFileData {
}
String pathname = destFolderData.getPathname();
String contentType = versionedFolderItem.getContentType();
- String targetName = getTargetName(name + "_v" + version, destFolderData);
+ String targetName = destFolderData.getTargetName(name + "_v" + version);
String user = ClientUtil.getUserName();
try {
BufferFile bufferFile = ((DatabaseItem) versionedFolderItem).open(version);
@@ -1697,7 +1765,9 @@ public class GhidraFileData {
/**
* Copy this file to make a private file if it is versioned. This method should be called
* only when a non shared project is being converted to a shared project.
- * @throws IOException
+ * @param monitor task monitor
+ * @throws IOException if an IO error occurs
+ * @throws CancelledException if task is cancelled
*/
void convertToPrivateFile(TaskMonitor monitor) throws IOException, CancelledException {
synchronized (fileSystem) {
@@ -1749,7 +1819,10 @@ public class GhidraFileData {
Map getMetadata() {
FolderItem item = (folderItem != null) ? folderItem : versionedFolderItem;
+ return getMetadata(item);
+ }
+ static Map getMetadata(FolderItem item) {
GenericDomainObjectDB genericDomainObj = null;
try {
if (item instanceof DatabaseItem) {
@@ -1767,7 +1840,7 @@ public class GhidraFileData {
// file created with newer version of Ghidra
}
catch (IOException e) {
- Msg.error(this, "Read meta-data error", e);
+ Msg.error(GhidraFileData.class, "Read meta-data error", e);
}
finally {
if (genericDomainObj != null) {
@@ -1782,13 +1855,17 @@ public class GhidraFileData {
if (fileManager == null) {
return name + "(disposed)";
}
+ ProjectLocator projectLocator = fileManager.getProjectLocator();
+ if (projectLocator.isTransient()) {
+ return fileManager.getProjectLocator().getName() + getPathname();
+ }
return fileManager.getProjectLocator().getName() + ":" + getPathname();
}
- private class GenericDomainObjectDB extends DomainObjectAdapterDB {
+ private static class GenericDomainObjectDB extends DomainObjectAdapterDB {
protected GenericDomainObjectDB(DBHandle dbh) throws IOException {
- super(dbh, "Generic", 500, GhidraFileData.this);
+ super(dbh, "Generic", 500, dbh);
loadMetadata();
}
@@ -1803,7 +1880,7 @@ public class GhidraFileData {
}
public void release() {
- release(GhidraFileData.this);
+ release(dbh);
}
}
@@ -1816,8 +1893,8 @@ class VersionIcon implements Icon {
private static Color VERSION_ICON_COLOR_LIGHT =
new GColor("color.bg.ghidra.file.data.version.icon.light");
- private static final int WIDTH = 18;
- private static final int HEIGHT = 17;
+ private static final int WIDTH = GhidraFileData.ICON_WIDTH;
+ private static final int HEIGHT = GhidraFileData.ICON_HEIGHT;
@Override
public int getIconHeight() {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java
index 12986ee1e3..3a3de7ff1e 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java
@@ -16,16 +16,19 @@
package ghidra.framework.data;
import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.util.List;
+import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.FileSystem;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.InvalidNameException;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
-import ghidra.util.task.TaskMonitorAdapter;
public class GhidraFolder implements DomainFolder {
@@ -83,19 +86,6 @@ public class GhidraFolder implements DomainFolder {
return fileData;
}
- GhidraFolderData getFolderPathData(String folderPath) throws FileNotFoundException {
- GhidraFolderData parentData = (folderPath.startsWith(FileSystem.SEPARATOR))
- ? fileManager.getRootFolderData()
- : getFolderData();
- GhidraFolderData folderData = parentData.getFolderPathData(folderPath, false);
- if (folderData == null) {
- String path = (folderPath.startsWith(FileSystem.SEPARATOR)) ? folderPath
- : getPathname(folderPath);
- throw new FileNotFoundException("folder " + path + " not found");
- }
- return folderData;
- }
-
GhidraFolderData getFolderData() throws FileNotFoundException {
if (parent == null) {
return fileManager.getRootFolderData();
@@ -140,10 +130,10 @@ public class GhidraFolder implements DomainFolder {
/**
* Refresh folder data - used for testing only
- * @throws IOException
+ * @throws IOException if an IO error occurs
*/
void refreshFolderData() throws IOException {
- getFolderData().refresh(false, true, TaskMonitorAdapter.DUMMY_MONITOR);
+ getFolderData().refresh(false, true, TaskMonitor.DUMMY);
}
@Override
@@ -193,6 +183,40 @@ public class GhidraFolder implements DomainFolder {
return path;
}
+ @Override
+ public URL getSharedProjectURL() {
+ ProjectLocator projectLocator = getProjectLocator();
+ URL projectURL = projectLocator.getURL();
+ if (!GhidraURL.isServerRepositoryURL(projectURL)) {
+ RepositoryAdapter repository = fileManager.getRepository();
+ if (repository == null) {
+ return null;
+ }
+ // NOTE: only supports ghidra protocol without extension protocol.
+ // Assumes any extension protocol use would be reflected in projectLocator URL.
+ ServerInfo serverInfo = repository.getServerInfo();
+ projectURL = GhidraURL.makeURL(serverInfo.getServerName(), serverInfo.getPortNumber(),
+ repository.getName());
+ }
+ try {
+ // Direct URL construction done so that ghidra protocol
+ // extension may be supported
+ String urlStr = projectURL.toExternalForm();
+ if (urlStr.endsWith(FileSystem.SEPARATOR)) {
+ urlStr = urlStr.substring(0, urlStr.length() - 1);
+ }
+ String path = getPathname();
+ if (!path.endsWith(FileSystem.SEPARATOR)) {
+ path += FileSystem.SEPARATOR;
+ }
+ urlStr += path;
+ return new URL(urlStr);
+ }
+ catch (MalformedURLException e) {
+ return null;
+ }
+ }
+
@Override
public boolean isInWritableProject() {
return !getProjectData().getLocalFileSystem().isReadOnly();
@@ -244,7 +268,9 @@ public class GhidraFolder implements DomainFolder {
return folderData.isEmpty();
}
catch (FileNotFoundException e) {
- return false; // TODO: what should we return if folder not found
+ // TODO: what should we return if folder not found or error occurs?
+ // True is returned to allow this method to be used to avoid continued access.
+ return true;
}
}
}
@@ -295,14 +321,14 @@ public class GhidraFolder implements DomainFolder {
public DomainFile createFile(String fileName, DomainObject obj, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
return createFolderData().createFile(fileName, obj,
- monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
+ monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
public DomainFile createFile(String fileName, File packFile, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
return createFolderData().createFile(fileName, packFile,
- monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
+ monitor != null ? monitor : TaskMonitor.DUMMY);
}
@Override
@@ -325,8 +351,11 @@ public class GhidraFolder implements DomainFolder {
if (parent == null) {
throw new UnsupportedOperationException("root folder may not be moved");
}
+ if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
+ throw new UnsupportedOperationException("newParent does not support moveTo");
+ }
GhidraFolderData folderData = getFolderData();
- GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation
+ GhidraFolder newGhidraParent = (GhidraFolder) newParent;
return folderData.moveTo(newGhidraParent.getFolderData());
}
@@ -334,9 +363,22 @@ public class GhidraFolder implements DomainFolder {
public GhidraFolder copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException {
GhidraFolderData folderData = getFolderData();
- GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation
+ if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
+ throw new UnsupportedOperationException("newParent does not support copyTo");
+ }
+ GhidraFolder newGhidraParent = (GhidraFolder) newParent;
return folderData.copyTo(newGhidraParent.getFolderData(),
- monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
+ monitor != null ? monitor : TaskMonitor.DUMMY);
+ }
+
+ @Override
+ public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
+ GhidraFolderData folderData = getFolderData();
+ if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
+ throw new UnsupportedOperationException("newParent does not support copyToAsLink");
+ }
+ GhidraFolder newGhidraParent = (GhidraFolder) newParent;
+ return folderData.copyToAsLink(newGhidraParent.getFolderData());
}
/**
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java
index 609b16dc1a..39e78a57fa 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java
@@ -16,10 +16,13 @@
package ghidra.framework.data;
import java.io.*;
+import java.net.URL;
import java.util.*;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.framework.protocol.ghidra.TransientProjectData;
import ghidra.framework.store.FileSystem;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.*;
@@ -257,9 +260,10 @@ class GhidraFolderData {
return folderList.isEmpty() && fileList.isEmpty();
}
catch (IOException e) {
- // ignore
+ // TODO: what should we return if folder not found or error occurs?
+ // True is returned to allow this method to be used to avoid continued access.
+ return true;
}
- return false;
}
List getFileNames() {
@@ -919,7 +923,7 @@ class GhidraFolderData {
DomainFile oldDf = doa.getDomainFile();
try {
- ContentHandler ch = DomainObjectAdapter.getContentHandler(doa);
+ ContentHandler> ch = DomainObjectAdapter.getContentHandler(doa);
ch.createFile(fileSystem, null, getPathname(), fileName, obj, monitor);
if (oldDf != null) {
@@ -1140,6 +1144,88 @@ class GhidraFolderData {
}
}
+ DomainFile copyToAsLink(GhidraFolderData newParentData) throws IOException {
+ synchronized (fileSystem) {
+ String linkFilename = name;
+ if (linkFilename == null) {
+ if (fileManager instanceof TransientProjectData) {
+ linkFilename = fileManager.getRepository().getName();
+ }
+ else {
+ linkFilename = fileManager.getProjectLocator().getName();
+ }
+ }
+ return newParentData.copyAsLink(fileManager, getPathname(), linkFilename,
+ FolderLinkContentHandler.INSTANCE);
+ }
+ }
+
+ DomainFile copyAsLink(ProjectData sourceProjectData, String pathname, String linkFilename,
+ LinkHandler> lh) throws IOException {
+ synchronized (fileSystem) {
+ if (fileSystem.isReadOnly()) {
+ throw new ReadOnlyException("copyAsLink permitted to writeable project only");
+ }
+
+ if (sourceProjectData == fileManager) {
+ // internal linking not yet supported
+ Msg.error(this, "Internal file/folder links not yet supported");
+ return null;
+ }
+
+ URL ghidraUrl = null;
+ if (sourceProjectData instanceof TransientProjectData) {
+ RepositoryAdapter repository = sourceProjectData.getRepository();
+ ServerInfo serverInfo = repository.getServerInfo();
+ ghidraUrl =
+ GhidraURL.makeURL(serverInfo.getServerName(), serverInfo.getPortNumber(),
+ repository.getName(), pathname);
+ }
+ else {
+ ProjectLocator projectLocator = sourceProjectData.getProjectLocator();
+ if (projectLocator.equals(fileManager.getProjectLocator())) {
+ return null; // local internal linking not supported
+ }
+ ghidraUrl = GhidraURL.makeURL(projectLocator, pathname, null);
+ }
+
+ String newName = linkFilename;
+ int i = 1;
+ while (true) {
+ GhidraFileData fileData = getFileData(newName, false);
+ if (fileData != null) {
+ // return existing file if link URL matches
+ if (ghidraUrl.equals(fileData.getLinkFileURL())) {
+ return getDomainFile(newName);
+ }
+ newName = linkFilename + "." + i;
+ ++i;
+ }
+ break;
+ }
+
+ try {
+ lh.createLink(ghidraUrl, fileSystem, getPathname(), newName);
+ }
+ catch (InvalidNameException e) {
+ throw new IOException(e); // unexpected
+ }
+
+ fileChanged(newName);
+ return getDomainFile(newName);
+ }
+ }
+
+ String getTargetName(String preferredName) throws IOException {
+ String newName = preferredName;
+ int i = 1;
+ while (getFileData(newName, false) != null) {
+ newName = preferredName + "." + i;
+ i++;
+ }
+ return newName;
+ }
+
/**
* used for testing
*/
@@ -1156,6 +1242,10 @@ class GhidraFolderData {
@Override
public String toString() {
+ ProjectLocator projectLocator = fileManager.getProjectLocator();
+ if (projectLocator.isTransient()) {
+ return fileManager.getProjectLocator().getName() + getPathname();
+ }
return fileManager.getProjectLocator().getName() + ":" + getPathname();
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java
new file mode 100644
index 0000000000..f9e65aacd5
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java
@@ -0,0 +1,210 @@
+/* ###
+ * 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 java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+
+import javax.help.UnsupportedOperationException;
+import javax.swing.Icon;
+
+import generic.theme.GIcon;
+import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.*;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
+import ghidra.framework.store.FileSystem;
+import ghidra.framework.store.FolderItem;
+import ghidra.framework.store.local.LocalFileSystem;
+import ghidra.util.InvalidNameException;
+import ghidra.util.exception.*;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * NOTE: ALL ContentHandler implementations MUST END IN "ContentHandler". If not,
+ * the ClassSearcher will not find them.
+ *
+ * LinkHandler defines an application interface for handling domain files which are
+ * shortcut links to another supported content type.
+ *
+ * @param {@link URLLinkObject} implementation class
+ */
+public abstract class LinkHandler extends DBContentHandler {
+
+ // TODO: Need to improve by making this meta data on file instead of database content.
+ // Metadata use would eliminate need for DB but we lack support for non-DB files.
+
+ public static final String URL_METADATA_KEY = "link.url";
+
+ // 16x16 link icon where link is placed in lower-left corner
+ public static final Icon LINK_ICON = new GIcon("icon.content.handler.link.overlay");
+
+ /**
+ * Create a link file using the specified URL
+ * @param ghidraUrl link URL (must be a Ghidra URL - see {@link GhidraURL}).
+ * @param fs filesystem where link file should be created
+ * @param folderPath folder path which should contain link file
+ * @param linkFilename link filename
+ * @throws IOException if an IO error occurs
+ * @throws InvalidNameException if invalid folderPath or linkFilename specified
+ */
+ protected final void createLink(URL ghidraUrl, LocalFileSystem fs, String folderPath,
+ String linkFilename) throws IOException, InvalidNameException {
+ URLLinkObject link = new URLLinkObject(linkFilename, ghidraUrl, this);
+ try {
+ createFile(fs, null, folderPath, linkFilename, link, TaskMonitor.DUMMY);
+ }
+ catch (CancelledException e) {
+ throw new AssertException(e); // won't happen
+ }
+ finally {
+ link.release(this);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final T getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
+ Object consumer, TaskMonitor monitor)
+ throws IOException, VersionException, CancelledException {
+
+ if (!okToUpgrade) {
+ throw new IllegalArgumentException("okToUpgrade must be true");
+ }
+
+ URL url = getURL(item);
+
+ Class> domainObjectClass = getDomainObjectClass();
+ if (domainObjectClass == null) {
+ throw new UnsupportedOperationException("");
+ }
+
+ GhidraURLWrappedContent wrappedContent = null;
+ Object content = null;
+ try {
+ GhidraURLConnection c = (GhidraURLConnection) url.openConnection();
+ Object obj = c.getContent(); // read-only access
+ if (c.getStatusCode() == StatusCode.UNAUTHORIZED) {
+ throw new IOException("Authorization failure");
+ }
+ if (!(obj instanceof GhidraURLWrappedContent)) {
+ throw new IOException("Unsupported linked content");
+ }
+ wrappedContent = (GhidraURLWrappedContent) obj;
+ content = wrappedContent.getContent(consumer);
+ if (!(content instanceof DomainFile)) {
+ throw new IOException("Unsupported linked content: " + content.getClass());
+ }
+ DomainFile linkedFile = (DomainFile) content;
+ if (!getDomainObjectClass().isAssignableFrom(linkedFile.getDomainObjectClass())) {
+ throw new BadLinkException(
+ "Excepted " + getDomainObjectClass() + " but linked to " +
+ linkedFile.getDomainObjectClass());
+ }
+ return (T) linkedFile.getReadOnlyDomainObject(consumer, version, monitor);
+ }
+ finally {
+ if (content != null) {
+ wrappedContent.release(content, consumer);
+ }
+ }
+ }
+
+ @Override
+ public final T getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
+ boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
+ throws IOException, CancelledException, VersionException {
+ // Always upgrade if needed for read-only object
+ return getReadOnlyObject(item, DomainFile.DEFAULT_VERSION, true, consumer, monitor);
+ }
+
+ @Override
+ public T getImmutableObject(FolderItem item, Object consumer, int version, int minChangeVersion,
+ TaskMonitor monitor) throws IOException, CancelledException, VersionException {
+ throw new UnsupportedOperationException("link-file does not support getImmutableObject");
+ }
+
+ @Override
+ public final ChangeSet getChangeSet(FolderItem versionedFolderItem, int olderVersion,
+ int newerVersion) throws VersionException, IOException {
+ return null;
+ }
+
+ @Override
+ public final DomainObjectMergeManager getMergeManager(DomainObject resultsObj,
+ DomainObject sourceObj,
+ DomainObject originalObj, DomainObject latestObj) {
+ return null;
+ }
+
+ @Override
+ public final boolean isPrivateContentType() {
+ // NOTE: URL must be checked - only repository-based links may be versioned
+ return true;
+ }
+
+ /**
+ * Get the link URL which corresponds to the specified link file.
+ * See {@link DomainFile#isLinkFile()}.
+ * @param linkFile link-file domain file
+ * @return link URL
+ * @throws MalformedURLException if link is bad or unsupported.
+ * @throws IOException if IO error or supported link file not specified
+ */
+ public static URL getURL(DomainFile linkFile) throws IOException {
+ String contentType = linkFile.getContentType();
+ ContentHandler> ch = DomainObjectAdapter.getContentHandler(contentType);
+ if (ch instanceof LinkHandler) {
+ Map metadata = linkFile.getMetadata();
+ String urlStr = metadata.get(URL_METADATA_KEY);
+ if (urlStr != null) {
+ return new URL(urlStr);
+ }
+ }
+ throw new IOException("Invalid link file: " + contentType);
+ }
+
+ /**
+ * Get the link URL which corresponds to the specified link file.
+ * See {@link DomainFile#isLinkFile()}.
+ * @param linkFile link-file folder item
+ * @return link URL
+ * @throws MalformedURLException if link is bad or unsupported.
+ * @throws IOException if IO error or supported link file not specified
+ */
+ static URL getURL(FolderItem linkFile) throws IOException {
+
+ String contentType = linkFile.getContentType();
+ ContentHandler> ch = DomainObjectAdapter.getContentHandler(contentType);
+ if (ch instanceof LinkHandler) {
+ Map metadata = GhidraFileData.getMetadata(linkFile);
+ String urlStr = metadata.get(URL_METADATA_KEY);
+ if (urlStr != null) {
+ return new URL(urlStr);
+ }
+ }
+ throw new IOException("Invalid link file: " + contentType);
+ }
+
+ /**
+ * Get the base icon for this link-file which does not include the
+ * link overlay icon.
+ */
+ @Override
+ abstract public Icon getIcon();
+
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java
new file mode 100644
index 0000000000..848f89c494
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java
@@ -0,0 +1,426 @@
+/* ###
+ * 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 java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+
+import javax.help.UnsupportedOperationException;
+import javax.swing.Icon;
+
+import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.framework.store.*;
+import ghidra.util.*;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.exception.VersionException;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * {@code LinkedGhidraFile} corresponds to a {@link DomainFile} contained within a
+ * {@link LinkedGhidraFolder}.
+ */
+class LinkedGhidraFile implements LinkedDomainFile {
+
+ private final LinkedGhidraSubFolder parent;
+ private final String fileName;
+
+ LinkedGhidraFile(LinkedGhidraSubFolder parent, String fileName) {
+ this.parent = parent;
+ this.fileName = fileName;
+ }
+
+ @Override
+ public DomainFile getLinkedFile() throws IOException {
+ return parent.getLinkedFile(fileName);
+ }
+
+ private DomainFile getLinkedFileNoError() {
+ return parent.getLinkedFileNoError(fileName);
+ }
+
+ @Override
+ public DomainFolder getParent() {
+ return parent;
+ }
+
+ @Override
+ public String getName() {
+ return fileName;
+ }
+
+ @Override
+ public int compareTo(DomainFile df) {
+ return fileName.compareToIgnoreCase(df.getName());
+ }
+
+ @Override
+ public boolean exists() {
+ return getLinkedFileNoError() != null;
+ }
+
+ @Override
+ public String getFileID() {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getFileID() : null;
+ }
+
+ @Override
+ public DomainFile setName(String newName) throws InvalidNameException, IOException {
+ throw new ReadOnlyException("linked file is read only");
+ }
+
+ @Override
+ public String getPathname() {
+ // pathname within project containing folder-link
+ // getParent() may return a non-linked folder
+ String path = getParent().getPathname();
+ if (path.length() != FileSystem.SEPARATOR.length()) {
+ path += FileSystem.SEPARATOR;
+ }
+ path += fileName;
+ return path;
+ }
+
+ @Override
+ public URL getSharedProjectURL() {
+ URL folderURL = parent.getSharedProjectURL();
+ if (GhidraURL.isServerRepositoryURL(folderURL)) {
+ // Direct URL construction done so that ghidra protocol
+ // extension may be supported
+ try {
+ return new URL(folderURL.toExternalForm() + fileName);
+ }
+ catch (MalformedURLException e) {
+ // ignore
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public ProjectLocator getProjectLocator() {
+ return parent.getProjectLocator();
+ }
+
+ @Override
+ public String getContentType() {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getContentType() : ContentHandler.UNKNOWN_CONTENT;
+ }
+
+ @Override
+ public Class extends DomainObject> getDomainObjectClass() {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getDomainObjectClass() : DomainObject.class;
+ }
+
+ @Override
+ public ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException {
+ return null;
+ }
+
+ @Override
+ public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
+ TaskMonitor monitor) throws VersionException, IOException, CancelledException {
+ return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor);
+ }
+
+ @Override
+ public DomainObject getOpenedDomainObject(Object consumer) {
+ return null;
+ }
+
+ @Override
+ public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor)
+ throws VersionException, IOException, CancelledException {
+ return getLinkedFile().getReadOnlyDomainObject(consumer, version, monitor);
+ }
+
+ @Override
+ public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor)
+ throws VersionException, IOException, CancelledException {
+ return getLinkedFile().getImmutableDomainObject(consumer, version, monitor);
+ }
+
+ @Override
+ public void save(TaskMonitor monitor) throws IOException, CancelledException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean canSave() {
+ return false;
+ }
+
+ @Override
+ public boolean canRecover() {
+ return false;
+ }
+
+ @Override
+ public boolean takeRecoverySnapshot() throws IOException {
+ return true;
+ }
+
+ @Override
+ public boolean isInWritableProject() {
+ return false; // While project may be writeable this folder/file is not
+ }
+
+ @Override
+ public long getLastModifiedTime() {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getLastModifiedTime() : 0;
+ }
+
+ @Override
+ public Icon getIcon(boolean disabled) {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getIcon(disabled) : UNSUPPORTED_FILE_ICON;
+ }
+
+ @Override
+ public boolean isCheckedOut() {
+ return false;
+ }
+
+ @Override
+ public boolean isCheckedOutExclusive() {
+ return false;
+ }
+
+ @Override
+ public boolean modifiedSinceCheckout() {
+ return false;
+ }
+
+ @Override
+ public boolean canCheckout() {
+ return false;
+ }
+
+ @Override
+ public boolean canCheckin() {
+ return false;
+ }
+
+ @Override
+ public boolean canMerge() {
+ return false;
+ }
+
+ @Override
+ public boolean canAddToRepository() {
+ return false;
+ }
+
+ @Override
+ public void setReadOnly(boolean state) throws IOException {
+ // ignore
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return true; // not reflected by icon
+ }
+
+ @Override
+ public boolean isVersioned() {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.isVersioned() : false;
+ }
+
+ @Override
+ public boolean isHijacked() {
+ return false;
+ }
+
+ @Override
+ public int getLatestVersion() {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getLatestVersion() : DomainFile.DEFAULT_VERSION;
+ }
+
+ @Override
+ public boolean isLatestVersion() {
+ return true;
+ }
+
+ @Override
+ public int getVersion() {
+ // TODO: Do we want to reveal linked-local-project checkout details?
+ return getLatestVersion();
+ }
+
+ @Override
+ public Version[] getVersionHistory() throws IOException {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getVersionHistory() : new Version[0];
+ }
+
+ @Override
+ public void addToVersionControl(String comment, boolean keepCheckedOut, TaskMonitor monitor)
+ throws IOException, CancelledException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean checkout(boolean exclusive, TaskMonitor monitor)
+ throws IOException, CancelledException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void checkin(CheckinHandler checkinHandler, boolean okToUpgrade, TaskMonitor monitor)
+ throws IOException, VersionException, CancelledException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void merge(boolean okToUpgrade, TaskMonitor monitor)
+ throws IOException, VersionException, CancelledException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void undoCheckout(boolean keep) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void undoCheckout(boolean keep, boolean force) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void terminateCheckout(long checkoutId) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ItemCheckoutStatus[] getCheckouts() throws IOException {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getCheckouts() : new ItemCheckoutStatus[0];
+ }
+
+ @Override
+ public ItemCheckoutStatus getCheckoutStatus() throws IOException {
+ // TODO: Do we want to reveal linked-local-project checkout details?
+ return null;
+ }
+
+ @Override
+ public void delete() throws IOException {
+ throw new ReadOnlyException("linked file is read only");
+ }
+
+ @Override
+ public void delete(int version) throws IOException {
+ throw new ReadOnlyException("linked file is read only");
+ }
+
+ @Override
+ public DomainFile moveTo(DomainFolder newParent) throws IOException {
+ throw new ReadOnlyException("linked file is read only");
+ }
+
+ @Override
+ public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
+ throws IOException, CancelledException {
+ return getLinkedFile().copyTo(newParent, monitor);
+ }
+
+ @Override
+ public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
+ throws IOException, CancelledException {
+ return getLinkedFile().copyVersionTo(version, destFolder, monitor);
+ }
+
+ @Override
+ public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
+ return getLinkedFile().copyToAsLink(newParent);
+ }
+
+ @Override
+ public boolean isLinkingSupported() {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.isLinkingSupported() : false;
+ }
+
+ @Override
+ public List> getConsumers() {
+ return List.of();
+ }
+
+ @Override
+ public boolean isChanged() {
+ return false;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return false; // domain file proxy always used
+ }
+
+ @Override
+ public boolean isBusy() {
+ return false; // domain file proxy always used
+ }
+
+ @Override
+ public void packFile(File file, TaskMonitor monitor) throws IOException, CancelledException {
+ getLinkedFile().packFile(file, monitor);
+ }
+
+ @Override
+ public Map getMetadata() {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getMetadata() : Map.of();
+ }
+
+ @Override
+ public long length() throws IOException {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.length() : 0;
+ }
+
+ @Override
+ public boolean isLinkFile() {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.isLinkFile() : false;
+ }
+
+ @Override
+ public DomainFolder followLink() {
+ try {
+ return FolderLinkContentHandler.getReadOnlyLinkedFolder(this);
+ }
+ catch (IOException e) {
+ Msg.error(this, "Failed to following folder-link: " + getPathname());
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "LinkedGhidraFile: " + 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
new file mode 100644
index 0000000000..6193d4864b
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java
@@ -0,0 +1,138 @@
+/* ###
+ * 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 java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URL;
+
+import javax.swing.Icon;
+
+import generic.theme.GIcon;
+import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.framework.store.FileSystem;
+
+/**
+ * {@code LinkedGhidraFolder} provides the base {@link LinkedDomainFolder} implementation which
+ * corresponds to a project folder-link (see {@link FolderLinkContentHandler}).
+ */
+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");
+
+ private final Project activeProject;
+ private final DomainFolder localParent;
+ private final URL folderUrl;
+
+ private String linkedPathname;
+
+ private URL projectUrl;
+
+ /**
+ * Construct a linked-folder.
+ * @param activeProject active project responsible for linked project life-cycle management.
+ * @param localParent local domain folder which contains folder-link or corresponds directly to
+ * folder-link (name=null).
+ * @param linkFilename folder-link filename
+ * @param folderUrl linked folder URL
+ */
+ LinkedGhidraFolder(Project activeProject, DomainFolder localParent, String linkFilename,
+ URL folderUrl) {
+ super(linkFilename);
+
+ if (!GhidraURL.isServerRepositoryURL(folderUrl) &&
+ !GhidraURL.isLocalProjectURL(folderUrl)) {
+ throw new IllegalArgumentException("Invalid Ghidra URL: " + folderUrl);
+ }
+
+ this.activeProject = activeProject;
+ this.localParent = localParent;
+ this.folderUrl = folderUrl;
+
+ linkedPathname = GhidraURL.getProjectPathname(folderUrl);
+ if (linkedPathname.length() > 0 && linkedPathname.endsWith(FileSystem.SEPARATOR)) {
+ linkedPathname = linkedPathname.substring(0, linkedPathname.length() - 1);
+ }
+ }
+
+ /**
+ * 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
+ */
+ public URL getProjectURL() {
+ if (projectUrl == null) {
+ projectUrl = GhidraURL.getProjectURL(folderUrl);
+ }
+ return projectUrl;
+ }
+
+ LinkedGhidraFolder getLinkedRootFolder() {
+ return this;
+ }
+
+ DomainFolder getLinkedFolder(String linkedPath) throws IOException {
+
+ ProjectData projectData = activeProject.addProjectView(getProjectURL(), false);
+ if (projectData == null) {
+ throw new FileNotFoundException();
+ }
+
+ DomainFolder folder = projectData.getFolder(linkedPath);
+ if (folder == null) {
+ throw new FileNotFoundException(folderUrl.toExternalForm());
+ }
+ return folder;
+ }
+
+ @Override
+ public String getLinkedPathname() {
+ return linkedPathname;
+ }
+
+ @Override
+ public ProjectLocator getProjectLocator() {
+ return activeProject.getProjectLocator();
+ }
+
+ @Override
+ public ProjectData getProjectData() {
+ return activeProject.getProjectData();
+ }
+
+ @Override
+ public DomainFolder getParent() {
+ return localParent;
+ }
+
+ @Override
+ public String toString() {
+ return "LinkedGhidraFolder: " + getPathname();
+ }
+
+ @Override
+ public Icon getIcon(boolean isOpen) {
+ return isOpen ? FOLDER_LINK_OPEN_ICON : FOLDER_LINK_CLOSED_ICON;
+ }
+
+ @Override
+ public boolean isLinked() {
+ return true;
+ }
+}
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
new file mode 100644
index 0000000000..e2577b2da8
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java
@@ -0,0 +1,294 @@
+/* ###
+ * 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 java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.swing.Icon;
+
+import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.framework.store.FileSystem;
+import ghidra.util.*;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+class LinkedGhidraSubFolder implements LinkedDomainFolder {
+
+ private final LinkedGhidraFolder linkedRootFolder;
+ private final LinkedGhidraSubFolder parent;
+ private final String folderName;
+
+ LinkedGhidraSubFolder(String folderName) {
+ this.linkedRootFolder = getLinkedRootFolder();
+ this.parent = null; // must override getParent()
+ this.folderName = folderName;
+ }
+
+ LinkedGhidraSubFolder(LinkedGhidraSubFolder parent, String folderName) {
+ this.linkedRootFolder = parent.getLinkedRootFolder();
+ this.parent = parent;
+ this.folderName = folderName;
+ }
+
+ /**
+ * Get the linked root folder which corresponds to a folder-link
+ * (see {@link FolderLinkContentHandler}).
+ * @return linked root folder
+ */
+ LinkedGhidraFolder getLinkedRootFolder() {
+ return linkedRootFolder;
+ }
+
+ @Override
+ public boolean isInWritableProject() {
+ return false; // While project may be writeable this folder is not
+ }
+
+ @Override
+ public DomainFolder getParent() {
+ return parent;
+ }
+
+ @Override
+ public String getName() {
+ return folderName;
+ }
+
+ @Override
+ public DomainFolder getLinkedFolder() throws IOException {
+ return linkedRootFolder.getLinkedFolder(getLinkedPathname());
+ }
+
+ @Override
+ public int compareTo(DomainFolder df) {
+ return getName().compareToIgnoreCase(df.getName());
+ }
+
+ @Override
+ public DomainFolder setName(String newName) throws InvalidNameException, IOException {
+ throw new ReadOnlyException("linked folder is read only");
+ }
+
+ @Override
+ public URL getSharedProjectURL() {
+ URL projectURL = getLinkedRootFolder().getProjectURL();
+ if (GhidraURL.isServerRepositoryURL(projectURL)) {
+ String urlStr = projectURL.toExternalForm();
+ if (urlStr.endsWith(FileSystem.SEPARATOR)) {
+ urlStr = urlStr.substring(0, urlStr.length() - 1);
+ }
+ String path = getLinkedPathname();
+ if (!path.endsWith(FileSystem.SEPARATOR)) {
+ path += FileSystem.SEPARATOR;
+ }
+ try {
+ return new URL(urlStr + path);
+ }
+ catch (MalformedURLException e) {
+ // ignore
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public ProjectLocator getProjectLocator() {
+ return parent.getProjectLocator();
+ }
+
+ @Override
+ public ProjectData getProjectData() {
+ return parent.getProjectData();
+ }
+
+ @Override
+ public String getPathname() {
+ // pathname within project containing folder-link
+ // getParent() may return a non-linked folder
+ String path = getParent().getPathname();
+ if (path.length() != FileSystem.SEPARATOR.length()) {
+ path += FileSystem.SEPARATOR;
+ }
+ path += folderName;
+ return path;
+ }
+
+ /**
+ * Get the pathname of this folder within the linked-project/repository
+ * @return absolute linked folder path within the linked-project/repository
+ */
+ public String getLinkedPathname() {
+ String path = parent.getLinkedPathname();
+ if (!path.endsWith(FileSystem.SEPARATOR)) {
+ path += FileSystem.SEPARATOR;
+ }
+ path += folderName;
+ return path;
+ }
+
+ @Override
+ public LinkedGhidraSubFolder[] getFolders() {
+ try {
+ DomainFolder linkedFolder = getLinkedFolder();
+ DomainFolder[] folders = linkedFolder.getFolders();
+ LinkedGhidraSubFolder[] linkedSubFolders = new LinkedGhidraSubFolder[folders.length];
+ for (int i = 0; i < folders.length; i++) {
+ linkedSubFolders[i] = new LinkedGhidraSubFolder(this, folders[i].getName());
+ }
+ return linkedSubFolders;
+ }
+ catch (IOException e) {
+ Msg.error(this, "Linked folder failure: " + e.getMessage());
+ return new LinkedGhidraSubFolder[0];
+ }
+ }
+
+ @Override
+ public LinkedGhidraSubFolder getFolder(String name) {
+ try {
+ DomainFolder linkedFolder = getLinkedFolder();
+ DomainFolder f = linkedFolder.getFolder(name);
+ if (f != null) {
+ return new LinkedGhidraSubFolder(this, name);
+ }
+ }
+ catch (IOException e) {
+ Msg.error(this, "Linked folder failure: " + e.getMessage());
+ }
+ return null;
+ }
+
+ @Override
+ public DomainFile[] getFiles() {
+ try {
+ DomainFolder linkedFolder = getLinkedFolder();
+ 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());
+ }
+ return linkedSubFolders;
+ }
+ catch (IOException e) {
+ Msg.error(this, "Linked folder failure: " + e.getMessage());
+ return new LinkedGhidraFile[0];
+ }
+ }
+
+ /**
+ * Get the true file within this linked folder.
+ * @param name file name
+ * @return file or null if not found or error occurs
+ */
+ public DomainFile getLinkedFileNoError(String name) {
+ try {
+ DomainFolder linkedFolder = getLinkedFolder();
+ return linkedFolder.getFile(name);
+ }
+ catch (IOException e) {
+ Msg.error(this, "Linked folder failure: " + e.getMessage());
+ }
+ return null;
+ }
+
+ DomainFile getLinkedFile(String name) throws IOException {
+ DomainFolder linkedFolder = getLinkedFolder();
+ DomainFile df = linkedFolder.getFile(name);
+ if (df == null) {
+ throw new FileNotFoundException("linked-file '" + name + "' not found");
+ }
+ return df;
+ }
+
+ @Override
+ public DomainFile getFile(String name) {
+ DomainFile f = getLinkedFileNoError(name);
+ return f != null ? new LinkedGhidraFile(this, name) : null;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ try {
+ DomainFolder linkedFolder = getLinkedFolder();
+ return linkedFolder.isEmpty();
+ }
+ catch (IOException e) {
+ Msg.error(this, "Linked folder failure: " + e.getMessage());
+ // TODO: what should we return if folder not found or error occurs?
+ // True is returned to allow this method to be used to avoid continued access.
+ return true;
+ }
+ }
+
+ @Override
+ public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor)
+ throws InvalidNameException, IOException, CancelledException {
+ throw new ReadOnlyException("linked folder is read only");
+ }
+
+ @Override
+ public DomainFile createFile(String name, File packFile, TaskMonitor monitor)
+ throws InvalidNameException, IOException, CancelledException {
+ throw new ReadOnlyException("linked folder is read only");
+ }
+
+ @Override
+ public DomainFolder createFolder(String name) throws InvalidNameException, IOException {
+ throw new ReadOnlyException("linked folder is read only");
+ }
+
+ @Override
+ public void delete() throws IOException {
+ throw new ReadOnlyException("linked folder is read only");
+ }
+
+ @Override
+ public DomainFolder moveTo(DomainFolder newParent) throws IOException {
+ throw new ReadOnlyException("linked folder is read only");
+ }
+
+ @Override
+ public DomainFolder copyTo(DomainFolder newParent, TaskMonitor monitor)
+ throws IOException, CancelledException {
+ DomainFolder linkedFolder = getLinkedFolder();
+ return linkedFolder.copyTo(newParent, monitor);
+ }
+
+ @Override
+ public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
+ DomainFolder linkedFolder = getLinkedFolder();
+ return linkedFolder.copyToAsLink(newParent);
+ }
+
+ @Override
+ public void setActive() {
+ // do nothing
+ }
+
+ @Override
+ public String toString() {
+ return "LinkedGhidraSubFolder: " + getPathname();
+ }
+
+ @Override
+ public Icon getIcon(boolean isOpen) {
+ return isOpen ? OPEN_FOLDER_ICON : CLOSED_FOLDER_ICON;
+ }
+
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectFileManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectFileManager.java
index 9f1044f07a..11c0d51ab2 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectFileManager.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectFileManager.java
@@ -16,13 +16,12 @@
package ghidra.framework.data;
import java.io.*;
-import java.net.URL;
import java.util.*;
+import docking.widgets.OptionDialog;
import generic.timer.GhidraSwinglessTimer;
import ghidra.framework.client.*;
import ghidra.framework.model.*;
-import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.remote.User;
import ghidra.framework.store.*;
import ghidra.framework.store.FileSystem;
@@ -82,6 +81,7 @@ public class ProjectFileManager implements ProjectData {
private TaskMonitorAdapter projectDisposalMonitor = new TaskMonitorAdapter();
+ private ProjectLock projectLock;
private String owner;
/**
@@ -91,34 +91,44 @@ public class ProjectFileManager implements ProjectData {
* @param resetOwner true to reset the project owner
* @throws IOException if an i/o error occurs
* @throws NotOwnerException if inProject is true and user is not owner
+ * @throws LockException if {@code isInWritableProject} is true and unable to establish project
+ * write lock (i.e., project in-use)
* @throws FileNotFoundException if project directory not found
*/
public ProjectFileManager(ProjectLocator localStorageLocator, boolean isInWritableProject,
- boolean resetOwner) throws NotOwnerException, IOException {
+ boolean resetOwner) throws NotOwnerException, IOException, LockException {
this.localStorageLocator = localStorageLocator;
- init(false, isInWritableProject);
- if (resetOwner) {
- owner = SystemUtilities.getUserName();
- properties.putString(OWNER, owner);
- properties.writeState();
- }
- else if (isInWritableProject && !SystemUtilities.getUserName().equals(owner)) {
- if (owner == null) {
- throw new NotOwnerException("Older projects may only be opened as a View.\n" +
- "You must first create a new project or open an existing current project, \n" +
- "then use the \"Project->View\" menu action to open the older project as a view.\n" +
- "You can then drag old files into your active project.");
+ boolean success = false;
+ try {
+ init(false, isInWritableProject);
+ if (resetOwner) {
+ owner = SystemUtilities.getUserName();
+ properties.putString(OWNER, owner);
+ properties.writeState();
+ }
+ else if (isInWritableProject && !SystemUtilities.getUserName().equals(owner)) {
+ if (owner == null) {
+ throw new NotOwnerException("Older projects may only be opened as a View.\n" +
+ "You must first create a new project or open an existing current project, \n" +
+ "then use the \"Project->View\" menu action to open the older project as a view.\n" +
+ "You can then drag old files into your active project.");
+ }
+ throw new NotOwnerException("Project is owned by " + owner);
}
- throw new NotOwnerException("Project is owned by " + owner);
- }
- synchronized (fileSystem) {
- getVersionedFileSystem(isInWritableProject);
- rootFolderData = new RootGhidraFolderData(this, listenerList);
- versionedFSListener = new MyFileSystemListener();
- versionedFileSystem.addFileSystemListener(versionedFSListener);
- scheduleUserDataReconcilation();
+ synchronized (fileSystem) {
+ getVersionedFileSystem(isInWritableProject);
+ rootFolderData = new RootGhidraFolderData(this, listenerList);
+ initVersionedFSListener();
+ scheduleUserDataReconcilation();
+ }
+ success = true;
+ }
+ finally {
+ if (!success) {
+ dispose();
+ }
}
}
@@ -128,34 +138,75 @@ public class ProjectFileManager implements ProjectData {
* @param repository a repository if this is a shared project or null if it is a private project
* @param isInWritableProject true if project content is writable, false if project is read-only
* @throws IOException if an i/o error occurs
+ * @throws LockException if {@code isInWritableProject} is true and unable to establish project
+ * lock (i.e., project in-use)
*/
public ProjectFileManager(ProjectLocator localStorageLocator, RepositoryAdapter repository,
- boolean isInWritableProject) throws IOException {
+ boolean isInWritableProject) throws IOException, LockException {
this.localStorageLocator = localStorageLocator;
this.repository = repository;
- init(true, isInWritableProject);
- synchronized (fileSystem) {
- createVersionedFileSystem();
- rootFolderData = new RootGhidraFolderData(this, listenerList);
- versionedFSListener = new MyFileSystemListener();
- versionedFileSystem.addFileSystemListener(versionedFSListener);
+ boolean success = false;
+ try {
+ init(true, isInWritableProject);
+ synchronized (fileSystem) {
+ createVersionedFileSystem();
+ rootFolderData = new RootGhidraFolderData(this, listenerList);
+ initVersionedFSListener();
+ }
+ success = true;
+ }
+ finally {
+ if (!success) {
+ dispose();
+ }
}
}
- ProjectFileManager(LocalFileSystem fileSystem, FileSystem versionedFileSystem) {
+ /**
+ * Constructor for test use only. A non-existing {@link ProjectLocator} is used without
+ * project locking.
+ * @param fileSystem an existing non-versioned local file-system
+ * @param versionedFileSystem an existing versioned file-system
+ * @throws IOException if an IO error occurs
+ */
+ ProjectFileManager(LocalFileSystem fileSystem, FileSystem versionedFileSystem)
+ throws IOException {
this.localStorageLocator = new ProjectLocator(null, "Test");
owner = SystemUtilities.getUserName();
- synchronized (fileSystem) {
- this.fileSystem = fileSystem;
- this.versionedFileSystem = versionedFileSystem;
- rootFolderData = new RootGhidraFolderData(this, listenerList);
- versionedFSListener = new MyFileSystemListener();
- versionedFileSystem.addFileSystemListener(versionedFSListener);
- scheduleUserDataReconcilation();
+ boolean success = false;
+ try {
+ synchronized (fileSystem) {
+ this.fileSystem = fileSystem;
+ this.versionedFileSystem = versionedFileSystem;
+ rootFolderData = new RootGhidraFolderData(this, listenerList);
+ initVersionedFSListener();
+ scheduleUserDataReconcilation();
+ success = true;
+ }
+ }
+ finally {
+ if (!success) {
+ dispose();
+ }
}
}
- private void init(boolean create, boolean isInWritableProject) throws IOException {
+ private void initVersionedFSListener() throws IOException {
+ // Listener not installed for local read-only versioned file-system
+ if (versionedFileSystem.isShared() || !versionedFileSystem.isReadOnly()) {
+ if (versionedFSListener == null) {
+ versionedFSListener = new MyFileSystemListener();
+ }
+ versionedFileSystem.addFileSystemListener(versionedFSListener);
+ }
+ else {
+ versionedFSListener = null;
+ }
+ }
+
+ private void init(boolean create, boolean isInWritableProject)
+ throws IOException, LockException {
+
projectDir = localStorageLocator.getProjectDir();
properties = new PropertyFile(projectDir, PROPERTY_FILENAME, "/", PROPERTY_FILENAME);
if (create) {
@@ -163,11 +214,13 @@ public class ProjectFileManager implements ProjectData {
throw new DuplicateFileException(
"Project directory already exists: " + projectDir.getCanonicalPath());
}
+ File markerFile = localStorageLocator.getMarkerFile();
+ if (markerFile.exists()) {
+ throw new DuplicateFileException(
+ "Project marker file already exists: " + markerFile.getCanonicalPath());
+ }
projectDir.mkdir();
localStorageLocator.getMarkerFile().createNewFile();
- owner = SystemUtilities.getUserName();
- properties.putString(OWNER, owner);
- properties.writeState();
}
else {
if (!projectDir.isDirectory()) {
@@ -178,22 +231,92 @@ public class ProjectFileManager implements ProjectData {
throw new ReadOnlyException(
"Project " + localStorageLocator.getName() + " is read-only");
}
- properties.readState();
owner = properties.getString(OWNER, SystemUtilities.getUserName());
}
- else if (isInWritableProject) {
- owner = SystemUtilities.getUserName();
- properties.putString(OWNER, owner);
- properties.writeState();
- }
else {
owner = ""; // Unknown owner
}
}
+
+ if (isInWritableProject) {
+ initLock(create);
+ }
+
getPrivateFileSystem(create, isInWritableProject);
getUserFileSystem(isInWritableProject);
}
+ private void initLock(boolean creatingProject) throws LockException, IOException {
+ this.projectLock = getProjectLock(localStorageLocator, !creatingProject);
+ if (projectLock == null) {
+ throw new LockException("Unable to lock project! " + localStorageLocator);
+ }
+
+ if (!properties.exists()) {
+ owner = SystemUtilities.getUserName();
+ properties.putString(OWNER, owner);
+ properties.writeState();
+ }
+ }
+
+ /**
+ * Creates a ProjectLock and attempts to lock it. This handles the case
+ * where the project was previously locked.
+ *
+ * @param locator the project locator
+ * @param allowInteractiveForce if true, when a lock cannot be obtained, the
+ * user will be prompted
+ * @return A locked ProjectLock or null if lock fails
+ */
+ private ProjectLock getProjectLock(ProjectLocator locator, boolean allowInteractiveForce) {
+ ProjectLock lock = new ProjectLock(locator);
+ if (lock.lock()) {
+ return lock;
+ }
+
+ // in headless mode, just spit out an error
+ if (!allowInteractiveForce || SystemUtilities.isInHeadlessMode()) {
+ return null;
+ }
+
+ String projectStr = "Project: " + HTMLUtilities.escapeHTML(locator.getLocation()) +
+ System.getProperty("file.separator") + HTMLUtilities.escapeHTML(locator.getName());
+ String lockInformation = lock.getExistingLockFileInformation();
+ if (!lock.canForceLock()) {
+ Msg.showInfo(getClass(), null, "Project Locked",
+ "Project is locked. You have another instance of Ghidra
" +
+ "already running with this project open (locally or remotely).
" +
+ projectStr + "
" + "Lock information: " + lockInformation);
+ return null;
+ }
+
+ int userChoice = OptionDialog.showOptionDialog(null, "Project Locked - Delete Lock?",
+ "Project is locked. You may have another instance of Ghidra
" +
+ "already running with this project opened (locally or remotely).
" + projectStr +
+ "
" + "If this is not the case, you can delete the lock file:
" +
+ locator.getProjectLockFile().getAbsolutePath() + ".
" +
+ "Lock information: " + lockInformation,
+ "Delete Lock", OptionDialog.QUESTION_MESSAGE);
+ if (userChoice == OptionDialog.OPTION_ONE) { // Delete Lock
+ if (lock.forceLock()) {
+ return lock;
+ }
+
+ Msg.showError(this, null, "Error", "Attempt to force lock failed! " + locator);
+ }
+ return null;
+ }
+
+ /**
+ * Determine if the specified project location currently has a write lock.
+ * @param locator project storage locator
+ * @return true if project data current has write-lock else false
+ */
+ public static boolean isLocked(ProjectLocator locator) {
+ ProjectLock lock = new ProjectLock(locator);
+ return lock.isLocked();
+ }
+
@Override
public int getMaxNameLength() {
return fileSystem.getMaxNameLength();
@@ -314,8 +437,9 @@ public class ProjectFileManager implements ProjectData {
/**
* Change the versioned filesystem associated with this project file manager.
- * This method is provided for testing. Care should be taken when using a
- * LocalFileSystem in a shared capacity since locking is not supported.
+ * This method is provided for testing (see {@code FakeSharedProject}).
+ * Care should be taken when using a LocalFileSystem in a shared capacity since
+ * locking is not supported.
* @param fs versioned filesystem
* @throws IOException if an IO error occurs
*/
@@ -323,9 +447,11 @@ public class ProjectFileManager implements ProjectData {
if (!fs.isVersioned()) {
throw new IllegalArgumentException("versioned filesystem required");
}
- versionedFileSystem.removeFileSystemListener(versionedFSListener);
+ if (versionedFSListener != null) {
+ versionedFileSystem.removeFileSystemListener(versionedFSListener);
+ }
versionedFileSystem = fs;
- versionedFileSystem.addFileSystemListener(versionedFSListener);
+ initVersionedFSListener();
rootFolderData.setVersionedFileSystem(versionedFileSystem);
}
@@ -385,18 +511,36 @@ public class ProjectFileManager implements ProjectData {
}
@Override
- public GhidraFolder getFolder(String path) {
+ public DomainFolder getFolder(String path) {
int len = path.length();
if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) {
throw new IllegalArgumentException(
"Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'");
}
- try {
- return getRootFolder().getFolderPathData(path).getDomainFolder();
+
+ DomainFolder folder = getRootFolder();
+ String[] split = path.split(FileSystem.SEPARATOR);
+ if (split.length == 0) {
+ return folder;
}
- catch (FileNotFoundException e) {
- return null;
+
+ for (int i = 1; i < split.length; i++) {
+ DomainFolder subFolder = folder.getFolder(split[i]);
+ if (subFolder == null) {
+ // Check for folder link-file if folder not found
+ // NOTE: if real folder name matches link-file name it will block
+ // use of folder link-file.
+ DomainFile file = folder.getFile(split[i]);
+ if (file != null && file.isLinkFile()) {
+ subFolder = file.followLink();
+ }
+ if (subFolder == null) {
+ return null;
+ }
+ }
+ folder = subFolder;
}
+ return folder;
}
@Override
@@ -468,19 +612,6 @@ public class ProjectFileManager implements ProjectData {
return fileIndex.getFileByID(fileID);
}
- @Override
- public URL getSharedFileURL(String path) {
- if (repository != null) {
- DomainFile df = getFile(path);
- if (df != null && df.isVersioned()) {
- ServerInfo server = repository.getServerInfo();
- return GhidraURL.makeURL(server.getServerName(), server.getPortNumber(),
- repository.getName(), path);
- }
- }
- return null;
- }
-
public void releaseDomainFiles(Object consumer) {
for (DomainObjectAdapter domainObj : openDomainObjects.values()) {
try {
@@ -707,7 +838,11 @@ public class ProjectFileManager implements ProjectData {
monitor.checkCanceled();
LocalFolderItem item = fileSystem.getItem(folderPath, name);
if (item.getCheckoutId() != FolderItem.DEFAULT_CHECKOUT_ID) {
- checkoutList.add(new GhidraFile(getFolder(folderPath), name));
+ GhidraFolderData folderData =
+ getRootFolderData().getFolderPathData(folderPath, false);
+ if (folderData != null) {
+ checkoutList.add(new GhidraFile(folderData.getDomainFolder(), name));
+ }
}
}
@@ -1037,16 +1172,24 @@ public class ProjectFileManager implements ProjectData {
listenerList.clearAll();
}
- synchronized (fileSystem) {
- rootFolderData.dispose();
- fileSystem.dispose();
- versionedFileSystem.dispose();
- versionedFileSystem.removeFileSystemListener(versionedFSListener);
- if (repository != null) {
- repository.disconnect();
- repository = null;
+ if (fileSystem != null) {
+ synchronized (fileSystem) {
+ if (versionedFSListener != null) {
+ versionedFileSystem.removeFileSystemListener(versionedFSListener);
+ }
+ if (repository != null) {
+ repository.disconnect();
+ repository = null;
+ }
+ rootFolderData.dispose();
+ versionedFileSystem.dispose();
+ fileSystem.dispose();
}
}
+
+ if (projectLock != null) {
+ projectLock.release();
+ }
}
GhidraFolderData getRootFolderData() {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/ProjectLock.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectLock.java
similarity index 97%
rename from Ghidra/Framework/Project/src/main/java/ghidra/framework/project/ProjectLock.java
rename to Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectLock.java
index 1509309fb0..cb1552bc45 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/ProjectLock.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectLock.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.framework.project;
+package ghidra.framework.data;
import java.io.File;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java
new file mode 100644
index 0000000000..c04cd40978
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java
@@ -0,0 +1,101 @@
+/* ###
+ * 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 java.io.File;
+import java.io.IOException;
+import java.net.URL;
+
+import javax.help.UnsupportedOperationException;
+
+import db.DBHandle;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.DomainObject;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * {@code DomainObjectAdapterLink} object provides a Ghidra URL (see {@link GhidraURL}) wrapper
+ * where the URL is intended to refer to a {@link DomainFile} within another local or remote
+ * project/repository. Link files which correspond to this type of {@link DomainObject} are
+ * not intended to be modified and should be created or deleted. A checkout may be used when
+ * an offline copy is required but otherwise serves no purpose since a modification and checkin
+ * is not supported.
+ */
+public class URLLinkObject extends DomainObjectAdapterDB {
+
+ // Use a reduced DB buffer size to reduce file size for minimal content.
+ // This will allow a 4-KByte DB buffer file to hold a URL upto ~470 bytes long.
+ // Longer URLs will rely on 1-KByte chained buffers which will increase file length.
+ private static final int DB_BUFFER_SIZE = 1024;
+
+ private URL url;
+
+ /**
+ * Constructs a new link file object
+ * @param name link name
+ * @param ghidraUrl link URL
+ * @param consumer the object that is using this program.
+ * @throws IOException if there is an error accessing the database or invalid URL specified.
+ */
+ public URLLinkObject(String name, URL ghidraUrl, Object consumer) throws IOException {
+ super(new DBHandle(DB_BUFFER_SIZE), name, 500, consumer);
+ metadata.put(LinkHandler.URL_METADATA_KEY, ghidraUrl.toString());
+ updateMetadata();
+ }
+
+ /**
+ * Constructs a link file object from a DBHandle (read-only)
+ * @param dbh a handle to an open program database.
+ * @param consumer the object that keeping the program open.
+ * @throws IOException if an error accessing the database occurs.
+ */
+ public URLLinkObject(DBHandle dbh, Object consumer) throws IOException {
+ super(dbh, "Untitled", 500, consumer);
+ loadMetadata();
+ String urlText = metadata.get(LinkHandler.URL_METADATA_KEY);
+ if (urlText == null) {
+ throw new IOException("Null link object");
+ }
+ url = new URL(urlText);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Link-File";
+ }
+
+ /**
+ * Get link URL
+ * @return link URL
+ */
+ public URL getLink() {
+ return url;
+ }
+
+ @Override
+ public final boolean isChangeable() {
+ return false;
+ }
+
+ @Override
+ public final void saveToPackedFile(File outputFile, TaskMonitor monitor)
+ throws IOException, CancelledException {
+ throw new UnsupportedOperationException();
+ }
+
+}
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 1d4617c933..2175211e14 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
@@ -40,6 +40,8 @@ import generic.theme.GIcon;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.GenericRunInfo;
import ghidra.framework.client.*;
+import ghidra.framework.data.FolderLinkContentHandler;
+import ghidra.framework.data.LinkedGhidraFolder;
import ghidra.framework.main.datatable.ProjectDataTablePanel;
import ghidra.framework.main.datatree.*;
import ghidra.framework.main.projectdata.actions.*;
@@ -69,7 +71,7 @@ import ghidra.util.filechooser.GhidraFileFilter;
)
//@formatter:on
public class FrontEndPlugin extends Plugin
- implements FrontEndService, RemoteAdapterListener, ProgramaticUseOnly {
+ implements FrontEndService, RemoteAdapterListener, ProjectViewListener, ProgramaticUseOnly {
private final static String TITLE_PREFIX = "Ghidra: ";
private final static String EXPORT_TOOL_ACTION_NAME = "Export Tool";
@@ -128,6 +130,7 @@ public class FrontEndPlugin extends Plugin
private ClearCutAction clearCutAction;
private ProjectDataCopyAction copyAction;
private ProjectDataPasteAction pasteAction;
+ private ProjectDataPasteLinkAction pasteLinkAction;
private ProjectDataRenameAction renameAction;
private ProjectDataOpenDefaultToolAction openAction;
private ProjectDataExpandAction expandAction;
@@ -217,6 +220,7 @@ public class FrontEndPlugin extends Plugin
clearCutAction = new ClearCutAction(owner);
copyAction = new ProjectDataCopyAction(owner, groupName);
pasteAction = new ProjectDataPasteAction(owner, groupName);
+ pasteLinkAction = new ProjectDataPasteLinkAction(owner, groupName);
groupName = "Delete/Rename";
renameAction = new ProjectDataRenameAction(owner, groupName);
@@ -239,6 +243,7 @@ public class FrontEndPlugin extends Plugin
tool.addAction(clearCutAction);
tool.addAction(copyAction);
tool.addAction(pasteAction);
+ tool.addAction(pasteLinkAction);
tool.addAction(deleteAction);
tool.addAction(openAction);
tool.addAction(renameAction);
@@ -393,6 +398,10 @@ public class FrontEndPlugin extends Plugin
toolChest.addToolChestChangeListener(toolBar);
toolChest.addToolChestChangeListener(toolChestChangeListener);
createToolSpecificOpenActions();
+
+ // Add project view listener
+ activeProject.addProjectViewListener(this);
+
// Add the repository listener
RepositoryAdapter repository = activeProject.getRepository();
if (repository != null) {
@@ -403,6 +412,16 @@ public class FrontEndPlugin extends Plugin
// gui.validate();
}
+ @Override
+ public void viewedProjectAdded(URL projectView) {
+ SwingUtilities.invokeLater(() -> rebuildRecentMenus());
+ }
+
+ @Override
+ public void viewedProjectRemoved(URL projectView) {
+ SwingUtilities.invokeLater(() -> rebuildRecentMenus());
+ }
+
/**
* sets the name of the project, using the default name if no project is active
*/
@@ -1074,24 +1093,56 @@ public class FrontEndPlugin extends Plugin
}
public void openDomainFile(DomainFile domainFile) {
- Project project = tool.getProject();
- final ToolServices toolServices = project.getToolServices();
- ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(domainFile);
- if (defaultToolTemplate == null) {
- // assume no tools in the tool chest
- Msg.showInfo(this, tool.getToolFrame(), "Cannot Find Tool",
- "Cannot find tool to open file: " +
- HTMLUtilities.escapeHTML(domainFile.getName()) +
- ".
Make sure you have an appropriate tool installed
from the " +
- "Tools->Import Default Tools... menu. Alternatively, you can " +
- "use Tool->Set Tool Associations menu to change how Ghidra " +
- "opens this type of file");
+ if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(domainFile.getContentType())) {
+ showLinkedFolder(domainFile);
return;
}
- ToolButton button = toolBar.getToolButtonForToolConfig(defaultToolTemplate);
- button.launchTool(domainFile);
+ Project project = tool.getProject();
+ final ToolServices toolServices = project.getToolServices();
+ ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(domainFile);
+ if (defaultToolTemplate != null) {
+ ToolButton button = toolBar.getToolButtonForToolConfig(defaultToolTemplate);
+ if (button != null) {
+ button.launchTool(domainFile);
+ return;
+ }
+ }
+
+ Msg.showInfo(this, tool.getToolFrame(), "Cannot Find Tool",
+ "Cannot find tool to open file: " +
+ HTMLUtilities.escapeHTML(domainFile.getName()) +
+ ".
Make sure you have an appropriate tool installed
from the " +
+ "Tools->Import Default Tools... menu. Alternatively, you can " +
+ "use Tool->Set Tool Associations menu to change how Ghidra " +
+ "opens this type of file");
+ }
+
+ private void showLinkedFolder(DomainFile domainFile) {
+
+ try {
+ LinkedGhidraFolder linkedFolder =
+ FolderLinkContentHandler.getReadOnlyLinkedFolder(domainFile);
+ if (linkedFolder == null) {
+ return; // unsupported use
+ }
+
+ ProjectDataTreePanel dtp = projectDataPanel.openView(linkedFolder.getProjectURL());
+ if (dtp == null) {
+ return;
+ }
+
+ DomainFolder domainFolder = linkedFolder.getLinkedFolder();
+ if (domainFolder != null) {
+ // delayed to ensure tree is displayd
+ Swing.runLater(() -> dtp.selectDomainFolder(domainFolder));
+ }
+ }
+ catch (IOException e) {
+ Msg.showError(this, projectDataPanel, "Linked-folder failure: " + domainFile.getName(),
+ e);
+ }
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java
index 4ee788cd3d..6ece295885 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java
@@ -554,10 +554,15 @@ class ProjectActionManager {
return;
}
- ProjectDataPanel pdp = plugin.getProjectDataPanel();
- pdp.openView(view);
- // also update the recent views menu
- plugin.rebuildRecentMenus();
+ try {
+ activeProject.addProjectView(view, true); // listener will trigger data panel panel display
+ }
+ catch (IOException e) {
+ ProjectManager projectManager = tool.getProjectManager();
+ projectManager.forgetViewedProject(view);
+ Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View",
+ "Failed to view project/repository: " + e.getMessage(), e);
+ }
}
private void editProjectAccess() {
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 7606c410e1..3e9cb85d9e 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
@@ -27,6 +27,7 @@ import javax.swing.*;
import docking.ActionContext;
import docking.ComponentProvider;
import docking.widgets.tabbedpane.DockingTabRenderer;
+import ghidra.framework.client.NotConnectedException;
import ghidra.framework.main.datatable.ProjectDataTablePanel;
import ghidra.framework.main.datatree.ProjectDataTreePanel;
import ghidra.framework.model.*;
@@ -40,7 +41,7 @@ import help.HelpService;
* Manages the data tree for the active project, and the trees for the
* project views.
*/
-class ProjectDataPanel extends JSplitPane {
+class ProjectDataPanel extends JSplitPane implements ProjectViewListener {
private final static String BORDER_PREFIX = "Active Project: ";
private final static String READ_ONLY_BORDER = "READ-ONLY Project Data";
private final static int TYPICAL_NUM_VIEWS = 2;
@@ -155,6 +156,21 @@ class ProjectDataPanel extends JSplitPane {
setViewsVisible(views.length > 0);
}
+ @Override
+ public void viewedProjectAdded(URL projectView) {
+ SwingUtilities.invokeLater(() -> openView(projectView));
+ }
+
+ @Override
+ public void viewedProjectRemoved(URL projectView) {
+ SwingUtilities.invokeLater(() -> {
+ ProjectDataTreePanel dtp = getViewPanel(projectView);
+ if (dtp != null) {
+ viewRemoved(dtp, projectView, false);
+ }
+ });
+ }
+
private void clearReadOnlyViews() {
readOnlyTab.removeAll();
readOnlyViews.clear();
@@ -167,7 +183,13 @@ class ProjectDataPanel extends JSplitPane {
this.setDividerLocation(visible ? DIVIDER_LOCATION : 1.0);
}
- void openView(URL projectView) {
+ /**
+ * Open specified project URL in tabbed READ-Only project views
+ * @param projectView project URL to be opened/added to view
+ * @return corresponding tree panel or null on failure
+ */
+ ProjectDataTreePanel openView(URL projectView) {
+
ProjectManager projectManager = tool.getProjectManager();
Project activeProject = tool.getProject();
@@ -176,19 +198,23 @@ class ProjectDataPanel extends JSplitPane {
if (dtp != null) {
readOnlyTab.setSelectedComponent(dtp);
try {
- activeProject.addProjectView(projectView);
+ activeProject.addProjectView(projectView, true);
projectManager.rememberViewedProject(projectView);
+ return dtp;
}
catch (Exception e) {
projectManager.forgetViewedProject(projectView);
Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View", e.toString());
}
- return;
+ return null;
}
try {
// TODO: addProjectView should be done in a model task
- ProjectData projectData = activeProject.addProjectView(projectView);
+ ProjectData projectData = activeProject.addProjectView(projectView, true);
+ if (projectData == null) {
+ return null; // repository connection may have been cancelled
+ }
projectManager.rememberViewedProject(projectView);
String viewName = projectData.getProjectLocator().getName();
final ProjectDataTreePanel newPanel =
@@ -204,13 +230,20 @@ class ProjectDataPanel extends JSplitPane {
readOnlyTab.setSelectedIndex(0);
readOnlyViews.put(projectData.getProjectLocator(), newPanel);
setViewsVisible(true);
+ return newPanel;
+ }
+ catch (NotConnectedException e) {
+ // already handled (e..g, cancelled login) - ignore
}
catch (Exception e) {
projectManager.forgetViewedProject(projectView);
Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View",
- "Failed to view project/repository: " + e.getMessage());
+ "Failed to view project/repository: " + e.getMessage(), e);
}
- validate();
+ finally {
+ validate();
+ }
+ return null;
}
ProjectLocator[] getProjectViews() {
@@ -315,6 +348,7 @@ class ProjectDataPanel extends JSplitPane {
treePanel.setProjectData(project.getName(), project.getProjectData());
tablePanel.setProjectData(project.getName(), project.getProjectData());
populateReadOnlyViews(project);
+ project.addProjectViewListener(this);
}
else {
tablePanel.setProjectData("No Active Project", null);
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 559ea7631f..28d95d2eed 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
@@ -182,9 +182,12 @@ public class ProjectDataTablePanel extends JPanel {
this.projectData.removeDomainFolderChangeListener(changeListener);
model.setProjectData(null);
SystemUtilities.runSwingLater(() -> {
- GGlassPane glassPane = (GGlassPane) gTable.getRootPane().getGlassPane();
- glassPane.removePainter(painter);
- glassPane.addPainter(painter);
+ JRootPane rootPane = gTable.getRootPane();
+ if (rootPane != null) {
+ GGlassPane glassPane = (GGlassPane) rootPane.getGlassPane();
+ glassPane.removePainter(painter);
+ glassPane.addPainter(painter);
+ }
});
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeAction.java
index 241af898ef..a62998cea7 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeAction.java
@@ -70,7 +70,7 @@ public abstract class ProjectTreeAction extends DockingAction {
@Override
public boolean isAddToPopup(ActionContext context) {
- if (!isEnabledForContext(context)) {
+ if (!(context instanceof FrontEndProjectTreeContext)) {
return false;
}
return isAddToPopup((FrontEndProjectTreeContext) context);
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 e8c60ab6b7..99da701708 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
@@ -176,40 +176,15 @@ class ChangeManager implements DomainFolderChangeListener {
if (lazy && !folderNode.isLoaded()) {
return null; // not visited
}
- // must look at all children since a folder and file may have the same name
- boolean found = false;
- for (GTreeNode node : folderNode.getChildren()) {
- if (!(node instanceof DomainFolderNode)) {
- continue;
- }
- if (name.equals(node.getName())) {
- folderNode = (DomainFolderNode) node;
- found = true;
- break;
- }
- }
- if (!found) {
+ folderNode =
+ (DomainFolderNode) folderNode.getChild(name, n -> (n instanceof DomainFolderNode));
+ if (folderNode == null) {
return null;
}
}
return folderNode;
}
-// private DomainFileNode findDomainFileNode(DomainFolder parent, String name, boolean lazy) {
-// DomainFolderNode folderNode = findDomainFolderNode(parent, lazy);
-// if (folderNode == null) {
-// return null;
-// }
-// if (lazy && !folderNode.isChildrenLoadedOrInProgress()) {
-// return null; // not visited
-// }
-// GTreeNode child = folderNode.getChild(name);
-// if (child instanceof DomainFileNode) {
-// return (DomainFileNode) child;
-// }
-// return null;
-// }
-
private DomainFileNode findDomainFileNode(DomainFile domainFile, boolean lazy) {
DomainFolderNode folderNode = findDomainFolderNode(domainFile.getParent(), lazy);
if (folderNode == null) {
@@ -219,11 +194,8 @@ class ChangeManager implements DomainFolderChangeListener {
return null; // not visited
}
- GTreeNode child = folderNode.getChild(domainFile.getName());
- if (child instanceof DomainFileNode) {
- return (DomainFileNode) child;
- }
- return null;
+ return (DomainFileNode) folderNode.getChild(domainFile.getName(),
+ n -> (n instanceof DomainFileNode));
}
private void updateFolderNode(DomainFolder parent) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java
index 1f283a3a60..e68dfa1c0e 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java
@@ -58,7 +58,7 @@ public class CheckInTask extends VersionControlTask implements CheckinHandler {
throw new CancelledException();
}
if (actionID != VersionControlDialog.APPLY_TO_ALL) {
- showDialog(false, df.getName()); // false==> checking in vs.
+ showDialog(false, df.getName(), df.isLinkFile()); // false==> checking in vs.
// adding to version control
if (actionID == VersionControlDialog.CANCEL) {
monitor.cancel();
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 400be85269..785a8c93d2 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
@@ -16,6 +16,7 @@
package ghidra.framework.main.datatree;
import java.io.IOException;
+import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -24,6 +25,8 @@ import javax.swing.SwingWorker;
import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
+import ghidra.framework.data.FolderLinkContentHandler;
+import ghidra.framework.data.LinkHandler;
import ghidra.framework.model.DomainFile;
import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.util.*;
@@ -174,7 +177,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
if (domainFile.isHijacked()) {
newDisplayName += " (hijacked)";
}
- else if (domainFile.isVersioned()) {
+ else if (domainFile.isVersioned() && !domainFile.isLinkFile()) {
int versionNumber = domainFile.getVersion();
String versionStr = "" + versionNumber;
@@ -208,20 +211,34 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
}
private void setToolTipText() {
- String newToolTipText = toolTipText;
+ String newToolTipText = null;
if (domainFile.isInWritableProject() && domainFile.isHijacked()) {
newToolTipText = "Hijacked file should be deleted or renamed";
}
else {
- long lastModified = domainFile.getLastModifiedTime();
- newToolTipText = "Last Modified " + formatter.format(new Date(lastModified));
+ StringBuilder buf = new StringBuilder();
+ try {
+ if (domainFile.isLinkFile()) {
+ URL url = LinkHandler.getURL(domainFile);
+ buf.append("URL: ");
+ buf.append(StringUtilities.trimMiddle(url.toString(), 120));
+ newToolTipText = buf.toString();
+ }
+ }
+ catch (IOException e1) {
+ // ignore
+ }
+ if (newToolTipText == null) {
+ long lastModified = domainFile.getLastModifiedTime();
+ newToolTipText = "Last Modified " + formatter.format(new Date(lastModified));
+ }
if (domainFile.isCheckedOut()) {
try {
ItemCheckoutStatus status = domainFile.getCheckoutStatus();
if (status != null) {
- newToolTipText = HTMLUtilities.toHTML(
- "Checked out " + formatter.format(new Date(status.getCheckoutTime())) +
- ";\n" + newToolTipText);
+ newToolTipText = "Checked out " +
+ formatter.format(new Date(status.getCheckoutTime())) +
+ "\n" + newToolTipText;
}
}
catch (IOException e) {
@@ -232,6 +249,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
if (domainFile.isReadOnly()) {
newToolTipText += " (read only)";
}
+ newToolTipText = HTMLUtilities.toLiteralHTML(newToolTipText, 0);
}
toolTipText = newToolTipText;
}
@@ -243,12 +261,38 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
@Override
public int compareTo(GTreeNode node) {
+ // Goal is to sort folder link-files similar to a folder
if (node instanceof DomainFolderNode) {
+ if (isFolderLink()) {
+ int c = super.compareTo(node);
+ if (c != 0) {
+ // A link-file name is permitted to match another folder node but
+ // should not be considered equal
+ return c;
+ }
+ }
return 1;
}
+ if (node instanceof DomainFileNode) {
+ DomainFileNode otherFileNode = (DomainFileNode) node;
+ if (isFolderLink()) {
+ if (otherFileNode.isFolderLink()) {
+ return super.compareTo(node);
+ }
+ return -1;
+ }
+ else if (otherFileNode.isFolderLink()) {
+ return 1;
+ }
+ }
return super.compareTo(node);
}
+ boolean isFolderLink() {
+ return FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE
+ .equals(domainFile.getContentType());
+ }
+
@Override
public void valueChanged(Object newValue) {
if (newValue.equals(getName())) {
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 1b027ed0b3..6c70770541 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
@@ -22,20 +22,18 @@ import javax.swing.Icon;
import docking.widgets.tree.GTreeLazyNode;
import docking.widgets.tree.GTreeNode;
-import generic.theme.GIcon;
import ghidra.framework.model.*;
-import ghidra.util.InvalidNameException;
-import ghidra.util.Msg;
+import ghidra.util.*;
import resources.ResourceManager;
/**
* Class to represent a node in the Data tree.
*/
public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
- private static final Icon ENABLED_OPEN_FOLDER =
- new GIcon("icon.datatree.node.domain.folder.open");
- private static final Icon ENABLED_CLOSED_FOLDER =
- new GIcon("icon.datatree.node.domain.folder.closed");
+
+ private static final Icon ENABLED_OPEN_FOLDER = DomainFolder.OPEN_FOLDER_ICON;
+ private static final Icon ENABLED_CLOSED_FOLDER = DomainFolder.CLOSED_FOLDER_ICON;
+
private static final Icon DISABLED_OPEN_FOLDER =
ResourceManager.getDisabledIcon(ENABLED_OPEN_FOLDER);
private static final Icon DISABLED_CLOSED_FOLDER =
@@ -55,11 +53,18 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
// TODO: how can the folder be null?...doesn't really make sense...I don't think it ever is
if (domainFolder != null) {
- toolTipText = domainFolder.getPathname();
+ toolTipText = StringUtilities.trimMiddle(domainFolder.getPathname(), 120);
+ toolTipText = HTMLUtilities.toLiteralHTML(toolTipText, 0);
isEditable = domainFolder.isInWritableProject();
}
}
+ @Override
+ public boolean isAutoExpandPermitted() {
+ // Prevent auto-expansion through linked-folders
+ return !domainFolder.isLinked();
+ }
+
/**
* Get the domain folder; returns null if this node represents a domain file.
*
@@ -96,6 +101,10 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
@Override
public Icon getIcon(boolean expanded) {
+ if (domainFolder instanceof LinkedDomainFolder) {
+ // NOTE: cut operation not supported
+ return ((LinkedDomainFolder) domainFolder).getIcon(expanded);
+ }
if (expanded) {
return isCut ? DISABLED_OPEN_FOLDER : ENABLED_OPEN_FOLDER;
}
@@ -119,8 +128,12 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
@Override
protected List generateChildren() {
+
List children = new ArrayList<>();
- if (domainFolder != null) {
+ if (domainFolder != null && !domainFolder.isEmpty()) {
+
+ // NOTE: isEmpty() is used to avoid multiple failed connection attempts on this folder
+
DomainFolder[] folders = domainFolder.getFolders();
for (DomainFolder folder : folders) {
children.add(new DomainFolderNode(folder, filter));
@@ -128,6 +141,13 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
DomainFile[] files = domainFolder.getFiles();
for (DomainFile domainFile : files) {
+ if (domainFile.isLinkFile() && filter != null && filter.followLinkedFolders()) {
+ DomainFolder folder = domainFile.followLink();
+ if (folder != null) {
+ children.add(new DomainFolderNode(folder, filter));
+ continue;
+ }
+ }
if (filter == null || filter.accept(domainFile)) {
children.add(new DomainFileNode(domainFile));
}
@@ -173,7 +193,8 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
@Override
public int compareTo(GTreeNode node) {
if (node instanceof DomainFileNode) {
- return -1;
+ // defer to DomainFileNode for comparison
+ return -((DomainFileNode) node).compareTo(this);
}
return super.compareTo(node);
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java
index 77aebb4327..5ef74d2d16 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java
@@ -423,6 +423,9 @@ public class ProjectDataTreePanel extends JPanel {
root = createRootNode(projectName);
tree = new DataTree(tool, root);
+ if (!isActiveProject) {
+ tree.setName(tree.getName() + ": " + projectName);
+ }
if (plugin != null) {
tree.addGTreeSelectionListener(e -> {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlDialog.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlDialog.java
index 4a10a953fb..f05062b9b6 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlDialog.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlDialog.java
@@ -117,12 +117,6 @@ public class VersionControlDialog extends DialogComponentProvider {
return keepFileCB.isSelected();
}
- void setCreateKeepFile(boolean selected) {
- if (!addToVersionControl) {
- keepFileCB.setSelected(selected);
- }
- }
-
/**
* Return the comments for the add to version control.
* @return may be the empty string
@@ -133,10 +127,14 @@ public class VersionControlDialog extends DialogComponentProvider {
/*
* Disable the check box for "keep checked out" because some files are still in use.
+ * @param enabled true if checkbox control should be enabled, false if disabled
+ * @param selected true if default state should be selected, else not-selected
+ * @param disabledMsg tooltip message if enabled is false, otherwise ignored.
*/
- public void setKeepCheckboxEnabled(boolean enabled) {
+ public void setKeepCheckboxEnabled(boolean enabled, boolean selected, String disabledMsg) {
keepCB.setEnabled(enabled);
- keepCB.setToolTipText(enabled ? "" : "Must keep Checked Out because the file is in use");
+ keepCB.setSelected(selected);
+ keepCB.setToolTipText(enabled ? "" : disabledMsg);
}
private JPanel buildMainPanel() {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java
index 1a424a16c3..9e21bbd4f3 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java
@@ -58,13 +58,21 @@ public abstract class VersionControlTask extends Task {
* @param addToVersionControl true if the dialog is for
* adding files to version control, false for checking in files.
* @param filename the name of the file currently to be added, whose comment we need.
+ * @param isLinkFile true if file is a link file, else false. Link-files may not be checked-out
+ * so keep-checked-out control disabled if this is true.
*/
- protected void showDialog(boolean addToVersionControl, String filename) {
+ protected void showDialog(boolean addToVersionControl, String filename, boolean isLinkFile) {
Runnable r = () -> {
VersionControlDialog vcDialog = new VersionControlDialog(addToVersionControl);
vcDialog.setCurrentFileName(filename);
vcDialog.setMultiFiles(list.size() > 1);
- vcDialog.setKeepCheckboxEnabled(!filesInUse);
+ if (isLinkFile) {
+ vcDialog.setKeepCheckboxEnabled(false, false, "Link files may not be Checked Out");
+ }
+ else if (filesInUse) {
+ vcDialog.setKeepCheckboxEnabled(false, true,
+ "Must keep Checked Out because the file is in use");
+ }
actionID = vcDialog.showDialog(tool, parent);
keepCheckedOut = vcDialog.keepCheckedOut();
createKeep = vcDialog.shouldCreateKeepFile();
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java
index 486cfa50d0..665558b33a 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java
@@ -50,10 +50,6 @@ public class ProjectDataCopyAction extends ProjectDataCopyCutBaseAction {
return false;
}
- if (!context.isInActiveProject()) {
- return false;
- }
-
return !context.containsRootFolder();
}
}
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 0b79f2e174..12cd31839a 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
@@ -52,6 +52,11 @@ public class ProjectDataNewFolderAction
return (context.getFolderCount() + context.getFileCount()) == 1;
}
+ @Override
+ protected boolean isEnabledForContext(T context) {
+ return getFolder(context).isInWritableProject();
+ }
+
private void createNewFolder(T context) {
DomainFolder parentFolder = getFolder(context);
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteLinkAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteLinkAction.java
new file mode 100644
index 0000000000..abaee7b9ff
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteLinkAction.java
@@ -0,0 +1,147 @@
+/* ###
+ * 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.projectdata.actions;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.swing.Icon;
+
+import docking.action.MenuData;
+import docking.widgets.tree.GTreeNode;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.main.datatable.ProjectTreeAction;
+import ghidra.framework.main.datatree.*;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.DomainFolder;
+import ghidra.util.Msg;
+import resources.MultiIcon;
+import resources.ResourceManager;
+
+public class ProjectDataPasteLinkAction extends ProjectTreeAction {
+ private static Icon baseIcon = ResourceManager.loadImage("images/page_paste.png");
+
+ public ProjectDataPasteLinkAction(String owner, String group) {
+ super("Paste Link", owner);
+ setPopupMenuData(new MenuData(new String[] { "Paste as Link" }, getIcon(), group));
+ }
+
+ private static Icon getIcon() {
+ MultiIcon multiIcon = new MultiIcon(baseIcon);
+ multiIcon.addIcon(LinkHandler.LINK_ICON);
+ return multiIcon;
+ }
+
+ @Override
+ protected void actionPerformed(FrontEndProjectTreeContext context) {
+ GTreeNode node = (GTreeNode) context.getContextObject();
+ DomainFolderNode destNode = getFolderForNode(node);
+ if (!isEnabledForContext(context)) {
+ Msg.showWarn(getClass(), context.getTree(), "Unsupported Operation",
+ "Unsupported paste link condition");
+ }
+
+ GTreeNode copyNode = getFolderOrFileCopyNode();
+ if (copyNode instanceof DomainFileNode) {
+ try {
+ DomainFile domainFile = ((DomainFileNode) copyNode).getDomainFile();
+ domainFile.copyToAsLink(destNode.getDomainFolder());
+ }
+ catch (IOException e) {
+ Msg.showError(getClass(), context.getTree(), "Cannot Create Link",
+ "Error occured while creating link file", e);
+ }
+ }
+ else {
+ try {
+ DomainFolder domainFolder = ((DomainFolderNode) copyNode).getDomainFolder();
+ domainFolder.copyToAsLink(destNode.getDomainFolder());
+ }
+ catch (IOException e) {
+ Msg.showError(getClass(), context.getTree(), "Cannot Create Link",
+ "Error occured while creating link file", e);
+ }
+ }
+
+ }
+
+ @Override
+ protected boolean isEnabledForContext(FrontEndProjectTreeContext context) {
+ if (!context.hasExactlyOneFileOrFolder()) {
+ return false;
+ }
+ if (!context.isInActiveProject()) {
+ return false;
+ }
+ GTreeNode node = (GTreeNode) context.getContextObject();
+ DomainFolderNode destNode = getFolderForNode(node);
+
+ GTreeNode copyNode = getFolderOrFileCopyNode();
+ if (copyNode == null || copyNode.getParent() == null) {
+ return false;
+ }
+
+ // local internal linking not supported
+ if (destNode.getRoot() == copyNode.getRoot()) {
+ return false;
+ }
+
+ if (copyNode instanceof DomainFileNode) {
+ DomainFile df = ((DomainFileNode) copyNode).getDomainFile();
+ return df.isLinkingSupported();
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean isAddToPopup(FrontEndProjectTreeContext context) {
+ if (!context.hasOneOrMoreFilesAndFolders()) {
+ return false;
+ }
+ if (!context.isInActiveProject()) {
+ return false;
+ }
+ GTreeNode copyNode = getFolderOrFileCopyNode();
+ return copyNode != null && copyNode.getParent() != null;
+ }
+
+ private DomainFolderNode getFolderForNode(GTreeNode node) {
+ if (node instanceof DomainFolderNode) {
+ return (DomainFolderNode) node;
+ }
+ return (DomainFolderNode) node.getParent();
+ }
+
+ private GTreeNode getFolderOrFileCopyNode() {
+ List list = DataTreeClipboardUtils.getDataTreeNodesFromClipboard();
+ if (list.size() != 1) {
+ return null;
+ }
+ GTreeNode copyNode = list.get(0);
+ if (copyNode instanceof DomainFileNode) {
+ if (!((DomainFileNode) copyNode).isCut()) {
+ return copyNode;
+ }
+ }
+ if (copyNode instanceof DomainFolderNode) {
+ if (!((DomainFolderNode) copyNode).isCut()) {
+ return copyNode;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlAddAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlAddAction.java
index 3352d4480d..d89d1637c6 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlAddAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlAddAction.java
@@ -79,7 +79,7 @@ public class VersionControlAddAction extends VersionControlAction {
}
List unversioned = new ArrayList<>();
for (DomainFile domainFile : domainFiles) {
- if (domainFile.isVersionControlSupported() && !domainFile.isVersioned()) {
+ if (domainFile.canAddToRepository()) {
unversioned.add(domainFile);
}
}
@@ -143,7 +143,7 @@ public class VersionControlAddAction extends VersionControlAction {
monitor.setMessage("Adding " + name + " to Version Control");
if (actionID != VersionControlDialog.APPLY_TO_ALL) {
- showDialog(true, name);
+ showDialog(true, name, df.isLinkFile());
}
if (actionID == VersionControlDialog.CANCEL) {
return;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlCheckInAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlCheckInAction.java
index d4d64548c0..af05b4b60d 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlCheckInAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlCheckInAction.java
@@ -69,7 +69,7 @@ public class VersionControlCheckInAction extends VersionControlAction {
List domainFiles = context.getSelectedFiles();
for (DomainFile domainFile : domainFiles) {
- if (domainFile.isCheckedOut() && domainFile.modifiedSinceCheckout()) {
+ if (domainFile.modifiedSinceCheckout()) {
return true; // At least one checked out file selected.
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java
index a012198a29..b5a01829ef 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java
@@ -16,17 +16,19 @@
package ghidra.framework.model;
import java.io.*;
+import java.net.URL;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
import ghidra.framework.client.NotConnectedException;
-import ghidra.framework.data.CheckinHandler;
+import ghidra.framework.data.*;
import ghidra.framework.store.*;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
+import resources.ResourceManager;
/**
* DomainFile provides a storage interface for project files. A
@@ -36,6 +38,9 @@ import ghidra.util.task.TaskMonitor;
*/
public interface DomainFile extends Comparable {
+ public static final Icon UNSUPPORTED_FILE_ICON =
+ ResourceManager.loadImage("images/unknownFile.gif");
+
/**
* Use with getDomainObject to request the default version. The default version is
* the private file or check-out file if one exists, or the latest version from the
@@ -74,7 +79,7 @@ public interface DomainFile extends Comparable {
* @throws DuplicateFileException if a file named newName
* already exists in this files domain folder.
* @throws FileInUseException if this file is in-use / checked-out.
- * @throws IOException thrown if an IO or access error occurs.
+ * @throws IOException if an IO or access error occurs.
*/
public DomainFile setName(String newName) throws InvalidNameException, IOException;
@@ -84,6 +89,14 @@ public interface DomainFile extends Comparable {
*/
public String getPathname();
+ /**
+ * Get a remote Ghidra URL for this domain file if available within the associated shared
+ * project repository. A null value will be returned if shared file does not exist and
+ * may be returned if shared repository is not connected or a connection error occurs.
+ * @return remote Ghidra URL for this file or null
+ */
+ public URL getSharedProjectURL();
+
/**
* Returns the local storage location for the project that this DomainFile belongs to.
* @return the location
@@ -92,13 +105,14 @@ public interface DomainFile extends Comparable {
/**
* Returns content-type string
- * @return the content type
+ * @return the file content type or a reserved content type {@link ContentHandler#MISSING_CONTENT}
+ * or {@link ContentHandler#UNKNOWN_CONTENT}.
*/
public String getContentType();
/**
* Returns the underlying Class for the domain object in this domain file.
- * @return the class
+ * @return the class or null if does not correspond to a domain object.
*/
public Class extends DomainObject> getDomainObjectClass();
@@ -138,7 +152,7 @@ public interface DomainFile extends Comparable {
* that the domain object cannot be upgraded to the current format. If okToUpgrade is false,
* then the VersionException only means the object is not in the current format - it
* may or may not be possible to upgrade.
- * @throws IOException thrown if an IO or access error occurs.
+ * @throws IOException if an IO or access error occurs.
* @throws CancelledException if monitor cancelled operation
*/
public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
@@ -166,7 +180,7 @@ public interface DomainFile extends Comparable {
* @throws VersionException if the domain object could not be read due
* to a version format change.
* @throws FileNotFoundException if the stored file/version was not found.
- * @throws IOException thrown if an IO or access error occurs.
+ * @throws IOException if an IO or access error occurs.
* @throws CancelledException if monitor cancelled operation
*/
public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor)
@@ -184,7 +198,7 @@ public interface DomainFile extends Comparable {
* @throws VersionException if the domain object could not be read due
* to a version format change.
* @throws FileNotFoundException if the stored file/version was not found.
- * @throws IOException thrown if an IO or access error occurs.
+ * @throws IOException if an IO or access error occurs.
* @throws CancelledException if monitor cancelled operation
*/
public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor)
@@ -195,7 +209,7 @@ public interface DomainFile extends Comparable {
* @param monitor monitor for the task that is doing the save on the file
* @throws FileInUseException if the file is open for update by someone else, or
* a transient-read is in progress.
- * @throws IOException thrown if an IO error occurs.
+ * @throws IOException if an IO error occurs.
* @throws CancelledException if monitor cancelled operation
*/
public void save(TaskMonitor monitor) throws IOException, CancelledException;
@@ -291,7 +305,7 @@ public interface DomainFile extends Comparable {
* for private files (i.e., not versioned).
* @param state if true file will be read-only and may not be updated, if false the
* file may be updated.
- * @throws IOException thrown if an IO error occurs.
+ * @throws IOException if an IO error occurs.
*/
public void setReadOnly(boolean state) throws IOException;
@@ -302,12 +316,6 @@ public interface DomainFile extends Comparable {
*/
public boolean isReadOnly();
- /**
- * Returns true if the versioned filesystem can be used to store this files content type.
- * @return true if supports version control
- */
- public boolean isVersionControlSupported();
-
/**
* Return true if this is a versioned database, else false
* @return true if versioned
@@ -352,7 +360,7 @@ public interface DomainFile extends Comparable {
* @param keepCheckedOut if true, the file will be initially checked-out
* @param monitor progress monitor
* @throws FileInUseException if this file is in-use.
- * @throws IOException thrown if an IO or access error occurs. Also thrown if file is not
+ * @throws IOException if an IO or access error occurs. Also if file is not
* private.
* @throws CancelledException if the monitor cancelled the operation
*/
@@ -367,7 +375,7 @@ public interface DomainFile extends Comparable {
* @return true if checkout successful, false if an exclusive checkout was not possible
* due to other users having checkouts of this file. A request for a non-exclusive checkout
* will never return false.
- * @throws IOException thrown if an IO or access error occurs.
+ * @throws IOException if an IO or access error occurs.
* @throws CancelledException if task monitor cancelled operation.
*/
public boolean checkout(boolean exclusive, TaskMonitor monitor)
@@ -406,7 +414,7 @@ public interface DomainFile extends Comparable {
* extension.
* @throws NotConnectedException if shared project and not connected to repository
* @throws FileInUseException if this file is in-use / checked-out.
- * @throws IOException thrown if file is not checked-out or an IO / access error occurs.
+ * @throws IOException if file is not checked-out or an IO / access error occurs.
*/
public void undoCheckout(boolean keep) throws IOException;
@@ -451,7 +459,7 @@ public interface DomainFile extends Comparable {
* Delete the entire database for this file, including any version files.
* @throws FileInUseException if this file is in-use / checked-out.
* @throws UserAccessException if the user does not have permission to delete the file.
- * @throws IOException thrown if an IO or access error occurs.
+ * @throws IOException if an IO or access error occurs.
*/
public void delete() throws IOException;
@@ -476,7 +484,7 @@ public interface DomainFile extends Comparable {
* @throws DuplicateFileException if a file with the same name
* already exists in newParent folder.
* @throws FileInUseException if this file is in-use / checked-out.
- * @throws IOException thrown if an IO or access error occurs.
+ * @throws IOException if an IO or access error occurs.
*/
public DomainFile moveTo(DomainFolder newParent) throws IOException;
@@ -486,7 +494,7 @@ public interface DomainFile extends Comparable {
* @param monitor task monitor
* @return newly created domain file
* @throws FileInUseException if this file is in-use / checked-out.
- * @throws IOException thrown if an IO or access error occurs.
+ * @throws IOException if an IO or access error occurs.
* @throws CancelledException if task monitor cancelled operation.
*/
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
@@ -498,12 +506,34 @@ public interface DomainFile extends Comparable {
* @param destFolder destination parent folder
* @param monitor task monitor
* @return the copied file
- * @throws IOException thrown if an IO or access error occurs.
+ * @throws IOException if an IO or access error occurs.
* @throws CancelledException if task monitor cancelled operation.
*/
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
throws IOException, CancelledException;
+ /**
+ * Copy this file into the newParent folder as a link file. Restrictions:
+ *
+ * - Specified newParent must reside within a different project since internal linking is
+ * not currently supported.
+ * - Content type must support linking (see {@link #isLinkingSupported()}).
+ *
+ * If this file is associated with a temporary transient project (i.e., not a locally
+ * managed project) the generated link will refer to the remote file with a remote
+ * Ghidra URL, otherwise a local project storage path will be used.
+ * @param newParent new parent folder
+ * @return newly created domain file or null if content type does not support link use.
+ * @throws IOException if an IO or access error occurs.
+ */
+ public DomainFile copyToAsLink(DomainFolder newParent) throws IOException;
+
+ /**
+ * Determine if this file's content type supports linking.
+ * @return true if linking is supported, else false.
+ */
+ public boolean isLinkingSupported();
+
/**
* Get the list of consumers (Objects) for this domain file.
* @return empty array list if there are no consumers
@@ -552,8 +582,30 @@ public interface DomainFile extends Comparable {
* used for storing this file, but does not account for additional storage space
* used to tracks changes, etc.
* @return file length
- * @throws IOException thrown if IO or access error occurs
+ * @throws IOException if IO or access error occurs
*/
public long length() throws IOException;
+ /**
+ * Determine if this file is a link file which corresponds to either a file or folder link.
+ * The {@link DomainObject} referenced by a link-file may be opened using
+ * {@link #getReadOnlyDomainObject(Object, int, TaskMonitor)}. The
+ * {@link #getDomainObject(Object, boolean, boolean, TaskMonitor)} method may also be used
+ * to obtain a read-only instance. {@link #getImmutableDomainObject(Object, int, TaskMonitor)}
+ * use is not supported.
+ * If the link-file content type equals {@value FolderLinkContentHandler#FOLDER_LINK_CONTENT_TYPE}
+ * the method {@link #followLink()} can be used to get the linked domain folder.
+ * The associated link URL may be obtained with {@link LinkHandler#getURL(DomainFile)}.
+ * The content type (see {@link #getContentType()} of a link file will differ from that of the
+ * linked object (e.g., "LinkedProgram" vs "Program").
+ * @return true if link file else false for a normal domain file
+ */
+ public boolean isLinkFile();
+
+ /**
+ * If this is a folder-link file get the corresponding linked folder.
+ * @return a linked domain folder or null if not a folder-link.
+ */
+ public DomainFolder followLink();
+
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java
index f57d3b4569..9ceb871e8f 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java
@@ -1,6 +1,5 @@
/* ###
* 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.
@@ -31,4 +30,13 @@ public interface DomainFileFilter {
*
*/
public boolean accept(DomainFile df);
+
+ /**
+ * Determine if linked folders represented by a link-file should be followed.
+ * If this method is not implemented the default will return {@code true}.
+ * @return true if linked-folders should be followed or false to ignore.
+ */
+ public default boolean followLinkedFolders() {
+ return true;
+ }
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java
index 61f43881f8..18ced6cd51 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java
@@ -17,7 +17,11 @@ package ghidra.framework.model;
import java.io.File;
import java.io.IOException;
+import java.net.URL;
+import javax.swing.Icon;
+
+import generic.theme.GIcon;
import ghidra.framework.store.FolderNotEmptyException;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.*;
@@ -30,6 +34,13 @@ import ghidra.util.task.TaskMonitor;
* referenced project folder.
*/
public interface DomainFolder extends Comparable {
+
+ public static final Icon OPEN_FOLDER_ICON =
+ new GIcon("icon.datatree.node.domain.folder.open");
+
+ public static final Icon CLOSED_FOLDER_ICON =
+ new GIcon("icon.datatree.node.domain.folder.closed");
+
/**
* Character used to separate folder and item names within a path string.
*/
@@ -78,6 +89,14 @@ public interface DomainFolder extends Comparable {
*/
public String getPathname();
+ /**
+ * Get a remote Ghidra URL for this domain folder within the associated shared
+ * project repository. URL path will end with "/". A null value will be returned if not
+ * associated with a shared project.
+ * @return remote Ghidra URL for this folder or null
+ */
+ public URL getSharedProjectURL();
+
/**
* Returns true if this file is in a writable project.
* @return true if writable
@@ -92,7 +111,7 @@ public interface DomainFolder extends Comparable {
/**
* Get DomainFolders in this folder.
- * This returns cached information and does not force a full refresh.
+ * This may return cached information and does not force a full refresh.
* @return list of sub-folders
*/
public DomainFolder[] getFolders();
@@ -119,7 +138,7 @@ public interface DomainFolder extends Comparable {
/**
* Get all domain files in this folder.
- * This returns cached information and does not force a full refresh.
+ * This may return cached information and does not force a full refresh.
* @return list of domain files
*/
public DomainFile[] getFiles();
@@ -203,8 +222,31 @@ public interface DomainFolder extends Comparable {
public DomainFolder copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException,
CancelledException;
+ /**
+ * Copy this folder into the newParent folder as a link file. Restrictions:
+ *
+ * - Specified newParent must reside within a different project since internal linking is
+ * not currently supported.
+ *
+ * If this folder is associated with a temporary transient project (i.e., not a locally
+ * managed project) the generated link will refer to the remote file with a remote
+ * Ghidra URL, otherwise a local project storage path will be used.
+ * @param newParent new parent folder
+ * @return newly created domain file or null if link use not supported.
+ * @throws IOException if an IO or access error occurs.
+ */
+ public DomainFile copyToAsLink(DomainFolder newParent) throws IOException;
+
/**
* Allows the framework to react to a request to make this folder the "active" one.
*/
public void setActive();
+
+ /**
+ * Determine if this folder corresponds to a linked-folder.
+ * @return true if folder corresponds to a linked-folder, else false.
+ */
+ public default boolean isLinked() {
+ return false;
+ }
}
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
new file mode 100644
index 0000000000..dab005fc34
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java
@@ -0,0 +1,33 @@
+/* ###
+ * 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.model;
+
+import java.io.IOException;
+
+/**
+ * {@code LinkedDomainFile} corresponds to a {@link DomainFile} contained within a
+ * {@link LinkedDomainFolder}.
+ */
+public interface LinkedDomainFile extends DomainFile {
+
+ /**
+ * Get the real domain file which corresponds to this file contained within a linked-folder.
+ * @return domain file
+ * @throws IOException if IO error occurs or file not found
+ */
+ public DomainFile getLinkedFile() throws IOException;
+
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java
new file mode 100644
index 0000000000..5ea918735e
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java
@@ -0,0 +1,44 @@
+/* ###
+ * 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.model;
+
+import java.io.IOException;
+
+import javax.swing.Icon;
+
+import ghidra.framework.data.FolderLinkContentHandler;
+
+/**
+ * {@code LinkedDomainFolder} extends {@link DomainFolder} for all folders which are
+ * accessable via a folder-link (see {@link FolderLinkContentHandler}).
+ */
+public interface LinkedDomainFolder extends DomainFolder {
+
+ /**
+ * Get the real domain folder which corresponds to this linked-folder.
+ * @return domain folder
+ * @throws IOException if an IO error occurs
+ */
+ public DomainFolder getLinkedFolder() throws IOException;
+
+ /**
+ * Get the appropriate icon for this folder
+ * @param isOpen true if open icon, false for closed
+ * @return folder icon
+ */
+ public Icon getIcon(boolean isOpen);
+
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java
index 5f7c8c207a..c5b4bbc62b 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java
@@ -16,7 +16,6 @@
package ghidra.framework.model;
import java.io.IOException;
-import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
@@ -84,12 +83,14 @@ public interface Project {
* Add the given project URL to this project's list project views.
* The project view allows users to look at data files from another
* project.
- * @param projectURL identifier for the project view
+ * @param projectURL identifier for the project view (ghidra protocol only).
+ * @param visible true if project may be made visible or false if hidden. Hidden viewed
+ * projects are used when only life-cycle management is required (e.g., close view project
+ * when this project is closed).
* @return project data for this view
* @throws IOException if I/O error occurs or if project/repository not found
- * @throws MalformedURLException if projectURL is invalid
*/
- public ProjectData addProjectView(URL projectURL) throws IOException, MalformedURLException;
+ public ProjectData addProjectView(URL projectURL, boolean visible) throws IOException;
/**
* Remove the project view from this project.
@@ -98,7 +99,7 @@ public interface Project {
public void removeProjectView(URL projectURL);
/**
- * Return the list of project views in this project.
+ * Return the list of visible project views in this project.
*/
public ProjectLocator[] getProjectViews();
@@ -174,9 +175,9 @@ public interface Project {
public ProjectData getProjectData(URL url);
/**
- * Get the project data for other projects that are
- * currently being viewed.
- * @return zero length array if there are no viewed projects open
+ * Get the project data for visible viewed projects that are
+ * managed by this project.
+ * @return zero length array if there are no visible viewed projects open
*/
public ProjectData[] getViewedProjectData();
@@ -186,4 +187,16 @@ public interface Project {
*/
public void releaseFiles(Object consumer);
+ /**
+ * Add a listener to be notified when a visible project view is added or removed.
+ * @param listener project view listener
+ */
+ public void addProjectViewListener(ProjectViewListener listener);
+
+ /**
+ * Remove a project view listener previously added.
+ * @param listener project view listener
+ */
+ public void removeProjectViewListener(ProjectViewListener listener);
+
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java
index a6c0f41aed..fb635f8739 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java
@@ -39,6 +39,7 @@ public interface ProjectData {
/**
* Returns the root folder of the project.
+ * @return root {@link DomainFolder} within project.
*/
public DomainFolder getRootFolder();
@@ -110,15 +111,6 @@ public interface ProjectData {
*/
public DomainFile getFileByID(String fileID);
- /**
- * Get a URL for a shared domain file which is available
- * within a remote repository.
- * @param path the absolute path of domain file relative to the root folder.
- * @return URL object for accessing shared file from outside of a project, or
- * null if file does not exist or is not shared.
- */
- public URL getSharedFileURL(String path);
-
/**
* Transform the specified name into an acceptable folder or file item name. Only an individual folder
* or file name should be specified, since any separators will be stripped-out.
@@ -130,6 +122,7 @@ public interface ProjectData {
/**
* Returns the projectLocator for the this ProjectData.
+ * @return project locator object
*/
public ProjectLocator getProjectLocator();
@@ -150,12 +143,14 @@ public interface ProjectData {
* Sync the Domain folder/file structure with the underlying file structure.
* @param force if true all folders will be be visited and refreshed, if false
* only those folders previously visited will be refreshed.
+ * @throws IOException if an IO error occurs
*/
public void refresh(boolean force) throws IOException;
/**
* Returns User object associated with remote repository or null if a remote repository
* is not used.
+ * @return current remote user identity or null
*/
public User getUser();
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java
index e3313d1702..5845a0900f 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java
@@ -1,6 +1,5 @@
/* ###
* 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.
@@ -16,39 +15,65 @@
*/
package ghidra.framework.model;
-import ghidra.framework.protocol.ghidra.GhidraURL;
-
import java.io.File;
import java.net.URL;
+import org.apache.commons.lang3.StringUtils;
+
+import ghidra.framework.protocol.ghidra.GhidraURL;
+
/**
* Lightweight descriptor of a local Project storage location.
*/
public class ProjectLocator {
- private static final String PROJECT_FILE_SUFFIX = ".gpr";
- private static final String PROJECT_DIR_SUFFIX = ".rep";
+ public static final String PROJECT_FILE_SUFFIX = ".gpr";
+ public static final String PROJECT_DIR_SUFFIX = ".rep";
+
private static final String LOCK_FILE_SUFFIX = ".lock";
- private String name;
- private String location;
+ private final String name;
+ private final String location;
+
private URL url;
/**
- * Construct a project URL.
- * @param path path to parent directory
+ * Construct a project locator object.
+ * @param path path to parent directory (may or may not exist). The user's temp directory
+ * will be used if this value is null or blank.
+ * WARNING: Use of a relative paths should be avoided (e.g., on a windows platform
+ * an absolute path should start with a drive letter specification such as C:\path,
+ * while this same path on a Linux platform would be treated as relative).
* @param name name of the project
*/
public ProjectLocator(String path, String name) {
- this.name = name;
if (name.endsWith(PROJECT_FILE_SUFFIX)) {
- this.name = name.substring(0, name.length() - PROJECT_FILE_SUFFIX.length());
+ name = name.substring(0, name.length() - PROJECT_FILE_SUFFIX.length());
}
- this.location = path;
- if (path == null) {
- this.location = System.getProperty("java.io.tmpdir");
+ this.name = name;
+ if (StringUtils.isBlank(path)) {
+ path = System.getProperty("java.io.tmpdir");
}
- this.url = GhidraURL.makeURL(location, name);
+ this.location = checkAbsolutePath(path);
+ url = GhidraURL.makeURL(location, name);
+ }
+
+ /**
+ * Ensure that absolute path is specified.
+ * @param path path to be checked and possibly modified.
+ * @return path to be used
+ */
+ private static String checkAbsolutePath(String path) {
+ if (path.startsWith("/") && path.length() >= 4 && path.indexOf(":/") == 2 &&
+ Character.isLetter(path.charAt(1))) {
+ // strip leading "/" on Windows paths (e.g., /C:/mydir) and transform separators to '\'
+ path = path.substring(1);
+ path = path.replace('/', '\\');
+ }
+ if (path.endsWith("/") || path.endsWith("\\")) {
+ path = path.substring(0, path.length() - 1);
+ }
+ return path;
}
/**
@@ -60,8 +85,8 @@ public class ProjectLocator {
}
/**
- * Returns the URL associated with this local project.
- * If this is a transient project, a remote repository URL will be returned.
+ * Returns the URL associated with this local project. If using a temporary transient
+ * project location this URL should not be used.
*/
public URL getURL() {
return url;
@@ -75,7 +100,10 @@ public class ProjectLocator {
}
/**
- * Get the location of the project.
+ * Get the location of the project which will contain marker file
+ * ({@link #getMarkerFile()}) and project directory ({@link #getProjectDir()}).
+ * Note: directory may or may not exist.
+ * @return project location directory
*/
public String getLocation() {
return location;
@@ -110,9 +138,6 @@ public class ProjectLocator {
return PROJECT_DIR_SUFFIX;
}
- /* (non-Javadoc)
- * @see java.lang.Object#equals(java.lang.Object)
- */
@Override
public boolean equals(Object obj) {
if (obj == null) {
@@ -125,23 +150,14 @@ public class ProjectLocator {
return false;
}
ProjectLocator projectLocator = (ProjectLocator) obj;
- if (hashCode() != projectLocator.hashCode()) {
- return false;
- }
return name.equals(projectLocator.name) && location.equals(projectLocator.location);
}
- /* (non-Javadoc)
- * @see java.lang.Object#hashCode()
- */
@Override
public int hashCode() {
return name.hashCode() + location.hashCode();
}
- /* (non-Javadoc)
- * @see java.lang.Object#toString()
- */
@Override
public String toString() {
return GhidraURL.getDisplayString(url);
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java
new file mode 100644
index 0000000000..9a65ec9ef8
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java
@@ -0,0 +1,42 @@
+/* ###
+ * 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.model;
+
+import java.net.URL;
+
+/**
+ * {@code ProjectViewListener} provides a listener interface for tracking project views added
+ * and removed from the associated project.
+ *
+ * NOTE: notification callbacks are not guarenteed to occur within the swing thread.
+ */
+public interface ProjectViewListener {
+
+ /**
+ * Provides notification that a read-only viewed project has been added which is intended to
+ * be visible. Notification for hidden viewed projects will not be provided.
+ * @param projectView project view URL
+ */
+ void viewedProjectAdded(URL projectView);
+
+ /**
+ * Provides notification that a viewed project is being removed from the project.
+ * Notification for hidden viewed project removal will not be provided.
+ * @param projectView project view URL
+ */
+ void viewedProjectRemoved(URL projectView);
+
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolServices.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolServices.java
index c4b07d6fe5..11b2dd12e6 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolServices.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolServices.java
@@ -16,10 +16,12 @@
package ghidra.framework.model;
import java.io.*;
+import java.net.URL;
import java.util.Set;
import ghidra.framework.plugintool.PluginEvent;
import ghidra.framework.plugintool.PluginTool;
+import ghidra.framework.protocol.ghidra.GhidraURL;
/**
* Services that the Tool uses.
@@ -73,15 +75,23 @@ public interface ToolServices {
public void displaySimilarTool(PluginTool tool, DomainFile domainFile, PluginEvent event);
/**
- * Returns the default tool template used to open the tool. Here default means the
- * tool that should be used to open the given file, whether defined by the user or the
- * system default.
+ * Returns the default/preferred tool template which should be used to open the specified
+ * domain file, whether defined by the user or the system default.
*
- * @param domainFile The file for which to find the preferred tool.
- * @return The preferred tool that should be used to open the given file.
+ * @param domainFile The file whose preferred tool should be found.
+ * @return The preferred tool that should be used to open the given file or null if none found.
*/
public ToolTemplate getDefaultToolTemplate(DomainFile domainFile);
+ /**
+ * Returns the default/preferred tool template which should be used to open the specified
+ * domain file content type, whether defined by the user or the system default.
+ *
+ * @param contentType The content type whose preferred tool should be found.
+ * @return The preferred tool that should be used to open the given file or null if none found.
+ */
+ public ToolTemplate getDefaultToolTemplate(String contentType);
+
/**
* Returns a set of tools that can open the given domain file class.
* @param domainClass The domain file class type for which to get tools
@@ -108,21 +118,43 @@ public interface ToolServices {
public void setContentTypeToolAssociations(Set infos);
/**
- * Launch the default tool; if domainFile is not null, this file will
- * be opened in the tool.
- * @param domainFile the file to open; may be null
- * @return the tool
+ * Launch the default tool and open the specified domainFile.
+ * @param domainFile the file to open
+ * @return the launched tool. Null returned if a suitable default tool
+ * for the file content type was not found.
*/
public PluginTool launchDefaultTool(DomainFile domainFile);
/**
- * Launch the tool with the given name
+ * Launch the tool with the given name. A domainFile may be specified and will be opened
+ * if its content type is supported by the tool.
* @param toolName name of the tool to launch
* @param domainFile the file to open; may be null
- * @return the tool
+ * @return the requested tool or null if the specified tool not found.
*/
public PluginTool launchTool(String toolName, DomainFile domainFile);
+ /**
+ * Launch the default tool and open the specified Ghidra URL resource.
+ * The tool choosen well be based upon the content type of the specified resource.
+ * @param ghidraUrl resource to be opened (see {@link GhidraURL})
+ * @return the launched tool. Null returned if a failure occurs while accessing the specified
+ * resource or a suitable default tool for the file content type was not found.
+ * @throws IllegalArgumentException if URL protocol is not supported. Currently, only
+ * the {@code ghidra} protocol is supported.
+ */
+ public PluginTool launchDefaultToolWithURL(URL ghidraUrl);
+
+ /**
+ * Launch the tool with the given name and attempt to open the specified Ghidra URL resource.
+ * @param toolName name of the tool to launch
+ * @param ghidraUrl resource to be opened (see {@link GhidraURL})
+ * @return the requested tool or null if the specified tool not found.
+ * @throws IllegalArgumentException if URL protocol is not supported. Currently, only
+ * the {@code ghidra} protocol is supported.
+ */
+ public PluginTool launchToolWithURL(String toolName, URL ghidraUrl);
+
/**
* Add a listener that will be notified when the default tool specification changes
* @param listener the listener
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/Plugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/Plugin.java
index 3b67a286b4..3efd06adc5 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/Plugin.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/Plugin.java
@@ -15,6 +15,7 @@
*/
package ghidra.framework.plugintool;
+import java.net.URL;
import java.util.*;
import docking.ComponentProvider;
@@ -330,6 +331,17 @@ public abstract class Plugin implements ExtensionPoint, PluginEventListener, Ser
return false;
}
+ /**
+ * Request plugin to process URL if supported. Actual processing may be delayed and
+ * interaction with user may occur (e.g., authentication, approval, etc.).
+ *
+ * @param url data URL
+ * @return boolean true if this plugin can process URL.
+ */
+ public boolean accept(URL url) {
+ return false;
+ }
+
/**
* Get the domain files that this plugin has open.
*
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginManager.java
index 7308f92f80..6cfae8f1d4 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginManager.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginManager.java
@@ -15,6 +15,7 @@
*/
package ghidra.framework.plugintool;
+import java.net.URL;
import java.util.*;
import java.util.Map.Entry;
@@ -52,6 +53,23 @@ class PluginManager {
return false;
}
+ /**
+ * Identify plugin which will accept specified URL. If no plugin accepts URL it will be
+ * rejected and false returned. If a plugin can accept the specified URL it will attempt to
+ * process and return true if successful.
+ * The user may be prompted if connecting to the URL requires user authentication.
+ * @param url read-only resource URL
+ * @return true if URL accepted and processed else false
+ */
+ boolean accept(URL url) {
+ for (Plugin p : pluginList) {
+ if (p.accept(url)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public void dispose() {
for (Iterator it = pluginList.iterator(); it.hasNext();) {
Plugin plugin = it.next();
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java
index 07fe6bc906..590148b7d5 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java
@@ -21,6 +21,7 @@ import java.awt.*;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
+import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
@@ -440,6 +441,18 @@ public abstract class PluginTool extends AbstractDockingTool {
return pluginMgr.acceptData(data);
}
+ /**
+ * Request tool to accept specified URL. Acceptance of URL depends greatly on the plugins
+ * confiugred into tool. If no plugin accepts URL it will be rejected and false returned.
+ * If a plugin can accept the specified URL it will attempt to process and return true if
+ * successful. The user may be prompted if connecting to the URL requires user authentication.
+ * @param url read-only resource URL
+ * @return true if URL accepted and processed else false
+ */
+ public boolean accept(URL url) {
+ return pluginMgr.accept(url);
+ }
+
public void addPropertyChangeListener(PropertyChangeListener l) {
propertyChangeMgr.addPropertyChangeListener(l);
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/ToolServicesAdapter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/ToolServicesAdapter.java
index b865f615e0..91a3f4cf78 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/ToolServicesAdapter.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/ToolServicesAdapter.java
@@ -16,6 +16,7 @@
package ghidra.framework.plugintool;
import java.io.*;
+import java.net.URL;
import java.util.Set;
import ghidra.framework.model.*;
@@ -61,6 +62,11 @@ public class ToolServicesAdapter implements ToolServices {
return null;
}
+ @Override
+ public ToolTemplate getDefaultToolTemplate(String contentType) {
+ return null;
+ }
+
@Override
public PluginTool[] getRunningTools() {
return null;
@@ -81,6 +87,16 @@ public class ToolServicesAdapter implements ToolServices {
return null;
}
+ @Override
+ public PluginTool launchDefaultToolWithURL(URL url) {
+ return null;
+ }
+
+ @Override
+ public PluginTool launchToolWithURL(String toolName, URL url) {
+ return null;
+ }
+
@Override
public void removeDefaultToolChangeListener(DefaultToolChangeListener listener) {
// override
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java
index 6e53c4e81f..3ea00eebac 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java
@@ -16,7 +16,6 @@
package ghidra.framework.project;
import java.io.*;
-import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.Map.Entry;
@@ -25,7 +24,6 @@ import org.jdom.*;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;
-import docking.widgets.OptionDialog;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.data.ProjectFileManager;
import ghidra.framework.data.TransientDataManager;
@@ -35,8 +33,11 @@ import ghidra.framework.project.tool.GhidraToolTemplate;
import ghidra.framework.project.tool.ToolManagerImpl;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.protocol.ghidra.GhidraURLConnection;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
import ghidra.framework.store.LockException;
import ghidra.util.*;
+import ghidra.util.datastruct.WeakDataStructureFactory;
+import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.xml.GenericXMLOutputter;
import ghidra.util.xml.XmlUtilities;
@@ -53,8 +54,6 @@ public class DefaultProject implements Project {
private static final String PROJECT_STATE = "projectState";
- private ProjectLock projectLock;
-
// this may be null
private DefaultProjectManager projectManager;
@@ -63,11 +62,14 @@ public class DefaultProject implements Project {
private ToolManagerImpl toolManager;
private boolean changed; // flag for whether the project configuration has changed
- private boolean isClosed;
+ private volatile boolean isClosed;
private Map dataMap = new HashMap<>();
- private HashMap projectConfigMap = new HashMap<>();
- private HashMap otherViews = new HashMap<>();
+ private Map projectConfigMap = new HashMap<>();
+ private Map otherViews = new HashMap<>();
+ private Set visibleViews = new HashSet<>();
+ private WeakSet viewListeners =
+ WeakDataStructureFactory.createCopyOnWriteWeakSet();
/**
* Constructor for creating a New project
@@ -83,10 +85,6 @@ public class DefaultProject implements Project {
RepositoryAdapter repository) throws IOException, LockException {
this.projectManager = projectManager;
this.projectLocator = projectLocator;
- this.projectLock = getProjectLock(projectLocator, false);
- if (projectLock == null) {
- throw new LockException("Unable to lock project! " + projectLocator);
- }
boolean success = false;
try {
@@ -102,7 +100,6 @@ public class DefaultProject implements Project {
if (fileMgr != null) {
fileMgr.dispose();
}
- projectLock.release();
}
}
initializeNewProject();
@@ -124,10 +121,6 @@ public class DefaultProject implements Project {
this.projectManager = projectManager;
this.projectLocator = projectLocator;
- this.projectLock = getProjectLock(projectLocator, true);
- if (projectLock == null) {
- throw new LockException("Unable to lock project! " + projectLocator);
- }
boolean success = false;
try {
@@ -143,7 +136,6 @@ public class DefaultProject implements Project {
if (fileMgr != null) {
fileMgr.dispose();
}
- projectLock.release();
}
}
}
@@ -188,54 +180,64 @@ public class DefaultProject implements Project {
return projectManager;
}
- /**
- * Creates a ProjectLock and attempts to lock it. This handles the case
- * where the project was previously locked.
- *
- * @param locator the project locator
- * @param allowInteractiveForce if true, when a lock cannot be obtained, the
- * user will be prompted
- * @return A locked ProjectLock
- * @throws ProjectLockException if lock failed
- */
- private ProjectLock getProjectLock(ProjectLocator locator, boolean allowInteractiveForce) {
- ProjectLock lock = new ProjectLock(locator);
- if (lock.lock()) {
- return lock;
- }
+// /**
+// * Determine if the specified project location currently has a write lock.
+// * @param locator project storage locator
+// * @return true if project data current has write-lock else false
+// */
+// public static boolean isLocked(ProjectLocator locator) {
+// ProjectLock lock = new ProjectLock(locator);
+// return lock.isLocked();
+// }
- // in headless mode, just spit out an error
- if (!allowInteractiveForce || SystemUtilities.isInHeadlessMode()) {
- return null;
- }
-
- String projectStr = "Project: " + HTMLUtilities.escapeHTML(locator.getLocation()) +
- System.getProperty("file.separator") + HTMLUtilities.escapeHTML(locator.getName());
- String lockInformation = lock.getExistingLockFileInformation();
- if (!lock.canForceLock()) {
- Msg.showInfo(getClass(), null, "Project Locked",
- "Project is locked. You have another instance of Ghidra
" +
- "already running with this project open (locally or remotely).
" +
- projectStr + "
" + "Lock information: " + lockInformation);
- return null;
- }
-
- int userChoice = OptionDialog.showOptionDialog(null, "Project Locked - Delete Lock?",
- "Project is locked. You may have another instance of Ghidra
" +
- "already running with this project opened (locally or remotely).
" + projectStr +
- "
" + "If this is not the case, you can delete the lock file:
" +
- locator.getProjectLockFile().getAbsolutePath() + ".
" +
- "Lock information: " + lockInformation,
- "Delete Lock", OptionDialog.QUESTION_MESSAGE);
- if (userChoice == OptionDialog.OPTION_ONE) { // Delete Lock
- if (lock.forceLock()) {
- return lock;
- }
-
- Msg.showError(this, null, "Error", "Attempt to force lock failed! " + locator);
- }
- return null;
- }
+// /**
+// * Creates a ProjectLock and attempts to lock it. This handles the case
+// * where the project was previously locked.
+// *
+// * @param locator the project locator
+// * @param allowInteractiveForce if true, when a lock cannot be obtained, the
+// * user will be prompted
+// * @return A locked ProjectLock
+// * @throws ProjectLockException if lock failed
+// */
+// private ProjectLock getProjectLock(ProjectLocator locator, boolean allowInteractiveForce) {
+// ProjectLock lock = new ProjectLock(locator);
+// if (lock.lock()) {
+// return lock;
+// }
+//
+// // in headless mode, just spit out an error
+// if (!allowInteractiveForce || SystemUtilities.isInHeadlessMode()) {
+// return null;
+// }
+//
+// String projectStr = "Project: " + HTMLUtilities.escapeHTML(locator.getLocation()) +
+// System.getProperty("file.separator") + HTMLUtilities.escapeHTML(locator.getName());
+// String lockInformation = lock.getExistingLockFileInformation();
+// if (!lock.canForceLock()) {
+// Msg.showInfo(getClass(), null, "Project Locked",
+// "Project is locked. You have another instance of Ghidra
" +
+// "already running with this project open (locally or remotely).
" +
+// projectStr + "
" + "Lock information: " + lockInformation);
+// return null;
+// }
+//
+// int userChoice = OptionDialog.showOptionDialog(null, "Project Locked - Delete Lock?",
+// "Project is locked. You may have another instance of Ghidra
" +
+// "already running with this project opened (locally or remotely).
" + projectStr +
+// "
" + "If this is not the case, you can delete the lock file:
" +
+// locator.getProjectLockFile().getAbsolutePath() + ".
" +
+// "Lock information: " + lockInformation,
+// "Delete Lock", OptionDialog.QUESTION_MESSAGE);
+// if (userChoice == OptionDialog.OPTION_ONE) { // Delete Lock
+// if (lock.forceLock()) {
+// return lock;
+// }
+//
+// Msg.showError(this, null, "Error", "Attempt to force lock failed! " + locator);
+// }
+// return null;
+// }
private void initializeNewProject() {
if (toolManager == null) {
@@ -261,54 +263,94 @@ public class DefaultProject implements Project {
}
@Override
- public ProjectData addProjectView(URL url) throws IOException, MalformedURLException {
+ public void addProjectViewListener(ProjectViewListener listener) {
+ viewListeners.add(listener);
+ }
- ProjectData pd = otherViews.get(url);
- if (pd != null) {
- return pd;
- }
+ @Override
+ public void removeProjectViewListener(ProjectViewListener listener) {
+ viewListeners.remove(listener);
+ }
- if (!GhidraURL.PROTOCOL.equals(url.getProtocol())) {
- throw new IOException("Invalid Ghidra URL specified: " + url);
+ private void notifyVisibleViewAdded(URL projectView) {
+ for (ProjectViewListener listener : viewListeners) {
+ listener.viewedProjectAdded(projectView);
}
+ }
+
+ private void notifyVisibleViewRemoved(URL projectView) {
+ for (ProjectViewListener listener : viewListeners) {
+ listener.viewedProjectRemoved(projectView);
+ }
+ }
+
+ private ProjectData openProjectView(URL url) throws IOException {
GhidraURLConnection c = (GhidraURLConnection) url.openConnection();
c.setAllowUserInteraction(true);
c.setReadOnly(true);
- int responseCode = c.getResponseCode();
- if (responseCode == GhidraURLConnection.GHIDRA_NOT_FOUND) {
+ StatusCode responseCode = c.getStatusCode();
+ if (responseCode == StatusCode.NOT_FOUND) {
throw new IOException(
"Project/repository not found: " + GhidraURL.getDisplayString(url));
}
- if (responseCode == GhidraURLConnection.GHIDRA_UNAUTHORIZED) {
- throw new IOException(
- "Authentication Failed for project/repository: " + GhidraURL.getDisplayString(url));
+ if (responseCode == StatusCode.UNAUTHORIZED) {
+ // assume already informed
+ return null;
}
ProjectFileManager projectData = (ProjectFileManager) c.getProjectData();
if (projectData == null) {
throw new IOException(
- "Failed to view specified project/repository: " + GhidraURL.getDisplayString(url));
+ "Failed to view specified project/repository: " +
+ GhidraURL.getDisplayString(url));
}
url = projectData.getProjectLocator().getURL(); // transform to repository root URL
otherViews.put(url, projectData);
- changed = true;
- Msg.info(this, "Opened project view: " + GhidraURL.getDisplayString(url));
return projectData;
}
+ @Override
+ public ProjectData addProjectView(URL url, boolean visible) throws IOException {
+ synchronized (otherViews) {
+ if (isClosed) {
+ throw new IOException("project is closed");
+ }
+
+ if (!GhidraURL.PROTOCOL.equals(url.getProtocol())) {
+ throw new IOException("Invalid Ghidra URL specified: " + url);
+ }
+
+ ProjectData projectData = otherViews.get(url);
+ if (projectData == null) {
+ projectData = openProjectView(url);
+ }
+
+ if (projectData != null && visible && visibleViews.add(url)) {
+ notifyVisibleViewAdded(url);
+ }
+
+ return projectData;
+ }
+ }
+
/**
* Remove the view from this project.
*/
@Override
public void removeProjectView(URL url) {
- ProjectFileManager dataMgr = otherViews.remove(url);
- if (dataMgr != null) {
- dataMgr.dispose();
- Msg.info(this, "Closed project view: " + GhidraURL.getDisplayString(url));
- changed = true;
+ synchronized (otherViews) {
+ ProjectFileManager dataMgr = otherViews.remove(url);
+ if (dataMgr != null) {
+ if (visibleViews.remove(url)) {
+ notifyVisibleViewRemoved(url);
+ }
+ dataMgr.dispose();
+ Msg.info(this, "Closed project view: " + GhidraURL.getDisplayString(url));
+ changed = true;
+ }
}
}
@@ -348,6 +390,7 @@ public class DefaultProject implements Project {
@Override
public ProjectLocator[] getProjectViews() {
+ // Only includes visible viewed projects
ProjectData[] pd = getViewedProjectData();
ProjectLocator[] views = new ProjectLocator[pd.length];
@@ -364,17 +407,21 @@ public class DefaultProject implements Project {
@Override
public void close() {
- Iterator iter = otherViews.values().iterator();
- while (iter.hasNext()) {
- ProjectFileManager dataMgr = iter.next();
- if (dataMgr != null) {
- dataMgr.dispose();
+ synchronized (otherViews) {
+ isClosed = true;
+
+ Iterator iter = otherViews.values().iterator();
+ while (iter.hasNext()) {
+ ProjectFileManager dataMgr = iter.next();
+ if (dataMgr != null) {
+ dataMgr.dispose();
+ }
}
+ otherViews.clear();
}
- otherViews.clear();
try {
- isClosed = true;
+
if (toolManager != null) {
toolManager.close();
toolManager.dispose();
@@ -382,12 +429,9 @@ public class DefaultProject implements Project {
if (projectManager != null) {
projectManager.projectClosed(this);
}
- fileMgr.dispose();
}
finally {
- if (projectLock != null) {
- projectLock.release();
- }
+ fileMgr.dispose();
}
}
@@ -450,7 +494,7 @@ public class DefaultProject implements Project {
String location = elem.getAttributeValue("LOCATION");
URL url = GhidraURL.makeURL(location, name);
try {
- addProjectView(url);
+ addProjectView(url, true);
}
catch (IOException e) {
Msg.error(this, e.getMessage());
@@ -462,7 +506,7 @@ public class DefaultProject implements Project {
String urlStr = elem.getAttributeValue("URL");
URL url = new URL(urlStr);
try {
- addProjectView(url);
+ addProjectView(url, true);
}
catch (IOException e) {
Msg.error(this, e.getMessage());
@@ -602,7 +646,7 @@ public class DefaultProject implements Project {
}
@Override
- public ProjectData getProjectData() {
+ public ProjectFileManager getProjectData() {
return fileMgr;
}
@@ -622,11 +666,14 @@ public class DefaultProject implements Project {
return fileMgr;
}
- for (ProjectData data : otherViews.values()) {
- if (locator.equals(data.getProjectLocator())) {
- return data;
+ synchronized (otherViews) {
+ for (ProjectData data : otherViews.values()) {
+ if (locator.equals(data.getProjectLocator())) {
+ return data;
+ }
}
}
+
return null;
}
@@ -640,18 +687,31 @@ public class DefaultProject implements Project {
@Override
public ProjectData[] getViewedProjectData() {
- ProjectData[] projectData = new ProjectData[otherViews.size()];
- otherViews.values().toArray(projectData);
- return projectData;
+ synchronized (otherViews) {
+
+ // only return visible viewed project
+ List list = new ArrayList<>();
+ for (URL url : otherViews.keySet()) {
+ if (visibleViews.contains(url)) {
+ list.add(otherViews.get(url));
+ }
+ }
+
+ ProjectData[] projectData = new ProjectData[list.size()];
+ list.toArray(projectData);
+ return projectData;
+ }
}
@Override
public void releaseFiles(Object consumer) {
fileMgr.releaseDomainFiles(consumer);
- Iterator it = otherViews.values().iterator();
- while (it.hasNext()) {
- ProjectFileManager mgr = it.next();
- mgr.releaseDomainFiles(consumer);
+ synchronized (otherViews) {
+ Iterator it = otherViews.values().iterator();
+ while (it.hasNext()) {
+ ProjectFileManager mgr = it.next();
+ mgr.releaseDomainFiles(consumer);
+ }
}
TransientDataManager.releaseFiles(consumer);
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java
index 397680a173..0e63ccb3e1 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java
@@ -152,7 +152,7 @@ public class GhidraToolTemplate implements ToolTemplate {
dtList.add(Class.forName(className));
}
catch (ClassNotFoundException e) {
- Msg.error(this, "Class not found: " + className, e);
+ Msg.warn(this, "Tool supported content class not found: " + className);
}
catch (Exception exc) {//TODO
Msg.error(this, "Unexpected Exception: " + exc.getMessage(), exc);
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java
index a78fa72238..cfebe63aee 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java
@@ -16,6 +16,7 @@
package ghidra.framework.project.tool;
import java.io.*;
+import java.net.URL;
import java.util.*;
import org.jdom.Document;
@@ -24,16 +25,18 @@ import org.jdom.output.XMLOutputter;
import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser;
import ghidra.framework.ToolUtils;
-import ghidra.framework.data.ContentHandler;
-import ghidra.framework.data.DomainObjectAdapter;
+import ghidra.framework.data.*;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginEvent;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.preferences.Preferences;
+import ghidra.framework.protocol.ghidra.GetUrlContentTypeTask;
+import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.filechooser.GhidraFileChooserModel;
import ghidra.util.filechooser.GhidraFileFilter;
+import ghidra.util.task.TaskLauncher;
import ghidra.util.xml.GenericXMLOutputter;
/**
@@ -48,7 +51,7 @@ class ToolServicesImpl implements ToolServices {
private ToolManagerImpl toolManager;
private List listeners = new ArrayList<>();
private ToolChestChangeListener toolChestChangeListener;
- private Set contentHandlers;
+ private Set> contentHandlers;
ToolServicesImpl(ToolChest toolChest, ToolManagerImpl toolManager) {
this.toolChest = toolChest;
@@ -187,38 +190,88 @@ class ToolServicesImpl implements ToolServices {
@Override
public PluginTool launchDefaultTool(DomainFile domainFile) {
ToolTemplate template = getDefaultToolTemplate(domainFile);
- if (template != null) {
- Workspace workspace = toolManager.getActiveWorkspace();
- PluginTool tool = workspace.runTool(template);
- tool.setVisible(true);
- if (domainFile != null) {
- tool.acceptDomainFiles(new DomainFile[] { domainFile });
- }
- return tool;
+ if (template == null) {
+ return null;
}
- return null;
+ Workspace workspace = toolManager.getActiveWorkspace();
+ PluginTool tool = workspace.runTool(template);
+ if (tool == null) {
+ return null;
+ }
+ tool.setVisible(true);
+ tool.acceptDomainFiles(new DomainFile[] { domainFile });
+ return tool;
}
@Override
public PluginTool launchTool(String toolName, DomainFile domainFile) {
ToolTemplate template = findToolChestToolTemplate(toolName);
- if (template != null) {
- Workspace workspace = toolManager.getActiveWorkspace();
- PluginTool tool = workspace.runTool(template);
- tool.setVisible(true);
- if (domainFile != null) {
- tool.acceptDomainFiles(new DomainFile[] { domainFile });
- }
- return tool;
+ if (template == null) {
+ return null;
}
- return null;
+ Workspace workspace = toolManager.getActiveWorkspace();
+ PluginTool tool = workspace.runTool(template);
+ if (tool == null) {
+ return null;
+ }
+ tool.setVisible(true);
+ if (domainFile != null) {
+ tool.acceptDomainFiles(new DomainFile[] { domainFile });
+ }
+ return tool;
+ }
+
+ @Override
+ public PluginTool launchDefaultToolWithURL(URL ghidraUrl) throws IllegalArgumentException {
+ String contentType = getContentType(ghidraUrl);
+ if (contentType == null) {
+ return null;
+ }
+ ToolTemplate template = getDefaultToolTemplate(contentType);
+ if (template == null) {
+ return null;
+ }
+ Workspace workspace = toolManager.getActiveWorkspace();
+ PluginTool tool = workspace.runTool(template);
+ if (tool == null) {
+ return null;
+ }
+ tool.setVisible(true);
+ tool.accept(ghidraUrl);
+ return tool;
+ }
+
+ @Override
+ public PluginTool launchToolWithURL(String toolName, URL ghidraUrl)
+ throws IllegalArgumentException {
+ if (!GhidraURL.isLocalProjectURL(ghidraUrl) &&
+ !GhidraURL.isServerRepositoryURL(ghidraUrl)) {
+ throw new IllegalArgumentException("unsupported URL");
+ }
+ ToolTemplate template = findToolChestToolTemplate(toolName);
+ if (template == null) {
+ return null;
+ }
+ Workspace workspace = toolManager.getActiveWorkspace();
+ PluginTool tool = workspace.runTool(template);
+ if (tool != null) {
+ tool.setVisible(true);
+ tool.accept(ghidraUrl);
+ }
+ return tool;
+ }
+
+ private String getContentType(URL url) throws IllegalArgumentException {
+ GetUrlContentTypeTask task = new GetUrlContentTypeTask(url);
+ TaskLauncher.launch(task); // blocking task
+ return task.getContentType();
}
@Override
public void setContentTypeToolAssociations(Set infos) {
for (ToolAssociationInfo info : infos) {
- ContentHandler handler = info.getContentHandler();
+ ContentHandler> handler = info.getContentHandler();
String contentType = handler.getContentType();
String preferenceKey = getToolAssociationPreferenceKey(contentType);
if (!info.isDefault()) {
@@ -254,15 +307,15 @@ class ToolServicesImpl implements ToolServices {
Set set = new HashSet<>();
// get all known content types
- Set handlers = getContentHandlers();
- for (ContentHandler contentHandler : handlers) {
+ Set> handlers = getContentHandlers();
+ for (ContentHandler> contentHandler : handlers) {
set.add(createToolAssociationInfo(contentHandler));
}
return set;
}
- private ToolAssociationInfo createToolAssociationInfo(ContentHandler contentHandler) {
+ private ToolAssociationInfo createToolAssociationInfo(ContentHandler> contentHandler) {
String contentType = contentHandler.getContentType();
String defaultToolName = contentHandler.getDefaultToolName();
String userPreferredToolName =
@@ -280,8 +333,11 @@ class ToolServicesImpl implements ToolServices {
@Override
public ToolTemplate getDefaultToolTemplate(DomainFile domainFile) {
- String contentType = domainFile.getContentType();
+ return getDefaultToolTemplate(domainFile.getContentType());
+ }
+ @Override
+ public ToolTemplate getDefaultToolTemplate(String contentType) {
String toolName =
Preferences.getProperty(getToolAssociationPreferenceKey(contentType), null, true);
if (toolName == null) {
@@ -312,8 +368,8 @@ class ToolServicesImpl implements ToolServices {
//
// Next, look through for all compatible content handlers find tools for them
//
- Set compatibleHandlers = getCompatibleContentHandlers(domainClass);
- for (ContentHandler handler : compatibleHandlers) {
+ Set> compatibleHandlers = getCompatibleContentHandlers(domainClass);
+ for (ContentHandler> handler : compatibleHandlers) {
String defaultToolName = handler.getDefaultToolName();
if (nameToTemplateMap.get(defaultToolName) != null) {
continue; // already have tool in the map by this name; prefer that tool
@@ -355,11 +411,11 @@ class ToolServicesImpl implements ToolServices {
return new HashSet<>(nameToTemplateMap.values());
}
- private Set getCompatibleContentHandlers(
+ private Set> getCompatibleContentHandlers(
Class extends DomainObject> domainClass) {
- Set set = new HashSet<>();
- Set handlers = getContentHandlers();
- for (ContentHandler contentHandler : handlers) {
+ Set> set = new HashSet<>();
+ Set> handlers = getContentHandlers();
+ for (ContentHandler> contentHandler : handlers) {
Class extends DomainObject> handlerDomainClass =
contentHandler.getDomainObjectClass();
if (handlerDomainClass == domainClass) {
@@ -374,8 +430,8 @@ class ToolServicesImpl implements ToolServices {
}
private String getDefaultToolAssociation(String contentType) {
- Set handlers = getContentHandlers();
- for (ContentHandler contentHandler : handlers) {
+ Set> handlers = getContentHandlers();
+ for (ContentHandler> contentHandler : handlers) {
String type = contentHandler.getContentType();
if (type.equals(contentType)) {
return contentHandler.getDefaultToolName();
@@ -384,25 +440,31 @@ class ToolServicesImpl implements ToolServices {
return null;
}
- private Set getContentHandlers() {
+ private Set> getContentHandlers() {
if (contentHandlers != null) {
return contentHandlers;
}
contentHandlers = new HashSet<>();
+ @SuppressWarnings("rawtypes")
List instances = ClassSearcher.getInstances(ContentHandler.class);
- for (ContentHandler contentHandler : instances) {
+ for (ContentHandler> contentHandler : instances) {
+
+ if (contentHandler instanceof FolderLinkContentHandler) {
+ continue; // ignore folder link handler
+ }
+
// a bit of validation
String contentType = contentHandler.getContentType();
if (contentType == null) {
- Msg.error(DomainObjectAdapter.class, "ContentHandler " +
+ Msg.error(DomainObjectAdapter.class, "ContentHandler> " +
contentHandler.getClass().getName() + " does not specify a content type");
continue;
}
-
+
String toolName = contentHandler.getDefaultToolName();
if (toolName == null) {
- Msg.error(DomainObjectAdapter.class, "ContentHandler " +
+ Msg.error(DomainObjectAdapter.class, "ContentHandler> " +
contentHandler.getClass().getName() + " does not specify a default tool");
continue;
}
@@ -479,7 +541,7 @@ class ToolServicesImpl implements ToolServices {
private PluginTool findToolUsingFile(PluginTool[] tools, DomainFile domainFile) {
PluginTool matchingTool = null;
for (int toolNum = 0; (toolNum < tools.length) && (matchingTool == null); toolNum++) {
- PluginTool pTool = (PluginTool) tools[toolNum];
+ PluginTool pTool = tools[toolNum];
// Is this tool the same as the type we are in.
DomainFile[] df = pTool.getDomainFiles();
for (DomainFile element : df) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java
index 8cc721cfab..456329843e 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java
@@ -19,8 +19,10 @@ import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
-import ghidra.framework.client.ClientUtil;
-import ghidra.framework.client.NotConnectedException;
+import javax.security.auth.login.LoginException;
+
+import ghidra.framework.client.*;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
import ghidra.util.Msg;
/**
@@ -44,36 +46,51 @@ public class DefaultGhidraProtocolConnector extends GhidraProtocolConnector {
@Override
public boolean isReadOnly() throws NotConnectedException {
- if (responseCode == -1) {
+ if (statusCode == null) {
throw new NotConnectedException("not connected");
}
return readOnly;
}
@Override
- public int connect(boolean readOnlyAccess) throws IOException {
+ public StatusCode connect(boolean readOnlyAccess) throws IOException {
- if (responseCode != -1) {
+ if (statusCode != null) {
throw new IllegalStateException("already connected");
}
this.readOnly = readOnlyAccess;
- responseCode = GhidraURLConnection.GHIDRA_NOT_FOUND; // just in case
+ statusCode = StatusCode.UNAVAILABLE; // if uncaught exception occurs
repositoryServerAdapter =
ClientUtil.getRepositoryServer(url.getHost(), url.getPort(), true);
if (repositoryName == null) {
- responseCode = GhidraURLConnection.GHIDRA_OK;
- return responseCode;
+ statusCode = StatusCode.OK;
+ return statusCode;
}
repositoryAdapter = repositoryServerAdapter.getRepository(repositoryName);
- repositoryAdapter.connect();
+ if (repositoryServerAdapter.isConnected()) {
+ try {
+ repositoryAdapter.connect();
+ }
+ catch (RepositoryNotFoundException e) {
+ statusCode = StatusCode.NOT_FOUND;
+ }
+ }
+ else if (!repositoryServerAdapter.isCancelled()) {
+ Throwable t = repositoryServerAdapter.getLastConnectError();
+ if (t instanceof LoginException) {
+ statusCode = StatusCode.UNAUTHORIZED;
+ }
+ //throw new NotConnectedException("Not connected to repository server", t);
+ return statusCode;
+ }
if (repositoryAdapter.isConnected()) {
- responseCode = GhidraURLConnection.GHIDRA_OK;
+ statusCode = StatusCode.OK;
if (!repositoryAdapter.getUser().hasWritePermission()) {
if (!readOnly) {
this.readOnly = true; // write access not permitted
@@ -84,10 +101,10 @@ public class DefaultGhidraProtocolConnector extends GhidraProtocolConnector {
resolveItemPath();
}
else {
- responseCode = GhidraURLConnection.GHIDRA_UNAUTHORIZED;
+ statusCode = StatusCode.UNAUTHORIZED;
}
- return responseCode;
+ return statusCode;
}
@Override
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolHandler.java
index 069bc5e0b8..ff456a69a4 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolHandler.java
@@ -18,11 +18,14 @@ package ghidra.framework.protocol.ghidra;
import java.net.MalformedURLException;
import java.net.URL;
+import org.apache.commons.lang3.StringUtils;
+
/**
* DefaultGhidraProtocolHandler provides the default protocol
* handler which corresponds to the original RMI-based Ghidra Server
* and local file-based Ghidra projects.
- * {@literal ghidra://host/repo/... or ghidra:/path/projectName}
+ * {@literal ghidra://host/repo/... or ghidra:/path/projectName/...}
+ * See {@link DefaultGhidraProtocolConnector} and {@link DefaultLocalGhidraProtocolConnector}
*/
public class DefaultGhidraProtocolHandler extends GhidraProtocolHandler {
@@ -35,7 +38,7 @@ public class DefaultGhidraProtocolHandler extends GhidraProtocolHandler {
public GhidraProtocolConnector getConnector(URL ghidraUrl) throws MalformedURLException {
String protocol = ghidraUrl.getProtocol();
if (protocol != null) {
- if (ghidraUrl.getAuthority() == null) {
+ if (StringUtils.isBlank(ghidraUrl.getAuthority())) {
return new DefaultLocalGhidraProtocolConnector(ghidraUrl);
}
return new DefaultGhidraProtocolConnector(ghidraUrl);
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java
index a9ef6e0e63..770a6117aa 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java
@@ -21,8 +21,12 @@ import java.net.URL;
import ghidra.framework.client.NotConnectedException;
import ghidra.framework.client.RepositoryAdapter;
+import ghidra.framework.data.ProjectFileManager;
import ghidra.framework.model.ProjectLocator;
-import ghidra.framework.store.FileSystem;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
+import ghidra.framework.store.LockException;
+import ghidra.util.NotOwnerException;
+import ghidra.util.ReadOnlyException;
/**
* DefaultLocalGhidraProtocolConnector provides support for the
@@ -67,10 +71,12 @@ public class DefaultLocalGhidraProtocolConnector extends GhidraProtocolConnector
@Override
protected String parseItemPath() throws MalformedURLException {
- // root folder access only - TODO: add support for specifying local item/folder path
- folderPath = FileSystem.SEPARATOR;
- folderItemName = null;
- return folderPath;
+
+ String path = url.getQuery();
+
+ initFolderItemPath(path);
+
+ return path != null ? path : folderPath;
}
@Override
@@ -94,22 +100,47 @@ public class DefaultLocalGhidraProtocolConnector extends GhidraProtocolConnector
@Override
public boolean isReadOnly() throws NotConnectedException {
- if (responseCode == -1) {
+ if (statusCode == null) {
throw new NotConnectedException("not connected");
}
return readOnly;
}
@Override
- public int connect(boolean readOnlyAccess) throws IOException {
+ public StatusCode connect(boolean readOnlyAccess) throws IOException {
this.readOnly = readOnlyAccess;
if (!localStorageLocator.exists()) {
- responseCode = GhidraURLConnection.GHIDRA_NOT_FOUND;
+ statusCode = StatusCode.NOT_FOUND;
}
else {
- responseCode = GhidraURLConnection.GHIDRA_OK;
+ statusCode = StatusCode.OK;
}
- return responseCode;
+ return statusCode;
+ }
+
+ /**
+ * Connect and establish loca project project data instance. Opening a project for
+ * write access is subject to in-use lock restriction.
+ * See {@link #getStatusCode()} if null is returned.
+ * @param readOnlyAccess true if project data should be read-only
+ * @return project data instance or null if project not found
+ * @throws IOException if IO error occurs
+ */
+ ProjectFileManager getLocalProjectData(boolean readOnlyAccess) throws IOException {
+ if (connect(readOnlyAccess) != StatusCode.OK) {
+ return null;
+ }
+
+ try {
+ return new ProjectFileManager(localStorageLocator, !readOnlyAccess, false);
+ }
+ catch (NotOwnerException | ReadOnlyException e) {
+ statusCode = StatusCode.UNAUTHORIZED;
+ }
+ catch (LockException e) {
+ statusCode = StatusCode.LOCKED;
+ }
+ return null;
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GetUrlContentTypeTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GetUrlContentTypeTask.java
new file mode 100644
index 0000000000..0e2e519d75
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GetUrlContentTypeTask.java
@@ -0,0 +1,113 @@
+/* ###
+ * 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.protocol.ghidra;
+
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
+import ghidra.util.Msg;
+import ghidra.util.task.Task;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * A blocking/modal Ghidra URL content type discovery task
+ */
+public class GetUrlContentTypeTask extends Task {
+
+ private final URL ghidraUrl;
+
+ private String contentType;
+ private boolean done = false;
+
+ /**
+ * Construct a Ghidra URL content type discovery task
+ * @param ghidraUrl Ghidra URL (local or remote)
+ * @throws IllegalArgumentException if specified URL is not a Ghidra URL
+ * (see {@link GhidraURL}).
+ */
+ public GetUrlContentTypeTask(URL ghidraUrl) {
+ super("Checking URL Content Type", true, false, true);
+ if (!GhidraURL.isLocalProjectURL(ghidraUrl) &&
+ !GhidraURL.isServerRepositoryURL(ghidraUrl)) {
+ throw new IllegalArgumentException("unsupported URL");
+ }
+ this.ghidraUrl = ghidraUrl;
+ }
+
+ /**
+ * Get the discovered content type (e.g., "Program")
+ * @return content type or null if error occured or unsupported URL content
+ * @throws IllegalStateException if task has not completed execution
+ */
+ public String getContentType() {
+ if (!done) {
+ throw new IllegalStateException("task has not completed");
+ }
+ return contentType;
+ }
+
+ @Override
+ public void run(TaskMonitor monitor) {
+ final Thread t = Thread.currentThread();
+ monitor.addCancelledListener(() -> {
+ t.interrupt();
+ });
+ GhidraURLWrappedContent wrappedContent = null;
+ Object content = null;
+ try {
+ GhidraURLConnection c = (GhidraURLConnection) ghidraUrl.openConnection();
+ Object obj = c.getContent(); // read-only access
+ if (c.getStatusCode() == StatusCode.UNAUTHORIZED) {
+ return; // assume user already notified
+ }
+ if (obj instanceof GhidraURLWrappedContent) {
+ wrappedContent = (GhidraURLWrappedContent) obj;
+ content = wrappedContent.getContent(this);
+ }
+ if (!(content instanceof DomainFile)) {
+ Msg.showError(this, null, "Unsupported Content",
+ "Invalid project file URL: " + ghidraUrl);
+ return;
+ }
+ contentType = ((DomainFile) content).getContentType();
+ }
+ catch (FileNotFoundException e) {
+ Msg.showError(this, null, "Content Not Found", e.getMessage());
+ }
+ catch (MalformedURLException e) {
+ Msg.showError(this, null, "Invalid Ghidra URL",
+ "Improperly formed Ghidra URL: " + ghidraUrl);
+ }
+ catch (InterruptedIOException e) {
+ // ignore - assume cancelled
+ }
+ catch (IOException e) {
+ Msg.showError(this, null, "URL Access Failure",
+ "Failed to open Ghidra URL: " + e.getMessage());
+ }
+ finally {
+ if (content != null) {
+ wrappedContent.release(content, this);
+ }
+ done = true;
+ }
+ }
+
+
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java
index 8251bebe3f..f41706b2ca 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java
@@ -22,6 +22,7 @@ import java.net.URL;
import org.apache.commons.lang3.StringUtils;
import ghidra.framework.client.*;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
import ghidra.framework.store.FileSystem;
/**
@@ -40,7 +41,7 @@ public abstract class GhidraProtocolConnector {
protected String folderPath;
protected String folderItemName = null;
- protected int responseCode = -1;
+ protected StatusCode statusCode = null;
protected RepositoryAdapter repositoryAdapter;
protected RepositoryServerAdapter repositoryServerAdapter;
@@ -109,7 +110,7 @@ public abstract class GhidraProtocolConnector {
String path = url.getPath();
// Divide path into pieces
- if (path == null || path.length() < 2 || path.charAt(0) != '/') {
+ if (StringUtils.isBlank(path) || path.length() < 2 || path.charAt(0) != '/') {
return null; // content corresponds to RepositoryServerAdapter
}
@@ -128,6 +129,51 @@ public abstract class GhidraProtocolConnector {
return path;
}
+ /**
+ * Initialize {@code folderPath} and {@code folderItemName} from specified {@code contentPath}.
+ * @param contentPath absolute content path (null not permitted)
+ * @throws MalformedURLException if non-null invalid {@code contentPath} specified
+ * @return full content path
+ */
+ protected final String initFolderItemPath(String contentPath) throws MalformedURLException {
+
+ if (StringUtils.isBlank(contentPath)) {
+ folderPath = FileSystem.SEPARATOR;
+ return folderPath;
+ }
+
+ if (!contentPath.startsWith(FileSystem.SEPARATOR)) {
+ throw new MalformedURLException("invalid content path specification");
+ }
+
+ boolean isFolder = contentPath.endsWith(FileSystem.SEPARATOR);
+ folderPath = "";
+ String pathToSplit =
+ isFolder ? contentPath.substring(0, contentPath.length() - 1) : contentPath;
+ String[] pieces =
+ StringUtils.splitByWholeSeparatorPreserveAllTokens(pathToSplit, FileSystem.SEPARATOR);
+ if (pieces.length == 0) {
+ folderPath = FileSystem.SEPARATOR;
+ return folderPath;
+ }
+ for (int i = 1; i < pieces.length; i++) {
+ String p = pieces[i];
+ if (p.length() == 0) {
+ throw new MalformedURLException("invalid content path specification");
+ }
+ if (!isFolder && i == (pieces.length - 1)) {
+ folderItemName = p;
+ }
+ else {
+ folderPath = folderPath + FileSystem.SEPARATOR + p;
+ }
+ }
+ if (folderPath.length() == 0) {
+ folderPath = FileSystem.SEPARATOR;
+ }
+ return contentPath;
+ }
+
/**
* Parse item path name from URL and establish initial values for folderPath and
* folderItemName.
@@ -144,48 +190,19 @@ public abstract class GhidraProtocolConnector {
// strip off repository name from path
path = path.substring(repositoryName.length() + 1);
- if (path.length() <= 1) {
- // root path specified
- folderPath = FileSystem.SEPARATOR;
- return folderPath; // repository URL, root folder
- }
- // Handles server repository URL case ghidra://:/[/]/[]
-
- boolean isFolder = path.endsWith(FileSystem.SEPARATOR);
- folderPath = "";
- String pathToSplit = isFolder ? path.substring(0, path.length() - 1) : path;
- String[] pieces =
- StringUtils.splitByWholeSeparatorPreserveAllTokens(pathToSplit, FileSystem.SEPARATOR);
- if (pieces.length == 0) {
- throw new MalformedURLException("invalid repository path specification");
- }
- for (int i = 1; i < pieces.length; i++) {
- String p = pieces[i];
- if (p.length() == 0) {
- throw new MalformedURLException("invalid repository path specification");
- }
- if (!isFolder && i == (pieces.length - 1)) {
- folderItemName = p;
- }
- else {
- folderPath = folderPath + FileSystem.SEPARATOR + p;
- }
- }
- if (folderPath.length() == 0) {
- folderPath = FileSystem.SEPARATOR;
- }
+ path = initFolderItemPath(path);
return path;
}
/**
- * Gets the status code from a Ghidra URL connect response.
- * @return the Ghidra Status-Code, or -1 if not yet connected
+ * Gets the status code from a Ghidra URL connect attempt.
+ * @return the Ghidra status code or null if not yet connected
* @see #connect(boolean)
*/
- public int getResponseCode() {
- return responseCode;
+ public StatusCode getStatusCode() {
+ return statusCode;
}
/**
@@ -247,12 +264,12 @@ public abstract class GhidraProtocolConnector {
/**
* Fully resolve folder/item reference once connected to the associated
* repository due to possible ambiguity
- * @throws IOException
+ * @throws IOException if an IO error occurs
*/
protected void resolveItemPath() throws IOException {
// NOTE: Assume path may correspond to non-existent folder if not found
- // - this is why GHIDRA_NOT_FOUND response code setting has been disabled
+ // - this is why NOT_FOUND status code setting has been disabled
if (folderItemName != null) {
if (itemPath.endsWith("/")) {
@@ -262,7 +279,7 @@ public abstract class GhidraProtocolConnector {
// if (readOnly && !repository.folderExists(itemPath)) {
// // TODO: URL location not found
-// responseCode = GHIDRA_NOT_FOUND;
+// statusCode = NOT_FOUND;
// return;
// }
}
@@ -278,7 +295,7 @@ public abstract class GhidraProtocolConnector {
// if (folderItemName != null) {
// // TODO: URL location not found
-// responseCode = GHIDRA_NOT_FOUND;
+// statusCode = NOT_FOUND;
// return;
// }
}
@@ -289,13 +306,13 @@ public abstract class GhidraProtocolConnector {
* Utilized a cached connection via the specified repository adapter.
* This method may only be invoked if not yet connected and the associated
* URL corresponds to a repository (getRepositoryName() != null). The connection
- * response code should be established based upon the availability of the
+ * status code should be established based upon the availability of the
* URL referenced repository resource (i.e., folder or file).
* @param repository existing connected repository adapter
- * @throws IOException
+ * @throws IOException if an IO error occurs
*/
protected void connect(RepositoryAdapter repository) throws IOException {
- if (responseCode != -1) {
+ if (statusCode != null) {
throw new IllegalStateException("already connected");
}
if (repositoryName == null || !repositoryName.equals(repository.getName())) {
@@ -304,7 +321,7 @@ public abstract class GhidraProtocolConnector {
if (!repository.isConnected()) {
throw new IllegalStateException("expected connected repository");
}
- responseCode = GhidraURLConnection.GHIDRA_OK;
+ statusCode = StatusCode.OK;
this.repositoryAdapter = repository;
this.repositoryServerAdapter = repository.getServer();
resolveItemPath();
@@ -314,10 +331,10 @@ public abstract class GhidraProtocolConnector {
* Connect to the resource specified by the associated URL. This method should only be invoked
* once, a second attempt may result in an IOException.
* @param readOnly if resource should be requested for write access.
- * @return connection response code @see {@link GhidraURLConnection}
+ * @return connection status code
* @throws IOException if a connection error occurs
*/
- public abstract int connect(boolean readOnly) throws IOException;
+ public abstract StatusCode connect(boolean readOnly) throws IOException;
/**
* Determines the read-only nature of a connected resource
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java
index cb7ab2d37a..b6ac57f582 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java
@@ -15,20 +15,30 @@
*/
package ghidra.framework.protocol.ghidra;
-import java.io.File;
import java.net.*;
+import java.util.Objects;
import java.util.regex.Pattern;
-import ghidra.framework.OperatingSystem;
-import ghidra.framework.Platform;
+import org.apache.commons.lang3.StringUtils;
+
import ghidra.framework.model.ProjectLocator;
import ghidra.framework.remote.GhidraServerHandle;
+/**
+ * Supported URL forms include:
+ *
+ * - {@literal ghidra://:/[/]/[[#ref]]}
+ * - {@literal ghidra:/[X:/]/[?[/]/[[#ref]]]}
+ *
+ */
public class GhidraURL {
public static final String PROTOCOL = "ghidra";
- private static Pattern IS_LOCAL_URL_PATTERN = Pattern.compile("^" + PROTOCOL + ":/[^/].*"); // e.g., ghidra:/path
+ private static final String PROTOCOL_URL_START = PROTOCOL + ":/";
+
+ private static Pattern IS_LOCAL_URL_PATTERN =
+ Pattern.compile("^" + PROTOCOL_URL_START + "[^/].*"); // e.g., ghidra:/path
public static final String MARKER_FILE_EXTENSION = ".gpr";
public static final String PROJECT_DIRECTORY_EXTENSION = ".rep";
@@ -39,29 +49,29 @@ public class GhidraURL {
/**
* Determine if the specified URL refers to a local project and
* it exists.
- * @param url
+ * @param url ghidra URL
* @return true if specified URL refers to a local project and
* it exists.
*/
public static boolean localProjectExists(URL url) {
- if (!isLocalProjectURL(url)) {
- return false;
- }
- String path = url.getPath(); // assume path always starts with '/'
- if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) {
- if (path.indexOf(":/") == 2) {
- path = path.substring(1);
- }
- }
- File markerFile = new File(path + MARKER_FILE_EXTENSION);
- File projectDir = new File(path + PROJECT_DIRECTORY_EXTENSION);
- return (markerFile.isFile() && projectDir.isDirectory());
+ ProjectLocator loc = getProjectStorageLocator(url);
+ return loc != null && loc.exists();
+ }
+
+ /**
+ * Determine if the specified string appears to be a possible ghidra URL
+ * (starts with "ghidra:/").
+ * @param str string to be checked
+ * @return true if string is possible ghidra URL
+ */
+ public static boolean isGhidraURL(String str) {
+ return str != null && str.startsWith(PROTOCOL_URL_START);
}
/**
* Determine if the specified URL is a local project URL.
* No checking is performed as to the existence of the project.
- * @param url
+ * @param url ghidra URL
* @return true if specified URL refers to a local
* project (ghidra:/path/projectName...)
*/
@@ -70,83 +80,106 @@ public class GhidraURL {
}
/**
- * Get the project name which corresponds to the specified
- * local project URL.
+ * Get the project locator which corresponds to the specified local project URL.
+ * Confirm local project URL with {@link #isLocalProjectURL(URL)} prior to method use.
* @param localProjectURL local Ghidra project URL
- * @return project name
- * @throws IllegalArgumentException URL is not a valid local project URL
- */
- public static String getProjectName(URL localProjectURL) {
- if (!isLocalProjectURL(localProjectURL)) {
- throw new IllegalArgumentException("Invalid local Ghidra project URL");
- }
- String path = localProjectURL.getPath();
- int index = path.lastIndexOf('/');
- return path.substring(index + 1);
- }
-
- /**
- * Get the project location path which corresponds to the specified
- * local project URL.
- * @param localProjectURL local Ghidra project URL
- * @return project location path
- * @throws IllegalArgumentException URL is not a valid local project URL
- */
- public static String getProjectLocation(URL localProjectURL) {
- if (!isLocalProjectURL(localProjectURL)) {
- throw new IllegalArgumentException("Invalid local Ghidra project URL");
- }
- String path = localProjectURL.getPath();
- int index = path.lastIndexOf('/');
- path = path.substring(0, index);
- if (path.indexOf(":/") == 2) {
- path = path.substring(1);
- path = path.replace('/', File.separatorChar);
- }
- return path;
- }
-
- /**
- * Get the project locator which corresponds to the specified
- * local project URL.
- * @param localProjectURL local Ghidra project URL
- * @return project locator
+ * @return project locator or null if invalid path specified
* @throws IllegalArgumentException URL is not a valid local project URL
*/
public static ProjectLocator getProjectStorageLocator(URL localProjectURL) {
if (!isLocalProjectURL(localProjectURL)) {
throw new IllegalArgumentException("Invalid local Ghidra project URL");
}
- String path = localProjectURL.getPath();
+
+ String path = localProjectURL.getPath(); // assume path always starts with '/'
+
+// if (path.indexOf(":/") == 2 && Character.isLetter(path.charAt(1))) { // check for drive letter after leading '/'
+// if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) {
+// path = path.substring(1); // Strip-off leading '/'
+// }
+// else {
+// // assume drive letter separator ':' should be removed for non-windows
+// path = path.substring(0, 2) + path.substring(3);
+// }
+// }
+
int index = path.lastIndexOf('/');
- String dirPath = path.substring(0, index);
- if (dirPath.endsWith(":")) {
- dirPath += "/";
- }
- if (dirPath.indexOf(":/") == 2) {
- dirPath = dirPath.substring(1);
- dirPath = dirPath.replace('/', File.separatorChar);
- }
+ String dirPath = index != 0 ? path.substring(0, index) : "/";
+
String name = path.substring(index + 1);
+ if (name.length() == 0) {
+ return null;
+ }
+
return new ProjectLocator(dirPath, name);
}
+ /**
+ * Get the shared repository name associated with a repository URL or null
+ * if not applicable. For ghidra URL extensions it is assumed that the first path element
+ * corresponds to the repository name.
+ * @param url ghidra URL for shared project resource
+ * @return repository name or null if not applicable to URL
+ */
+ public static String getRepositoryName(URL url) {
+ if (!isServerRepositoryURL(url)) {
+ return null;
+ }
+ String path = url.getPath();
+ if (!path.startsWith("/")) {
+ // handle possible ghidra protocol extension use which is assumed to encode
+ // repository and file path the same as standard ghidra URL.
+ try {
+ URL extensionURL = new URL(path);
+ path = extensionURL.getPath();
+ }
+ catch (MalformedURLException e) {
+ path = "";
+ }
+ }
+ path = path.substring(1);
+ int ix = path.indexOf("/");
+ if (ix > 0) {
+ path = path.substring(0, ix);
+ }
+ return path;
+ }
+
/**
* Determine if the specified URL is any type of server "repository" URL.
* No checking is performed as to the existence of the server or repository.
- * @param url
+ * NOTE: ghidra protocol extensions are not currently supported (e.g., ghidra:http://...).
+ * @param url ghidra URL
* @return true if specified URL refers to a Ghidra server
* repository (ghidra://host/repositoryNAME/path...)
*/
public static boolean isServerRepositoryURL(URL url) {
+ if (!isServerURL(url)) {
+ return false;
+ }
String path = url.getPath();
- return isServerURL(url) && path != null && path.length() > 0;
+ if (StringUtils.isBlank(path)) {
+ return false;
+ }
+ if (!path.startsWith("/")) {
+ try {
+ URL extensionURL = new URL(path);
+ path = extensionURL.getPath();
+ if (StringUtils.isBlank(path)) {
+ return false;
+ }
+ }
+ catch (MalformedURLException e) {
+ return false;
+ }
+ }
+ return path.charAt(0) == '/' && path.length() > 1 && path.charAt(1) != '/';
}
/**
* Determine if the specified URL is any type of server URL.
* No checking is performed as to the existence of the server or repository.
- * @param url
+ * @param url ghidra URL
* @return true if specified URL refers to a Ghidra server
* repository (ghidra://host/repositoryNAME/path...)
*/
@@ -157,14 +190,21 @@ public class GhidraURL {
return Handler.isSupportedURL(url);
}
+ /**
+ * Ensure that absolute path is specified. Any use of Windows
+ * separator (back-slash) will be converted to a forward-slash.
+ * @param path path to be checked and possibly modified.
+ * @return path to be used
+ */
private static String checkAbsolutePath(String path) {
path = path.replace('\\', '/');
if (!path.startsWith("/")) {
- if (path.length() >= 3 && path.substring(1).startsWith(":/")) {
+ if (path.length() >= 3 && path.indexOf(":/") == 1 &&
+ Character.isLetter(path.charAt(0))) {
// prepend a "/" on Windows paths (e.g., C:/mydir)
path = "/" + path;
}
- else {
+ else { // absence of drive letter is tolerated even if not absolute on windows
throw new IllegalArgumentException("Absolute directory path required");
}
}
@@ -174,24 +214,28 @@ public class GhidraURL {
/**
* Create a Ghidra URL from a string form of Ghidra URL or local project path.
* This method can consume strings produced by the getDisplayString method.
- * @param projectPathOrURL {@literal project path (/)}
+ * @param projectPathOrURL {@literal project path (/)} or
+ * string form of Ghidra URL.
* @return local Ghidra project URL
* @see #getDisplayString(URL)
* @throws IllegalArgumentException invalid path or URL specified
*/
public static URL toURL(String projectPathOrURL) {
- String path = projectPathOrURL;
- if (!path.startsWith(PROTOCOL + ":")) {
- path = checkAbsolutePath(projectPathOrURL);
- int index = path.lastIndexOf('/');
- if (index <= 0 || index == (path.length() - 1)) {
- // Ensure that path includes projectName
- throw new IllegalArgumentException("Invalid project path or URL");
+ if (!projectPathOrURL.startsWith(PROTOCOL + ":")) {
+ if (projectPathOrURL.endsWith(ProjectLocator.PROJECT_DIR_SUFFIX) ||
+ projectPathOrURL.endsWith(ProjectLocator.PROJECT_FILE_SUFFIX)) {
+ String ext = projectPathOrURL.substring(projectPathOrURL.lastIndexOf('.'));
+ throw new IllegalArgumentException("Project path must omit extension: " + ext);
}
- path = PROTOCOL + ":" + path;
+ if (projectPathOrURL.contains("?") || projectPathOrURL.contains("#")) {
+ throw new IllegalArgumentException("Unsupported query/ref used with project path");
+ }
+ projectPathOrURL = checkAbsolutePath(projectPathOrURL);
+ String[] splitName = splitOffName(projectPathOrURL);
+ return makeURL(splitName[0], splitName[1]);
}
try {
- return new URL(path);
+ return new URL(projectPathOrURL);
}
catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
@@ -199,26 +243,151 @@ public class GhidraURL {
}
/**
- * Get a normalized URL which eliminates use of host names and additional URL refs
+ * Get normalized URL which corresponds to the local-project or repository
+ * @param ghidraUrl ghidra file/folder URL (server-only URL not permitted)
+ * @return local-project or repository URL
+ */
+ public static URL getProjectURL(URL ghidraUrl) {
+ if (!PROTOCOL.equals(ghidraUrl.getProtocol())) {
+ throw new IllegalArgumentException("ghidra protocol required");
+ }
+
+ if (isLocalProjectURL(ghidraUrl)) {
+ String urlStr = ghidraUrl.toExternalForm();
+ int queryIx = urlStr.indexOf('?');
+ if (queryIx < 0) {
+ return ghidraUrl;
+ }
+ urlStr = urlStr.substring(0, queryIx);
+ try {
+ return new URL(urlStr);
+ }
+ catch (MalformedURLException e) {
+ throw new RuntimeException(e); // unexpected
+ }
+ }
+
+ if (isServerRepositoryURL(ghidraUrl)) {
+
+ String path = ghidraUrl.getPath();
+ // handle possible ghidra protocol extension use which is assumed to encode
+ // repository and file path the same as standard ghidra URL.
+ if (!path.startsWith("/")) {
+ try {
+ URL extensionURL = new URL(path);
+ path = extensionURL.getPath();
+ }
+ catch (MalformedURLException e) {
+ path = "/";
+ }
+ }
+
+ // Truncate ghidra URL
+ String urlStr = ghidraUrl.toExternalForm();
+
+ String tail = null;
+ int ix = path.indexOf('/', 1);
+ if (ix > 0) {
+ // identify path tail to be removed
+ tail = path.substring(ix);
+ }
+
+ int refIx = urlStr.indexOf('#');
+ if (refIx > 0) {
+ urlStr = urlStr.substring(0, refIx);
+ }
+ int queryIx = urlStr.indexOf('?');
+ if (queryIx > 0) {
+ urlStr = urlStr.substring(0, queryIx);
+ }
+
+ if (tail != null) {
+ urlStr = urlStr.substring(0, urlStr.lastIndexOf(tail));
+ }
+ try {
+ return new URL(urlStr);
+ }
+ catch (MalformedURLException e) {
+ // ignore
+ }
+ }
+
+ throw new IllegalArgumentException("Invalid project/repository URL: " + ghidraUrl);
+ }
+
+ /**
+ * Get the project pathname referenced by the specified Ghidra file/folder URL.
+ * If path is missing root folder is returned.
+ * @param ghidraUrl ghidra file/folder URL (server-only URL not permitted)
+ * @return pathname of file or folder
+ */
+ public static String getProjectPathname(URL ghidraUrl) {
+
+ if (isLocalProjectURL(ghidraUrl)) {
+ String query = ghidraUrl.getQuery();
+ return StringUtils.isBlank(query) ? "/" : query;
+ }
+
+ if (isServerRepositoryURL(ghidraUrl)) {
+ String path = ghidraUrl.getPath();
+ // handle possible ghidra protocol extension use
+ if (!path.startsWith("/")) {
+ try {
+ URL extensionURL = new URL(path);
+ path = extensionURL.getPath();
+ }
+ catch (MalformedURLException e) {
+ path = "/";
+ }
+ }
+ // skip repo name (first path element)
+ int ix = path.indexOf('/', 1);
+ if (ix > 1) {
+ return path.substring(ix);
+ }
+ return "/";
+ }
+
+ throw new IllegalArgumentException("not project/repository URL");
+ }
+
+ /**
+ * Get hostname as an IP address if possible
+ * @param host hostname
+ * @return host IP address or original host name
+ */
+ private static String getHostAsIpAddress(String host) {
+ if (!StringUtils.isBlank(host)) {
+ try {
+ InetAddress addr = InetAddress.getByName(host);
+ host = addr.getHostAddress();
+ }
+ catch (UnknownHostException e) {
+ // just use hostname if unable to resolve
+ }
+ }
+ return host;
+ }
+
+ /**
+ * Get a normalized URL which eliminates use of host names and optional URL ref
* which may prevent direct comparison.
- * @param url
+ * @param url ghidra URL
* @return normalized url
*/
public static URL getNormalizedURL(URL url) {
- if (!isServerRepositoryURL(url)) {
- // TODO: May need to add support for other ghidra URL forms
- return url;
- }
String host = url.getHost();
- try {
- InetAddress addr = InetAddress.getByName(host);
- host = addr.getHostAddress();
+ String revisedHost = getHostAsIpAddress(host);
+ if (Objects.equals(host, revisedHost) && url.getRef() == null) {
+ return url; // no change
}
- catch (UnknownHostException e) {
- // just use hostname if unable to resolve
+ String file = url.getPath();
+ String query = url.getQuery();
+ if (!StringUtils.isBlank(query)) {
+ file += "?" + query;
}
try {
- return new URL(PROTOCOL, host, url.getPort(), url.getPath());
+ return new URL(PROTOCOL, revisedHost, url.getPort(), file);
}
catch (MalformedURLException e) {
throw new RuntimeException(e);
@@ -228,14 +397,15 @@ public class GhidraURL {
/**
* Generate preferred display string for Ghidra URLs.
* Form can be parsed by the toURL method.
- * @param url
- * @return
+ * @param url ghidra URL
+ * @return formatted URL display string
* @see #toURL(String)
*/
public static String getDisplayString(URL url) {
- if (isLocalProjectURL(url)) {
+ if (isLocalProjectURL(url) && StringUtils.isBlank(url.getQuery()) &&
+ StringUtils.isBlank(url.getRef())) {
String path = url.getPath();
- if (path.indexOf(":/") == 2) {
+ if (path.indexOf(":/") == 2 && Character.isLetter(path.charAt(1))) {
// assume windows path
path = path.substring(1);
path = path.replace('/', '\\');
@@ -245,27 +415,6 @@ public class GhidraURL {
return url.toString();
}
- /**
- * Create a local project URL for a specified project marker file.
- * @param projectMarkerFile project marker file
- * @return local project URL
- */
- public static URL makeURL(File projectMarkerFile) {
- String name = projectMarkerFile.getName();
- if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) {
- if (!name.endsWith(MARKER_FILE_EXTENSION) &&
- !name.toLowerCase().endsWith(MARKER_FILE_EXTENSION)) {
- throw new IllegalArgumentException("Invalid project marker file");
- }
- }
- else if (!name.endsWith(MARKER_FILE_EXTENSION)) {
- throw new IllegalArgumentException("Invalid project marker file");
- }
- name = name.substring(0, name.length() - MARKER_FILE_EXTENSION.length());
- String location = projectMarkerFile.getParentFile().getAbsolutePath();
- return makeURL(location, name);
- }
-
/**
* Create a URL which refers to a local Ghidra project
* @param dirPath absolute path of project location directory
@@ -273,12 +422,54 @@ public class GhidraURL {
* @return local Ghidra project URL
*/
public static URL makeURL(String dirPath, String projectName) {
- String path = checkAbsolutePath(dirPath);
+ return makeURL(dirPath, projectName, null, null);
+ }
+
+ /**
+ * Create a URL which refers to a local Ghidra project
+ * @param projectLocator absolute project location
+ * @return local Ghidra project URL
+ * @throws IllegalArgumentException if {@code projectLocator} does not have an absolute location
+ */
+ public static URL makeURL(ProjectLocator projectLocator) {
+ return makeURL(projectLocator, null, null);
+ }
+
+ /**
+ * Create a URL which refers to a local Ghidra project with optional project file and ref
+ * @param projectLocation absolute path of project location directory
+ * @param projectName name of project
+ * @param projectFilePath file path (e.g., /a/b/c, may be null)
+ * @param ref location reference (may be null)
+ * @return local Ghidra project URL
+ */
+ public static URL makeURL(String projectLocation, String projectName, String projectFilePath,
+ String ref) {
+ if (StringUtils.isBlank(projectLocation) || StringUtils.isBlank(projectName)) {
+ throw new IllegalArgumentException("Inavlid project location and/or name");
+ }
+ String path = checkAbsolutePath(projectLocation);
if (!path.endsWith("/")) {
path += "/";
}
+ StringBuilder buf = new StringBuilder(PROTOCOL);
+ buf.append(":");
+ buf.append(path);
+ buf.append(projectName);
+
+ if (!StringUtils.isBlank(projectFilePath)) {
+ if (!projectFilePath.startsWith("/") || projectFilePath.contains("\\")) {
+ throw new IllegalArgumentException("Invalid project file path");
+ }
+ buf.append("?");
+ buf.append(projectFilePath);
+ }
+ if (!StringUtils.isBlank(ref)) {
+ buf.append("#");
+ buf.append(ref);
+ }
try {
- return new URL(PROTOCOL + ":" + path + projectName);
+ return new URL(buf.toString());
}
catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
@@ -286,17 +477,30 @@ public class GhidraURL {
}
/**
- * Create a URL which refers to Ghidra Server repository content. Path may correspond
- * to either a file or folder.
- * @param host server host name/address
- * @param port optional server port (a value <= 0 refers to the default port)
- * @param repositoryName repository name
- * @param repositoryPath absolute folder or file path within repository.
- * Folder paths should end with a '/' character.
- * @return Ghidra Server repository content URL
+ * Create a URL which refers to a local Ghidra project with optional project file and ref
+ * @param projectLocator local project locator
+ * @param projectFilePath file path (e.g., /a/b/c, may be null)
+ * @param ref location reference (may be null)
+ * @return local Ghidra project URL
+ * @throws IllegalArgumentException if invalid {@code projectFilePath} specified or if URL
+ * instantion fails.
*/
- public static URL makeURL(String host, int port, String repositoryName, String repositoryPath) {
- return makeURL(host, port, repositoryName, repositoryPath, null, null);
+ public static URL makeURL(ProjectLocator projectLocator, String projectFilePath, String ref) {
+ return makeURL(projectLocator.getLocation(), projectLocator.getName(), projectFilePath,
+ ref);
+ }
+
+ private static String[] splitOffName(String path) {
+ String name = "";
+ if (!StringUtils.isBlank(path) && !path.endsWith("/")) {
+ int index = path.lastIndexOf('/');
+ if (index >= 0) {
+ // last name may or may not be a folder name
+ name = path.substring(index + 1);
+ path = path.substring(0, index);
+ }
+ }
+ return new String[] { path, name };
}
/**
@@ -305,40 +509,76 @@ public class GhidraURL {
* @param host server host name/address
* @param port optional server port (a value <= 0 refers to the default port)
* @param repositoryName repository name
- * @param repositoryPath absolute folder path within repository.
- * @param fileName name of a file contained within the specified repository/path
- * @param ref optional URL ref or null
+ * @param repositoryPath absolute folder or file path within repository.
+ * Folder paths should end with a '/' character.
+ * @return Ghidra Server repository content URL
+ */
+ public static URL makeURL(String host, int port, String repositoryName, String repositoryPath) {
+ String[] splitName = splitOffName(repositoryPath);
+ return makeURL(host, port, repositoryName, splitName[0], splitName[1], null);
+ }
+
+ /**
+ * Create a URL which refers to Ghidra Server repository content. Path may correspond
+ * to either a file or folder.
+ * @param host server host name/address
+ * @param port optional server port (a value <= 0 refers to the default port)
+ * @param repositoryName repository name
+ * @param repositoryPath absolute folder or file path within repository.
+ * @param ref ref or null
* Folder paths should end with a '/' character.
* @return Ghidra Server repository content URL
*/
public static URL makeURL(String host, int port, String repositoryName, String repositoryPath,
- String fileName, String ref) {
- if (host == null) {
+ String ref) {
+ String[] splitName = splitOffName(repositoryPath);
+ return makeURL(host, port, repositoryName, splitName[0], splitName[1], ref);
+ }
+
+ /**
+ * Create a URL which refers to Ghidra Server repository content. Path may correspond
+ * to either a file or folder.
+ * @param host server host name/address
+ * @param port optional server port (a value <= 0 refers to the default port)
+ * @param repositoryName repository name
+ * @param repositoryFolderPath absolute folder path within repository.
+ * @param fileName name of a file or folder contained within the specified {@code repositoryFolderPath}
+ * @param ref optional URL ref or null
+ * Folder paths should end with a '/' character.
+ * @return Ghidra Server repository content URL
+ * @throws IllegalArgumentException if invalid arguments are specified
+ */
+ public static URL makeURL(String host, int port, String repositoryName,
+ String repositoryFolderPath, String fileName, String ref) {
+ if (StringUtils.isBlank(host)) {
throw new IllegalArgumentException("host required");
}
- if (repositoryName == null) {
+ if (StringUtils.isBlank(repositoryName)) {
throw new IllegalArgumentException("repository name required");
}
if (port == 0 || port == GhidraServerHandle.DEFAULT_PORT) {
port = -1;
}
String path = "/" + repositoryName;
- if (repositoryPath != null) {
- if (!repositoryPath.startsWith("/") || repositoryPath.indexOf('\\') >= 0) {
+ if (!StringUtils.isBlank(repositoryFolderPath)) {
+ if (!repositoryFolderPath.startsWith("/") || repositoryFolderPath.indexOf('\\') >= 0) {
throw new IllegalArgumentException("Invalid repository path");
}
- path += repositoryPath;
+ path += repositoryFolderPath;
+ if (!path.endsWith("/")) {
+ path += "/";
+ }
}
- else {
- path += "/";
- }
- if (fileName != null) {
+ if (!StringUtils.isBlank(fileName)) {
+ if (fileName.contains("/")) {
+ throw new IllegalArgumentException("Invalid folder/file name: " + fileName);
+ }
if (!path.endsWith("/")) {
path += "/";
}
path += fileName;
}
- if (ref != null) {
+ if (!StringUtils.isBlank(ref)) {
path += "#" + ref;
}
try {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java
index e210fa05cb..e3e75b906c 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java
@@ -21,30 +21,85 @@ import java.net.*;
import ghidra.framework.client.*;
import ghidra.framework.data.ProjectFileManager;
import ghidra.framework.model.ProjectData;
-import ghidra.framework.model.ProjectLocator;
-import ghidra.util.NotOwnerException;
import ghidra.util.exception.AssertException;
public class GhidraURLConnection extends URLConnection {
+ /**
+ * Connection status codes
+ */
+ public enum StatusCode {
+ OK(20, "OK"),
+ /**
+ * Ghidra Status-Code 401: Unauthorized.
+ * This status code occurs when repository access is denied.
+ */
+ UNAUTHORIZED(401, "Unauthorized"),
+ /**
+ * Ghidra Status-Code 404: Not Found.
+ * This status code occurs when repository or project does not exist.
+ */
+ NOT_FOUND(404, "Not Found"),
+ /**
+ * Ghidra Status-Code 423: Locked.
+ * This status code occurs when project is locked (i.e., in use).
+ */
+ LOCKED(423, "Locked Project"),
+ /**
+ * Ghidra Status-Code 503: Unavailable.
+ * This status code includes a variety of connection errors
+ * which are reported/logged by the Ghidra Server support code.
+ */
+ UNAVAILABLE(503, "Unavailable");
+
+ private int code;
+ private String description;
+
+ private StatusCode(int code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+ }
+
// TODO: consider implementing request and response headers
- /**
- * Ghidra Status-Code 200: OK.
- */
- public static final int GHIDRA_OK = 200;
-
- /**
- * Ghidra Status-Code 401: Unauthorized.
- * This response code includes a variety of connection errors
- * which are reported/logged by the Ghidra Server support code.
- */
- public static final int GHIDRA_UNAUTHORIZED = 401;
-
- /**
- * Ghidra Status-Code 404: Not Found.
- */
- public static final int GHIDRA_NOT_FOUND = 404;
+// /**
+// * Ghidra Status-Code 200: OK.
+// */
+// public static final int GHIDRA_OK = 200;
+//
+// /**
+// * Ghidra Status-Code 401: Unauthorized.
+// * This response code includes a variety of connection errors
+// * which are reported/logged by the Ghidra Server support code.
+// */
+// public static final int GHIDRA_UNAUTHORIZED = 401;
+//
+// /**
+// * Ghidra Status-Code 404: Not Found.
+// */
+// public static final int GHIDRA_NOT_FOUND = 404;
+//
+// /**
+// * Ghidra Status-Code 423: Locked
+// * Caused by attempt to open local project data with write-access when project is
+// * already opened and locked.
+// */
+// public static final int GHIDRA_LOCKED = 423;
+//
+// /**
+// * Ghidra Status-Code 503: Unavailable
+// * Caused by other connection failure
+// */
+// public static final int GHIDRA_UNAVAILABLE = 503;
/**
* Ghidra content type - domain folder/file wrapped within GhidraURLWrappedContent object.
@@ -58,7 +113,7 @@ public class GhidraURLConnection extends URLConnection {
*/
public static final String REPOSITORY_SERVER_CONTENT = "RepositoryServer";
- private int responseCode = -1;
+ private StatusCode statusCode = null;
private GhidraProtocolConnector protocolConnector;
@@ -110,16 +165,23 @@ public class GhidraURLConnection extends URLConnection {
}
/**
- * Set the read-only state of the content.
- * Extreme care must be taken when setting the state to false for local projects
- * without the use of a ProjectLock. This setting is currently ignored
- * for server repositories which are always read-only in Headed mode and
- * read-write in Headless mode.
+ * Set the read-only state for this connection prior to connecting or getting content.
+ * The default access is read-only. Extreme care must be taken when setting the state to false
+ * for local projects without the use of a ProjectLock.
+ *
+ * NOTE: Local project URL connections only support read-only access.
* @param state read-only if true, otherwise read-write
+ * @throws UnsupportedOperationException if an attempt is made to enable write access for
+ * a local project URL.
+ * @throws IllegalStateException if already connected
*/
public void setReadOnly(boolean state) {
if (connected)
throw new IllegalStateException("Already connected");
+ if (GhidraURL.isLocalProjectURL(url) && !state) {
+ // local project write-access not supported due to inadequate cleanup/disposal strategy
+ throw new UnsupportedOperationException("write access to local projects not supported");
+ }
readOnly = state;
}
@@ -159,19 +221,19 @@ public class GhidraURLConnection extends URLConnection {
}
/**
- * Gets the status code from a Ghidra URL response.
+ * Gets the status code from a Ghidra URL connect attempt.
* @throws IOException if an error occurred connecting to the server.
- * @return the Ghidra Status-Code, or -1
+ * @return the Ghidra connection status code or null
*/
- public int getResponseCode() throws IOException {
+ public StatusCode getStatusCode() throws IOException {
- if (responseCode != -1) {
- return responseCode;
+ if (statusCode != null) {
+ return statusCode;
}
getContent(); // Ensure that we have connected to the server.
- return responseCode;
+ return statusCode;
}
@Override
@@ -195,7 +257,7 @@ public class GhidraURLConnection extends URLConnection {
* @return URL content generally in the form of GhidraURLWrappedContent, although other
* special cases may result in different content (Example: a server-only URL could result in
* content class of RepositoryServerAdapter).
- * @throws IOException
+ * @throws IOException if an IO error occurs
*/
@Override
public Object getContent() throws IOException {
@@ -214,7 +276,7 @@ public class GhidraURLConnection extends URLConnection {
* failure to do so may prevent release of repository handle to server.
* Only a single call to this method is permitted.
* @return transient project data or null if unavailable
- * @throws IOException
+ * @throws IOException if an IO error occurs
*/
public ProjectData getProjectData() throws IOException {
@@ -228,24 +290,6 @@ public class GhidraURLConnection extends URLConnection {
return projectData;
}
- private void localConnect(ProjectLocator localProjectLocator) throws IOException {
-
- responseCode = protocolConnector.connect(readOnly);
- if (responseCode != GHIDRA_OK) {
- return;
- }
-
- try {
- projectData = new ProjectFileManager(localProjectLocator, !readOnly, false);
-
- refObject = new GhidraURLWrappedContent(this);
- responseCode = GHIDRA_OK;
- }
- catch (NotOwnerException e) {
- responseCode = GHIDRA_UNAUTHORIZED;
- }
- }
-
@Override
public void connect() throws IOException {
@@ -258,9 +302,13 @@ public class GhidraURLConnection extends URLConnection {
if (protocolConnector instanceof DefaultLocalGhidraProtocolConnector) {
// local project connection
DefaultLocalGhidraProtocolConnector localConnector =
- ((DefaultLocalGhidraProtocolConnector) protocolConnector);
- localConnect(localConnector.getLocalProjectLocator());
+ (DefaultLocalGhidraProtocolConnector) protocolConnector;
+ projectData = localConnector.getLocalProjectData(readOnly);
+ statusCode = localConnector.getStatusCode();
connected = true;
+ if (statusCode == StatusCode.OK) {
+ refObject = new GhidraURLWrappedContent(this);
+ }
return;
}
@@ -268,9 +316,9 @@ public class GhidraURLConnection extends URLConnection {
if (repoName == null) {
// assume only server adapter connection without repository name specified
// any RepositoryServerAdapter caching is responsibility of connector
- responseCode = protocolConnector.connect(readOnly);
+ statusCode = protocolConnector.connect(readOnly);
connected = true;
- if (responseCode == GHIDRA_OK) {
+ if (statusCode == StatusCode.OK) {
refObject = protocolConnector.getRepositoryServerAdapter();
if (refObject == null) {
throw new AssertException("expected RepositoryServerAdapter content");
@@ -292,9 +340,9 @@ public class GhidraURLConnection extends URLConnection {
transientProjectManager.getTransientProject(protocolConnector, readOnly);
connected = true;
- responseCode = protocolConnector.getResponseCode();
+ statusCode = protocolConnector.getStatusCode();
- if (responseCode != GHIDRA_OK) {
+ if (statusCode != StatusCode.OK) {
return;
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLWrappedContent.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLWrappedContent.java
index 9946bc4404..8e919c29a5 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLWrappedContent.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLWrappedContent.java
@@ -1,6 +1,5 @@
/* ###
* 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.
@@ -16,14 +15,14 @@
*/
package ghidra.framework.protocol.ghidra;
-import ghidra.framework.model.*;
-import ghidra.util.InvalidNameException;
-import ghidra.util.exception.NotFoundException;
-
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import ghidra.framework.model.*;
+import ghidra.util.InvalidNameException;
+
/**
* GhidraURLWrappedContent provides controlled access to a Ghidra folder/file
* associated with a Ghidra URL. It is important to note the issuance of this object does
@@ -81,9 +80,11 @@ public class GhidraURLWrappedContent {
throw new RuntimeException("consumer not found");
}
+ /**
+ * Close associated {@link ProjectData} when all consumers have released wrapped object.
+ */
private void closeProjectData() {
- // Local project data can only be closed once and is not handled here
- if (projectData instanceof TransientProjectData) {
+ if (projectData != null) {
projectData.close();
}
projectData = null;
@@ -103,7 +104,7 @@ public class GhidraURLWrappedContent {
return folder;
}
- private void resolve() throws IOException, NotFoundException {
+ private void resolve() throws IOException, FileNotFoundException {
if (projectData != null) {
return;
@@ -125,13 +126,13 @@ public class GhidraURLWrappedContent {
catch (InvalidNameException e) {
// TODO: URL folder path is invalid
closeProjectData();
- throw new NotFoundException("URL specifies invalid path: " + folderPath);
+ throw new IOException("URL specifies invalid path: " + folderPath);
}
}
if (folder == null) {
// TODO: URL location not found
closeProjectData();
- throw new NotFoundException("URL specifies unknown path: " + folderPath);
+ throw new FileNotFoundException("URL specifies unknown path: " + folderPath);
}
if (folderItemName == null) {
@@ -145,22 +146,29 @@ public class GhidraURLWrappedContent {
return;
}
+ DomainFolder subfolder = folder.getFolder(folderItemName);
+ if (subfolder != null) {
+ refObject = subfolder;
+ return;
+ }
+
closeProjectData();
- throw new NotFoundException("URL specifies unknown path: " + folderPath);
+ throw new FileNotFoundException("URL specifies unknown path: " + folderPath);
}
/**
* Get the domain folder or file associated with the Ghidra URL.
* The consumer is responsible for releasing the content object via the release method
- * when it is no longer in use.
+ * when it is no longer in use (see {@link #release(Object, Object)}}).
* @param consumer object which is responsible for releasing the content
* @return domain file or folder
- * @throws IOException
- * @throws NotFoundException if the Ghidra URL does no correspond to a folder or file
+ * @throws IOException if an IO error occurs
+ * @throws FileNotFoundException if the Ghidra URL does no correspond to a folder or file
* within the Ghidra repository/project.
* @see #release(Object, Object)
*/
- public synchronized Object getContent(Object consumer) throws IOException, NotFoundException {
+ public synchronized Object getContent(Object consumer)
+ throws IOException, FileNotFoundException {
addConsumer(consumer);
boolean success = false;
try {
@@ -180,8 +188,8 @@ public class GhidraURLWrappedContent {
* no longer in-use and the underlying connection may be closed. A read-only
* or immutable domain object may remain open and in-use after its associated
* domain folder/file has been released.
- * @param content
- * @param consumer
+ * @param content object obtained via {@link #getContent(Object)}
+ * @param consumer object consumer which was specified to {@link #getContent(Object)}
*/
public synchronized void release(Object content, Object consumer) {
if (content == null || content != refObject) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/RepositoryInfo.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/RepositoryInfo.java
index 5021420aa6..045ae46e55 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/RepositoryInfo.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/RepositoryInfo.java
@@ -29,6 +29,14 @@ public class RepositoryInfo {
this.readOnly = readOnly;
}
+ /**
+ * Get the Ghidra URL which corresponds to the repository
+ * @return repository URL
+ */
+ public URL getURL() {
+ return repositoryURL;
+ }
+
@Override
public boolean equals(Object obj) {
if (!(obj instanceof RepositoryInfo)) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectData.java
index 389b7a4f15..411bf33734 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectData.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectData.java
@@ -22,6 +22,7 @@ import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.data.ProjectFileManager;
import ghidra.framework.model.ProjectLocator;
import ghidra.framework.remote.RepositoryHandle;
+import ghidra.framework.store.LockException;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import utilities.util.FileUtilities;
@@ -38,7 +39,8 @@ public class TransientProjectData extends ProjectFileManager {
private GhidraSwinglessTimer cleanupTimer;
TransientProjectData(TransientProjectManager dataMgr, ProjectLocator tmpProjectLocation,
- RepositoryInfo repositoryInfo, RepositoryAdapter repository) throws IOException {
+ RepositoryInfo repositoryInfo, RepositoryAdapter repository)
+ throws IOException, LockException {
// Resulting data is read-only in GUI mode, read-write in Headless mode
// Allowing more control will cause issues for caching of transient project data -
// although we could use two caches one for each mode
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java
index f53ca14da5..fbd2a35c3c 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java
@@ -25,6 +25,8 @@ import java.util.Set;
import ghidra.framework.client.NotConnectedException;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.model.ProjectLocator;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
+import ghidra.framework.store.LockException;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.WeakValueHashMap;
@@ -94,17 +96,11 @@ public class TransientProjectManager {
* @param protocolConnector Ghidra protocol connector
* @param readOnly true if project data should be treated as read-only
* @return transient project data
- * @throws IOException
+ * @throws IOException if an IO error occurs
*/
synchronized TransientProjectData getTransientProject(GhidraProtocolConnector protocolConnector,
boolean readOnly) throws IOException {
- TransientProjectData projectData;
-
- // try to avoid excessive accumulation of unreferenced transient project data.
- // It is assumed that calls to this method are generally infrequent and may be slow
- System.gc();
-
String repoName = protocolConnector.getRepositoryName();
if (repoName == null) {
throw new IllegalArgumentException(
@@ -114,17 +110,18 @@ public class TransientProjectManager {
RepositoryInfo repositoryInfo =
new RepositoryInfo(protocolConnector.getRepositoryRootGhidraURL(), repoName, readOnly);
- projectData = repositoryMap.get(repositoryInfo);
+ TransientProjectData projectData = repositoryMap.get(repositoryInfo);
if (projectData == null || !projectData.stopCleanupTimer()) { // cleanup suspended
- if (protocolConnector.connect(readOnly) != GhidraURLConnection.GHIDRA_OK) {
- return null;
+ StatusCode statusCode = protocolConnector.connect(readOnly);
+ if (statusCode != StatusCode.OK) {
+ throw new NotConnectedException(statusCode.getDescription());
}
RepositoryAdapter repositoryAdapter = protocolConnector.getRepositoryAdapter();
if (repositoryAdapter == null || !repositoryAdapter.isConnected()) {
- throw new NotConnectedException("protocol connector not connected to repository");
+ throw new NotConnectedException("Not connected to repository");
}
projectData = createTransientProject(repositoryAdapter, repositoryInfo);
@@ -182,7 +179,12 @@ public class TransientProjectManager {
ProjectLocator tmpProjectLocation = new TransientProjectStorageLocator(
tmp.getParentFile().getAbsolutePath(), tmp.getName(), repositoryInfo);
- return new TransientProjectData(this, tmpProjectLocation, repositoryInfo, repository);
+ try {
+ return new TransientProjectData(this, tmpProjectLocation, repositoryInfo, repository);
+ }
+ catch (LockException e) {
+ throw new IOException(e); // unexpected for transient project storage
+ }
}
private static class TransientProjectStorageLocator extends ProjectLocator {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java b/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java
new file mode 100644
index 0000000000..6722c55c15
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java
@@ -0,0 +1,30 @@
+/* ###
+ * 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.util.exception;
+
+import java.io.IOException;
+
+/**
+ * BadLinkException occurs when a link-file expected linked content type does not
+ * match the actual content type of the linked file.
+ */
+public class BadLinkException extends IOException {
+
+ public BadLinkException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/Ghidra/Framework/Project/src/main/resources/images/link.png b/Ghidra/Framework/Project/src/main/resources/images/link.png
new file mode 100644
index 0000000000..024b295283
Binary files /dev/null and b/Ghidra/Framework/Project/src/main/resources/images/link.png differ
diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java
index 0ec40eb501..5e4ec72188 100644
--- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java
+++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java
@@ -17,6 +17,7 @@ package ghidra.framework.model;
import java.io.File;
import java.io.IOException;
+import java.net.URL;
import java.util.ArrayList;
import java.util.Map;
@@ -100,11 +101,26 @@ public class TestDummyDomainFile implements DomainFile {
throw new UnsupportedOperationException();
}
+ @Override
+ public URL getSharedProjectURL() {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public String getContentType() {
throw new UnsupportedOperationException();
}
+ @Override
+ public boolean isLinkFile() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DomainFolder followLink() {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public Class extends DomainObject> getDomainObjectClass() {
throw new UnsupportedOperationException();
@@ -223,11 +239,6 @@ public class TestDummyDomainFile implements DomainFile {
return isReadOnly;
}
- @Override
- public boolean isVersionControlSupported() {
- throw new UnsupportedOperationException();
- }
-
@Override
public synchronized boolean isVersioned() {
return isVersioned;
@@ -325,6 +336,16 @@ public class TestDummyDomainFile implements DomainFile {
throw new UnsupportedOperationException();
}
+ @Override
+ public boolean isLinkingSupported() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException {
diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java
index 6f8fad1d82..df115ddac6 100644
--- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java
+++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java
@@ -17,6 +17,7 @@ package ghidra.framework.model;
import java.io.File;
import java.io.IOException;
+import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@@ -82,6 +83,11 @@ public class TestDummyDomainFolder implements DomainFolder {
return "/";
}
+ @Override
+ public URL getSharedProjectURL() {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public boolean isInWritableProject() {
throw new UnsupportedOperationException();
@@ -162,6 +168,11 @@ public class TestDummyDomainFolder implements DomainFolder {
throw new UnsupportedOperationException();
}
+ @Override
+ public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public void setActive() {
throw new UnsupportedOperationException();
diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java
index 3a25bb8ccb..ca01d34d90 100644
--- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java
+++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java
@@ -16,7 +16,6 @@
package ghidra.framework.model;
import java.io.IOException;
-import java.net.URL;
import java.util.List;
import ghidra.framework.client.RepositoryAdapter;
@@ -84,12 +83,6 @@ public class TestDummyProjectData implements ProjectData {
return null;
}
- @Override
- public URL getSharedFileURL(String path) {
- // stub
- return null;
- }
-
@Override
public String makeValidName(String name) {
// stub
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveContentHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveContentHandler.java
index 623a2e421e..f5d328b6c8 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveContentHandler.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveContentHandler.java
@@ -24,7 +24,8 @@ import db.DBHandle;
import db.buffers.BufferFile;
import db.buffers.ManagedBufferFile;
import generic.theme.GIcon;
-import ghidra.framework.data.*;
+import ghidra.framework.data.DBContentHandler;
+import ghidra.framework.data.DomainObjectMergeManager;
import ghidra.framework.model.ChangeSet;
import ghidra.framework.model.DomainObject;
import ghidra.framework.store.*;
@@ -39,13 +40,19 @@ import ghidra.util.task.TaskMonitor;
* and FolderItem storage. This class also produces the appropriate Icon for
* DataTypeArchive files.
*/
-public class DataTypeArchiveContentHandler extends DBContentHandler {
+public class DataTypeArchiveContentHandler extends DBContentHandler {
- private static Icon DATA_TYPE_ARCHIVE_ICON;
+ static Icon DATA_TYPE_ARCHIVE_ICON = new GIcon("icon.content.handler.archive.dt");
- private final static String PROGRAM_ICON_ID = "icon.content.handler.archive.dt";
public final static String DATA_TYPE_ARCHIVE_CONTENT_TYPE = "Archive";
+ final static Class DATA_TYPE_ARCHIVE_DOMAIN_OBJECT_CLASS =
+ DataTypeArchiveDB.class;
+ final static String DATA_TYPE_ARCHIVE_CONTENT_DEFAULT_TOOL = "CodeBrowser";
+
+ private static final DataTypeArchiveLinkContentHandler linkHandler =
+ new DataTypeArchiveLinkContentHandler();
+
@Override
public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
DomainObject obj, TaskMonitor monitor)
@@ -59,7 +66,7 @@ public class DataTypeArchiveContentHandler extends DBContentHandler {
}
@Override
- public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version,
+ public DataTypeArchiveDB getImmutableObject(FolderItem item, Object consumer, int version,
int minChangeVersion, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
String contentType = item.getContentType();
@@ -113,7 +120,7 @@ public class DataTypeArchiveContentHandler extends DBContentHandler {
}
@Override
- public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
+ public DataTypeArchiveDB getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
@@ -168,7 +175,7 @@ public class DataTypeArchiveContentHandler extends DBContentHandler {
}
@Override
- public DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
+ public DataTypeArchiveDB getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
boolean okToUpgrade, boolean recover, Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
@@ -323,8 +330,8 @@ public class DataTypeArchiveContentHandler extends DBContentHandler {
}
@Override
- public Class extends DomainObject> getDomainObjectClass() {
- return DataTypeArchiveDB.class;
+ public Class getDomainObjectClass() {
+ return DATA_TYPE_ARCHIVE_DOMAIN_OBJECT_CLASS;
}
@Override
@@ -339,16 +346,11 @@ public class DataTypeArchiveContentHandler extends DBContentHandler {
@Override
public String getDefaultToolName() {
- return "CodeBrowser";
+ return DATA_TYPE_ARCHIVE_CONTENT_DEFAULT_TOOL;
}
@Override
public Icon getIcon() {
- synchronized (DataTypeArchiveContentHandler.class) {
- if (DATA_TYPE_ARCHIVE_ICON == null) {
- DATA_TYPE_ARCHIVE_ICON = new GIcon(PROGRAM_ICON_ID);
- }
- }
return DATA_TYPE_ARCHIVE_ICON;
}
@@ -364,4 +366,9 @@ public class DataTypeArchiveContentHandler extends DBContentHandler {
originalObj, latestObj);
}
+ @Override
+ public DataTypeArchiveLinkContentHandler getLinkHandler() {
+ return linkHandler;
+ }
+
}
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java
new file mode 100644
index 0000000000..7c64fdf1ac
--- /dev/null
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java
@@ -0,0 +1,71 @@
+/* ###
+ * 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.program.database;
+
+import java.io.IOException;
+
+import javax.swing.Icon;
+
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.URLLinkObject;
+import ghidra.framework.model.DomainObject;
+import ghidra.framework.store.FileSystem;
+import ghidra.util.InvalidNameException;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class DataTypeArchiveLinkContentHandler extends LinkHandler {
+
+ public static final String ARCHIVE_LINK_CONTENT_TYPE = "ArchiveLink";
+
+ @Override
+ public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
+ DomainObject obj, TaskMonitor monitor)
+ throws IOException, InvalidNameException, CancelledException {
+ if (!(obj instanceof URLLinkObject)) {
+ throw new IOException("Unsupported domain object: " + obj.getClass().getName());
+ }
+ return createFile((URLLinkObject) obj, ARCHIVE_LINK_CONTENT_TYPE, fs, path, name,
+ monitor);
+ }
+
+ @Override
+ public String getContentType() {
+ return ARCHIVE_LINK_CONTENT_TYPE;
+ }
+
+ @Override
+ public String getContentTypeDisplayString() {
+ return "Data Type Archive Link";
+ }
+
+ @Override
+ public Class getDomainObjectClass() {
+ // return linked content class
+ return DataTypeArchiveContentHandler.DATA_TYPE_ARCHIVE_DOMAIN_OBJECT_CLASS;
+ }
+
+ @Override
+ public Icon getIcon() {
+ return DataTypeArchiveContentHandler.DATA_TYPE_ARCHIVE_ICON;
+ }
+
+ @Override
+ public String getDefaultToolName() {
+ return DataTypeArchiveContentHandler.DATA_TYPE_ARCHIVE_CONTENT_DEFAULT_TOOL;
+ }
+
+}
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramContentHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramContentHandler.java
index 7f124b27d9..a7d6c10c0b 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramContentHandler.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramContentHandler.java
@@ -23,7 +23,8 @@ import db.*;
import db.buffers.BufferFile;
import db.buffers.ManagedBufferFile;
import generic.theme.GIcon;
-import ghidra.framework.data.*;
+import ghidra.framework.data.DBWithUserDataContentHandler;
+import ghidra.framework.data.DomainObjectMergeManager;
import ghidra.framework.model.ChangeSet;
import ghidra.framework.model.DomainObject;
import ghidra.framework.store.*;
@@ -38,11 +39,16 @@ import ghidra.util.task.TaskMonitor;
* and FolderItem storage. This class also produces the appropriate Icon for
* Program files.
*/
-public class ProgramContentHandler extends DBContentHandler {
+public class ProgramContentHandler extends DBWithUserDataContentHandler {
+
+ public static final String PROGRAM_CONTENT_TYPE = "Program";
public static Icon PROGRAM_ICON = new GIcon("icon.content.handler.program");
- public static final String PROGRAM_CONTENT_TYPE = "Program";
+ static final Class PROGRAM_DOMAIN_OBJECT_CLASS = ProgramDB.class;
+ static final String PROGRAM_CONTENT_DEFAULT_TOOL = "CodeBrowser";
+
+ private static final ProgramLinkContentHandler linkHandler = new ProgramLinkContentHandler();
@Override
public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
@@ -52,11 +58,12 @@ public class ProgramContentHandler extends DBContentHandler {
if (!(obj instanceof ProgramDB)) {
throw new IOException("Unsupported domain object: " + obj.getClass().getName());
}
- return createFile((ProgramDB) obj, PROGRAM_CONTENT_TYPE, fs, path, name, monitor);
+ return createFile((ProgramDB) obj, PROGRAM_CONTENT_TYPE, fs, path, name,
+ monitor);
}
@Override
- public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version,
+ public ProgramDB getImmutableObject(FolderItem item, Object consumer, int version,
int minChangeVersion, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
String contentType = item.getContentType();
@@ -107,7 +114,7 @@ public class ProgramContentHandler extends DBContentHandler {
}
@Override
- public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
+ public ProgramDB getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
@@ -161,7 +168,7 @@ public class ProgramContentHandler extends DBContentHandler {
}
@Override
- public DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
+ public ProgramDB getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
boolean okToUpgrade, boolean recover, Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
@@ -316,8 +323,8 @@ public class ProgramContentHandler extends DBContentHandler {
}
@Override
- public Class extends DomainObject> getDomainObjectClass() {
- return ProgramDB.class;
+ public Class getDomainObjectClass() {
+ return PROGRAM_DOMAIN_OBJECT_CLASS;
}
@Override
@@ -327,12 +334,12 @@ public class ProgramContentHandler extends DBContentHandler {
@Override
public String getContentTypeDisplayString() {
- return "Program";
+ return PROGRAM_CONTENT_TYPE;
}
@Override
public String getDefaultToolName() {
- return "CodeBrowser";
+ return PROGRAM_CONTENT_DEFAULT_TOOL;
}
@Override
@@ -352,4 +359,9 @@ public class ProgramContentHandler extends DBContentHandler {
originalObj, latestObj);
}
+ @Override
+ public ProgramLinkContentHandler getLinkHandler() {
+ return linkHandler;
+ }
+
}
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java
new file mode 100644
index 0000000000..09d02a94c1
--- /dev/null
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java
@@ -0,0 +1,71 @@
+/* ###
+ * 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.program.database;
+
+import java.io.IOException;
+
+import javax.swing.Icon;
+
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.URLLinkObject;
+import ghidra.framework.model.DomainObject;
+import ghidra.framework.store.FileSystem;
+import ghidra.util.InvalidNameException;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class ProgramLinkContentHandler extends LinkHandler {
+
+ public static final String PROGRAM_LINK_CONTENT_TYPE = "ProgramLink";
+
+ @Override
+ public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
+ DomainObject obj, TaskMonitor monitor)
+ throws IOException, InvalidNameException, CancelledException {
+ if (!(obj instanceof URLLinkObject)) {
+ throw new IOException("Unsupported domain object: " + obj.getClass().getName());
+ }
+ return createFile((URLLinkObject) obj, PROGRAM_LINK_CONTENT_TYPE, fs, path, name,
+ monitor);
+ }
+
+ @Override
+ public String getContentType() {
+ return PROGRAM_LINK_CONTENT_TYPE;
+ }
+
+ @Override
+ public String getContentTypeDisplayString() {
+ return PROGRAM_LINK_CONTENT_TYPE;
+ }
+
+ @Override
+ public Class getDomainObjectClass() {
+ // return linked content class
+ return ProgramContentHandler.PROGRAM_DOMAIN_OBJECT_CLASS;
+ }
+
+ @Override
+ public Icon getIcon() {
+ return ProgramContentHandler.PROGRAM_ICON;
+ }
+
+ @Override
+ public String getDefaultToolName() {
+ return ProgramContentHandler.PROGRAM_CONTENT_DEFAULT_TOOL;
+ }
+
+}
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java
index d866d23408..af1083569b 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java
@@ -19,7 +19,6 @@ import java.io.IOException;
import java.util.*;
import db.*;
-import ghidra.framework.data.ContentHandler;
import ghidra.framework.data.DomainObjectAdapterDB;
import ghidra.framework.store.FileSystem;
import ghidra.program.database.map.AddressMapDB;
@@ -44,6 +43,8 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData
// TODO: WARNING! This implementation does not properly handle undo/redo in terms of cache invalidation
+ private static ProgramContentHandler programContentHandler = new ProgramContentHandler();
+
/**
* DB_VERSION should be incremented any time a change is made to the overall
* database schema associated with any of the managers.
@@ -669,10 +670,7 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData
else {
FileSystem userfs = program.getAssociatedUserFilesystem();
if (userfs != null) {
- ContentHandler contentHandler = getContentHandler(program);
- if (contentHandler != null) {
- contentHandler.saveUserDataFile(program, dbh, userfs, monitor);
- }
+ programContentHandler.saveUserDataFile(program, dbh, userfs, monitor);
setChanged(false);
}
}
diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java
index d26eb3a9b2..ed7dd59c1c 100644
--- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java
+++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java
@@ -36,6 +36,7 @@ import docking.wizard.WizardPanel;
import generic.theme.GThemeDefaults.Colors;
import ghidra.app.plugin.core.archive.RestoreDialog;
import ghidra.framework.data.GhidraFileData;
+import ghidra.framework.data.ProjectFileManager;
import ghidra.framework.main.*;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.dialog.*;
@@ -351,8 +352,13 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
@Test
public void testProjectExists() {
- OkDialog.show("Project Exists",
- "Cannot restore project because project named TestPrj already exists.");
+ runSwing(() -> {
+ OkDialog.show("Project Exists",
+ "Cannot restore project because project named TestPrj already exists.");
+ }, false);
+
+ waitForSwing();
+
captureDialog();
}
@@ -651,25 +657,60 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
@Test
public void testViewOtherProjects()
throws IOException, LockException, InvalidNameException, CancelledException {
- ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT);
+
Project project = env.getProject();
program = env.getProgram("WinHelloCPP.exe");
ProjectData projectData = project.getProjectData();
projectData.getRootFolder().createFile("HelloCpp.exe", program, TaskMonitor.DUMMY);
- project.close();
-
+ // Create other project to be viewed
+ ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT);
Project otherProject = ProjectTestUtils.getProject(TEMP_DIR, OTHER_PROJECT);
-
Language language = getZ80_LANGUAGE();
ProjectTestUtils.createProgramFile(otherProject, "Program1", language,
language.getDefaultCompilerSpec(), null);
ProjectTestUtils.createProgramFile(otherProject, "Program2", language,
language.getDefaultCompilerSpec(), null);
+ otherProject.close();
+
+ waitForSwing();
+
+ performAction("View Project", "FrontEndPlugin", false);
+ final GhidraFileChooser fileChooser = (GhidraFileChooser) getDialog();
+ runSwing(() -> fileChooser.setSelectedFile(new File(TEMP_DIR, OTHER_PROJECT)));
+ pressButtonOnDialog("Select Project");
+ setToolSize(500, 600);
+ captureToolWindow(700, 600);
+
+ ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT);
+ ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT);
+
+ }
+
+ @Test
+ public void testLinkOtherProject()
+ throws IOException, LockException, InvalidNameException, CancelledException {
+
+ Project project = env.getProject();
+ program = env.getProgram("WinHelloCPP.exe");
+ ProjectFileManager projectData = (ProjectFileManager) project.getProjectData();
+ projectData.getRootFolder().createFile("HelloCpp.exe", program, TaskMonitor.DUMMY);
+
+ // Create other project to be viewed
+ ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT);
+ Project otherProject = ProjectTestUtils.getProject(TEMP_DIR, OTHER_PROJECT);
+ Language language = getZ80_LANGUAGE();
+ DomainFile otherFile =
+ ProjectTestUtils.createProgramFile(otherProject, "Program1", language,
+ language.getDefaultCompilerSpec(), null);
+ ProjectTestUtils.createProgramFile(otherProject, "Program2", language,
+ language.getDefaultCompilerSpec(), null);
+
+ otherFile.copyToAsLink(projectData.getRootFolder());
otherProject.close();
- waitForSwing();
- project = ProjectTestUtils.getProject(TEMP_DIR, PROJECT_NAME);
+
+ waitForBusyTool(tool);
performAction("View Project", "FrontEndPlugin", false);
final GhidraFileChooser fileChooser = (GhidraFileChooser) getDialog();
diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/VersionControlScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/VersionControlScreenShots.java
index 2ea3e10283..9588524b5e 100644
--- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/VersionControlScreenShots.java
+++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/VersionControlScreenShots.java
@@ -55,7 +55,6 @@ public class VersionControlScreenShots extends GhidraScreenShotGenerator {
VersionControlDialog dialog = new VersionControlDialog(false);
dialog.setCurrentFileName(FrontEndTestEnv.PROGRAM_A);
- dialog.setKeepCheckboxEnabled(true);
runSwing(() -> tool.showDialog(dialog), false);
VersionControlDialog d = waitForDialogComponent(VersionControlDialog.class);
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java
new file mode 100644
index 0000000000..9bd51ec1fa
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java
@@ -0,0 +1,343 @@
+/* ###
+ * 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 static org.junit.Assert.*;
+
+import java.io.File;
+import java.net.URL;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.*;
+
+import docking.DialogComponentProvider;
+import docking.test.AbstractDockingTest;
+import ghidra.app.services.CodeViewerService;
+import ghidra.app.services.ProgramManager;
+import ghidra.framework.data.DomainFileProxy;
+import ghidra.framework.model.*;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.framework.protocol.ghidra.Handler;
+import ghidra.program.database.*;
+import ghidra.program.model.address.Address;
+import ghidra.program.model.address.AddressSpace;
+import ghidra.program.model.listing.Program;
+import ghidra.program.model.symbol.*;
+import ghidra.program.util.ProgramLocation;
+import ghidra.server.remote.ServerTestUtil;
+import ghidra.test.*;
+import ghidra.util.exception.AssertException;
+import ghidra.util.task.TaskMonitor;
+import utilities.util.FileUtilities;
+
+public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private TestEnv env;
+ private ProgramDB program;
+
+ private File serverRoot;
+
+ private static final String FILENAME = "Test";
+ private static final String FOLDER = "/";
+ private static final String FILEPATH = FOLDER + FILENAME;
+ private static final String NAMESPACE_NAME = "foo";
+ private static final String SYMBOL_NAME = "xyz";
+ private static final String REF = NAMESPACE_NAME + Namespace.DELIMITER + SYMBOL_NAME;
+ private static final String SYMBOL_ADDR = "0x1001030";
+ private static final String REPO_NAME = "Test";
+
+ private URL remoteFileUrl;
+
+ @Before
+ public void setUp() throws Exception {
+
+ env = new TestEnv();
+
+ // NOTE: Use of tool templates requires active front-end tool
+ env.getFrontEndTool();
+
+ program = (ProgramDB) buildProgram();
+ Project project = env.getProject();
+ DomainFolder rootFolder = project.getProjectData().getRootFolder();
+ rootFolder.createFile("Test", program, TaskMonitor.DUMMY);
+ }
+
+ @After
+ public void tearDown() {
+ killServer();
+ env.dispose();
+ }
+
+ private Program buildProgram() throws Exception {
+ ToyProgramBuilder builder = new ToyProgramBuilder(FILENAME, true, ProgramBuilder._TOY);
+ builder.createMemory("test1", "0x1001000", 0xb000);
+ builder.addBytesFallthrough("0x1001010");
+ builder.addBytesFallthrough("0x1001020");
+ builder.addBytesFallthrough("0x1001030");
+ builder.addBytesFallthrough("0x1001040");
+ builder.disassemble("0x1001010", 1);
+ builder.disassemble("0x1001020", 1);
+ builder.disassemble("0x1001030", 1);
+ builder.disassemble("0x1001040", 1);
+ Program p = builder.getProgram();
+
+ int txId = p.startTransaction("Add Label");
+ try {
+ AddressSpace space = p.getAddressFactory().getDefaultAddressSpace();
+ Address addr = space.getAddress(SYMBOL_ADDR);
+ SymbolTable symbolTable = p.getSymbolTable();
+ Namespace ns =
+ symbolTable.createNameSpace(null, NAMESPACE_NAME, SourceType.USER_DEFINED);
+ symbolTable.createLabel(addr, SYMBOL_NAME, ns, SourceType.USER_DEFINED);
+ }
+ catch (Exception e) {
+ throw new AssertException(e);
+ }
+ finally {
+ p.endTransaction(txId, true);
+ }
+
+ return p;
+ }
+
+ @Test
+ public void testLocalLaunchDefaultTool() throws Exception {
+
+ Project project = env.getProject();
+ setupDefaultTestTool(project);
+
+ ProjectLocator projectLocator = env.getProject().getProjectLocator();
+
+ URL url = GhidraURL.makeURL(projectLocator, FILEPATH, REF);
+
+ AtomicReference ref = new AtomicReference<>();
+ runSwing(() -> {
+ boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
+ ToolServices toolServices = project.getToolServices();
+ ref.set(toolServices.launchDefaultToolWithURL(url));
+ AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
+ });
+
+ verifyLaunch(ref.get());
+ }
+
+ @Test
+ public void testLocalLaunchNamedTool() throws Exception {
+
+ Project project = env.getProject();
+ ProjectLocator projectLocator = project.getProjectLocator();
+
+ URL url = GhidraURL.makeURL(projectLocator, FILEPATH, REF);
+
+ AtomicReference ref = new AtomicReference<>();
+ runSwing(() -> {
+ boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
+ ToolServices toolServices = project.getToolServices();
+ ref.set(toolServices.launchToolWithURL(DEFAULT_TEST_TOOL_NAME, url));
+ AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
+ });
+
+ verifyLaunch(ref.get());
+ }
+
+ @Test
+ public void testBad1LocalLaunchDefaultTool() throws Exception {
+
+ Project project = env.getProject();
+ setupDefaultTestTool(project);
+
+ ProjectLocator projectLocator = env.getProject().getProjectLocator();
+
+ URL url = GhidraURL.makeURL(projectLocator, FOLDER, null);
+
+ ToolServices toolServices = project.getToolServices();
+ PluginTool tool = toolServices.launchDefaultToolWithURL(url);
+ assertNull(tool);
+
+ DialogComponentProvider dlg = waitForDialogComponent("Unsupported Content");
+ assertNotNull("Error dialog expected", dlg);
+ runSwing(() -> dlg.close());
+ }
+
+ @Test
+ public void testBad2LocalLaunchDefaultTool() throws Exception {
+
+ Project project = env.getProject();
+ setupDefaultTestTool(project);
+
+ ProjectLocator projectLocator = env.getProject().getProjectLocator();
+
+ URL url = GhidraURL.makeURL(projectLocator, "/x/y", null);
+
+ ToolServices toolServices = project.getToolServices();
+ PluginTool tool = toolServices.launchDefaultToolWithURL(url);
+ assertNull(tool);
+
+ DialogComponentProvider dlg = waitForDialogComponent("Content Not Found");
+ assertNotNull("Error dialog expected", dlg);
+ runSwing(() -> dlg.close());
+ }
+
+ @Test
+ public void testRemoteLaunchDefaultTool() throws Exception {
+ startServer(); // also changes user's identity
+
+ Project project = env.getProject();
+ setupDefaultTestTool(project);
+
+ AtomicReference ref = new AtomicReference<>();
+ runSwing(() -> {
+ boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
+ ToolServices toolServices = project.getToolServices();
+ ref.set(toolServices.launchDefaultToolWithURL(remoteFileUrl));
+ AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
+ });
+
+ verifyLaunch(ref.get());
+ }
+
+ @Test
+ public void testRemoteLaunchNamedTool() throws Exception {
+ startServer(); // also changes user's identity
+
+ Project project = env.getProject();
+ setupDefaultTestTool(project);
+
+ AtomicReference ref = new AtomicReference<>();
+ runSwing(() -> {
+ boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
+ ToolServices toolServices = project.getToolServices();
+ ref.set(toolServices.launchToolWithURL(DEFAULT_TEST_TOOL_NAME, remoteFileUrl));
+ AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
+ });
+
+ verifyLaunch(ref.get());
+ }
+
+ @Test
+ public void testRemoteBad1LaunchDefaultTool() throws Exception {
+ startServer(); // also changes user's identity
+
+ Project project = env.getProject();
+ setupDefaultTestTool(project);
+
+ URL badUrl = GhidraURL.makeURL(ServerTestUtil.LOCALHOST,
+ ServerTestUtil.GHIDRA_TEST_SERVER_PORT, REPO_NAME, FOLDER, null, null);
+
+ ToolServices toolServices = project.getToolServices();
+ PluginTool tool = toolServices.launchDefaultToolWithURL(badUrl);
+ assertNull(tool);
+
+ DialogComponentProvider dlg = waitForDialogComponent("Unsupported Content");
+ assertNotNull("Error dialog expected", dlg);
+ runSwing(() -> dlg.close());
+ }
+
+ @Test
+ public void testRemoteBad2LaunchDefaultTool() throws Exception {
+ startServer(); // also changes user's identity
+
+ Project project = env.getProject();
+ setupDefaultTestTool(project);
+
+ URL badUrl = GhidraURL.makeURL(ServerTestUtil.LOCALHOST,
+ ServerTestUtil.GHIDRA_TEST_SERVER_PORT, REPO_NAME, FOLDER, "x", REF);
+
+ ToolServices toolServices = project.getToolServices();
+ PluginTool tool = toolServices.launchDefaultToolWithURL(badUrl);
+ assertNull(tool);
+
+ DialogComponentProvider dlg = waitForDialogComponent("Content Not Found");
+ assertNotNull("Error dialog expected", dlg);
+ runSwing(() -> dlg.close());
+ }
+
+ private void verifyLaunch(PluginTool tool) throws Exception {
+ assertNotNull("tool failed to launch", tool);
+
+ ProgramManager pm = tool.getService(ProgramManager.class);
+ assertNotNull("ProgramManager not found", pm);
+
+ CodeViewerService codeViewer = tool.getService(CodeViewerService.class);
+ assertNotNull("CodeViewerService not found", codeViewer);
+
+ ProgramLocation currentLocation = codeViewer.getCurrentLocation();
+ assertNotNull("Failed to determine current location", currentLocation);
+
+ Program p = currentLocation.getProgram();
+
+ // Verify that it was not directly opened via active Project
+ assertTrue(p.getDomainFile() instanceof DomainFileProxy);
+
+ AddressSpace space = p.getAddressFactory().getDefaultAddressSpace();
+ Address addr = space.getAddress("0x1001030");
+ assertEquals(addr, currentLocation.getAddress());
+ }
+
+ private void setupDefaultTestTool(Project project) {
+ ToolServices toolServices = project.getToolServices();
+ ProgramContentHandler handler = new ProgramContentHandler();
+ ToolTemplate toolTemplate =
+ project.getLocalToolChest().getToolTemplate(DEFAULT_TEST_TOOL_NAME);
+ assertNotNull(toolTemplate);
+ ToolAssociationInfo info =
+ new ToolAssociationInfo(handler, DEFAULT_TEST_TOOL_NAME, toolTemplate, toolTemplate);
+ toolServices.setContentTypeToolAssociations(Set.of(info));
+ }
+
+ private void killServer() {
+
+ if (serverRoot == null) {
+ return;
+ }
+
+ ServerTestUtil.disposeServer();
+
+ FileUtilities.deleteDir(serverRoot);
+ }
+
+ private void startServer() throws Exception {
+
+ // register ghidra protocol and define remote URL to access Test file
+ Handler.registerHandler();
+ remoteFileUrl = GhidraURL.makeURL(ServerTestUtil.LOCALHOST,
+ ServerTestUtil.GHIDRA_TEST_SERVER_PORT, REPO_NAME, FOLDER, FILENAME, REF);
+
+ // Create server instance
+ serverRoot = new File(getTestDirectoryPath(), "TestServer");
+ FileUtilities.deleteDir(serverRoot);
+
+ // Authorized admin user "test" is predefined by ServerTestUtil.createPopulatedTestServer
+ ServerTestUtil.setLocalUser(ServerTestUtil.ADMIN_USER);
+
+ ServerTestUtil.createPopulatedTestServer(serverRoot.getAbsolutePath(),
+ REPO_NAME, fs -> {
+ try {
+ ServerTestUtil.createRepositoryItem(fs, FILENAME, FOLDER, program);
+ }
+ catch (Exception e) {
+ failWithException("Failed added server content", e);
+ }
+ });
+
+ ServerTestUtil.startServer(serverRoot.getAbsolutePath(),
+ ServerTestUtil.GHIDRA_TEST_SERVER_PORT, -1, false, false, false);
+
+ }
+
+}
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java
index 9b2dcdd337..d006558cbc 100644
--- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java
@@ -21,6 +21,7 @@ import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.security.KeyStore.PrivateKeyEntry;
import java.util.ArrayList;
+import java.util.function.Consumer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -114,7 +115,6 @@ public class ServerTestUtil {
private static class ShutdownHook extends Thread {
@Override
public void run() {
- Msg.debug(ServerTestUtil.class, "\n\n\n\n\tSHUTDOWN HOOK RUNNING");
disposeServer();
}
}
@@ -626,7 +626,6 @@ public class ServerTestUtil {
System.setProperty(ApplicationTrustManagerFactory.GHIDRA_CACERTS_PATH_PROPERTY, "");
- Msg.debug(ServerTestUtil.class, "disposeServer() - process exist? " + serverProcess);
if (serverProcess != null) {
cmdOut.dispose();
@@ -752,7 +751,7 @@ public class ServerTestUtil {
* dispose method on the returned object.
* @throws IOException
*/
- public static LocalFileSystem createRepository(String dirPath, String repoName,
+ private static LocalFileSystem createRepository(String dirPath, String repoName,
String... userAccessLines) throws IOException {
File repoDir = new File(dirPath, NamingUtilities.mangle(repoName));
@@ -767,7 +766,7 @@ public class ServerTestUtil {
return repoFileSystem;
}
- public static void createRepositoryItem(LocalFileSystem repoFilesystem, String name,
+ private static void createRepositoryItem(LocalFileSystem repoFilesystem, String name,
String folderPath, int numFunctions) throws Exception {
ToyProgramBuilder builder = new ToyProgramBuilder(name, true);
@@ -787,14 +786,7 @@ public class ServerTestUtil {
setProgramHashes(program);
- ContentHandler contentHandler = DomainObjectAdapter.getContentHandler(program);
- long checkoutId = contentHandler.createFile(repoFilesystem, null, folderPath, name,
- program, TaskMonitor.DUMMY);
- LocalFolderItem item = repoFilesystem.getItem(folderPath, name);
- if (item == null) {
- throw new IOException("Item not found: " + FileSystem.SEPARATOR + name);
- }
- item.terminateCheckout(checkoutId, false);
+ createRepositoryItem(repoFilesystem, name, folderPath, program);
}
catch (CancelledException e) {
throw new RuntimeException(e); // unexpected
@@ -807,6 +799,19 @@ public class ServerTestUtil {
}
}
+ public static void createRepositoryItem(LocalFileSystem repoFilesystem, String name,
+ String folderPath, Program program) throws Exception {
+
+ ContentHandler contentHandler = DomainObjectAdapter.getContentHandler(program);
+ long checkoutId = contentHandler.createFile(repoFilesystem, null, folderPath, name,
+ program, TaskMonitor.DUMMY);
+ LocalFolderItem item = repoFilesystem.getItem(folderPath, name);
+ if (item == null) {
+ throw new IOException("Item not found: " + FileSystem.SEPARATOR + name);
+ }
+ item.terminateCheckout(checkoutId, false);
+ }
+
/**
* Create and populate server test repositories "Test" and "Test1" with the specified
* users added. The ADMIN_USER "test" is added by default.
@@ -854,6 +859,43 @@ public class ServerTestUtil {
}
}
+ /**
+ * Create and populate server test repositories "Test" and "Test1" with the specified
+ * users added. The ADMIN_USER "test" is added by default.
+ * @param dirPath server root
+ * @param repoName repository name
+ * @param contentProvider repository content provider callback
+ * (use {@link #createRepositoryItem(LocalFileSystem, String, String, Program)} to add content.
+ * @param users optional inclusion of USER_A and/or USER_B to be added with no authentication required
+ * @throws Exception
+ */
+ public static void createPopulatedTestServer(String dirPath, String repoName,
+ Consumer contentProvider, String... users) throws Exception {
+
+ Msg.info(ServerTestUtil.class, "Constructing Ghidra Server for testing: " + dirPath);
+
+ File rootDir = new File(dirPath);
+ FileUtilities.deleteDir(rootDir);
+ FileUtilities.mkdirs(rootDir);
+
+ String[] userArray = new String[users.length + 1];
+ userArray[0] = ADMIN_USER;
+ System.arraycopy(users, 0, userArray, 1, users.length);
+ createUsers(dirPath, userArray);
+
+ String keys[] = SSHKeyUtil.generateSSHRSAKeys();
+ addSSHKeys(dirPath, keys[0], "test.key", keys[1], "test.pub");
+
+ LocalFileSystem repoFilesystem = createRepository(dirPath, repoName, ADMIN_USER + "=ADMIN",
+ USER_A + "=READ_ONLY", USER_B + "=WRITE");
+ try {
+ contentProvider.accept(repoFilesystem);
+ }
+ finally {
+ repoFilesystem.dispose();
+ }
+ }
+
/**
* Sets dummy hash values for the given program.
*
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java
new file mode 100644
index 0000000000..bcd6d81151
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java
@@ -0,0 +1,152 @@
+/* ###
+ * 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 java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.tree.TreePath;
+
+import docking.ActionContext;
+import docking.test.AbstractDockingTest;
+import docking.widgets.tree.GTree;
+import docking.widgets.tree.GTreeNode;
+import generic.test.AbstractGTest;
+import generic.test.AbstractGenericTest;
+import ghidra.framework.main.datatable.ProjectDataContext;
+import ghidra.framework.main.datatree.*;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.DomainFolder;
+import ghidra.framework.store.FileSystem;
+
+/**
+ * This class provides some convenience methods for interacting with a {@link DataTree}.
+ */
+public class DataTreeHelper {
+
+ private boolean isFrontEndTree;
+ private DataTree tree;
+ private DomainFolderRootNode rootNode;
+
+ public DataTreeHelper(DataTree tree, boolean isFrontEndTree) {
+ this.tree = tree;
+ this.isFrontEndTree = isFrontEndTree;
+ rootNode = (DomainFolderRootNode) tree.getViewRoot();
+ }
+
+ public void waitForTree() {
+ AbstractDockingTest.waitForTree(tree);
+ }
+
+ public DomainFolder getRootFolder() {
+ return rootNode.getDomainFolder();
+ }
+
+ public GTree getTree() {
+ return tree;
+ }
+
+ public GTreeNode getRootNode() {
+ return tree.getModelRoot();
+ }
+
+ private GTreeNode getDataTreeNodeByPath(String path) {
+ int len = path.length();
+ if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) {
+ throw new IllegalArgumentException(
+ "Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'");
+ }
+
+ GTreeNode node = rootNode;
+ String[] split = path.split(FileSystem.SEPARATOR);
+ if (split.length == 0) {
+ return node;
+ }
+
+ for (int i = 1; i < split.length; i++) {
+ GTreeNode child = getChild(node, split[i]);
+ if (child == null) {
+ return null;
+ }
+ node = child;
+ }
+ return node;
+ }
+
+ private GTreeNode getChild(GTreeNode parent, String name) {
+ return AbstractGTest.waitForValue(() -> parent.getChild(name));
+ }
+
+ public GTreeNode waitForTreeNode(String name) {
+ return name.startsWith(FileSystem.SEPARATOR) ? getDataTreeNodeByPath(name)
+ : getChild(rootNode, name);
+ }
+
+ public DomainFileNode waitForFileNode(String name) {
+ return (DomainFileNode) waitForTreeNode(name);
+ }
+
+ public DomainFolderNode waitForFolderNode(String name) {
+ return (DomainFolderNode) waitForTreeNode(name);
+ }
+
+ public void clearTreeSelection() {
+ AbstractGenericTest.runSwing(() -> tree.clearSelection());
+ }
+
+ public void setTreeSelection(final TreePath[] paths) throws Exception {
+ tree.setSelectionPaths(paths);
+ waitForTree();
+ }
+
+ public void selectNodes(GTreeNode... nodes) {
+ tree.setSelectedNodes(nodes);
+ waitForTree();
+ }
+
+ public void expandNode(GTreeNode node) {
+ tree.expandPath(node);
+ waitForTree();
+ }
+
+ public ActionContext getDomainFileActionContext(GTreeNode... nodes) {
+
+ List fileList = new ArrayList<>();
+ List folderList = new ArrayList<>();
+ TreePath[] treePaths = new TreePath[nodes.length];
+ for (int i = 0; i < nodes.length; i++) {
+ GTreeNode node = nodes[i];
+ treePaths[i] = node.getTreePath();
+ if (node instanceof DomainFileNode) {
+ fileList.add(((DomainFileNode) node).getDomainFile());
+ }
+ else if (node instanceof DomainFolderNode) {
+ folderList.add(((DomainFolderNode) node).getDomainFolder());
+ }
+ }
+
+ if (isFrontEndTree) {
+ boolean isActiveProject = tree.getName().equals("Data Tree");
+ return new FrontEndProjectTreeContext(null, rootNode.getDomainFolder().getProjectData(),
+ treePaths, folderList, fileList, tree, isActiveProject);
+ }
+
+ return new ProjectDataContext(null, rootNode.getDomainFolder().getProjectData(), nodes[0],
+ folderList, fileList, tree, true);
+
+ }
+
+}
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndTestEnv.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndTestEnv.java
index f7141a5476..0ffc529e39 100644
--- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndTestEnv.java
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndTestEnv.java
@@ -33,7 +33,6 @@ import generic.test.AbstractGTest;
import generic.test.AbstractGenericTest;
import ghidra.framework.main.FrontEndTool;
import ghidra.framework.main.SharedProjectUtil;
-import ghidra.framework.main.datatable.ProjectDataContext;
import ghidra.framework.main.datatree.*;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool;
@@ -61,9 +60,8 @@ public class FrontEndTestEnv {
// TODO make private
protected TestEnv env;
protected FrontEndTool frontEndTool;
- protected DataTree tree;
- protected DomainFolder rootFolder;
- protected GTreeNode rootNode;
+ protected DomainFolder rootFolder; // active project root
+ protected DataTreeHelper projectTreeHelper;
public FrontEndTestEnv() throws Exception {
this(false);
@@ -79,7 +77,12 @@ public class FrontEndTestEnv {
startServer();
}
- tree = AbstractGenericTest.findComponent(frontEndTool.getToolFrame(), DataTree.class);
+ // assume read-only project views are not active and only active
+ // project tree is present
+ DataTree tree =
+ AbstractGenericTest.findComponent(frontEndTool.getToolFrame(), DataTree.class);
+ projectTreeHelper = new DataTreeHelper(tree, true);
+
Project project = frontEndTool.getProject();
rootFolder = project.getProjectData().getRootFolder();
@@ -87,7 +90,6 @@ public class FrontEndTestEnv {
rootFolder.createFile(PROGRAM_A, p, TaskMonitor.DUMMY);
p.release(this);
- rootNode = tree.getViewRoot();
waitForTree();
}
@@ -122,7 +124,7 @@ public class FrontEndTestEnv {
}
public void waitForTree() {
- AbstractDockingTest.waitForTree(tree);
+ projectTreeHelper.waitForTree();
}
public DomainFolder getRootFolder() {
@@ -130,11 +132,11 @@ public class FrontEndTestEnv {
}
public GTree getTree() {
- return tree;
+ return projectTreeHelper.getTree();
}
public GTreeNode getRootNode() {
- return tree.getModelRoot();
+ return projectTreeHelper.getRootNode();
}
/**
@@ -142,27 +144,27 @@ public class FrontEndTestEnv {
* @return the default program node named {@link #PROGRAM_A}
*/
public DomainFileNode getProgramNode() {
- return getTreeNode(PROGRAM_A);
+ return waitForFileNode(PROGRAM_A);
}
public DomainFileNode getTreeNode(String name) {
- return waitForTreeNode(name);
+ return projectTreeHelper.waitForFileNode(name);
}
- public DomainFileNode waitForTreeNode(String name) {
- return (DomainFileNode) AbstractGTest.waitForValue(() -> rootNode.getChild(name));
+ public GTreeNode waitForTreeNode(String name) {
+ return projectTreeHelper.waitForTreeNode(name);
}
public DomainFileNode waitForFileNode(String name) {
- return (DomainFileNode) AbstractGTest.waitForValue(() -> rootNode.getChild(name));
+ return projectTreeHelper.waitForFileNode(name);
}
public DomainFolderNode waitForFolderNode(String name) {
- return (DomainFolderNode) AbstractGTest.waitForValue(() -> rootNode.getChild(name));
+ return projectTreeHelper.waitForFolderNode(name);
}
public void clearTreeSelection() {
- runSwing(() -> tree.clearSelection());
+ projectTreeHelper.clearTreeSelection();
}
public void waitForSwing() {
@@ -182,18 +184,15 @@ public class FrontEndTestEnv {
}
public void setTreeSelection(final TreePath[] paths) throws Exception {
- tree.setSelectionPaths(paths);
- waitForTree();
+ projectTreeHelper.setTreeSelection(paths);
}
public void selectNodes(GTreeNode... nodes) {
- tree.setSelectedNodes(nodes);
- waitForTree();
+ projectTreeHelper.selectNodes(nodes);
}
public void expandNode(GTreeNode node) {
- tree.expandPath(node);
- waitForTree();
+ projectTreeHelper.expandNode(node);
}
public void dispose() {
@@ -312,20 +311,7 @@ public class FrontEndTestEnv {
}
public ActionContext getDomainFileActionContext(GTreeNode... nodes) {
- List fileList = new ArrayList<>();
- List folderList = new ArrayList<>();
- for (GTreeNode node : nodes) {
- if (node instanceof DomainFileNode) {
- fileList.add(((DomainFileNode) node).getDomainFile());
- }
- else if (node instanceof DomainFolderNode) {
- folderList.add(((DomainFolderNode) node).getDomainFolder());
- }
- }
-
- return new ProjectDataContext(null, rootFolder.getProjectData(), nodes[0], folderList,
- fileList, tree, true);
-
+ return projectTreeHelper.getDomainFileActionContext(nodes);
}
public FrontEndTool getFrontEndTool() {
@@ -447,4 +433,30 @@ public class FrontEndTestEnv {
public interface ModifyProgramCallback {
public void call(Program p) throws Exception;
}
+
+ /**
+ * Get the named READ-ONLY project view tree helper
+ * @param tabName project view name (should match tab name)
+ * @return named READ-ONLY project view tree helper or null if view tree not found
+ */
+ public DataTreeHelper getReadOnlyProjectTreeHelper(String tabName) {
+ String dataTreeName = "Data Tree: " + tabName;
+ DataTree tree = (DataTree) AbstractGenericTest
+ .findComponentByName(frontEndTool.getToolFrame(), dataTreeName);
+ return tree != null ? new DataTreeHelper(tree, true) : null;
+ }
+
+ /**
+ * Get the READ-ONLY project view tree helper for first view found
+ * @return READ-ONLY project view tree helper for first view found or null if none displayed
+ */
+ public DataTreeHelper getFirstReadOnlyProjectTreeHelper() {
+ for (DataTree tree : AbstractGenericTest.findComponents(frontEndTool.getToolFrame(),
+ DataTree.class)) {
+ if (tree.getName().startsWith("Data Tree:")) {
+ return new DataTreeHelper(tree, true);
+ }
+ }
+ return null;
+ }
}
diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java
new file mode 100644
index 0000000000..2c052ed32d
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java
@@ -0,0 +1,128 @@
+/* ###
+ * 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.protocol.ghidra;
+
+import static org.junit.Assert.*;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.junit.Test;
+
+import generic.test.AbstractGenericTest;
+
+public class DefaultGhidraProtocolConnectorParseTest extends AbstractGenericTest {
+
+ static {
+ Handler.registerHandler();
+ }
+
+ @Test
+ public void testParseURL() throws Exception {
+
+ DefaultGhidraProtocolConnector pp =
+ new DefaultGhidraProtocolConnector(new URL("ghidra://myhost"));
+
+ try {
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost//"));
+ fail();
+ }
+ catch (MalformedURLException e) {
+ // expected
+ }
+
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/"));
+ assertNull(pp.getRepositoryName());
+ assertNull(pp.getFolderPath());
+ assertNull(pp.getFolderItemName());
+ assertNull(getInstanceField("itemPath", pp));
+
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo"));
+ assertEquals("repo", pp.getRepositoryName());
+ assertEquals("/", pp.getFolderPath());
+ assertNull(pp.getFolderItemName());
+ assertEquals("/", getInstanceField("itemPath", pp));
+
+ try {
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo//"));
+ fail();
+ }
+ catch (MalformedURLException e) {
+ // expected
+ }
+
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/"));
+ assertEquals("repo", pp.getRepositoryName());
+ assertEquals("/", pp.getFolderPath());
+ assertNull(pp.getFolderItemName());
+ assertEquals("/", getInstanceField("itemPath", pp));
+
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a"));
+ assertEquals("repo", pp.getRepositoryName());
+ assertEquals("/", pp.getFolderPath());
+ assertEquals("a", pp.getFolderItemName());
+ assertEquals("/a", getInstanceField("itemPath", pp));
+
+ try {
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a//"));
+ fail();
+ }
+ catch (MalformedURLException e) {
+ // expected
+ }
+
+ try {
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a///"));
+ fail();
+ }
+ catch (MalformedURLException e) {
+ // expected
+ }
+
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/"));
+ assertEquals("repo", pp.getRepositoryName());
+ assertEquals("/a", pp.getFolderPath());
+ assertNull(pp.getFolderItemName());
+ assertEquals("/a/", getInstanceField("itemPath", pp));
+
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b"));
+ assertEquals("repo", pp.getRepositoryName());
+ assertEquals("/a", pp.getFolderPath());
+ assertEquals("b", pp.getFolderItemName());
+ assertEquals("/a/b", getInstanceField("itemPath", pp));
+
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b/"));
+ assertEquals("repo", pp.getRepositoryName());
+ assertEquals("/a/b", pp.getFolderPath());
+ assertNull(pp.getFolderItemName());
+ assertEquals("/a/b/", getInstanceField("itemPath", pp));
+
+ try {
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b//"));
+ fail();
+ }
+ catch (MalformedURLException e) {
+ // expected
+ }
+
+ pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b#junk"));
+ assertEquals("repo", pp.getRepositoryName());
+ assertEquals("/a", pp.getFolderPath());
+ assertEquals("b", pp.getFolderItemName());
+ assertEquals("/a/b", getInstanceField("itemPath", pp));
+ }
+
+}
diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java
new file mode 100644
index 0000000000..f9442e70a2
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java
@@ -0,0 +1,77 @@
+/* ###
+ * 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.protocol.ghidra;
+
+import static org.junit.Assert.*;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.junit.Test;
+
+import generic.test.AbstractGenericTest;
+
+public class DefaultLocalGhidraProtocolConnectorParseTest extends AbstractGenericTest {
+
+ static {
+ Handler.registerHandler();
+ }
+
+ @Test
+ public void testParseURL() throws Exception {
+
+ DefaultLocalGhidraProtocolConnector pp =
+ new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/C:/x/y/proj"));
+ assertNull(pp.getRepositoryName());
+ assertEquals("/", pp.getFolderPath());
+ assertNull(pp.getFolderItemName());
+ assertEquals("/", getInstanceField("itemPath", pp));
+
+ pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj"));
+ assertNull(pp.getRepositoryName());
+ assertEquals("/", pp.getFolderPath());
+ assertNull(pp.getFolderItemName());
+ assertEquals("/", getInstanceField("itemPath", pp));
+
+ pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?/"));
+ assertNull(pp.getRepositoryName());
+ assertEquals("/", pp.getFolderPath());
+ assertNull(pp.getFolderItemName());
+ assertEquals("/", getInstanceField("itemPath", pp));
+
+ pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?/a"));
+ assertNull(pp.getRepositoryName());
+ assertEquals("/", pp.getFolderPath());
+ assertEquals("a", pp.getFolderItemName());
+ assertEquals("/a", getInstanceField("itemPath", pp));
+
+ pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?/a/b#ref"));
+ assertNull(pp.getRepositoryName());
+ assertEquals("/a", pp.getFolderPath());
+ assertEquals("b", pp.getFolderItemName());
+ assertEquals("/a/b", getInstanceField("itemPath", pp));
+
+ try {
+ pp =
+ new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?//"));
+ fail();
+ }
+ catch (MalformedURLException e) {
+ // expected
+ }
+
+ }
+}
diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java
new file mode 100644
index 0000000000..72686a2b8b
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java
@@ -0,0 +1,499 @@
+/* ###
+ * 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.protocol.ghidra;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import generic.test.AbstractGenericTest;
+import ghidra.framework.client.*;
+import ghidra.framework.model.ProjectLocator;
+import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
+
+public class GhidraURLTest extends AbstractGenericTest {
+
+ @Before
+ public void setUp() throws Exception {
+ Handler.registerHandler();
+ }
+
+ // makeURL(ProjectLocator)
+ @Test
+ public void testMakeLocalProjectURL() throws Exception {
+ ProjectLocator loc = new ProjectLocator("C:\\junk", "Test");
+ URL ghidraUrl = GhidraURL.makeURL(loc);
+ URL url = new URL("ghidra:/C:/junk/Test");
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("C:\\junk\\", "Test");
+ ghidraUrl = GhidraURL.makeURL(loc);
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/C:/junk", "Test");
+ ghidraUrl = GhidraURL.makeURL(loc);
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/C:/junk/", "Test");
+ ghidraUrl = GhidraURL.makeURL(loc);
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/a/b", "Test");
+ ghidraUrl = GhidraURL.makeURL(loc);
+ url = new URL("ghidra:/a/b/Test");
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/a/b/", "Test");
+ ghidraUrl = GhidraURL.makeURL(loc);
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ try {
+ loc = new ProjectLocator("a/b", "Test");
+ fail("relative path shold not be permitted");
+ }
+ catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ // makeURL(String, String)
+ @Test
+ public void testMakeLocalProjectURL2() throws Exception {
+ ProjectLocator loc = new ProjectLocator("C:\\junk", "Test");
+ URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test");
+ URL url = new URL("ghidra:/C:/junk/Test");
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("C:\\junk\\", "Test");
+ ghidraUrl = GhidraURL.makeURL("C:\\junk\\", "Test");
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/C:/junk", "Test");
+ ghidraUrl = GhidraURL.makeURL("/C:/junk", "Test");
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/C:/junk/", "Test");
+ ghidraUrl = GhidraURL.makeURL("/C:/junk/", "Test");
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/a/b", "Test");
+ ghidraUrl = GhidraURL.makeURL("/a/b", "Test");
+ url = new URL("ghidra:/a/b/Test");
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/a/b/", "Test");
+ ghidraUrl = GhidraURL.makeURL("/a/b/", "Test");
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ try {
+ ghidraUrl = GhidraURL.makeURL("a/b/", "Test");
+ fail("relative path shold not be permitted");
+ }
+ catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ // makeURL(ProjectLocator, String, String)
+ @Test
+ public void testMakeLocalProjectFileURL() throws Exception {
+ ProjectLocator loc = new ProjectLocator("C:\\junk", "Test");
+
+ URL ghidraUrl = GhidraURL.makeURL(loc, "/a", "ref");
+ URL url = new URL("ghidra:/C:/junk/Test?/a#ref");
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL(loc, "/a/", "ref");
+ url = new URL("ghidra:/C:/junk/Test?/a/#ref");
+ assertEquals(url, ghidraUrl);
+
+ try {
+ ghidraUrl = GhidraURL.makeURL(loc, "a/b", "ref");
+ fail("relative path shold not be permitted");
+ }
+ catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ // makeURL(String, String, String, String)
+ @Test
+ public void testMakeLocalProjectFileURL2() throws Exception {
+ ProjectLocator loc = new ProjectLocator("C:\\junk", "Test");
+
+ URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref");
+ URL url = new URL("ghidra:/C:/junk/Test?/a#ref");
+ assertEquals(url, ghidraUrl);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref");
+ url = new URL("ghidra:/C:/junk/Test?/a/#ref");
+ assertEquals(url, ghidraUrl);
+
+ try {
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "a/b", "ref");
+ fail("relative path shold not be permitted");
+ }
+ catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ // makeURL(String, int, String)
+ @Test
+ public void testMakeServerRepoURL() throws Exception {
+ URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test");
+ URL url = new URL("ghidra", "localhost", 123, "/Test");
+ assertEquals(url, ghidraUrl);
+ }
+
+ // makeURL(String, int, String, String)
+ @Test
+ public void testMakeServerRepoFileURL() throws Exception {
+ URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/");
+ URL url = new URL("ghidra", "localhost", 123, "/Test/foo/");
+ assertEquals(url, ghidraUrl);
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo");
+ url = new URL("ghidra", "localhost", 123, "/Test/foo");
+ assertEquals(url, ghidraUrl);
+
+ }
+
+ // makeURL(String, int, String, String, String, String)
+ @Test
+ public void testMakeServerRepoFileURL2() throws Exception {
+ URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", null, null);
+ URL url = new URL("ghidra", "localhost", 123, "/Test/foo/");
+ assertEquals(url, ghidraUrl);
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", null, null);
+ assertEquals(url, ghidraUrl);
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref");
+ url = new URL("ghidra", "localhost", 123, "/Test/foo/bar#ref");
+ assertEquals(url, ghidraUrl);
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref");
+ assertEquals(url, ghidraUrl);
+ }
+
+// makeURL(String, int, String, String)
+ @Test
+ public void testMakeServerRepoFileURL3() throws Exception {
+ URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo");
+ URL url = new URL("ghidra", "localhost", 123, "/Test/foo");
+ assertEquals(url, ghidraUrl);
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/");
+ url = new URL("ghidra", "localhost", 123, "/Test/foo/");
+ assertEquals(url, ghidraUrl);
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/bar", "ref");
+ url = new URL("ghidra", "localhost", 123, "/Test/foo/bar#ref");
+ assertEquals(url, ghidraUrl);
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/bar");
+ url = new URL("ghidra", "localhost", 123, "/Test/foo/bar");
+ assertEquals(url, ghidraUrl);
+ }
+
+ // getProjectStorageLocator(URL)
+ @Test
+ public void testGetProjectStorageLocator() throws Exception {
+ ProjectLocator loc = new ProjectLocator("C:\\junk", "Test");
+ URL ghidraUrl = GhidraURL.makeURL(loc);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("C:\\junk\\", "Test");
+ ghidraUrl = GhidraURL.makeURL(loc);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/C:/junk", "Test");
+ ghidraUrl = GhidraURL.makeURL(loc);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/C:/junk/", "Test");
+ ghidraUrl = GhidraURL.makeURL(loc);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/a/b", "Test");
+ ghidraUrl = GhidraURL.makeURL(loc);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+
+ loc = new ProjectLocator("/a/b/", "Test");
+ ghidraUrl = GhidraURL.makeURL(loc);
+ assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl));
+ }
+
+ // isLocalProjectURL(URL)
+ @Test
+ public void testIsLocalProjectURL() throws Exception {
+ URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test");
+ assertTrue(GhidraURL.isLocalProjectURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref");
+ assertTrue(GhidraURL.isLocalProjectURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref");
+ assertTrue(GhidraURL.isLocalProjectURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test");
+ assertFalse(GhidraURL.isLocalProjectURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref");
+ assertFalse(GhidraURL.isLocalProjectURL(ghidraUrl));
+ }
+
+ // isServerRepositoryURL(URL)
+ @Test
+ public void testIsServerRepositoryURL() throws Exception {
+ URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test");
+ assertNull(GhidraURL.getRepositoryName(ghidraUrl));
+ assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref");
+ assertNull(GhidraURL.getRepositoryName(ghidraUrl));
+ assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref");
+ assertNull(GhidraURL.getRepositoryName(ghidraUrl));
+ assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test");
+ assertEquals("Test", GhidraURL.getRepositoryName(ghidraUrl));
+ assertTrue(GhidraURL.isServerRepositoryURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref");
+ assertEquals("Test", GhidraURL.getRepositoryName(ghidraUrl));
+ assertTrue(GhidraURL.isServerRepositoryURL(ghidraUrl));
+
+ ghidraUrl = new URL("ghidra", "localhost", 123, "");
+ assertNull(GhidraURL.getRepositoryName(ghidraUrl));
+ assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl));
+ }
+
+ // isServerURL(URL)
+ @Test
+ public void testIsServerURL() throws Exception {
+ URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test");
+ assertFalse(GhidraURL.isServerURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref");
+ assertFalse(GhidraURL.isServerURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref");
+ assertFalse(GhidraURL.isServerURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test");
+ assertTrue(GhidraURL.isServerURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref");
+ assertTrue(GhidraURL.isServerURL(ghidraUrl));
+
+ ghidraUrl = new URL("ghidra", "localhost", 123, "");
+ assertTrue(GhidraURL.isServerURL(ghidraUrl));
+ }
+
+ // toURL(String)
+ @Test
+ public void testToURL() throws Exception {
+ URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test");
+ assertEquals(ghidraUrl, GhidraURL.toURL("C:\\junk\\Test"));
+ assertEquals(ghidraUrl, GhidraURL.toURL("/C:/junk/Test"));
+ assertEquals(ghidraUrl, GhidraURL.toURL("C:/junk/Test"));
+ assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test"));
+ assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString()));
+ assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl)));
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref");
+ assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test?/a#ref"));
+ assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString()));
+ assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl)));
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref");
+ assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test?/a/#ref"));
+ assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString()));
+ assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl)));
+
+ ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref");
+ assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/x/y/Test?/a/#ref"));
+ assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString()));
+ assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl)));
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref");
+ assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString()));
+ assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl)));
+
+ }
+
+ @Test
+ public void testGetProjectURL() throws Exception {
+
+ URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test");
+ assertEquals(new URL("ghidra:/C:/junk/Test"), GhidraURL.getProjectURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref");
+ assertEquals(new URL("ghidra:/C:/junk/Test"), GhidraURL.getProjectURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref");
+ assertEquals(new URL("ghidra:/x/y/Test"), GhidraURL.getProjectURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test");
+ assertEquals(new URL("ghidra://localhost:123/Test"), GhidraURL.getProjectURL(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref");
+ assertEquals(new URL("ghidra://localhost:123/Test"), GhidraURL.getProjectURL(ghidraUrl));
+
+ ghidraUrl = new URL("ghidra", "localhost", 123, "");
+ try {
+ GhidraURL.getProjectURL(ghidraUrl);
+ fail("Expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ }
+
+ // getDisplayString(URL)
+ @Test
+ public void testGetDisplayString() throws Exception {
+ URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test");
+ assertEquals("C:\\junk\\Test", GhidraURL.getDisplayString(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref");
+ assertEquals("ghidra:/C:/junk/Test?/a#ref", GhidraURL.getDisplayString(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref");
+ assertEquals("ghidra:/C:/junk/Test?/a/#ref", GhidraURL.getDisplayString(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref");
+ assertEquals("ghidra:/x/y/Test?/a/#ref", GhidraURL.getDisplayString(ghidraUrl));
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref");
+ assertEquals(ghidraUrl.toString(), GhidraURL.getDisplayString(ghidraUrl));
+
+ }
+
+ // getNormalizedURL(URL)
+ @Test
+ public void testNormalizedURL() throws Exception {
+ URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test");
+ assertEquals("ghidra:/C:/junk/Test", GhidraURL.getNormalizedURL(ghidraUrl).toString());
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref");
+ assertEquals("ghidra:/C:/junk/Test?/a", GhidraURL.getNormalizedURL(ghidraUrl).toString());
+
+ ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref");
+ assertEquals("ghidra:/C:/junk/Test?/a/", GhidraURL.getNormalizedURL(ghidraUrl).toString());
+
+ ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref");
+ assertEquals("ghidra:/x/y/Test?/a/", GhidraURL.getNormalizedURL(ghidraUrl).toString());
+
+ ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref");
+ assertEquals("ghidra://127.0.0.1:123/Test/foo/bar",
+ GhidraURL.getNormalizedURL(ghidraUrl).toString());
+ }
+
+ @Test
+ public void testTransientProjectURL() throws Exception {
+ // Dummy class implementations (see below) are used to stub objects required to establish
+ // transient project for URL verification testing only
+ URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test");
+ DummyGhidraProtocolConnector dummyRepoConnector =
+ new DummyGhidraProtocolConnector(ghidraUrl);
+ TransientProjectManager transientProjectManager =
+ TransientProjectManager.getTransientProjectManager();
+ try {
+ TransientProjectData transientProject =
+ transientProjectManager.getTransientProject(dummyRepoConnector, true);
+ ProjectLocator projectLocator = transientProject.getProjectLocator();
+ assertTrue(GhidraURL.isServerRepositoryURL(projectLocator.getURL()));
+ }
+ finally {
+ transientProjectManager.dispose();
+ }
+ }
+
+ private static class DummyGhidraProtocolConnector extends GhidraProtocolConnector {
+
+ private URL repositoryURL;
+ private DummyRepositoryAdapter repoAdapter;
+
+ DummyGhidraProtocolConnector(URL repositoryURL) throws MalformedURLException {
+ super(repositoryURL);
+ this.repositoryURL = repositoryURL;
+ repoAdapter = new DummyRepositoryAdapter();
+ }
+
+ @Override
+ protected URL getRepositoryRootGhidraURL() {
+ return repositoryURL;
+ }
+
+ @Override
+ public StatusCode connect(boolean readOnly) throws IOException {
+ return StatusCode.OK;
+ }
+
+ @Override
+ public boolean isReadOnly() throws NotConnectedException {
+ return true;
+ }
+
+ @Override
+ public RepositoryAdapter getRepositoryAdapter() {
+ return repoAdapter;
+ }
+
+ }
+
+ private static class DummyRepositoryAdapter extends RepositoryAdapter {
+ DummyRepositoryAdapter() {
+ super(new DummyRepositoryServerAdapter(), "test");
+ }
+
+ @Override
+ public boolean isConnected() {
+ return true;
+ }
+ }
+
+ private static class DummyRepositoryServerAdapter extends RepositoryServerAdapter {
+ DummyRepositoryServerAdapter() {
+ super(null, null);
+ }
+ }
+}