extends AbstractDat
openObjectsTable = new GFilterTable<>(new OpenObjectsTableModel());
GTable table = openObjectsTable.getTable();
- table.getSelectionModel()
- .setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ table.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
openObjectsTable.addSelectionListener(e -> {
setOkEnabled(true);
okButton.setToolTipText("Use the selected " + domainObjectClass.getSimpleName());
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/ProgramFileChooser.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/ProgramFileChooser.java
new file mode 100644
index 0000000000..9be017109a
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/ProgramFileChooser.java
@@ -0,0 +1,65 @@
+/* ###
+ * 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 java.awt.Component;
+
+import ghidra.framework.model.*;
+import ghidra.program.model.listing.Program;
+
+/**
+ * {@link ProgramFileChooser} facilitates selection of an existing project Program file including
+ * Program link-files which may link to either internal or external program files.
+ * This chooser operates in the {@link DataTreeDialogType#OPEN open mode} for selecting
+ * an existing file only.
+ *
+ * This chooser should not be used to facilitate an immediate or
+ * future save-as operation or to open a Program for update since it can return a read-only file.
+ * A more taylored {@link DataTreeDialog} should be used for case where the file will be written.
+ */
+public class ProgramFileChooser extends DataTreeDialog {
+
+ /**
+ * This file filter permits selection of any program including those than can be
+ * found by following bother internal and external folder and files links.
+ */
+ public static final DomainFileFilter PROGRAM_FILE_FILTER =
+ new DefaultDomainFileFilter(Program.class, false);
+
+ /**
+ * Construct a new ProgramChooser for the active project.
+ *
+ * @param parent dialog's parent
+ * @param title title to use
+ * @throws IllegalArgumentException if invalid type is specified
+ */
+ public ProgramFileChooser(Component parent, String title) {
+ super(parent, title, DataTreeDialogType.OPEN, PROGRAM_FILE_FILTER);
+ }
+
+ /**
+ * Construct a new DataTreeDialog for the given project.
+ *
+ * @param parent dialog's parent
+ * @param title title to use
+ * @param project the project to browse
+ * @throws IllegalArgumentException if invalid type is specified
+ */
+ public ProgramFileChooser(Component parent, String title, Project project) {
+ super(parent, title, DataTreeDialogType.OPEN, PROGRAM_FILE_FILTER, project);
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/AbstractFileListFlavorHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/AbstractFileListFlavorHandler.java
index 397e0c06f3..f5784576af 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/AbstractFileListFlavorHandler.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/AbstractFileListFlavorHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -19,7 +19,6 @@ import java.awt.Component;
import java.io.File;
import java.util.List;
-import docking.widgets.tree.GTreeNode;
import ghidra.app.services.FileImporterService;
import ghidra.app.util.FileOpenDataFlavorHandler;
import ghidra.framework.model.DomainFolder;
@@ -61,15 +60,4 @@ abstract class AbstractFileListFlavorHandler
}
});
}
-
- protected DomainFolder getDomainFolder(GTreeNode destinationNode) {
- if (destinationNode instanceof DomainFolderNode) {
- return ((DomainFolderNode) destinationNode).getDomainFolder();
- }
- else if (destinationNode instanceof DomainFileNode) {
- DomainFolderNode parent = (DomainFolderNode) destinationNode.getParent();
- return parent.getDomainFolder();
- }
- return null;
- }
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/JavaFileListHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/JavaFileListHandler.java
index 8ff42780bc..a365ba733d 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/JavaFileListHandler.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/JavaFileListHandler.java
@@ -46,7 +46,7 @@ public final class JavaFileListHandler extends AbstractFileListFlavorHandler {
if (fileList.isEmpty()) {
return false;
}
- doImport(getDomainFolder(destinationNode), fileList, tool, dataTree);
+ doImport(DataTree.getRealInternalFolderForNode(destinationNode), fileList, tool, dataTree);
return true;
}
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java
index 19050e5123..bd398d87e6 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -38,9 +38,8 @@ public final class LinuxFileUrlHandler extends AbstractFileListFlavorHandler {
* Linux URL-based file list {@link DataFlavor} to be used during handler registration
* using {@link DataTreeDragNDropHandler#addActiveDataFlavorHandler}.
*/
- public static final DataFlavor linuxFileUrlFlavor =
- new DataFlavor("application/x-java-serialized-object;class=java.lang.String",
- "String file URL");
+ public static final DataFlavor linuxFileUrlFlavor = new DataFlavor(
+ "application/x-java-serialized-object;class=java.lang.String", "String file URL");
@Override
// This is for the FileOpenDataFlavorHandler for handling file drops from Linux to a Tool
@@ -57,7 +56,7 @@ public final class LinuxFileUrlHandler extends AbstractFileListFlavorHandler {
if (files.isEmpty()) {
return false;
}
- doImport(getDomainFolder(destinationNode), files, tool, dataTree);
+ doImport(DataTree.getRealInternalFolderForNode(destinationNode), files, tool, dataTree);
return true;
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java
index c02319c162..27998ca008 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java
@@ -142,7 +142,7 @@ public class ImporterDialog extends DialogComponentProvider {
*/
public void setDestinationFolder(DomainFolder folder) {
destinationFolder = folder;
- folderNameTextField.setText(destinationFolder.toString());
+ folderNameTextField.setText(destinationFolder.getPathname());
validateFormInput();
}
@@ -521,7 +521,7 @@ public class ImporterDialog extends DialogComponentProvider {
String parentPath = FilenameUtils.getFullPathNoEndSeparator(pathName);
String fileOrFolderName = FilenameUtils.getName(pathName);
DomainFolder localDestFolder =
- (parentPath != null) ? ProjectDataUtils.lookupDomainPath(destinationFolder, parentPath)
+ (parentPath != null) ? ProjectDataUtils.getDomainFolder(destinationFolder, parentPath)
: destinationFolder;
if (localDestFolder != null) {
if (isFolder && localDestFolder.getFolder(fileOrFolderName) != null ||
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java
index f51f3bddb6..63ba37eea8 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java
@@ -28,6 +28,7 @@ import docking.action.*;
import docking.tool.ToolConstants;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
+import docking.widgets.tree.GTreeNode;
import ghidra.app.CorePluginPackage;
import ghidra.app.context.ListingActionContext;
import ghidra.app.events.ProgramActivatedPluginEvent;
@@ -441,17 +442,14 @@ public class ImporterPlugin extends Plugin
}
private static DomainFolder getFolderFromContext(ActionContext context) {
+ DomainFolder folder = null;
Object contextObj = context.getContextObject();
- if (contextObj instanceof DomainFolderNode) {
- DomainFolderNode node = (DomainFolderNode) contextObj;
- return node.getDomainFolder();
+ if (contextObj instanceof GTreeNode dataTreeNode) {
+ folder = DataTree.getRealInternalFolderForNode(dataTreeNode);
}
- if (contextObj instanceof DomainFileNode) {
- DomainFileNode node = (DomainFileNode) contextObj;
- DomainFile domainFile = node.getDomainFile();
- return domainFile != null ? domainFile.getParent() : null;
+ if (folder != null && folder.isInWritableProject()) {
+ return folder;
}
-
return AppInfo.getActiveProject().getProjectData().getRootFolder();
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java b/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java
index 24bc1852e0..c370d82a95 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java
@@ -15,8 +15,7 @@
*/
package ghidra.test;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.model.address.*;
@@ -628,4 +627,21 @@ public class ToyProgramBuilder extends ProgramBuilder {
disassemble(address, 1);
}
+ /**
+ * Create simple Toy program with a single initialized memory block at 0x1001000-0x1002fff
+ * @param programName program name
+ * @param consumer object consumer responsible for releasing the returned program
+ * @return new in-memory program instance
+ * @throws Exception if an error occurs
+ */
+ public static Program buildSimpleProgram(String programName, Object consumer) throws Exception {
+ Objects.requireNonNull(consumer);
+ ProgramBuilder builder = new ProgramBuilder(programName, ProgramBuilder._TOY);
+ builder.createMemory("test1", Long.toHexString(0x1001000), 0x2000);
+ Program p = builder.getProgram();
+ p.addConsumer(consumer);
+ p.release(builder);
+ return p;
+ }
+
}
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 e9048fe1f7..89f84aec35 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,10 +22,10 @@ import java.awt.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
@@ -52,7 +52,6 @@ import ghidra.program.util.ProgramLocation;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.util.bean.field.AnnotatedTextFieldElement;
import ghidra.util.task.TaskMonitor;
-import util.CollectionUtils;
public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
@@ -365,7 +364,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element);
click(spyNavigatable, spyServiceProvider, annotatedElement);
- assertErrorDialog("No Symbol");
+ assertErrorDialog("Symbol Not Found");
assertTrue(spyServiceProvider.programOpened(programName));
assertTrue(spyServiceProvider.programClosed(programName));
@@ -422,7 +421,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element);
click(spyNavigatable, spyServiceProvider, annotatedElement);
- assertErrorDialog("No Symbol");
+ assertErrorDialog("Symbol Not Found");
assertTrue(spyServiceProvider.programOpened(programName));
assertTrue(spyServiceProvider.programClosed(programName));
@@ -478,7 +477,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element);
click(spyNavigatable, spyServiceProvider, annotatedElement);
- assertErrorDialog("No Program");
+ assertErrorDialog("Program Not Found");
assertFalse(spyServiceProvider.programOpened(programName));
}
@@ -495,8 +494,8 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
String otherProgramPath = "folder1/folder2/program_f1_f2.exe";
// real path
- String realPath = "folder1/program_f1_f2.exe";
- addFakeProgramByPath(spyServiceProvider, realPath);
+ String realPath = "/folder1/program_f1_f2.exe";
+ addFakeProgramByPath(spyServiceProvider, realPath, program);
String annotationText = "{@program " + otherProgramPath + "@" + addresstring + "}";
String rawComment = "My comment - " + annotationText;
@@ -513,7 +512,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element);
click(spyNavigatable, spyServiceProvider, annotatedElement);
- assertErrorDialog("No Folder");
+ assertErrorDialog("Folder Not Found");
assertFalse(spyServiceProvider.programOpened(otherProgramPath));
}
@@ -525,7 +524,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
SpyServiceProvider spyServiceProvider = new SpyServiceProvider();
String otherProgramPath = "/folder1/folder2/program_f1_f2.exe";
- addFakeProgramByPath(spyServiceProvider, otherProgramPath);
+ addFakeProgramByPath(spyServiceProvider, otherProgramPath, program);
String annotationText = "{@program " + otherProgramPath + "}";
String rawComment = "My comment - " + annotationText;
@@ -557,7 +556,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
Address address = program.getAddressFactory().getAddress(addresstring);
String otherProgramPath = "/folder1/folder2/program_f1_f2.exe";
- addFakeProgramByPath(spyServiceProvider, otherProgramPath);
+ addFakeProgramByPath(spyServiceProvider, otherProgramPath, program);
String annotationText = "{@program " + otherProgramPath + "@" + addresstring + "}";
String rawComment = "My comment - " + annotationText;
@@ -591,7 +590,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
String otherProgramPath = "/folder1/folder2/program_f1_f2.exe";
String annotationPath = "\\folder1\\folder2\\program_f1_f2.exe";
- addFakeProgramByPath(spyServiceProvider, otherProgramPath);
+ addFakeProgramByPath(spyServiceProvider, otherProgramPath, program);
String annotationText = "{@program " + annotationPath + "@" + addresstring + "}";
String rawComment = "My comment - " + annotationText;
@@ -622,9 +621,9 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
String addresstring = "1001000";
Address address = program.getAddressFactory().getAddress(addresstring);
- String otherProgramPath = "folder1/folder2/program_f1_f2.exe";
+ String otherProgramPath = "/folder1/folder2/program_f1_f2.exe";
String annotationPath = "folder1\\folder2\\program_f1_f2.exe";
- addFakeProgramByPath(spyServiceProvider, otherProgramPath);
+ addFakeProgramByPath(spyServiceProvider, otherProgramPath, program);
String annotationText = "{@program " + annotationPath + "@" + addresstring + "}";
String rawComment = "My comment - " + annotationText;
@@ -656,8 +655,8 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
String addresstring = "1001000";
Address address = program.getAddressFactory().getAddress(addresstring);
- String otherProgramPath = "folder1/folder2/program_f1_f2.exe";
- addFakeProgramByPath(spyServiceProvider, otherProgramPath);
+ String otherProgramPath = "/folder1/folder2/program_f1_f2.exe";
+ addFakeProgramByPath(spyServiceProvider, otherProgramPath, program);
String annotationText = "{@program " + otherProgramPath + "@" + addresstring + "}";
String rawComment = "My comment - " + annotationText;
@@ -883,30 +882,29 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
return new FieldElement[] { fieldElement };
}
- private void addFakeProgramByPath(SpyServiceProvider provider, String path) {
+ private void addFakeProgramByPath(SpyServiceProvider provider, String path, Program p) {
SpyProjectDataService spyProjectData =
(SpyProjectDataService) provider.getService(ProjectDataService.class);
FakeRootFolder root = spyProjectData.fakeProjectData.fakeRootFolder;
- String parentPath = FilenameUtils.getFullPath(path);
- String programName = FilenameUtils.getName(path);
-
- String[] paths = parentPath.split("/");
- TestDummyDomainFolder parent = root;
- String pathSoFar = root.getPathname();
- for (String folderName : paths) {
- pathSoFar += folderName;
- TestDummyDomainFolder folder = (TestDummyDomainFolder) root.getFolder(pathSoFar);
- if (folder == null) {
- folder = new TestDummyDomainFolder(parent, folderName);
- root.addFolder(folder);
- }
- parent = folder;
+ if (StringUtils.isBlank(path) || path.charAt(0) != FileSystem.SEPARATOR_CHAR) {
+ throw new IllegalArgumentException(
+ "Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'");
}
+ else if (path.charAt(path.length() - 1) == FileSystem.SEPARATOR_CHAR) {
+ throw new IllegalArgumentException("Missing file name in path");
+ }
+ int ix = path.lastIndexOf(FileSystem.SEPARATOR);
+ String folderPath = "/";
+ if (ix > 0) {
+ folderPath = path.substring(0, ix);
+ }
+ String programName = path.substring(ix + 1);
try {
- parent.createFile(programName, (DomainObject) null, TaskMonitor.DUMMY);
+ DomainFolder parent = ProjectDataUtils.createDomainFolderPath(root, folderPath);
+ parent.createFile(programName, p, TaskMonitor.DUMMY);
}
catch (Exception e) {
failWithException("Unable to create a dummy domain file", e);
@@ -973,41 +971,50 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
}
@Override
- public DomainFolder getFolder(String path) {
- return fakeRootFolder.getFolder(path);
+ public DomainFolder getFolder(String path, DomainFolderFilter filter) {
+ return ProjectDataUtils.getDomainFolder(fakeRootFolder, path, filter);
+ }
+
+ @Override
+ public DomainFile getFile(String path, DomainFileFilter filter) {
+ if (StringUtils.isBlank(path) || path.charAt(0) != FileSystem.SEPARATOR_CHAR) {
+ throw new IllegalArgumentException(
+ "Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'");
+ }
+ else if (path.charAt(path.length() - 1) == FileSystem.SEPARATOR_CHAR) {
+ throw new IllegalArgumentException("Missing file name in path");
+ }
+ int ix = path.lastIndexOf(FileSystem.SEPARATOR);
+
+ DomainFolder folder;
+ String fileName = path;
+ if (ix > 0) {
+ folder = getFolder(path.substring(0, ix), filter);
+ fileName = path.substring(ix + 1);
+ }
+ else {
+ folder = getRootFolder();
+ }
+ if (folder != null) {
+ DomainFile file = folder.getFile(fileName);
+ if (file != null && filter.accept(file)) {
+ return file;
+ }
+ }
+ return null;
}
}
private class FakeRootFolder extends TestDummyDomainFolder {
- private List folders = CollectionUtils.asList(this);
-
- private List folderFiles =
- CollectionUtils.asList(new TestDummyDomainFile(this, OTHER_PROGRAM_NAME));
-
public FakeRootFolder() {
super(null, "Fake Root Folder");
- }
-
- void addFolder(TestDummyDomainFolder f) {
- folders.add(f);
+ files.add(new TestDummyDomainFile(this, OTHER_PROGRAM_NAME, "Program"));
}
@Override
- public synchronized DomainFile[] getFiles() {
- return folderFiles.toArray(new TestDummyDomainFile[folderFiles.size()]);
- }
-
- @Override
- public synchronized DomainFolder getFolder(String path) {
- for (TestDummyDomainFolder folder : folders) {
- String folderPath = folder.getPathname();
- if (folderPath.equals(path)) {
- return folder;
- }
- }
-
- return null;
+ public boolean isInWritableProject() {
+ return true;
}
}
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 3751e477e8..9539770432 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
@@ -87,10 +87,8 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
showFiltered("tN");
JTree tree = getJTree();
- List expectedFilteredNames = names.stream()
- .filter(s -> s.startsWith("tN"))
- .sorted()
- .collect(Collectors.toList());
+ List expectedFilteredNames =
+ names.stream().filter(s -> s.startsWith("tN")).sorted().collect(Collectors.toList());
TreeModel model = tree.getModel();
GTreeNode root = (GTreeNode) model.getRoot();
@@ -424,8 +422,8 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
private void showFiltered(final String startsWith) {
Swing.runLater(() -> {
- dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog",
- OPEN, f -> f.getName().startsWith(startsWith));
+ dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog", OPEN,
+ f -> f.getName().startsWith(startsWith));
dialog.showComponent();
});
waitForSwing();
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginActionsTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginActionsTest.java
index 2c78427209..e161b9b1cd 100644
--- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginActionsTest.java
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginActionsTest.java
@@ -732,11 +732,22 @@ public class FrontEndPluginActionsTest extends AbstractGhidraHeadedIntegrationTe
performAction(selectAction, getTreeActionContext(), true);
waitForTree();
+ // NOTE: All nodes except the root node should be selected.
+ // Root is not selected to allow for most popup actions to
+ // be enabled and work as expected
+
BreadthFirstIterator it = new BreadthFirstIterator(rootNode);
+ int count = 0;
while (it.hasNext()) {
GTreeNode node = it.next();
- assertTrue(tree.isPathSelected(node.getTreePath()));
+ if (tree.isPathSelected(node.getTreePath())) {
+ ++count;
+ }
+ else {
+ assertTrue(node.isRoot());
+ }
}
+ assertEquals(7, count);
}
@Test
@@ -954,11 +965,12 @@ public class FrontEndPluginActionsTest extends AbstractGhidraHeadedIntegrationTe
for (TreePath path : paths) {
GTreeNode node = (GTreeNode) path.getLastPathComponent();
- if (node instanceof DomainFileNode) {
- fileList.add(((DomainFileNode) node).getDomainFile());
+ if (node instanceof DomainFileNode fileNode) {
+ // NOTE: File may be a linked-folder. Treatment as folder or file depends on action
+ fileList.add(fileNode.getDomainFile());
}
- else if (node instanceof DomainFolderNode) {
- folderList.add(((DomainFolderNode) node).getDomainFolder());
+ else if (node instanceof DomainFolderNode folderNode) {
+ folderList.add(folderNode.getDomainFolder());
}
}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/bundle/FullOatBundle.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/bundle/FullOatBundle.java
index 083618ae86..cb98f5c43d 100644
--- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/bundle/FullOatBundle.java
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/bundle/FullOatBundle.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,9 +15,8 @@
*/
package ghidra.file.formats.android.oat.bundle;
-import java.util.*;
-
import java.io.IOException;
+import java.util.*;
import org.apache.commons.io.FilenameUtils;
@@ -30,6 +29,7 @@ import ghidra.file.formats.android.oat.OatHeader;
import ghidra.file.formats.android.vdex.*;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
+import ghidra.program.database.ProgramContentHandler;
import ghidra.program.model.listing.Program;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@@ -45,8 +45,7 @@ public class FullOatBundle implements OatBundle {
private boolean isLittleEndian;
- FullOatBundle(Program oatProgram, OatHeader oatHeader, TaskMonitor monitor,
- MessageLog log) {
+ FullOatBundle(Program oatProgram, OatHeader oatHeader, TaskMonitor monitor, MessageLog log) {
this.oatProgram = oatProgram;
this.oatHeader = oatHeader;
@@ -120,12 +119,11 @@ public class FullOatBundle implements OatBundle {
DomainFolder parentFolder = domainFile.getParent();
//first, look in current project for VDEX file....
- if (lookInProjectFolder(HeaderType.VDEX, parentFolder,
- vdexProgramName, monitor, log)) {
+ if (lookInProjectFolder(HeaderType.VDEX, parentFolder, vdexProgramName, monitor, log)) {
return;
}
- if (lookInProjectFolder(HeaderType.VDEX, parentFolder.getParent(),
- vdexProgramName, monitor, log)) {
+ if (lookInProjectFolder(HeaderType.VDEX, parentFolder.getParent(), vdexProgramName, monitor,
+ log)) {
return;
}
}
@@ -140,8 +138,8 @@ public class FullOatBundle implements OatBundle {
break;
}
if (file.getName().startsWith(CLASSES) && file.getName().endsWith(DEX)) {
- lookInProjectFolder(HeaderType.DEX, odexApkFolder, file.getName(),
- monitor, log);
+ lookInProjectFolder(HeaderType.DEX, odexApkFolder, file.getName(), monitor,
+ log);
}
}
}
@@ -153,8 +151,8 @@ public class FullOatBundle implements OatBundle {
break;
}
if (file.getName().startsWith(CLASSES) && file.getName().endsWith(DEX)) {
- lookInProjectFolder(HeaderType.DEX, apkOrJarFolder, file.getName(),
- monitor, log);
+ lookInProjectFolder(HeaderType.DEX, apkOrJarFolder, file.getName(), monitor,
+ log);
}
}
}
@@ -166,8 +164,8 @@ public class FullOatBundle implements OatBundle {
break;
}
if (file.getName().startsWith(CDEX)) {
- lookInProjectFolder(HeaderType.CDEX, appVdexFolder, file.getName(),
- monitor, log);
+ lookInProjectFolder(HeaderType.CDEX, appVdexFolder, file.getName(), monitor,
+ log);
}
}
}
@@ -183,12 +181,11 @@ public class FullOatBundle implements OatBundle {
DomainFolder parentFolder = domainFile.getParent();
//first, look in current project for ART file....
- if (lookInProjectFolder(HeaderType.ART, parentFolder,
- artProgramName, monitor, log)) {
+ if (lookInProjectFolder(HeaderType.ART, parentFolder, artProgramName, monitor, log)) {
return;
}
- if (lookInProjectFolder(HeaderType.ART, parentFolder.getParent(),
- artProgramName, monitor, log)) {
+ if (lookInProjectFolder(HeaderType.ART, parentFolder.getParent(), artProgramName, monitor,
+ log)) {
return;
}
}
@@ -203,14 +200,15 @@ public class FullOatBundle implements OatBundle {
* @param log the message log
*/
private boolean lookInProjectFolder(HeaderType type, DomainFolder parentFolder,
- String programName,
- TaskMonitor monitor, MessageLog log) {
+ String programName, TaskMonitor monitor, MessageLog log) {
- DomainFile child = parentFolder.getFile(programName);
- if (child != null) {
+ DomainFile file = parentFolder.getFile(programName);
+ // Constrain to Program files only and not program link-files
+ if (file != null &&
+ ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(file.getContentType())) {
Program program = null;
try {
- program = (Program) child.getDomainObject(this, true, true, monitor);
+ program = (Program) file.getDomainObject(this, true, true, monitor);
ByteProvider provider =
MemoryByteProvider.createProgramHeaderByteProvider(program, false);
return makeHeader(type, programName, provider, monitor);
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/generic/iOS_KextStubFixupAnalyzer.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/generic/iOS_KextStubFixupAnalyzer.java
index fed8ac4753..b906ca92f6 100644
--- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/generic/iOS_KextStubFixupAnalyzer.java
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/generic/iOS_KextStubFixupAnalyzer.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,6 +22,7 @@ import ghidra.file.analyzers.FileFormatAnalyzer;
import ghidra.framework.main.AppInfo;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool;
+import ghidra.program.database.ProgramContentHandler;
import ghidra.program.model.address.*;
import ghidra.program.model.data.PointerDataType;
import ghidra.program.model.lang.Processor;
@@ -295,6 +296,7 @@ public class iOS_KextStubFixupAnalyzer extends FileFormatAnalyzer {
private DestinationProgramInfo recurseFolder(DomainFolder folder, Address destinationAddress,
ProgramManager programManager, TaskMonitor monitor) {
+ // NOTE: All folder-links and file-links are ignored
DomainFolder[] folders = folder.getFolders();
for (DomainFolder child : folders) {
if (monitor.isCancelled()) {
@@ -311,30 +313,31 @@ public class iOS_KextStubFixupAnalyzer extends FileFormatAnalyzer {
if (monitor.isCancelled()) {
break;
}
- DomainObject domainObject = null;
+ if (!file.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
+ continue;
+ }
+ Program program = null;
try {
- domainObject = file.getDomainObject(this, true /* upgrade */,
+ program = (Program) file.getDomainObject(this, true /* upgrade */,
false /* do not recover */, monitor);
- if (domainObject instanceof Program) {
- Program program = (Program) domainObject;
- if (program.getMemory().contains(destinationAddress)) {
- if (programManager != null) {
- programManager.openProgram(program, ProgramManager.OPEN_VISIBLE);//once program is located, open it, so lookup is faster next time!
- }
- SymbolTable symbolTable = program.getSymbolTable();
- Symbol symbol = symbolTable.getPrimarySymbol(destinationAddress);
- String symbolName = symbol == null ? null : symbol.getName();
- return new DestinationProgramInfo(program.getName(), file.getPathname(),
- symbolName);
+ if (program.getMemory().contains(destinationAddress)) {
+ if (programManager != null) {
+ //once program is located, open it, so lookup is faster next time!
+ programManager.openProgram(program, ProgramManager.OPEN_VISIBLE);
}
+ SymbolTable symbolTable = program.getSymbolTable();
+ Symbol symbol = symbolTable.getPrimarySymbol(destinationAddress);
+ String symbolName = symbol == null ? null : symbol.getName();
+ return new DestinationProgramInfo(program.getName(), file.getPathname(),
+ symbolName);
}
}
catch (Exception e) {
Msg.warn(this, e);
}
finally {
- if (domainObject != null) {
- domainObject.release(this);
+ if (program != null) {
+ program.release(this);
}
}
}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java
index 373e554732..b60c668974 100644
--- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java
@@ -118,7 +118,7 @@ public class GFileSystemLoadKernelTask extends Task {
ProjectIndexService projectIndex = ProjectIndexService.getIndexFor(project);
DomainFile existingDF = projectIndex.findFirstByFSRL(file.getFSRL());
- if ( existingDF != null && programManager != null ) {
+ if (existingDF != null && programManager != null) {
programManager.openProgram(existingDF);
return;
}
@@ -138,6 +138,9 @@ public class GFileSystemLoadKernelTask extends Task {
AppInfo.getActiveProject().getProjectData().getRootFolder(),
file.getParentFile().getPath());
String fileName = ProjectDataUtils.getUniqueName(folder, program.getName());
+ if (fileName == null) {
+ throw new IOException("Unable to find unique name for " + program.getName());
+ }
GhidraProgramUtilities.markProgramAnalyzed(program);
diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java
index 6e4461a77d..bbd374ff10 100644
--- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java
+++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -250,12 +250,12 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
}
@Override
- public void checkCompatibility(int serverInterfaceVersion) throws RemoteException {
- if (serverInterfaceVersion > INTERFACE_VERSION) {
+ public void checkCompatibility(int minServerInterfaceVersion) throws RemoteException {
+ if (minServerInterfaceVersion > INTERFACE_VERSION) {
throw new RemoteException(
"Incompatible server interface, a newer Ghidra Server version is required.");
}
- else if (serverInterfaceVersion < INTERFACE_VERSION) {
+ else if (minServerInterfaceVersion < MINIMUM_INTERFACE_VERSION) {
throw new RemoteException(
"Incompatible server interface, the minimum supported Ghidra version is " +
MIN_GHIDRA_VERSION);
diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java
index 8a81debdab..d868205915 100644
--- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java
+++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java
@@ -381,6 +381,22 @@ public class RepositoryHandleImpl extends UnicastRemoteObject
}
}
+ @Override
+ public void createTextDataFile(String parentPath, String itemName, String fileID,
+ String contentType, String textData, String comment)
+ throws InvalidNameException, IOException {
+ synchronized (syncObject) {
+ validate();
+ repository.validateWritePrivilege(currentUser);
+ RepositoryFolder folder = repository.getFolder(currentUser, parentPath, true);
+ if (folder == null) {
+ throw new IOException("Failed to create repository Folder " + parentPath);
+ }
+ folder.createTextDataFile(itemName, fileID, contentType, textData, comment,
+ currentUser);
+ }
+ }
+
@Override
public RemoteManagedBufferFileHandle createDatabase(String parentPath, String itemName,
String fileID, int bufferSize, String contentType, String projectPath)
diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java
index 6c399840c9..d0b9714bd9 100644
--- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java
+++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java
@@ -39,7 +39,7 @@ public class RepositoryFile {
private LocalFileSystem fileSystem;
private RepositoryFolder parent;
private String name;
- private LocalDatabaseItem databaseItem;
+ private LocalFolderItem folderItem;
private RepositoryItem repositoryItem;
private boolean deleted = false;
@@ -69,11 +69,10 @@ public class RepositoryFile {
if (deleted) {
throw new FileNotFoundException(getPathname() + " not found");
}
- if (databaseItem == null) {
+ if (folderItem == null) {
repositoryItem = null;
- LocalFolderItem folderItem = fileSystem.getItem(parent.getPathname(), name);
- if (folderItem == null || !folderItem.isVersioned() ||
- !(folderItem instanceof LocalDatabaseItem)) {
+ folderItem = fileSystem.getItem(parent.getPathname(), name);
+ if (folderItem == null) {
// must build pathname just in case folderItem does not exist
String pathname = parent.getPathname();
if (pathname.length() != 1) {
@@ -84,7 +83,6 @@ public class RepositoryFile {
"file is corrupt or unsupported", null);
throw new FileNotFoundException(pathname + " is corrupt or unsupported");
}
- this.databaseItem = (LocalDatabaseItem) folderItem;
}
}
}
@@ -127,16 +125,33 @@ public class RepositoryFile {
synchronized (fileSystem) {
try {
validate();
- if (repositoryItem == null) {
- repositoryItem =
- new RepositoryItem(parent.getPathname(), name, databaseItem.getFileID(),
- RepositoryItem.DATABASE, databaseItem.getContentType(),
- databaseItem.getCurrentVersion(), databaseItem.lastModified());
+ if (repositoryItem == null && folderItem != null) {
+ String textData = null;
+ int itemType = -1;
+ if (folderItem instanceof DatabaseItem) {
+ itemType = RepositoryItem.DATABASE;
+ }
+ else if (folderItem instanceof TextDataItem textItem) {
+ itemType = RepositoryItem.TEXT_DATA_FILE;
+ textData = textItem.getTextData();
+ }
+ else {
+ repository.log(getPathname(),
+ "Unsupported item type: " + folderItem.getClass().getSimpleName(),
+ null);
+ }
+
+ repositoryItem = new RepositoryItem(parent.getPathname(), name,
+ folderItem.getFileID(), itemType, folderItem.getContentType(),
+ folderItem.getCurrentVersion(), folderItem.lastModified(), textData);
}
}
catch (IOException e) {
+ repository.log(getPathname(), "Item failure: " + e.getMessage(), null);
+ }
+ if (repository == null) {
repositoryItem = new RepositoryItem(parent.getPathname(), name, null,
- RepositoryItem.DATABASE, "INVALID", 0, 0);
+ RepositoryItem.FILE, "INVALID", 0, 0, null);
}
return repositoryItem;
}
@@ -157,9 +172,14 @@ public class RepositoryFile {
synchronized (fileSystem) {
validate();
repository.validateReadPrivilege(user);
+ if (!(folderItem instanceof LocalDatabaseItem databaseItem)) {
+ throw new IOException(
+ "Unsupported operation for " + folderItem.getClass().getSimpleName());
+ }
LocalManagedBufferFile bf = databaseItem.open(version, minChangeDataVer);
- repository.log(getPathname(), "version " +
- (version < 0 ? databaseItem.getCurrentVersion() : version) + " opened read-only",
+ repository.log(
+ getPathname(), "version " +
+ (version < 0 ? folderItem.getCurrentVersion() : version) + " opened read-only",
user);
return bf;
}
@@ -177,7 +197,11 @@ public class RepositoryFile {
synchronized (fileSystem) {
validate();
repository.validateWritePrivilege(user);
- ItemCheckoutStatus coStatus = databaseItem.getCheckout(checkoutId);
+ if (!(folderItem instanceof LocalDatabaseItem databaseItem)) {
+ throw new IOException(
+ "Unsupported operation for " + folderItem.getClass().getSimpleName());
+ }
+ ItemCheckoutStatus coStatus = folderItem.getCheckout(checkoutId);
if (coStatus == null) {
throw new IOException("Illegal checkin");
}
@@ -202,7 +226,7 @@ public class RepositoryFile {
synchronized (fileSystem) {
validate();
repository.validateReadPrivilege(user);
- return databaseItem.getVersions();
+ return folderItem.getVersions();
}
}
@@ -216,7 +240,7 @@ public class RepositoryFile {
public long length() throws IOException {
synchronized (fileSystem) {
validate();
- return databaseItem.length();
+ return folderItem.length();
}
}
@@ -234,7 +258,7 @@ public class RepositoryFile {
User userObj = repository.validateWritePrivilege(user);
if (!userObj.isAdmin()) {
- Version[] versions = databaseItem.getVersions();
+ Version[] versions = folderItem.getVersions();
if (deleteVersion == -1) {
for (Version version : versions) {
if (!user.equals(version.getUser())) {
@@ -259,21 +283,13 @@ public class RepositoryFile {
throw new IOException("Only the oldest or latest version may be deleted");
}
}
- String oldPath = getPathname();
- if (databaseItem == null) {
- // forced removal by repo Admin
- }
- else {
- databaseItem.delete(deleteVersion, user);
+ if (folderItem != null) {
+ folderItem.delete(deleteVersion, user);
}
deleted = true;
repositoryItem = null;
parent.fileDeleted(this);
- RepositoryFile newRf = parent.getFile(name);
- if (newRf == null) {
- RepositoryManager.log(repository.getName(), oldPath, "file deleted", user);
- }
parent = null;
}
}
@@ -320,7 +336,7 @@ public class RepositoryFile {
synchronized (fileSystem) {
validate();
repository.validateWritePrivilege(user); // don't allow checkout if read-only
- ItemCheckoutStatus coStatus = databaseItem.checkout(checkoutType, user, projectPath);
+ ItemCheckoutStatus coStatus = folderItem.checkout(checkoutType, user, projectPath);
if (coStatus != null && checkoutType != CheckoutType.NORMAL && repositoryItem != null &&
repositoryItem.getFileID() == null) {
repositoryItem = null; // force refresh since fileID should get reset
@@ -340,7 +356,7 @@ public class RepositoryFile {
throws IOException {
synchronized (fileSystem) {
validate();
- databaseItem.updateCheckoutVersion(checkoutId, checkoutVersion, user);
+ folderItem.updateCheckoutVersion(checkoutId, checkoutVersion, user);
}
}
@@ -354,14 +370,14 @@ public class RepositoryFile {
public void terminateCheckout(long checkoutId, String user, boolean notify) throws IOException {
synchronized (fileSystem) {
validate();
- ItemCheckoutStatus coStatus = databaseItem.getCheckout(checkoutId);
+ ItemCheckoutStatus coStatus = folderItem.getCheckout(checkoutId);
if (coStatus != null) {
User userObj = repository.getUser(user);
if (!userObj.isAdmin() && !coStatus.getUser().equals(user)) {
throw new IOException(
"Undo-checkout not permitted, checkout was made by " + coStatus.getUser());
}
- databaseItem.terminateCheckout(checkoutId, notify);
+ folderItem.terminateCheckout(checkoutId, notify);
}
}
}
@@ -378,7 +394,7 @@ public class RepositoryFile {
synchronized (fileSystem) {
validate();
repository.validateReadPrivilege(user);
- return databaseItem.getCheckout(checkoutId);
+ return folderItem.getCheckout(checkoutId);
}
}
@@ -393,7 +409,7 @@ public class RepositoryFile {
synchronized (fileSystem) {
validate();
repository.validateReadPrivilege(user);
- return databaseItem.getCheckouts();
+ return folderItem.getCheckouts();
}
}
@@ -405,7 +421,7 @@ public class RepositoryFile {
public boolean hasCheckouts() throws IOException {
synchronized (fileSystem) {
validate();
- return databaseItem.hasCheckouts();
+ return folderItem.hasCheckouts();
}
}
@@ -417,7 +433,7 @@ public class RepositoryFile {
public boolean isCheckinActive() throws IOException {
synchronized (fileSystem) {
validate();
- return databaseItem.isCheckinActive();
+ return folderItem.isCheckinActive();
}
}
@@ -436,7 +452,7 @@ public class RepositoryFile {
void pathChanged() {
synchronized (fileSystem) {
repositoryItem = null;
- databaseItem = null;
+ folderItem = null;
}
}
diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java
index 5af3ff774e..01465e7121 100644
--- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java
+++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java
@@ -24,8 +24,7 @@ import org.apache.logging.log4j.Logger;
import db.buffers.LocalManagedBufferFile;
import ghidra.framework.store.*;
-import ghidra.framework.store.local.LocalFileSystem;
-import ghidra.framework.store.local.LocalFolderItem;
+import ghidra.framework.store.local.*;
import ghidra.server.Repository;
import ghidra.server.RepositoryManager;
import ghidra.util.InvalidNameException;
@@ -94,20 +93,20 @@ public class RepositoryFolder {
private void init() throws IOException {
String path = getPathname();
String[] names = fileSystem.getFolderNames(path);
- for (String name2 : names) {
- RepositoryFolder subfolder = new RepositoryFolder(repository, fileSystem, this, name2);
- folderMap.put(name2, subfolder);
+ for (String folderName : names) {
+ RepositoryFolder subfolder =
+ new RepositoryFolder(repository, fileSystem, this, folderName);
+ folderMap.put(folderName, subfolder);
}
names = fileSystem.getItemNames(path);
int badItemCount = 0;
- for (String name2 : names) {
- LocalFolderItem item = fileSystem.getItem(path, name2);
- if (item == null || !(item instanceof DatabaseItem)) {
+ for (String itemName : names) {
+ LocalFolderItem item = fileSystem.getItem(path, itemName);
+ if (item == null || (item instanceof UnknownFolderItem)) {
++badItemCount;
- continue;
}
- RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, name2);
- fileMap.put(name2, rf);
+ RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, itemName);
+ fileMap.put(itemName, rf);
}
if (badItemCount != 0) {
log.error("Repository '" + repository.getName() + "' contains " + badItemCount +
@@ -217,7 +216,7 @@ public class RepositoryFolder {
if (fileSystem.fileExists(getPathname(), fileName)) {
try {
LocalFolderItem item = fileSystem.getItem(getPathname(), fileName);
- if (item == null || !(item instanceof DatabaseItem)) {
+ if (item == null) {
log.error("Repository '" + repository.getName() + "' contains bad item: " +
makePathname(getPathname(), fileName));
return null;
@@ -262,6 +261,41 @@ public class RepositoryFolder {
}
}
+ /**
+ * Creates a new text data file within the specified parent folder.
+ * @param itemName new data file name
+ * @param fileID file ID to be associated with new file or null
+ * @param contentType application defined content type
+ * @param textData text data (required)
+ * @param comment file comment (may be null)
+ * @param user user who is initiating request
+ * @throws DuplicateFileException Thrown if a folderItem with that name already exists.
+ * @throws InvalidNameException if the name has illegal characters.
+ * @throws IOException if an IO error occurs.
+ */
+ public void createTextDataFile(String itemName, String fileID, String contentType,
+ String textData, String comment, String user) throws InvalidNameException, IOException {
+ synchronized (fileSystem) {
+ repository.validate();
+ repository.validateWritePrivilege(user);
+ if (getFile(itemName) != null) {
+ throw new DuplicateFileException(itemName + " already exists");
+ }
+
+ LocalTextDataItem textDataItem = fileSystem.createTextDataItem(getPathname(), itemName,
+ fileID, contentType, textData, null); // comment conveyed with Version info below
+
+ Version singleVersion = new Version(1, System.currentTimeMillis(), user, comment);
+ textDataItem.setVersionInfo(singleVersion);
+
+ RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, itemName);
+ fileMap.put(itemName, rf);
+
+ RepositoryManager.log(repository.getName(), makePathname(getPathname(), itemName),
+ "file created", user);
+ }
+ }
+
/**
* Create a new database file/item within this folder.
* @param itemName name of new database
@@ -445,4 +479,5 @@ public class RepositoryFolder {
: parentPath;
return path + FileSystem.SEPARATOR + childName;
}
+
}
diff --git a/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java b/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java
index e53d1162a0..681c97ad4e 100644
--- a/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java
+++ b/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -87,6 +87,7 @@
//
//@category Version Tracking
import ghidra.app.script.GhidraScript;
+import ghidra.feature.vt.api.db.VTSessionContentHandler;
import ghidra.feature.vt.api.db.VTSessionDB;
import ghidra.feature.vt.api.main.VTSession;
import ghidra.feature.vt.api.util.VTOptions;
@@ -107,8 +108,8 @@ public class AutoVersionTrackingScript extends GhidraScript {
@Override
public void run() throws Exception {
-
- if(currentProgram == null) {
+
+ if (currentProgram == null) {
println("Please open the destination program.");
return;
}
@@ -175,8 +176,8 @@ public class AutoVersionTrackingScript extends GhidraScript {
return;
}
- Program sourceProgram = (Program) sourceProgramDF.getDomainObject(this, autoUpgradeIfNeeded,
- false, monitor);
+ Program sourceProgram =
+ (Program) sourceProgramDF.getDomainObject(this, autoUpgradeIfNeeded, false, monitor);
VTSession session = null;
try {
@@ -258,19 +259,8 @@ public class AutoVersionTrackingScript extends GhidraScript {
* @throws CancelledException if cancelled
*/
private boolean hasExistingSession(String name, DomainFolder folder) throws CancelledException {
-
- DomainFile[] files = folder.getFiles();
-
- for (DomainFile file : files) {
- monitor.checkCancelled();
-
- if (file.getName().equals(name)) {
- if (file.getContentType().equals("VersionTracking")) {
- return true;
- }
- }
- }
- return false;
+ DomainFile file = folder.getFile(name);
+ return file != null && file.getContentType().equals(VTSessionContentHandler.CONTENT_TYPE);
}
/**
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 23af40fd72..edf1778e71 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
@@ -333,7 +333,7 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession {
}
catch (VersionException e) {
VersionExceptionHandler.showVersionError(null, domainFile.getName(), type, "open",
- e);
+ false, e);
}
catch (IOException e) {
Msg.showError(this, null, "Can't open " + type + ": " + domainFile.getName(),
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 57bdb334b8..5109e99cb7 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -25,8 +25,8 @@ import ghidra.feature.vt.api.main.VTSession;
import ghidra.feature.vt.gui.plugin.VTController;
import ghidra.feature.vt.gui.plugin.VTPlugin;
import ghidra.framework.main.DataTreeDialog;
+import ghidra.framework.model.DefaultDomainFileFilter;
import ghidra.framework.model.DomainFile;
-import ghidra.framework.model.DomainFileFilter;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HelpLocation;
@@ -48,7 +48,7 @@ public class OpenVersionTrackingSessionAction extends DockingAction {
PluginTool tool = controller.getTool();
DataTreeDialog dialog =
new DataTreeDialog(tool.getToolFrame(), "Open Version Tracking Session", OPEN,
- new VTDomainFileFilter());
+ new DefaultDomainFileFilter(VTSession.class, true));
tool.showDialog(dialog);
if (!dialog.wasCancelled()) {
@@ -57,16 +57,4 @@ public class OpenVersionTrackingSessionAction extends DockingAction {
}
}
- class VTDomainFileFilter implements DomainFileFilter {
- @Override
- public boolean accept(DomainFile f) {
- 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/plugin/VTControllerImpl.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java
index 2473ee06f8..c13f1d8f61 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -217,7 +217,7 @@ public class VTControllerImpl
}
catch (VersionException e) {
VersionExceptionHandler.showVersionError(null, domainFile.getName(), "VT Session",
- "open", e);
+ "open", false, e);
}
catch (IOException e) {
Msg.showError(this, null, "Can't open VT Session: " + domainFile.getName(),
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/SessionConfigurationPanel.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/SessionConfigurationPanel.java
index a03f71b445..8e430dbbf2 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/SessionConfigurationPanel.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/SessionConfigurationPanel.java
@@ -31,8 +31,7 @@ import generic.theme.GIcon;
import generic.theme.GThemeDefaults.Ids.Fonts;
import generic.theme.Gui;
import ghidra.framework.main.DataTreeDialog;
-import ghidra.framework.model.DomainFile;
-import ghidra.framework.model.DomainFolder;
+import ghidra.framework.model.*;
import ghidra.util.StringUtilities;
import utility.function.Callback;
@@ -230,8 +229,8 @@ public class SessionConfigurationPanel extends JPanel {
JButton button = new BrowseButton();
button.setName("SOURCE_BUTTON");
button.addActionListener(e -> {
- DomainFile programFile = VTWizardUtils.chooseDomainFile(SessionConfigurationPanel.this,
- "a source program", VTWizardUtils.PROGRAM_FILTER, null);
+ DomainFile programFile = VTWizardUtils.chooseProgramFile(SessionConfigurationPanel.this,
+ "a source program", null);
if (programFile != null) {
setSourceFile(programFile);
statusChangedCallback.call();
@@ -244,8 +243,8 @@ public class SessionConfigurationPanel extends JPanel {
JButton button = new BrowseButton();
button.setName("DESTINATION_BUTTON");
button.addActionListener(e -> {
- DomainFile programFile = VTWizardUtils.chooseDomainFile(SessionConfigurationPanel.this,
- "a destination program", VTWizardUtils.PROGRAM_FILTER, null);
+ DomainFile programFile = VTWizardUtils.chooseProgramFile(SessionConfigurationPanel.this,
+ "a destination program", null);
if (programFile != null) {
setDestinationFile(programFile);
statusChangedCallback.call();
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/VTWizardUtils.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/VTWizardUtils.java
index bd12ffbda2..ad4f58c195 100644
--- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/VTWizardUtils.java
+++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/VTWizardUtils.java
@@ -25,8 +25,7 @@ import docking.widgets.OptionDialog;
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.framework.model.*;
import ghidra.program.model.listing.Program;
import ghidra.util.HTMLUtilities;
import ghidra.util.task.TaskLauncher;
@@ -37,29 +36,13 @@ public class VTWizardUtils {
DomainFile df;
}
- public static final DomainFileFilter VT_SESSION_FILTER = new DomainFileFilter() {
+ public static final DomainFileFilter VT_SESSION_FILTER =
+ new DefaultDomainFileFilter(VTSession.class, true);
- @Override
- public boolean accept(DomainFile df) {
- return VTSession.class.isAssignableFrom(df.getDomainObjectClass());
- }
-
- @Override
- public boolean followLinkedFolders() {
- return false;
- }
- };
-
- public static final DomainFileFilter PROGRAM_FILTER = f -> {
- return Program.class.isAssignableFrom(f.getDomainObjectClass());
- };
-
- public static DomainFile chooseDomainFile(Component parent, String domainIdentifier,
- DomainFileFilter filter, DomainFile fileToSelect) {
- final DataTreeDialog dataTreeDialog = filter == null
- ? new DataTreeDialog(parent, "Choose " + domainIdentifier, OPEN)
- : new DataTreeDialog(parent, "Choose " + domainIdentifier, OPEN,
- filter);
+ public static DomainFile chooseProgramFile(Component parent, String domainIdentifier,
+ DomainFile fileToSelect) {
+ final DataTreeDialog dataTreeDialog = new DataTreeDialog(parent,
+ "Choose " + domainIdentifier, OPEN, new DefaultDomainFileFilter(Program.class, true));
final DomainFileBox box = new DomainFileBox();
dataTreeDialog.addOkActionListener(new ActionListener() {
@Override
diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java
index 0e58b47074..337de9be54 100644
--- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java
+++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -34,7 +34,7 @@ import ghidra.program.model.mem.Memory;
import ghidra.program.model.mem.StubMemory;
import ghidra.program.model.symbol.*;
-public class VTBaseTestCase extends AbstractGenericTest {
+public abstract class VTBaseTestCase extends AbstractGenericTest {
private DomainFile sourceDomainFile = new TestDummyDomainFile(null, "SourceDomainFile") {
@Override
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 3260963776..93112a060f 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
@@ -448,7 +448,14 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable
+ * NOTE: Iterator will not include children of a node where {@link GTreeNode#isAutoExpandPermitted()}
+ * returns false.
*/
public class BreadthFirstIterator implements Iterator {
private Queue nodeQueue = new LinkedList();
@@ -39,7 +42,7 @@ public class BreadthFirstIterator implements Iterator {
@Override
public GTreeNode next() {
lastNode = nodeQueue.poll();
- if (lastNode != null) {
+ if (lastNode != null && lastNode.isAutoExpandPermitted()) {
List children = lastNode.getChildren();
nodeQueue.addAll(children);
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/DepthFirstIterator.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/DepthFirstIterator.java
index a23f13ae29..4aa992beda 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/DepthFirstIterator.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/DepthFirstIterator.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -25,6 +25,9 @@ import docking.widgets.tree.GTreeNode;
/**
* Implements an iterator over all GTreeNodes in some gTree (or subtree). The nodes are
* return in depth first order.
+ *
+ * NOTE: Iterator will not include children of a node where {@link GTreeNode#isAutoExpandPermitted()}
+ * returns false.
*/
public class DepthFirstIterator implements Iterator {
private Stack> stack = new Stack<>();
@@ -49,7 +52,7 @@ public class DepthFirstIterator implements Iterator {
it = stack.pop();
}
lastNode = it.next();
- if (lastNode.getChildCount() > 0) {
+ if (lastNode.isAutoExpandPermitted() && lastNode.getChildCount() > 0) {
if (it.hasNext()) {
stack.push(it);
}
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 819f819860..711bee3bb1 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
@@ -81,6 +81,7 @@ public class RepositoryAdapter implements RemoteAdapterListener {
/**
* Returns true if connection recently was lost unexpectedly
+ * @return true if connection recently was lost unexpectedly
*/
public boolean hadUnexpectedDisconnect() {
return unexpectedDisconnect;
@@ -151,6 +152,7 @@ public class RepositoryAdapter implements RemoteAdapterListener {
/**
* Returns true if connected.
+ * @return true if connected.
*/
public boolean isConnected() {
return repository != null;
@@ -177,7 +179,7 @@ public class RepositoryAdapter implements RemoteAdapterListener {
if (repository == null) {
serverAdapter.connect(); // may cause auto-reconnect of repository
}
- if (repository == null) {
+ if (repository == null && serverAdapter.isConnected()) {
repository = serverAdapter.getRepositoryHandle(name);
unexpectedDisconnect = false;
if (repository == null) {
@@ -193,8 +195,7 @@ public class RepositoryAdapter implements RemoteAdapterListener {
/**
* Event reader for change dispatcher.
- * @return
- * @throws IOException
+ * @return events
* @throws InterruptedIOException if repository handle is closed
*/
RepositoryChangeEvent[] getEvents() throws InterruptedIOException {
@@ -227,21 +228,24 @@ public class RepositoryAdapter implements RemoteAdapterListener {
}
/**
- * Returns repository name
+ * Get the associated repository name
+ * @return repository name
*/
public String getName() {
return name;
}
/**
- * Returns server adapter
+ * Get the associated server adapter
+ * @return server adapter
*/
public RepositoryServerAdapter getServer() {
return serverAdapter;
}
/**
- * Returns server information
+ * Returns associated server information
+ * @return server information
*/
public ServerInfo getServerInfo() {
return serverAdapter.getServerInfo();
@@ -280,9 +284,11 @@ public class RepositoryAdapter implements RemoteAdapterListener {
}
/**
- * Returns repository user object.
+ * Returns repository connected user object.
+ * @return connected user object
* @throws UserAccessException user no longer has any permission to use repository.
* @throws NotConnectedException if server/repository connection is down (user already informed)
+ * @throws IOException if an IO error occurs
* @see ghidra.framework.remote.RemoteRepositoryHandle#getUser()
*/
public User getUser() throws IOException {
@@ -305,7 +311,7 @@ public class RepositoryAdapter implements RemoteAdapterListener {
/**
* @return true if anonymous access allowed by this repository
- * @throws IOException
+ * @throws IOException if an IO error occurs
*/
public boolean anonymousAccessAllowed() throws IOException {
synchronized (serverAdapter) {
@@ -323,10 +329,11 @@ public class RepositoryAdapter implements RemoteAdapterListener {
}
/**
- * Returns list of repository users.
- * @throws IOException
+ * Returns list of repository users with repository access permission
+ * @return return users with repository access permission
* @throws UserAccessException user no longer has any permission to use repository.
* @throws NotConnectedException if server/repository connection is down (user already informed)
+ * @throws IOException if an IO error occurs
* @see RemoteRepositoryHandle#getUserList()
*/
public User[] getUserList() throws IOException {
@@ -345,10 +352,11 @@ public class RepositoryAdapter implements RemoteAdapterListener {
}
/**
- * Returns list of all users known to server.
- * @throws IOException
+ * Returns list of all user names known to server.
+ * @return list of all user names known to server.
* @throws UserAccessException user no longer has any permission to use repository.
* @throws NotConnectedException if server/repository connection is down (user already informed)
+ * @throws IOException if an IO error occurs
* @see RemoteRepositoryHandle#getServerUserList()
*/
public String[] getServerUserList() throws IOException {
@@ -371,8 +379,8 @@ public class RepositoryAdapter implements RemoteAdapterListener {
* @param users list of user and access permissions.
* @param anonymousAccessAllowed true to permit anonymous access (also requires anonymous
* access to be enabled for server)
- * @throws UserAccessException
- * @throws IOException
+ * @throws UserAccessException user is not a repository Admin
+ * @throws IOException if an IO error occurs
* @throws NotConnectedException if server/repository connection is down (user already informed)
* @see RemoteRepositoryHandle#setUserList(User[], boolean)
*/
@@ -392,7 +400,36 @@ public class RepositoryAdapter implements RemoteAdapterListener {
}
}
- /**
+ /*
+ * @see RepositoryHandle#createTextDataFile(String, String, String, String, String, String)
+ */
+ public void createTextDataFile(String parentPath, String itemName, String fileID,
+ String contentType, String textData, String comment)
+ throws IOException, InvalidNameException {
+ synchronized (serverAdapter) {
+ checkRepository();
+ try {
+ repository.createTextDataFile(parentPath, itemName, fileID, contentType, textData,
+ comment);
+ }
+ catch (NotConnectedException | RemoteException e) {
+ checkUnmarshalException(e, "createTextDataFile");
+ if (recoverConnection(e)) {
+ try {
+ repository.createTextDataFile(parentPath, itemName, fileID, contentType,
+ textData, comment);
+ }
+ catch (RemoteException e1) {
+ checkUnmarshalException(e1, "createTextDataFile");
+ throw e1;
+ }
+ }
+ throw e;
+ }
+ }
+ }
+
+ /*
* @see RepositoryHandle#createDatabase(String, String, String, int, String, String)
*/
public ManagedBufferFileAdapter createDatabase(String parentPath, String itemName,
@@ -530,8 +567,8 @@ public class RepositoryAdapter implements RemoteAdapterListener {
/**
* Convert UnmarshalException into UnsupportedOperationException
- * @param e
- * @throws UnsupportedOperationException
+ * @param e IOException to be converted if appropriate
+ * @throws UnsupportedOperationException unsupported operation exception
*/
private void checkUnmarshalException(IOException e, String operation)
throws UnsupportedOperationException {
@@ -931,5 +968,4 @@ public class RepositoryAdapter implements RemoteAdapterListener {
public int getOpenFileHandleCount() {
return openFileHandleCount;
}
-
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java
index c14e328eb8..b367cb3028 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java
@@ -168,13 +168,14 @@ class ServerConnectTask extends Task {
monitor.setCancelEnabled(false);
monitor.setMessage("Connecting...");
- Registry reg =
- LocateRegistry.getRegistry(server.getServerName(), server.getPortNumber(),
- new SslRMIClientSocketFactory());
+ Registry reg = LocateRegistry.getRegistry(server.getServerName(),
+ server.getPortNumber(), new SslRMIClientSocketFactory());
checkServerBindNames(reg);
gsh = (GhidraServerHandle) reg.lookup(GhidraServerHandle.BIND_NAME);
- gsh.checkCompatibility(GhidraServerHandle.INTERFACE_VERSION);
+
+ // Check interface compatibility with the minimum supported version
+ gsh.checkCompatibility(GhidraServerHandle.MINIMUM_INTERFACE_VERSION);
}
catch (NotBoundException e) {
throw new IOException(e.getMessage());
@@ -237,8 +238,7 @@ class ServerConnectTask extends Task {
* @throws LoginException login failure
*/
private RemoteRepositoryServerHandle getRepositoryServerHandle(String defaultUserID,
- TaskMonitor monitor)
- throws IOException, LoginException, CancelledException {
+ TaskMonitor monitor) throws IOException, LoginException, CancelledException {
GhidraServerHandle gsh = getGhidraServerHandle(server, monitor);
@@ -296,7 +296,8 @@ class ServerConnectTask extends Task {
"Client PKI certificate has not been installed");
}
- if (ApplicationKeyManagerFactory.usingGeneratedSelfSignedCertificate()) {
+ if (ApplicationKeyManagerFactory
+ .usingGeneratedSelfSignedCertificate()) {
Msg.warn(this,
"Server connect - client is using self-signed PKI certificate");
}
@@ -394,7 +395,7 @@ class ServerConnectTask extends Task {
monitor.setCancelEnabled(true);
monitor.setMessage("Checking Server Liveness...");
-
+
// Perform simple socket test connection with short timeout to verify connectivity.
try (Socket socket = new FastConnectionFailSocket(serverName, sslRmiPort);
ConnectCancelledListener cancelListener =
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java
index 13277e4885..6fcc450366 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -50,8 +50,20 @@ public interface GhidraServerHandle extends Remote {
* - version 9.1 switched to using SSL/TLS for RMI registry connection preventing
* older clients the ability to connect to the server. Remote interface remained
* unchanged allowing 9.1 clients to connect to 9.0 server.
+ * 12: Revised RepositoryFile serialization to facilitate support for text-data used
+ * for link-file storage.
*/
- public static final int INTERFACE_VERSION = 11;
+
+ /**
+ * The server interface version that the server will use and is the maximum version that the
+ * client can operate with.
+ */
+ public static final int INTERFACE_VERSION = 12;
+
+ /**
+ * The minimum server interface version that the client can operate with.
+ */
+ public static final int MINIMUM_INTERFACE_VERSION = 11;
/**
* Minimum version of Ghidra which utilized the current INTERFACE_VERSION
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java
index 661f64513d..6a291821f5 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -70,6 +70,10 @@ public interface RemoteRepositoryHandle extends RepositoryHandle, Remote {
int bufferSize, String contentType, String projectPath)
throws IOException, InvalidNameException;
+ @Override
+ void createTextDataFile(String parentPath, String itemName, String fileID, String contentType,
+ String textData, String comment) throws InvalidNameException, IOException;
+
@Override
ManagedBufferFileHandle openDatabase(String parentPath, String itemName, int version,
int minChangeDataVer) throws IOException;
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 f317923ae2..2240b2c8b6 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -123,6 +123,21 @@ public interface RepositoryHandle {
*/
RepositoryItem getItem(String fileID) throws IOException;
+ /**
+ * Creates a new text data file within the specified parent folder.
+ * @param parentPath folder path of parent
+ * @param itemName new data file name
+ * @param fileID unique file ID
+ * @param contentType application defined content type
+ * @param textData text data (required)
+ * @param comment file comment (may be null)
+ * @throws DuplicateFileException Thrown if a folderItem with that name already exists.
+ * @throws InvalidNameException if the name has illegal characters.
+ * @throws IOException if an IO error occurs.
+ */
+ void createTextDataFile(String parentPath, String itemName, String fileID, String contentType,
+ String textData, String comment) throws InvalidNameException, IOException;
+
/**
* Create a new empty database item within the repository.
* @param parentPath parent folder path
@@ -138,8 +153,8 @@ public interface RepositoryHandle {
* @throws InvalidNameException if itemName or parentPath contains invalid characters
*/
ManagedBufferFileHandle createDatabase(String parentPath, String itemName, String fileID,
- int bufferSize, String contentType, String projectPath) throws IOException,
- InvalidNameException;
+ int bufferSize, String contentType, String projectPath)
+ throws IOException, InvalidNameException;
/**
* Open an existing version of a database buffer file for non-update read-only use.
@@ -212,8 +227,8 @@ public interface RepositoryHandle {
* @throws DuplicateFileException if target item already exists
* @throws IOException if an IO error occurs
*/
- void moveItem(String oldParentPath, String newParentPath, String oldItemName, String newItemName)
- throws InvalidNameException, IOException;
+ void moveItem(String oldParentPath, String newParentPath, String oldItemName,
+ String newItemName) throws InvalidNameException, IOException;
/**
* Perform a checkout on the specified item.
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryItem.java
index 830a8910b1..f62055f765 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryItem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryItem.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,9 +15,12 @@
*/
package ghidra.framework.remote;
-import ghidra.framework.store.FileSystem;
-
import java.io.IOException;
+import java.io.InvalidClassException;
+
+import org.apache.commons.lang3.StringUtils;
+
+import ghidra.framework.store.FileSystem;
/**
* RepositoryItemStatus provides status information for a
@@ -25,18 +28,36 @@ import java.io.IOException;
*/
public class RepositoryItem implements java.io.Serializable {
+ // Serial version 2 supports an expandable schema which allows a newer repository server
+ // to remain usable by older clients, and a newer client to deserialize data from an older
+ // server. The optional schema version if present can be used to identify the additional
+ // serialized data which may following the schema version number.
+
public final static long serialVersionUID = 2L;
- public final static int FILE = 1;
- public final static int DATABASE = 2;
+ private static final byte SERIALIZATION_SCHEMA_VERSION = 1;
- protected String folderPath;
- protected String itemName;
- protected String fileID;
- protected int itemType;
- protected String contentType;
- protected int version;
- protected long versionTime;
+ public final static int FILE = 1; // DataFileItem (not yet supported)
+ public final static int DATABASE = 2; // DatabaseItem
+ public final static int TEXT_DATA_FILE = 3; // TextDataItem
+
+ //
+ // Client use can support reading from older server which presents serialVersionUID==2
+ //
+
+ private String folderPath;
+ private String itemName;
+ private String fileID;
+ private int itemType;
+ private String contentType;
+ private int version;
+ private long versionTime;
+
+ // Variables below were added after serialVersionUID == 2 was established and rely on
+ // additional serialization version byte to identify the optional data fields added
+ // after original serialVersionUID == 2 fields.
+
+ private String textData; // applies to TEXT_DATA_FILE introduced with GhidraServerHandle v12
/**
* Default constructor needed for de-serialization
@@ -53,9 +74,10 @@ public class RepositoryItem implements java.io.Serializable {
* @param contentType content type associated with item
* @param version repository item version or -1 if versioning not supported
* @param versionTime version creation time
+ * @param textData related text data (may be null)
*/
public RepositoryItem(String folderPath, String itemName, String fileID, int itemType,
- String contentType, int version, long versionTime) {
+ String contentType, int version, long versionTime, String textData) {
this.folderPath = folderPath;
this.itemName = itemName;
this.fileID = fileID;
@@ -63,6 +85,7 @@ public class RepositoryItem implements java.io.Serializable {
this.contentType = contentType;
this.version = version;
this.versionTime = versionTime;
+ this.textData = textData;
}
/**
@@ -71,6 +94,7 @@ public class RepositoryItem implements java.io.Serializable {
* @throws IOException if an IO error occurs
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+
out.writeLong(serialVersionUID);
out.writeUTF(folderPath);
out.writeUTF(itemName);
@@ -79,6 +103,12 @@ public class RepositoryItem implements java.io.Serializable {
out.writeUTF(contentType != null ? contentType : "");
out.writeInt(version);
out.writeLong(versionTime);
+
+ // Variables below were added after serialVersionUID == 2 was established
+
+ out.writeByte(SERIALIZATION_SCHEMA_VERSION);
+ out.writeUTF(textData != null ? textData : "");
+
}
/**
@@ -87,11 +117,11 @@ public class RepositoryItem implements java.io.Serializable {
* @throws IOException if IO error occurs
* @throws ClassNotFoundException if unrecognized serialVersionUID detected
*/
- private void readObject(java.io.ObjectInputStream in) throws IOException,
- ClassNotFoundException {
+ private void readObject(java.io.ObjectInputStream in)
+ throws IOException, ClassNotFoundException {
long serialVersion = in.readLong();
if (serialVersion != serialVersionUID) {
- throw new ClassNotFoundException("Unsupported version of RepositoryItemStatus");
+ throw new ClassNotFoundException("Unsupported version of RepositoryItem");
}
folderPath = in.readUTF();
itemName = in.readUTF();
@@ -106,6 +136,31 @@ public class RepositoryItem implements java.io.Serializable {
}
version = in.readInt();
versionTime = in.readLong();
+
+ // Variable handling below was added after serialVersionUID == 2 was established
+
+ int available = in.available();
+ if (available == 0) {
+ // assume original schema before serializationSchemaVersion was employed
+ return;
+ }
+
+ // Since we do not serialize class implementations with RMI the older client must be able to
+ // read the initial data sequence that was previously supported. Newer clients that have this
+ // class will use the presence of the version byte to handle communicating with either an
+ // older server (no version byte) or a newer server (version byte and subsequent data is read)
+ byte serializationSchemaVersion = in.readByte();
+ if (serializationSchemaVersion < 1 ||
+ serializationSchemaVersion > SERIALIZATION_SCHEMA_VERSION) {
+ throw new InvalidClassException("RepositoryItem",
+ "RepositoryItem has incompatible serialization schema version: " +
+ serializationSchemaVersion);
+ }
+
+ textData = in.readUTF();
+ if (StringUtils.isBlank(textData)) {
+ textData = null;
+ }
}
/**
@@ -162,4 +217,11 @@ public class RepositoryItem implements java.io.Serializable {
return versionTime;
}
+ /**
+ * Get related text data
+ * @return text data or null
+ */
+ public String getTextData() {
+ return textData;
+ }
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/DataFileItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/DataFileItem.java
index 96346548a3..54c8d3f6f6 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/DataFileItem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/DataFileItem.java
@@ -1,13 +1,12 @@
/* ###
* IP: GHIDRA
- * REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -24,19 +23,19 @@ import java.io.OutputStream;
* DataFileItem corresponds to a private serialized
* data file within a FileSystem. Methods are provided for opening
* the underlying file as an input or output stream.
- *
+ *
* NOTE: The use of DataFile is not encouraged and is not fully
* supported.
*/
public interface DataFileItem extends FolderItem {
-
+
/**
* Open the current version of this item for reading.
* @return input stream
* @throws FileNotFoundException
*/
InputStream getInputStream() throws FileNotFoundException;
-
+
/**
* Open a new version of this item for writing.
* @return output stream.
@@ -50,5 +49,5 @@ public interface DataFileItem extends FolderItem {
* @throws FileNotFoundException
*/
InputStream getInputStream(int version) throws FileNotFoundException;
-
+
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystem.java
index 6c67fc96c2..5345dae893 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystem.java
@@ -16,10 +16,13 @@
package ghidra.framework.store;
import java.io.*;
+import java.util.ArrayList;
+import java.util.NoSuchElementException;
import db.buffers.BufferFile;
import db.buffers.ManagedBufferFile;
-import ghidra.framework.store.local.UnknownFolderItem;
+import ghidra.framework.store.local.LocalFileSystem;
+import ghidra.framework.store.remote.RemoteFileSystem;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
@@ -40,38 +43,38 @@ public interface FileSystem {
* Get user name associated with this filesystem. In the case of a remote filesystem
* this will correspond to the name used during login/authentication. A null value may
* be returned if user name unknown.
+ * @return user name used to authenticate or null if not-applicable
*/
- String getUserName();
+ public String getUserName();
/**
- * Returns true if the file-system requires check-outs when
- * modifying folder items.
+ * {@return true if the file-system requires check-outs when
+ * modifying folder items.}
*/
public boolean isVersioned();
/**
- * Returns true if file-system is on-line.
+ * {@return true if file-system is on-line.}
*/
public boolean isOnline();
/**
- * Returns true if file-system is read-only.
- * @throws IOException
+ * {@return true if file-system is read-only.}
+ * @throws IOException if IO error occurs
*/
public boolean isReadOnly() throws IOException;
/**
- * Returns the number of folder items contained within this file-system.
- * @throws IOException
+ * {@return the number of folder items contained within this file-system.}
+ * @throws IOException if an IO error occurs
* @throws UnsupportedOperationException if file-system does not support this operation
*/
public int getItemCount() throws IOException, UnsupportedOperationException;
/**
- * Returns a list of the folder item names contained in the given folder.
+ * {@return a list of the folder item names contained in the given folder.}
* @param folderPath the path of the folder.
- * @return a list of folder item names.
- * @throws IOException
+ * @throws IOException if an IO error occurs
*/
public String[] getItemNames(String folderPath) throws IOException;
@@ -81,7 +84,7 @@ public interface FileSystem {
* @return a list of folder items. Null items may exist if index contained item name
* while storage was not found. An {@link UnknownFolderItem} may be returned if unsupported
* item storage encountered.
- * @throws IOException
+ * @throws IOException if an IO error occurs
*/
public FolderItem[] getItems(String folderPath) throws IOException;
@@ -105,6 +108,8 @@ public interface FileSystem {
/**
* Return a list of subfolders (by name) that are stored within the specified folder path.
+ * @param folderPath folder path
+ * @return subfolders names
* @throws FileNotFoundException if folder path does not exist.
* @throws IOException if IO error occurs.
*/
@@ -122,6 +127,16 @@ public interface FileSystem {
public void createFolder(String parentPath, String folderName)
throws InvalidNameException, IOException;
+ /**
+ * Determine if the specified folder item is supported by this filesystem's interface and
+ * storage. This method primarily exists to determine if a remote server can support
+ * the specified content. This can come into play as new storage formats are added
+ * to a {@link LocalFileSystem} but may not be supported by a connected {@link RemoteFileSystem}.
+ * @param folderItem folder item
+ * @return true if folder item storage is supported
+ */
+ public boolean isSupportedItemType(FolderItem folderItem);
+
/**
* Create a new database item within the specified parent folder using the contents
* of the specified BufferFile.
@@ -162,8 +177,7 @@ public interface FileSystem {
* @return an empty BufferFile open for read-write.
* @throws FileNotFoundException thrown if parent folder does not exist.
* @throws DuplicateFileException if a folder item exists with this name
- * @throws InvalidNameException if the name does not have
- * all alphanumerics
+ * @throws InvalidNameException if the name has illegal characters.
* @throws IOException if an IO error occurs.
*/
public ManagedBufferFile createDatabase(String parentPath, String name, String fileID,
@@ -182,7 +196,6 @@ public interface FileSystem {
* @return new data file
* @throws DuplicateFileException Thrown if a folderItem with that name already exists.
* @throws InvalidNameException if the name has illegal characters.
- * all alphanumerics
* @throws IOException if an IO error occurs.
* @throws CancelledException if cancelled by monitor
*/
@@ -190,6 +203,23 @@ public interface FileSystem {
String comment, String contentType, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException;
+ /**
+ * Creates a new text data file within the specified parent folder.
+ * @param parentPath folder path of parent
+ * @param name new data file name
+ * @param fileID file ID to be associated with new file or null
+ * @param contentType application defined content type
+ * @param textData text data (required)
+ * @param comment file comment (may be null, only used if versioning is enabled)
+ * @return new data file
+ * @throws DuplicateFileException Thrown if a folderItem with that name already exists.
+ * @throws InvalidNameException if the name has illegal characters.
+ * @throws IOException if an IO error occurs.
+ */
+ public TextDataItem createTextDataItem(String parentPath, String name, String fileID,
+ String contentType, String textData, String comment)
+ throws InvalidNameException, IOException;
+
/**
* Creates a new file item from a packed file.
* The content/item type must be determined from the input stream.
@@ -252,7 +282,8 @@ public interface FileSystem {
* Moves the specified item to a new folder.
* @param folderPath path of folder containing the item.
* @param name name of the item to be moved.
- * @param newFolderPath path of folder where item is to be moved.
+ * @param newFolderPath path of folder where item is to be moved to.
+ * @param newName new item name to be applied
* @throws FileNotFoundException if the item does not exist.
* @throws DuplicateFileException if item with the same name exists within the new parent folder.
* @throws FileInUseException if the item is in-use or checked-out
@@ -263,14 +294,14 @@ public interface FileSystem {
throws IOException, InvalidNameException;
/**
- * Adds the given listener to be notified of file system changes.
+ * Adds a file system listener to be notified of file system changes.
* @param listener the listener to be added.
*/
public void addFileSystemListener(FileSystemListener listener);
/**
- * Removes the listener from being notified of file system changes.
- * @param listener
+ * Removes a file system listener from being notified of file system changes.
+ * @param listener file system listener
*/
public void removeFileSystemListener(FileSystemListener listener);
@@ -283,7 +314,7 @@ public interface FileSystem {
public boolean folderExists(String folderPath) throws IOException;
/**
- * Returns true if the file exists
+ * {@return true if the file exists}
* @param folderPath the folderPath of the folder that may contain the file.
* @param name the name of the file to check for existence.
* @throws IOException if an IO error occurs.
@@ -291,7 +322,7 @@ public interface FileSystem {
public boolean fileExists(String folderPath, String name) throws IOException;
/**
- * Returns true if this file system is shared
+ * {@return true if this file system is shared}
*/
public boolean isShared();
@@ -300,4 +331,58 @@ public interface FileSystem {
*/
public void dispose();
+ /**
+ * Normalize an absolute path, removing all "." and ".." use.
+ *
+ * NOTE: This method does not consider possible linked folder traversal which may
+ * get ignored when flattening/simplifying path.
+ *
+ * @param path absolute filesystem path which may contain "." or ".." path elements.
+ * @return normalized path
+ * @throws IllegalArgumentException if an absolute path starting with {@link #SEPARATOR}
+ * was not specified or an illegal path was specified.
+ */
+ public static String normalizePath(String path) throws IllegalArgumentException {
+ if (!path.startsWith(SEPARATOR)) {
+ throw new IllegalArgumentException("Absolute path required");
+ }
+
+ String[] split = path.split(SEPARATOR);
+
+ ArrayList elements = new ArrayList<>();
+ for (int i = 1; i < split.length; i++) {
+ String e = split[i];
+ if (e.length() == 0) {
+ throw new IllegalArgumentException("Invalid path with empty element: " + path);
+ }
+ if ("..".equals(e)) {
+ try {
+ // remove last element
+ elements.removeLast();
+ }
+ catch (NoSuchElementException ex) {
+ throw new IllegalArgumentException("Invalid path: " + path);
+ }
+ }
+ else if (".".equals(e)) {
+ // ignore element
+ continue;
+ }
+ else {
+ elements.add(e);
+ }
+ }
+
+ if (elements.isEmpty()) {
+ return SEPARATOR;
+ }
+
+ StringBuilder buf = new StringBuilder();
+ for (String e : elements) {
+ buf.append(SEPARATOR);
+ buf.append(e);
+ }
+ return buf.toString();
+ }
+
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FolderItem.java
index 80915343b8..1aa48dee02 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FolderItem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FolderItem.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -43,6 +43,11 @@ public interface FolderItem {
*/
public static final int DATAFILE_FILE_TYPE = 1;
+ /**
+ * Item type is associated with metadata only (e.g., URL)
+ */
+ public static final int LINK_FILE_TYPE = 2;
+
/**
* Default checkout ID used when a checkout is not applicable.
*/
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/TextDataItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/TextDataItem.java
new file mode 100644
index 0000000000..33a667975c
--- /dev/null
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/TextDataItem.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.framework.store;
+
+/**
+ * TextDataItem corresponds to a file which contains text data only
+ * and relies only on property file storage (i.e., no separate database or data file).
+ */
+public interface TextDataItem extends FolderItem {
+
+ /**
+ * Get the text data that was stored with this item
+ * @return text data
+ */
+ public String getTextData();
+
+}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/UnknownFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/UnknownFolderItem.java
new file mode 100644
index 0000000000..792ca0d379
--- /dev/null
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/UnknownFolderItem.java
@@ -0,0 +1,37 @@
+/* ###
+ * 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.store;
+
+/**
+ * UnknownFolderItem corresponds to a folder item which has an unknown storage type
+ * or has encountered a storage failure.
+ */
+public interface UnknownFolderItem extends FolderItem {
+
+ public static final String UNKNOWN_CONTENT_TYPE = "Unknown-File";
+
+ /**
+ * Get the file type:
+ *
+ * - {@link FolderItem#DATABASE_FILE_TYPE}
+ * - {@link FolderItem#DATAFILE_FILE_TYPE}
+ * - {@link FolderItem#LINK_FILE_TYPE}
+ *
+ * @return file type or {@link FolderItem#UNKNOWN_FILE_TYPE} (-1) if unknown
+ */
+ public int getFileType();
+
+}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/db/PackedDatabase.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/db/PackedDatabase.java
index 3b53054de3..8731c57840 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/db/PackedDatabase.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/db/PackedDatabase.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -39,7 +39,7 @@ import utilities.util.FileUtilities;
/**
* PackedDatabase provides a packed form of Database
* which compresses a single version into a file.
- *
+ *
* When opening a packed database, a PackedDBHandle is returned
* after first expanding the file into a temporary Database.
*/
@@ -114,7 +114,7 @@ public class PackedDatabase extends Database {
* @throws CancelledException is unpack is cancelled
* @throws IOException if IO error occurs
*/
- PackedDatabase(CachedDB cachedDb, ResourceFile packedDbFile, LockFile packedDbLock,
+ PackedDatabase(CachedDB cachedDb, ResourceFile packedDbFile, LockFile packedDbLock,
TaskMonitor monitor) throws CancelledException, IOException {
super(cachedDb.dbDir, null, false);
this.packedDbFile = packedDbFile;
@@ -276,8 +276,8 @@ public class PackedDatabase extends Database {
* @throws IOException if IO error occurs
* @throws CancelledException if unpack/open is cancelled
*/
- public static synchronized PackedDatabase getPackedDatabase(ResourceFile packedDbFile, boolean neverCache,
- TaskMonitor monitor) throws IOException, CancelledException {
+ public static synchronized PackedDatabase getPackedDatabase(ResourceFile packedDbFile,
+ boolean neverCache, TaskMonitor monitor) throws IOException, CancelledException {
if (!neverCache && PackedDatabaseCache.isEnabled()) {
try {
return PackedDatabaseCache.getCache().getCachedDB(packedDbFile, monitor);
@@ -633,7 +633,7 @@ public class PackedDatabase extends Database {
tmpFile = Application.createTempFile("pack", ".tmp");
tmpFile.delete();
dbh.saveAs(tmpFile, false, monitor);
- try (InputStream itemIn = new BufferedInputStream(new FileInputStream(tmpFile))){
+ try (InputStream itemIn = new BufferedInputStream(new FileInputStream(tmpFile))) {
ItemSerializer.outputItem(itemName, contentType, FolderItem.DATABASE_FILE_TYPE,
tmpFile.length(), itemIn, outputFile, monitor);
}
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 40a28987ea..3533d45866 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
@@ -598,7 +598,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
}
}
- private boolean addFileToIndex(PropertyFile pfile) throws IOException, NotFoundException {
+ private boolean addFileToIndex(ItemPropertyFile pfile) throws IOException, NotFoundException {
String parentPath = pfile.getParentPath();
String name = pfile.getName();
@@ -832,7 +832,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
catch (NotFoundException e) {
// ignore - handled below
}
- throw new FileNotFoundException("Item not found: " + folderPath + SEPARATOR + itemName);
+ throw new FileNotFoundException("Item not found: " + getPath(folderPath, itemName));
}
/**
@@ -1207,7 +1207,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
String newFolderPath = folder.getPathname();
for (Item item : folder.items.values()) {
ItemStorage itemStorage = item.itemStorage;
- PropertyFile pfile = item.itemStorage.getPropertyFile();
+ ItemPropertyFile pfile = item.itemStorage.getPropertyFile();
pfile.moveTo(itemStorage.dir, itemStorage.storageName, newFolderPath,
itemStorage.itemName);
itemStorage.folderPath = newFolderPath;
@@ -1236,7 +1236,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
folder = getFolder(folderPath, GetFolderOption.READ_ONLY);
if (folder.parent.folders.get(newFolderName) != null) {
throw new DuplicateFileException(
- parentPath + SEPARATOR + newFolderName + " already exists.");
+ getPath(parentPath, newFolderName) + " already exists.");
}
indexJournal.moveFolder(folderPath, getPath(parentPath, newFolderName));
@@ -1462,7 +1462,6 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
}
private void replayJournal() throws IndexReadException {
- Msg.info(this, "restoring data storage index...");
int lineNum = 0;
BufferedReader journalReader = null;
try {
@@ -1778,7 +1777,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
}
@Override
- PropertyFile getPropertyFile() throws IOException {
+ ItemPropertyFile getPropertyFile() throws IOException {
return new IndexedPropertyFile(dir, storageName, folderPath, itemName);
}
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedPropertyFile.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedPropertyFile.java
index 031b882fde..defd60d7cf 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedPropertyFile.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedPropertyFile.java
@@ -1,13 +1,12 @@
/* ###
* IP: GHIDRA
- * REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -16,59 +15,68 @@
*/
package ghidra.framework.store.local;
-import ghidra.framework.store.FileSystem;
-import ghidra.util.PropertyFile;
-import ghidra.util.exception.DuplicateFileException;
-
import java.io.*;
-public class IndexedPropertyFile extends PropertyFile {
+import ghidra.util.exception.DuplicateFileException;
- public final static String NAME_PROPERTY = "NAME";
- public final static String PARENT_PATH_PROPERTY = "PARENT";
+public class IndexedPropertyFile extends ItemPropertyFile {
+
+ protected static final String NAME_PROPERTY = "NAME";
+ protected static final String PARENT_PATH_PROPERTY = "PARENT";
/**
* Construct a new or existing PropertyFile.
- * This form ignores retained property values for NAME and PARENT path.
+ * This constructor ignores retained property values for NAME and PARENT path.
+ * This constructor will not throw an exception if the file does not exist.
* @param dir parent directory
* @param storageName stored property file name (without extension)
* @param parentPath path to parent
* @param name name of the property file
- * @throws IOException
+ * @throws InvalidObjectException if a file parse error occurs
+ * @throws IOException if an IO error occurs reading an existing file
*/
public IndexedPropertyFile(File dir, String storageName, String parentPath, String name)
throws IOException {
super(dir, storageName, parentPath, name);
-// if (exists() &&
-// (!name.equals(getString(NAME_PROPERTY, null)) || !parentPath.equals(getString(
-// PARENT_PATH_PROPERTY, null)))) {
-// throw new AssertException();
-// }
- putString(NAME_PROPERTY, name);
- putString(PARENT_PATH_PROPERTY, parentPath);
+ if (contains(NAME_PROPERTY) && contains(PARENT_PATH_PROPERTY)) {
+ this.name = getString(NAME_PROPERTY, name);
+ this.parentPath = getString(PARENT_PATH_PROPERTY, parentPath);
+ }
+ else {
+ // new property file
+ putString(NAME_PROPERTY, name);
+ putString(PARENT_PATH_PROPERTY, parentPath);
+ }
}
/**
- * Construct an existing PropertyFile.
+ * Construct a existing PropertyFile.
+ * This constructor uses property values for NAME and PARENT path.
* @param dir parent directory
* @param storageName stored property file name (without extension)
* @throws FileNotFoundException if property file does not exist
+ * @throws InvalidObjectException if a file parse error occurs
* @throws IOException if error occurs reading property file
*/
public IndexedPropertyFile(File dir, String storageName) throws IOException {
- super(dir, storageName, FileSystem.SEPARATOR, storageName);
+ super(dir, storageName, null, null);
if (!exists()) {
- throw new FileNotFoundException();
+ throw new FileNotFoundException(
+ new File(dir, storageName + PROPERTY_EXT) + " not found");
}
+ name = getString(NAME_PROPERTY, null);
+ parentPath = getString(PARENT_PATH_PROPERTY, null);
if (name == null || parentPath == null) {
throw new IOException("Invalid indexed property file: " + propertyFile);
}
}
/**
- * Construct an existing PropertyFile.
- * @param file
+ * Construct a existing PropertyFile.
+ * This constructor uses property values for NAME and PARENT path.
+ * @param file property file
* @throws FileNotFoundException if property file does not exist
+ * @throws InvalidObjectException if a file parse error occurs
* @throws IOException if error occurs reading property file
*/
public IndexedPropertyFile(File file) throws IOException {
@@ -82,27 +90,17 @@ public class IndexedPropertyFile extends PropertyFile {
return propertyFileName.substring(0, propertyFileName.length() - PROPERTY_EXT.length());
}
- @Override
- public void readState() throws IOException {
- super.readState();
- name = getString(NAME_PROPERTY, null);
- parentPath = getString(PARENT_PATH_PROPERTY, null);
- }
-
@Override
public void moveTo(File newParent, String newStorageName, String newParentPath, String newName)
throws DuplicateFileException, IOException {
-
+ String oldName = name;
+ String oldParentPath = parentPath;
super.moveTo(newParent, newStorageName, newParentPath, newName);
-// if (!parentPath.equals(newParentPath)) {
-// throw new AssertException();
-// }
-// if (!name.equals(newName)) {
-// throw new AssertException();
-// }
- putString(NAME_PROPERTY, newName);
- putString(PARENT_PATH_PROPERTY, newParentPath);
- writeState();
+ if (!newParentPath.equals(oldParentPath) || !newName.equals(oldName)) {
+ putString(NAME_PROPERTY, name);
+ putString(PARENT_PATH_PROPERTY, parentPath);
+ writeState();
+ }
}
}
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 1647da5dbc..125f690ae4 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
@@ -19,7 +19,6 @@ import java.io.*;
import java.util.HashMap;
import ghidra.util.Msg;
-import ghidra.util.PropertyFile;
import ghidra.util.exception.NotFoundException;
/**
@@ -94,7 +93,7 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem {
}
@Override
- protected synchronized void fileIdChanged(PropertyFile pfile, String oldFileId)
+ protected synchronized void fileIdChanged(ItemPropertyFile pfile, String oldFileId)
throws IOException {
indexJournal.open();
try {
@@ -143,12 +142,19 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem {
if (item == null) {
return null;
}
+ ItemStorage itemStorage = item.itemStorage;
try {
- PropertyFile propertyFile = item.itemStorage.getPropertyFile();
+ ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
if (propertyFile.exists()) {
return LocalFolderItem.getFolderItem(this, propertyFile);
}
}
+ catch (InvalidObjectException e) {
+ // Use unknown placeholder item on failure
+ InvalidPropertyFile invalidFile = new InvalidPropertyFile(itemStorage.dir,
+ itemStorage.storageName, itemStorage.folderPath, itemStorage.itemName);
+ return new LocalUnknownFolderItem(this, invalidFile);
+ }
catch (FileNotFoundException e) {
// ignore
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/InvalidPropertyFile.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/InvalidPropertyFile.java
new file mode 100644
index 0000000000..faf24ec7a0
--- /dev/null
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/InvalidPropertyFile.java
@@ -0,0 +1,47 @@
+/* ###
+ * 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.store.local;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * {@link InvalidPropertyFile} provides a substitue {@link ItemPropertyFile} when one
+ * fails to parse. This allows the item's existance to be managed even if the item cannot
+ * be opened.
+ */
+public class InvalidPropertyFile extends ItemPropertyFile {
+
+ /**
+ * Construct an invalid property file instance if it previously failed to parse.
+ * @param dir native directory where this file is stored
+ * @param storageName stored property file name (without extension)
+ * @param parentPath logical parent path for the associated item
+ * @param name name of the associated item
+ * @throws IOException (never thrown since file is never read)
+ */
+ public InvalidPropertyFile(File dir, String storageName, String parentPath, String name)
+ throws IOException {
+ super(dir, storageName, parentPath, name);
+ // NOTE: IOException is prevented by having a do-nothing readState method below
+ }
+
+ @Override
+ public final void readState() {
+ // avoid potential parse failure
+ }
+
+}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/ItemPropertyFile.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/ItemPropertyFile.java
new file mode 100644
index 0000000000..f9f5b37491
--- /dev/null
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/ItemPropertyFile.java
@@ -0,0 +1,145 @@
+/* ###
+ * 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.store.local;
+
+import java.io.*;
+
+import javax.help.UnsupportedOperationException;
+
+import ghidra.framework.store.FileSystem;
+import ghidra.framework.store.FolderItem;
+import ghidra.util.PropertyFile;
+import ghidra.util.exception.DuplicateFileException;
+
+/**
+ * {@link ItemPropertyFile} provides basic property storage which is primarily intended to
+ * store limited information related to a logical {@link FolderItem}. The file
+ * extension used is {@link #PROPERTY_EXT}.
+ */
+public class ItemPropertyFile extends PropertyFile {
+
+ private static final String FILE_ID_PROPERTY = "FILE_ID";
+
+ protected String name;
+ protected String parentPath;
+
+ /**
+ * Construct a new or existing PropertyFile.
+ * This constructor ignores retained property values for NAME and PARENT path.
+ * This constructor will not throw an exception if the file does not exist.
+ * @param dir native directory where this file is stored
+ * @param storageName stored property file name (without extension)
+ * @param parentPath logical parent path for the associated item
+ * @param name name of the associated item
+ * @throws InvalidObjectException if a file parse error occurs
+ * @throws IOException if an IO error occurs reading an existing file
+ */
+ public ItemPropertyFile(File dir, String storageName, String parentPath, String name)
+ throws IOException {
+ super(dir, storageName);
+ this.name = name;
+ this.parentPath = parentPath;
+ }
+
+ /**
+ * Return the name of the item associated with this PropertyFile. A null value may be returned
+ * if this is an older property file and the name was not specified at
+ * time of construction.
+ * @return associated item name or null if unknown
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Return the logical path of the item associated with this PropertyFile. A null value may be
+ * returned if this is an older property file and the name and parentPath was not specified at
+ * time of construction.
+ * @return logical path of the associated item or null if unknown
+ */
+ public String getPath() {
+ if (parentPath == null || name == null) {
+ return null;
+ }
+ if (parentPath.length() == 1) {
+ return parentPath + name;
+ }
+ return parentPath + FileSystem.SEPARATOR_CHAR + name;
+ }
+
+ /**
+ * Return the logical parent path containing the item descibed by this PropertyFile.
+ * @return logical parent directory path
+ */
+ public String getParentPath() {
+ return parentPath;
+ }
+
+ /**
+ * Returns the FileID associated with this file.
+ * @return FileID associated with this file or null
+ */
+ public String getFileID() {
+ return getString(FILE_ID_PROPERTY, null);
+ }
+
+ /**
+ * Set the FileID associated with this file.
+ * @param fileId unique file ID
+ */
+ public void setFileID(String fileId) {
+ putString(FILE_ID_PROPERTY, fileId);
+ }
+
+ /**
+ * Move this PropertyFile to the newParent file.
+ * @param newStorageParent new storage parent of the native file
+ * @param newStorageName new storage name for this property file
+ * @param newParentPath new logical parent path
+ * @param newName new logical item name
+ * @throws IOException thrown if there was a problem accessing the
+ * @throws DuplicateFileException thrown if a file with the newName
+ * already exists
+ */
+ public void moveTo(File newStorageParent, String newStorageName, String newParentPath,
+ String newName) throws DuplicateFileException, IOException {
+ super.moveTo(newStorageParent, newStorageName);
+ if (!newParentPath.equals(parentPath) || !newName.equals(name)) {
+ parentPath = newParentPath;
+ name = newName;
+ }
+ }
+
+ /**
+ * NOTE!! This method must not be used.
+ *
+ * Movement of an item is related to its logical pathname and must be accomplished
+ * with the {@link #moveTo(File, String, String, String)} method. There is no supported
+ * direct use of this method.
+ *
+ * @param newStorageParent new storage parent of the native file
+ * @param newStorageName new storage name for this property file
+ * @throws UnsupportedOperationException always thrown
+ * @deprecated method must not be used
+ */
+ @Deprecated(forRemoval = false, since = "11.4")
+ @Override
+ public final void moveTo(File newStorageParent, String newStorageName)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFile.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFileItem.java
similarity index 83%
rename from Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFile.java
rename to Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFileItem.java
index 7778c9f8b2..c7826dcdd5 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFile.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFileItem.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,27 +17,38 @@ package ghidra.framework.store.local;
import ghidra.framework.store.DataFileItem;
import ghidra.framework.store.FolderItem;
-import ghidra.util.PropertyFile;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.task.TaskMonitor;
import java.io.*;
+import org.apache.commons.lang3.StringUtils;
+
/**
- * LocalDataFile provides a FolderItem implementation
+ * LocalDataFileItem provides a FolderItem implementation
* for a local serialized data file. This implementation supports
* a non-versioned file-system only.
*
* This item utilizes a data directory for storing the serialized
* data file.
+ *
+ * NOTE: The use of this file item type is not fully supported.
*/
-public class LocalDataFile extends LocalFolderItem implements DataFileItem {
+public class LocalDataFileItem extends LocalFolderItem implements DataFileItem {
private final static int IO_BUFFER_SIZE = 32 * 1024;
private static final String DATA_FILE = "data.1.gdf";
- public LocalDataFile(LocalFileSystem fileSystem, PropertyFile propertyFile) throws IOException {
+ /**
+ * Constructor for an existing local serialized=data file item which corresponds to the specified
+ * property file.
+ * @param fileSystem file system
+ * @param propertyFile database property file
+ * @throws IOException if an IO Error occurs
+ */
+ public LocalDataFileItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile)
+ throws IOException {
super(fileSystem, propertyFile, true, false);
if (fileSystem.isVersioned()) {
@@ -50,7 +61,7 @@ public class LocalDataFile extends LocalFolderItem implements DataFileItem {
}
/**
- * Create a new local data file item.
+ * Create a new local serialized-data file item.
* @param fileSystem file system
* @param propertyFile serialized data property file
* @param istream data source input stream (should be a start of data and will be read to end of file).
@@ -61,9 +72,9 @@ public class LocalDataFile extends LocalFolderItem implements DataFileItem {
* @throws IOException if an IO Error occurs
* @throws CancelledException if monitor cancels operation
*/
- public LocalDataFile(LocalFileSystem fileSystem, PropertyFile propertyFile,
- InputStream istream, String contentType, TaskMonitor monitor) throws IOException,
- CancelledException {
+ public LocalDataFileItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile,
+ InputStream istream, String contentType, TaskMonitor monitor)
+ throws IOException, CancelledException {
super(fileSystem, propertyFile, true, true);
if (fileSystem.isVersioned()) {
@@ -71,6 +82,11 @@ public class LocalDataFile extends LocalFolderItem implements DataFileItem {
throw new UnsupportedOperationException("Versioning not yet supported for DataFiles");
}
+ if (StringUtils.isBlank(contentType)) {
+ abortCreate();
+ throw new IllegalArgumentException("Missing content-type");
+ }
+
File dataFile = getDataFile();
if (dataFile.exists()) {
throw new DuplicateFileException(getName() + " already exists.");
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDatabaseItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDatabaseItem.java
index d8c5288347..fcabd092c6 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDatabaseItem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDatabaseItem.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -18,10 +18,13 @@ package ghidra.framework.store.local;
import java.io.File;
import java.io.IOException;
+import org.apache.commons.lang3.StringUtils;
+
import db.buffers.*;
import ghidra.framework.store.*;
import ghidra.framework.store.db.*;
-import ghidra.util.*;
+import ghidra.util.Msg;
+import ghidra.util.ReadOnlyException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@@ -49,8 +52,8 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem {
* @param create if true the data directory will be created
* @throws IOException
*/
- private LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile, boolean create)
- throws IOException {
+ private LocalDatabaseItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile,
+ boolean create) throws IOException {
super(fileSystem, propertyFile, true, create);
if (isVersioned) {
versionedDbListener = new LocalVersionedDbListener();
@@ -63,7 +66,8 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem {
* @param fileSystem file system
* @param propertyFile database property file
*/
- LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile) throws IOException {
+ LocalDatabaseItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile)
+ throws IOException {
super(fileSystem, propertyFile, true, false);
if (isVersioned) {
@@ -94,11 +98,16 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem {
* @throws IOException if error occurs
* @throws CancelledException if database creation cancelled by user
*/
- LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile, BufferFile srcFile,
+ LocalDatabaseItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, BufferFile srcFile,
String contentType, String fileID, String comment, boolean resetDatabaseId,
TaskMonitor monitor, String user) throws IOException, CancelledException {
super(fileSystem, propertyFile, true, true);
+ if (StringUtils.isBlank(contentType)) {
+ abortCreate();
+ throw new IllegalArgumentException("Missing content-type");
+ }
+
boolean success = false;
long checkoutId = DEFAULT_CHECKOUT_ID;
try {
@@ -154,7 +163,7 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem {
* @throws IOException if error occurs
* @throws CancelledException if database creation cancelled by user
*/
- LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile, File packedFile,
+ LocalDatabaseItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, File packedFile,
String contentType, TaskMonitor monitor, String user)
throws IOException, CancelledException {
super(fileSystem, propertyFile, true, true);
@@ -222,7 +231,7 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem {
* @throws IOException if error occurs
*/
static LocalManagedBufferFile create(final LocalFileSystem fileSystem,
- PropertyFile propertyFile, int bufferSize, String contentType, String fileID,
+ ItemPropertyFile propertyFile, int bufferSize, String contentType, String fileID,
String user, String projectPath) throws IOException {
final LocalDatabaseItem dbItem = new LocalDatabaseItem(fileSystem, propertyFile, true);
@@ -257,6 +266,7 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem {
db.setSynchronizationObject(dbItem.fileSystem);
dbItem.privateDb = (PrivateDatabase) db;
}
+ dbItem.log("file created", user);
dbItem.fireItemCreated();
}
}
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 fbce15768b..6c74955c57 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
@@ -82,11 +82,12 @@ public abstract class LocalFileSystem implements FileSystem {
/**
* Construct a local filesystem for existing data
- * @param rootPath
- * @param create
- * @param isVersioned
- * @param readOnly
- * @param enableAsyncronousDispatching
+ * @param rootPath filesystem root directory (the directory must exist and must not have any
+ * contents if {@code create} is true)
+ * @param create true if creating new filesystem from the empty directory at rootPath
+ * @param isVersioned true if creating a versioned filesystem
+ * @param readOnly true if file system is read-only (ignored if {@code create} is true).
+ * @param enableAsyncronousDispatching true if async event dispatching should be performed
* @return local filesystem
* @throws FileNotFoundException if specified rootPath does not exist
* @throws IOException if error occurs while reading/writing index files
@@ -103,10 +104,6 @@ public abstract class LocalFileSystem implements FileSystem {
throw new IOException("new filesystem directory is not empty: " + rootPath);
}
if (create) {
-// if (isCreateMangledFileSystemEnabled()) {
-// return new MangledLocalFileSystem(rootPath, isVersioned, readOnly,
-// enableAsyncronousDispatching);
-// }
return new IndexedV1LocalFileSystem(rootPath, isVersioned, readOnly,
enableAsyncronousDispatching, true);
}
@@ -154,7 +151,7 @@ public abstract class LocalFileSystem implements FileSystem {
/**
* Returns true if any file found within dir whose name starts
* with '~' character (e.g., ~index.dat, etc)
- * @param dir
+ * @param dir directory to inspect
* @return true if any hidden file found with '~' prefix
*/
private static boolean hasAnyHiddenFiles(File dir) {
@@ -237,7 +234,7 @@ public abstract class LocalFileSystem implements FileSystem {
/**
* Associate file system with a specific repository logger
- * @param repositoryLogger
+ * @param repositoryLogger repository logger (may be null)
*/
public void setAssociatedRepositoryLogger(RepositoryLogger repositoryLogger) {
this.repositoryLogger = repositoryLogger;
@@ -317,8 +314,14 @@ public abstract class LocalFileSystem implements FileSystem {
return pfile.exists();
}
- PropertyFile getPropertyFile() throws IOException {
- return new PropertyFile(dir, storageName, folderPath, itemName);
+ /**
+ * Get property file associated with this item storage
+ * @return property file
+ * @throws InvalidObjectException if a file parse error occurs
+ * @throws IOException if an IO error occurs reading an existing file
+ */
+ ItemPropertyFile getPropertyFile() throws IOException {
+ return new ItemPropertyFile(dir, storageName, folderPath, itemName);
}
@Override
@@ -336,19 +339,19 @@ public abstract class LocalFileSystem implements FileSystem {
/**
* Find an existing storage location
- * @param folderPath
- * @param itemName
+ * @param folderPath folder path of item
+ * @param itemName item name
* @return storage location. A non-null value does not guarantee that the associated
* item actually exists.
- * @throws FileNotFoundException
+ * @throws FileNotFoundException if existing storage allocation not found
*/
protected abstract ItemStorage findItemStorage(String folderPath, String itemName)
throws FileNotFoundException;
/**
* Allocate a new storage location
- * @param folderPath
- * @param itemName
+ * @param folderPath folder path of item
+ * @param itemName item name
* @return storage location
* @throws DuplicateFileException if item path has previously been allocated
* @throws IOException if invalid path/item name specified
@@ -359,9 +362,9 @@ public abstract class LocalFileSystem implements FileSystem {
/**
* Deallocate item storage
- * @param folderPath
- * @param itemName
- * @throws IOException
+ * @param folderPath folder path of item
+ * @param itemName item name
+ * @throws IOException if an IO error occurs
*/
protected abstract void deallocateItemStorage(String folderPath, String itemName)
throws IOException;
@@ -376,15 +379,27 @@ public abstract class LocalFileSystem implements FileSystem {
@Override
public synchronized LocalFolderItem getItem(String folderPath, String name) throws IOException {
+ ItemStorage itemStorage = null;
try {
- ItemStorage itemStorage = findItemStorage(folderPath, name);
+ itemStorage = findItemStorage(folderPath, name);
if (itemStorage == null) {
return null;
}
- PropertyFile propertyFile = itemStorage.getPropertyFile();
+ ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
if (propertyFile.exists()) {
return LocalFolderItem.getFolderItem(this, propertyFile);
}
+
+ // force cleanup of bad storage allocation
+ Msg.warn(this, "Attempting item cleanup due to missing property file: " +
+ new File(propertyFile.getParentStorageDirectory(), propertyFile.getStorageName()));
+ itemDeleted(folderPath, name);
+ }
+ catch (InvalidObjectException e) {
+ // Use unknown placeholder item on failure
+ InvalidPropertyFile invalidFile = new InvalidPropertyFile(itemStorage.dir,
+ itemStorage.storageName, itemStorage.folderPath, itemStorage.itemName);
+ return new LocalUnknownFolderItem(this, invalidFile);
}
catch (FileNotFoundException e) {
// ignore
@@ -394,11 +409,12 @@ public abstract class LocalFileSystem implements FileSystem {
/**
* Notification that FileID has been changed within propertyFile
- * @param propertyFile
- * @param oldFileId
- * @throws IOException
+ * @param propertyFile item property file
+ * @param oldFileId old FileId
+ * @throws IOException if an IO error occurs
*/
- protected void fileIdChanged(PropertyFile propertyFile, String oldFileId) throws IOException {
+ protected void fileIdChanged(ItemPropertyFile propertyFile, String oldFileId)
+ throws IOException {
// do nothing by default
}
@@ -418,6 +434,12 @@ public abstract class LocalFileSystem implements FileSystem {
return folderItems;
}
+ @Override
+ public boolean isSupportedItemType(FolderItem folderItem) {
+ return (folderItem instanceof DatabaseItem) || (folderItem instanceof TextDataItem) ||
+ (folderItem instanceof DataFileItem);
+ }
+
@Override
public synchronized LocalDatabaseItem createDatabase(String parentPath, String name,
String fileID, BufferFile bufferFile, String comment, String contentType,
@@ -434,7 +456,7 @@ public abstract class LocalFileSystem implements FileSystem {
ItemStorage itemStorage = allocateItemStorage(parentPath, name);
LocalDatabaseItem item = null;
try {
- PropertyFile propertyFile = itemStorage.getPropertyFile();
+ ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
item = new LocalDatabaseItem(this, propertyFile, bufferFile, contentType, fileID,
comment, resetDatabaseId, monitor, user);
}
@@ -462,7 +484,7 @@ public abstract class LocalFileSystem implements FileSystem {
ItemStorage itemStorage = allocateItemStorage(parentPath, hiddenName);
LocalDatabaseItem item = null;
try {
- PropertyFile propertyFile = itemStorage.getPropertyFile();
+ ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
item = new LocalDatabaseItem(this, propertyFile, bufferFile, contentType, fileID, null,
resetDatabaseId, monitor, null);
}
@@ -489,7 +511,7 @@ public abstract class LocalFileSystem implements FileSystem {
ItemStorage itemStorage = allocateItemStorage(parentPath, name);
LocalManagedBufferFile bufferFile = null;
try {
- PropertyFile propertyFile = itemStorage.getPropertyFile();
+ ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
bufferFile = LocalDatabaseItem.create(this, propertyFile, bufferSize, contentType,
fileID, user, projectPath);
}
@@ -502,7 +524,7 @@ public abstract class LocalFileSystem implements FileSystem {
}
@Override
- public synchronized LocalDataFile createDataFile(String parentPath, String name,
+ public synchronized LocalDataFileItem createDataFile(String parentPath, String name,
InputStream istream, String comment, String contentType, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
@@ -514,11 +536,12 @@ public abstract class LocalFileSystem implements FileSystem {
testValidName(name, false);
ItemStorage itemStorage = allocateItemStorage(parentPath, name);
- LocalDataFile dataFile = null;
+ LocalDataFileItem dataFile = null;
try {
//TODO handle comment
- PropertyFile propertyFile = itemStorage.getPropertyFile();
- dataFile = new LocalDataFile(this, propertyFile, istream, contentType, monitor);
+ ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
+ dataFile = new LocalDataFileItem(this, propertyFile, istream, contentType, monitor);
+ dataFile.log("file created", getUserName());
}
finally {
if (dataFile == null) {
@@ -531,6 +554,38 @@ public abstract class LocalFileSystem implements FileSystem {
return dataFile;
}
+ @Override
+ public synchronized LocalTextDataItem createTextDataItem(String parentPath, String name,
+ String fileID, String contentType, String textData, String ignoredComment)
+ throws InvalidNameException, IOException {
+
+ // comment is ignored
+
+ if (readOnly) {
+ throw new ReadOnlyException();
+ }
+
+ testValidName(parentPath, true);
+ testValidName(name, false);
+
+ ItemStorage itemStorage = allocateItemStorage(parentPath, name);
+ LocalTextDataItem linkFile = null;
+ try {
+ ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
+ linkFile = new LocalTextDataItem(this, propertyFile, fileID, contentType, textData);
+ linkFile.log("file created", getUserName());
+ }
+ finally {
+ if (linkFile == null) {
+ deallocateItemStorage(parentPath, name);
+ }
+ }
+
+ eventManager.itemCreated(parentPath, name);
+
+ return linkFile;
+ }
+
@Override
public LocalDatabaseItem createFile(String parentPath, String name, File packedFile,
TaskMonitor monitor, String user)
@@ -561,7 +616,7 @@ public abstract class LocalFileSystem implements FileSystem {
ItemStorage itemStorage = allocateItemStorage(parentPath, name);
LocalDatabaseItem item = null;
try {
- PropertyFile propertyFile = itemStorage.getPropertyFile();
+ ItemPropertyFile propertyFile = itemStorage.getPropertyFile();
item =
new LocalDatabaseItem(this, propertyFile, packedFile, contentType, monitor, user);
}
@@ -661,6 +716,7 @@ public abstract class LocalFileSystem implements FileSystem {
/**
* Returns file system listener.
+ * @return file system listener or null
*/
FileSystemListener getListener() {
return eventManager;
@@ -716,6 +772,7 @@ public abstract class LocalFileSystem implements FileSystem {
}
/**
+ * @param c character to check
* @return true if c is a valid character within the FileSystem.
*/
public static boolean isValidNameCharacter(char c) {
@@ -756,9 +813,9 @@ public abstract class LocalFileSystem implements FileSystem {
/**
* Notify the filesystem that the property file and associated data files for
* an item have been removed from the filesystem.
- * @param folderPath
- * @param itemName
- * @throws IOException
+ * @param folderPath folder path of item
+ * @param itemName item name
+ * @throws IOException if an IO error occurs
*/
protected synchronized void itemDeleted(String folderPath, String itemName) throws IOException {
// do nothing
@@ -768,6 +825,7 @@ public abstract class LocalFileSystem implements FileSystem {
* Returns the full path for a specific folder or item
* @param parentPath full parent path
* @param name child folder or item name
+ * @return pathname
*/
protected final static String getPath(String parentPath, String name) {
if (parentPath.length() == 1) {
@@ -848,7 +906,7 @@ public abstract class LocalFileSystem implements FileSystem {
/**
* Escape hidden prefix chars in name
- * @param name
+ * @param name name to be escaped
* @return escaped name
*/
public static final String escapeHiddenDirPrefixChars(String name) {
@@ -867,7 +925,7 @@ public abstract class LocalFileSystem implements FileSystem {
/**
* Unescape a non-hidden directory name
- * @param name
+ * @param name name to be unescaped
* @return unescaped name or null if name is a hidden name
*/
public static final String unescapeHiddenDirPrefixChars(String name) {
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java
index 7424607d0f..b6c90e5be5 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java
@@ -21,7 +21,8 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import ghidra.framework.store.*;
-import ghidra.util.*;
+import ghidra.util.Msg;
+import ghidra.util.ReadOnlyException;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;
@@ -49,7 +50,7 @@ public abstract class LocalFolderItem implements FolderItem {
static final String DATA_DIR_EXTENSION = ".db";
- final PropertyFile propertyFile;
+ final ItemPropertyFile propertyFile;
final CheckoutManager checkoutMgr;
final HistoryManager historyMgr;
final LocalFileSystem fileSystem;
@@ -69,7 +70,7 @@ public abstract class LocalFolderItem implements FolderItem {
* @param fileSystem file system
* @param propertyFile property file
*/
- LocalFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile) {
+ LocalFolderItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile) {
this.fileSystem = fileSystem;
this.propertyFile = propertyFile;
this.isVersioned = fileSystem.isVersioned();
@@ -90,7 +91,7 @@ public abstract class LocalFolderItem implements FolderItem {
* @param create if true the data directory will be created
* @throws IOException
*/
- LocalFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile, boolean useDataDir,
+ LocalFolderItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, boolean useDataDir,
boolean create) throws IOException {
this.fileSystem = fileSystem;
this.propertyFile = propertyFile;
@@ -121,7 +122,7 @@ public abstract class LocalFolderItem implements FolderItem {
throw new FileNotFoundException(getName() + " not found");
}
- if (isVersioned) {
+ if (isVersioned && useDataDir) {
checkoutMgr = new CheckoutManager(this, create);
historyMgr = new HistoryManager(this, create);
}
@@ -161,7 +162,7 @@ public abstract class LocalFolderItem implements FolderItem {
final File getDataDir() {
synchronized (fileSystem) {
// Use hidden DB directory
- return new File(propertyFile.getFolder(),
+ return new File(propertyFile.getParentStorageDirectory(),
LocalFileSystem.HIDDEN_DIR_PREFIX +
LocalFileSystem.escapeHiddenDirPrefixChars(propertyFile.getStorageName()) +
DATA_DIR_EXTENSION);
@@ -234,6 +235,9 @@ public abstract class LocalFolderItem implements FolderItem {
*/
void beginCheckin(long checkoutId) throws FileInUseException {
synchronized (fileSystem) {
+ if (checkoutMgr == null) {
+ throw new UnsupportedOperationException("item does not support checkin/checkout");
+ }
if (checkinId != DEFAULT_CHECKOUT_ID) {
ItemCheckoutStatus status;
try {
@@ -426,7 +430,7 @@ public abstract class LocalFolderItem implements FolderItem {
synchronized (fileSystem) {
checkInUse();
- File oldFolder = propertyFile.getFolder();
+ File oldFolder = propertyFile.getParentStorageDirectory();
String oldStorageName = propertyFile.getStorageName();
String oldPath = propertyFile.getParentPath();
File oldDbDir = getDataDir();
@@ -491,41 +495,6 @@ public abstract class LocalFolderItem implements FolderItem {
return propertyFile.getName();
}
-// /**
-// * Change the name of this item's property file and hidden data directory
-// * based upon the new item name.
-// * If in-use files prevent renaming a FileInUseException will be thrown.
-// * @param name new name for this item
-// * @throws InvalidNameException invalid name was specified
-// * @throws IOException an error occurred
-// */
-// void doSetName(String name) throws InvalidNameException, IOException {
-// synchronized (fileSystem) {
-// File oldDbDir = getDataDir();
-// String oldName = getName();
-//
-// boolean success = false;
-// try {
-// propertyFile.setName(name);
-// File newDbDir = getDataDir();
-// if (useDataDir) {
-// if (newDbDir.exists()) {
-// throw new DuplicateFileException(getName() + " already exists");
-// }
-// else if (!oldDbDir.renameTo(newDbDir)) {
-// throw new FileInUseException(oldName + " is in use");
-// }
-// }
-// success = true;
-// }
-// finally {
-// if (!success && !propertyFile.getName().equals(oldName)) {
-// propertyFile.setName(oldName);
-// }
-// }
-// }
-// }
-
/**
* @see ghidra.framework.store.FolderItem#getParentPath()
*/
@@ -590,6 +559,10 @@ public abstract class LocalFolderItem implements FolderItem {
throw new UnsupportedOperationException(
"Non-versioned item does not support getVersions");
}
+ if (historyMgr == null) {
+ throw new UnsupportedOperationException(
+ "getVersions not supported without history manager");
+ }
return historyMgr.getVersions();
}
}
@@ -652,12 +625,16 @@ public abstract class LocalFolderItem implements FolderItem {
@Override
public ItemCheckoutStatus checkout(CheckoutType checkoutType, String user, String projectPath)
throws IOException {
+ if (checkoutMgr == null) {
+ throw new UnsupportedOperationException("item does not support checkin/checkout");
+ }
if (!isVersioned) {
throw new UnsupportedOperationException("Non-versioned item does not support checkout");
}
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException();
}
+
synchronized (fileSystem) {
ItemCheckoutStatus coStatus =
@@ -672,6 +649,9 @@ public abstract class LocalFolderItem implements FolderItem {
@Override
public void terminateCheckout(long checkoutId, boolean notify) throws IOException {
+ if (checkoutMgr == null) {
+ throw new UnsupportedOperationException("item does not support checkin/checkout");
+ }
if (!isVersioned) {
throw new UnsupportedOperationException("Non-versioned item does not support checkout");
}
@@ -700,6 +680,9 @@ public abstract class LocalFolderItem implements FolderItem {
throw new UnsupportedOperationException(
"Non-versioned item does not support checkout");
}
+ if (checkoutMgr == null) {
+ return null;
+ }
return checkoutMgr.getCheckout(checkoutId);
}
}
@@ -711,6 +694,9 @@ public abstract class LocalFolderItem implements FolderItem {
throw new UnsupportedOperationException(
"Non-versioned item does not support checkout");
}
+ if (checkoutMgr == null) {
+ return new ItemCheckoutStatus[0];
+ }
return checkoutMgr.getAllCheckouts();
}
}
@@ -802,33 +788,39 @@ public abstract class LocalFolderItem implements FolderItem {
* @param propertyFile property file which identifies the folder item.
* @return folder item
*/
- static LocalFolderItem getFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile) {
+ static LocalFolderItem getFolderItem(LocalFileSystem fileSystem,
+ ItemPropertyFile propertyFile) {
int fileType = propertyFile.getInt(FILE_TYPE, UNKNOWN_FILE_TYPE);
try {
if (fileType == DATAFILE_FILE_TYPE) {
- return new LocalDataFile(fileSystem, propertyFile);
+ return new LocalDataFileItem(fileSystem, propertyFile);
}
else if (fileType == DATABASE_FILE_TYPE) {
return new LocalDatabaseItem(fileSystem, propertyFile);
}
+ else if (fileType == LINK_FILE_TYPE) {
+ return new LocalTextDataItem(fileSystem, propertyFile);
+ }
else if (fileType == UNKNOWN_FILE_TYPE) {
- log.error("Folder item has unspecified file type: " +
- new File(propertyFile.getFolder(), propertyFile.getStorageName()));
+ log.error("Folder item has unspecified file type: " + new File(
+ propertyFile.getParentStorageDirectory(), propertyFile.getStorageName()));
}
else {
- log.error("Folder item has unsupported file type (" + fileType + "): " +
- new File(propertyFile.getFolder(), propertyFile.getStorageName()));
+ log.error("Folder item has unsupported file type (" + fileType + "): " + new File(
+ propertyFile.getParentStorageDirectory(), propertyFile.getStorageName()));
}
}
catch (FileNotFoundException e) {
log.error("Folder item may be corrupt due to missing file: " +
- new File(propertyFile.getFolder(), propertyFile.getStorageName()), e);
+ new File(propertyFile.getParentStorageDirectory(), propertyFile.getStorageName()),
+ e);
}
catch (IOException e) {
log.error("Folder item may be corrupt: " +
- new File(propertyFile.getFolder(), propertyFile.getStorageName()), e);
+ new File(propertyFile.getParentStorageDirectory(), propertyFile.getStorageName()),
+ e);
}
- return new UnknownFolderItem(fileSystem, propertyFile);
+ return new LocalUnknownFolderItem(fileSystem, propertyFile);
}
@Override
@@ -836,7 +828,7 @@ public abstract class LocalFolderItem implements FolderItem {
synchronized (fileSystem) {
if (isVersioned) {
try {
- return checkoutMgr.isCheckedOut();
+ return checkoutMgr != null && checkoutMgr.isCheckedOut();
}
catch (IOException e) {
Msg.error(getName() + " versioning error", e);
@@ -865,6 +857,11 @@ public abstract class LocalFolderItem implements FolderItem {
return false;
}
+ @Override
+ public int hashCode() {
+ return propertyFile.hashCode();
+ }
+
/**
* Update this non-versioned item with the latest version of the specified versioned item.
* @param versionedFolderItem versioned item which corresponds to this
@@ -892,6 +889,9 @@ public abstract class LocalFolderItem implements FolderItem {
@Override
public void updateCheckoutVersion(long checkoutId, int checkoutVersion, String user)
throws IOException {
+ if (checkoutMgr == null) {
+ throw new UnsupportedOperationException("item does not support checkin/checkout");
+ }
if (!isVersioned) {
throw new UnsupportedOperationException(
"updateCheckoutVersion is not applicable to non-versioned item");
@@ -907,4 +907,5 @@ public abstract class LocalFolderItem implements FolderItem {
checkoutMgr.updateCheckout(checkoutId, checkoutVersion);
}
}
+
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalTextDataItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalTextDataItem.java
new file mode 100644
index 0000000000..ecf7b61f44
--- /dev/null
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalTextDataItem.java
@@ -0,0 +1,170 @@
+/* ###
+ * 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.store.local;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.commons.lang3.StringUtils;
+
+import ghidra.framework.store.*;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * LocalTextDataItem provides a {@link LocalFolderItem} implementation
+ * which stores text data within the associated propertyFile and without any other data storage.
+ */
+public class LocalTextDataItem extends LocalFolderItem implements TextDataItem {
+
+ private static final String TEXT_PROPERTY = "TEXT";
+ private static final String VERSION_CREATE_USER = "CREATE_USER";
+ private static final String VERSION_CREATE_TIME = "CREATE_TIME";
+ private static final String VERSION_CREATE_COMMENT = "CREATE_COMMENT";
+
+ /**
+ * Constructor for an existing local link file item which corresponds to the specified
+ * property file.
+ * @param fileSystem file system
+ * @param propertyFile database property file
+ * @throws IOException if an IO Error occurs
+ */
+ public LocalTextDataItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile)
+ throws IOException {
+ super(fileSystem, propertyFile, false, false);
+ }
+
+ /**
+ * Create a new local text data file item.
+ * @param fileSystem file system
+ * @param propertyFile serialized data property file
+ * @param fileID file ID to be associated with new file or null
+ * @param contentType user content type
+ * @param textData text to be stored within associated property file
+ * @throws IOException if an IO Error occurs
+ */
+ public LocalTextDataItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile,
+ String fileID, String contentType, String textData) throws IOException {
+ super(fileSystem, propertyFile, false, true);
+
+ if (StringUtils.isBlank(contentType)) {
+ abortCreate();
+ throw new IllegalArgumentException("Missing content-type");
+ }
+
+ if (StringUtils.isBlank(textData)) {
+ abortCreate();
+ throw new IllegalArgumentException("Missing text data");
+ }
+
+ propertyFile.putInt(FILE_TYPE, LINK_FILE_TYPE);
+ propertyFile.putBoolean(READ_ONLY, false);
+ propertyFile.putString(CONTENT_TYPE, contentType);
+ if (fileID != null) {
+ propertyFile.setFileID(fileID);
+ }
+
+ propertyFile.putString(TEXT_PROPERTY, textData);
+
+ propertyFile.writeState();
+ }
+
+ /**
+ * Get the text data that was stored with this item
+ * @return text data
+ */
+ public String getTextData() {
+ return propertyFile.getString(TEXT_PROPERTY, null);
+ }
+
+ @Override
+ public long length() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public void updateCheckout(FolderItem versionedFolderItem, boolean updateItem,
+ TaskMonitor monitor) throws IOException {
+ throw new IOException("Versioning updates not supported");
+ }
+
+ @Override
+ public void updateCheckout(FolderItem item, int checkoutVersion) throws IOException {
+ throw new IOException("Versioning updates not supported");
+ }
+
+ @Override
+ void deleteMinimumVersion(String user) throws IOException {
+ throw new UnsupportedOperationException("Versioning updates not supported");
+ }
+
+ @Override
+ void deleteCurrentVersion(String user) throws IOException {
+ throw new UnsupportedOperationException("Versioning updates not supported");
+ }
+
+ @Override
+ public void output(File outputFile, int version, TaskMonitor monitor) throws IOException {
+ throw new IOException("Output not supported");
+ }
+
+ @Override
+ int getMinimumVersion() {
+ return getCurrentVersion();
+ }
+
+ @Override
+ public int getCurrentVersion() {
+ return 1; // only a single version of the file may exist
+ }
+
+ @Override
+ public boolean canRecover() {
+ return false;
+ }
+
+ /**
+ * Set the version info associated with this versioned file. Only a single version is
+ * supported.
+ * @param version version information (only user, create time and comment is retained)
+ * @throws IOException if an IO error occurs
+ */
+ public void setVersionInfo(Version version) throws IOException {
+ synchronized (fileSystem) {
+ if (!isVersioned()) {
+ throw new UnsupportedOperationException("Versioning not supported");
+ }
+ propertyFile.putString(VERSION_CREATE_USER, version.getUser());
+ propertyFile.putLong(VERSION_CREATE_TIME, version.getCreateTime());
+ propertyFile.putString(VERSION_CREATE_COMMENT, version.getComment());
+ propertyFile.writeState();
+ }
+ }
+
+ @Override
+ public synchronized Version[] getVersions() throws IOException {
+ synchronized (fileSystem) {
+ if (!isVersioned) {
+ throw new UnsupportedOperationException(
+ "Non-versioned item does not support getVersions");
+ }
+ String createUser = propertyFile.getString(VERSION_CREATE_USER, "");
+ long createTime = propertyFile.getLong(VERSION_CREATE_TIME, 0);
+ String comment = propertyFile.getString(VERSION_CREATE_COMMENT, null);
+ return new Version[] { new Version(1, createTime, createUser, comment) };
+ }
+ }
+
+}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/UnknownFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalUnknownFolderItem.java
similarity index 65%
rename from Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/UnknownFolderItem.java
rename to Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalUnknownFolderItem.java
index c630e32ae3..716ed09732 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/UnknownFolderItem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalUnknownFolderItem.java
@@ -19,16 +19,13 @@ import java.io.File;
import java.io.IOException;
import ghidra.framework.store.*;
-import ghidra.util.PropertyFile;
import ghidra.util.task.TaskMonitor;
/**
* UnknownFolderItem acts as a LocalFolderItem place-holder for
* items of an unknown type.
*/
-public class UnknownFolderItem extends LocalFolderItem {
-
- public static final String UNKNOWN_CONTENT_TYPE = "Unknown-File";
+public class LocalUnknownFolderItem extends LocalFolderItem implements UnknownFolderItem {
private final int fileType;
@@ -37,11 +34,11 @@ public class UnknownFolderItem extends LocalFolderItem {
* @param fileSystem local file system
* @param propertyFile property file associated with this item
*/
- UnknownFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile) {
+ LocalUnknownFolderItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile) {
super(fileSystem, propertyFile);
fileType = propertyFile.getInt(FILE_TYPE, UNKNOWN_FILE_TYPE);
}
-
+
/**
* Get the file type
* @return file type or -1 if unspecified
@@ -55,134 +52,82 @@ public class UnknownFolderItem extends LocalFolderItem {
return 0;
}
- /*
- * @see ghidra.framework.store.FolderItem#updateCheckout(ghidra.framework.store.FolderItem, boolean, ghidra.util.task.TaskMonitor)
- */
@Override
public void updateCheckout(FolderItem versionedFolderItem, boolean updateItem,
TaskMonitor monitor) throws IOException {
throw new UnsupportedOperationException();
}
- /*
- * @see ghidra.framework.store.FolderItem#updateCheckout(ghidra.framework.store.FolderItem, int)
- */
@Override
public void updateCheckout(FolderItem item, int checkoutVersion) throws IOException {
throw new UnsupportedOperationException();
}
- /*
- * @see ghidra.framework.store.FolderItem#checkout(java.lang.String)
- */
public synchronized ItemCheckoutStatus checkout(String user) throws IOException {
- throw new IOException(propertyFile.getName() +
- " may not be checked-out, item may be corrupt");
+ throw new IOException(
+ propertyFile.getName() + " may not be checked-out, item may be corrupt");
}
- /*
- * @see ghidra.framework.store.FolderItem#terminateCheckout(long)
- */
public synchronized void terminateCheckout(long checkoutId) {
// Do nothing
}
- /*
- * @see ghidra.framework.store.FolderItem#clearCheckout()
- */
@Override
public void clearCheckout() throws IOException {
// Do nothing
}
- /*
- * @see ghidra.framework.store.FolderItem#setCheckout(long, int, int)
- */
public void setCheckout(long checkoutId, int checkoutVersion, int localVersion) {
// Do nothing
}
- /*
- * @see ghidra.framework.store.FolderItem#getCheckout(long)
- */
@Override
public synchronized ItemCheckoutStatus getCheckout(long checkoutId) throws IOException {
return null;
}
- /*
- * @see ghidra.framework.store.FolderItem#getCheckouts()
- */
@Override
public synchronized ItemCheckoutStatus[] getCheckouts() throws IOException {
return new ItemCheckoutStatus[0];
}
- /*
- * @see ghidra.framework.store.FolderItem#getVersions()
- */
@Override
public synchronized Version[] getVersions() throws IOException {
throw new IOException("History data is unavailable for " + propertyFile.getName());
}
- /*
- * @see ghidra.framework.store.FolderItem#getContentType()
- */
@Override
public String getContentType() {
+ // NOTE: We could get the content type from the property file but we don't want any
+ // attempt to use it
return UNKNOWN_CONTENT_TYPE;
}
- /*
- * @see ghidra.framework.store.local.LocalFolderItem#deleteMinimumVersion(java.lang.String)
- */
@Override
void deleteMinimumVersion(String user) throws IOException {
-
throw new UnsupportedOperationException("Versioning not supported for UnknownFolderItems");
-
}
- /*
- * @see ghidra.framework.store.local.LocalFolderItem#deleteCurrentVersion(java.lang.String)
- */
@Override
void deleteCurrentVersion(String user) throws IOException {
-
throw new UnsupportedOperationException("Versioning not supported for UnknownFolderItems");
-
}
- /*
- * @see ghidra.framework.store.FolderItem#output(java.io.File, int, ghidra.util.task.TaskMonitor)
- */
@Override
public void output(File outputFile, int version, TaskMonitor monitor) throws IOException {
-
throw new UnsupportedOperationException("Output not supported for UnknownFolderItems");
-
}
- /*
- * @see ghidra.framework.store.local.LocalFolderItem#getMinimumVersion()
- */
@Override
int getMinimumVersion() throws IOException {
return -1;
}
- /*
- * @see ghidra.framework.store.FolderItem#getCurrentVersion()
- */
@Override
public int getCurrentVersion() {
return -1;
}
- /*
- * @see ghidra.framework.store.FolderItem#canRecover()
- */
@Override
public boolean canRecover() {
return false;
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteDatabaseItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteDatabaseItem.java
index b9e3d0b70f..43a9546403 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteDatabaseItem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteDatabaseItem.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -47,11 +47,6 @@ public class RemoteDatabaseItem extends RemoteFolderItem implements DatabaseItem
return repository.getLength(parentPath, itemName);
}
- @Override
- int getItemType() {
- return RepositoryItem.DATABASE;
- }
-
@Override
public boolean canRecover() {
return false;
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFileSystem.java
index 24f10a4700..1c57805418 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFileSystem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFileSystem.java
@@ -120,10 +120,13 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
if (items[i].getItemType() == RepositoryItem.DATABASE) {
folderItems[i] = new RemoteDatabaseItem(repository, items[i]);
}
+ else if (items[i].getItemType() == RepositoryItem.TEXT_DATA_FILE) {
+ folderItems[i] = new RemoteTextDataItem(repository, items[i]);
+ }
else {
- Msg.error(this,
- "Unsupported respository item encountered (" + items[i].getItemType() + "): " +
- folderPath + items[i].getName());
+ Msg.error(this, "Unsupported respository item encountered (" +
+ items[i].getItemType() + "): " + folderPath + items[i].getName());
+ folderItems[i] = new RemoteUnknownFolderItem(repository, items[i]);
}
}
return folderItems;
@@ -138,7 +141,10 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
if (item.getItemType() == RepositoryItem.DATABASE) {
return new RemoteDatabaseItem(repository, item);
}
- throw new IOException("Unsupported repository item type (" + item.getItemType() + ")");
+ if (item.getItemType() == RepositoryItem.TEXT_DATA_FILE) {
+ return new RemoteTextDataItem(repository, item);
+ }
+ return new RemoteUnknownFolderItem(repository, item);
}
@Override
@@ -150,7 +156,10 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
if (item.getItemType() == RepositoryItem.DATABASE) {
return new RemoteDatabaseItem(repository, item);
}
- throw new IOException("Unsupported repository item type (" + item.getItemType() + ")");
+ if (item.getItemType() == RepositoryItem.TEXT_DATA_FILE) {
+ return new RemoteTextDataItem(repository, item);
+ }
+ return new RemoteUnknownFolderItem(repository, item);
}
@Override
@@ -163,6 +172,17 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
throw new UnsupportedOperationException();
}
+ @Override
+ public boolean isSupportedItemType(FolderItem folderItem) {
+ if (folderItem instanceof DatabaseItem) {
+ return true; // assume this is always supported
+ }
+ if (folderItem instanceof TextDataItem) {
+ return true;
+ }
+ return false;
+ }
+
@Override
public ManagedBufferFile createDatabase(String parentPath, String name, String fileID,
String contentType, int bufferSize, String user, String projectPath)
@@ -207,6 +227,14 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener {
return (DataFileItem) getItem(parentPath, name);
}
+ @Override
+ public TextDataItem createTextDataItem(String parentPath, String name, String fileID,
+ String contentType, String textData, String comment)
+ throws InvalidNameException, IOException {
+ repository.createTextDataFile(parentPath, name, fileID, contentType, textData, comment);
+ return (TextDataItem) getItem(parentPath, name);
+ }
+
@Override
public FolderItem createFile(String parentPath, String name, File packedFile,
TaskMonitor monitor, String user)
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFolderItem.java
index 1da09bb400..aa077eee3c 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFolderItem.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFolderItem.java
@@ -16,11 +16,11 @@
package ghidra.framework.store.remote;
import java.io.IOException;
+import java.util.Objects;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.remote.RepositoryItem;
import ghidra.framework.store.*;
-import ghidra.framework.store.local.UnknownFolderItem;
/**
* RemoteFolderItem provides an abstract FolderItem implementation
@@ -36,6 +36,8 @@ public abstract class RemoteFolderItem implements FolderItem {
protected int version;
protected long versionTime;
+ protected String textData; // applies to TextDataItem only
+
protected RepositoryAdapter repository;
/**
@@ -56,15 +58,9 @@ public abstract class RemoteFolderItem implements FolderItem {
version = item.getVersion();
versionTime = item.getVersionTime();
- }
- /**
- * Returns the item type as defined by RepositoryItem which corresponds to specific
- * implementation of this class.
- * @return item type (Only {@link RepositoryItem#DATABASE} is supported).
- * @see ghidra.framework.remote.RepositoryItem
- */
- abstract int getItemType();
+ textData = item.getTextData();
+ }
@Override
public String getName() {
@@ -74,11 +70,13 @@ public abstract class RemoteFolderItem implements FolderItem {
@Override
public RemoteFolderItem refresh() throws IOException {
RepositoryItem item = repository.getItem(parentPath, itemName);
- if (item == null) {
+ if (item == null || !Objects.equals(fileID, item.getFileID()) ||
+ !contentType.equals(item.getContentType())) {
return null;
}
version = item.getVersion();
versionTime = item.getVersionTime();
+ textData = item.getTextData();
return this;
}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteTextDataItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteTextDataItem.java
new file mode 100644
index 0000000000..09652a749d
--- /dev/null
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteTextDataItem.java
@@ -0,0 +1,72 @@
+/* ###
+ * 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.store.remote;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.help.UnsupportedOperationException;
+
+import ghidra.framework.client.RepositoryAdapter;
+import ghidra.framework.remote.RepositoryItem;
+import ghidra.framework.store.TextDataItem;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class RemoteTextDataItem extends RemoteFolderItem implements TextDataItem {
+
+ RemoteTextDataItem(RepositoryAdapter repository, RepositoryItem item) {
+ super(repository, item);
+ }
+
+ @Override
+ public long length() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public boolean hasCheckouts() throws IOException {
+ return false;
+ }
+
+ @Override
+ public boolean canRecover() {
+ return false;
+ }
+
+ @Override
+ public boolean isCheckinActive() throws IOException {
+ return false;
+ }
+
+ @Override
+ public void updateCheckoutVersion(long checkoutId, int checkoutVersion, String user)
+ throws IOException {
+ throw new UnsupportedOperationException("Text data files do not support checkin");
+ }
+
+ @Override
+ public void output(File outputFile, int ver, TaskMonitor monitor)
+ throws IOException, CancelledException {
+ throw new UnsupportedOperationException("Text data files do not support serial output");
+ }
+
+ @Override
+ public String getTextData() {
+ return textData;
+ }
+
+}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteUnknownFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteUnknownFolderItem.java
new file mode 100644
index 0000000000..c95e7b4688
--- /dev/null
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteUnknownFolderItem.java
@@ -0,0 +1,75 @@
+/* ###
+ * 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.store.remote;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.help.UnsupportedOperationException;
+
+import ghidra.framework.client.RepositoryAdapter;
+import ghidra.framework.remote.RepositoryItem;
+import ghidra.framework.store.UnknownFolderItem;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class RemoteUnknownFolderItem extends RemoteFolderItem implements UnknownFolderItem {
+
+ private final int fileType;
+
+ RemoteUnknownFolderItem(RepositoryAdapter repository, RepositoryItem item) {
+ super(repository, item);
+ fileType = item.getItemType();
+ }
+
+ @Override
+ public int getFileType() {
+ return fileType;
+ }
+
+ @Override
+ public long length() throws IOException {
+ return 0;
+ }
+
+ @Override
+ public boolean hasCheckouts() throws IOException {
+ return false;
+ }
+
+ @Override
+ public boolean canRecover() {
+ return false;
+ }
+
+ @Override
+ public boolean isCheckinActive() throws IOException {
+ return false;
+ }
+
+ @Override
+ public void updateCheckoutVersion(long checkoutId, int checkoutVersion, String user)
+ throws IOException {
+ throw new UnsupportedOperationException("Text data files do not support checkin");
+ }
+
+ @Override
+ public void output(File outputFile, int ver, TaskMonitor monitor)
+ throws IOException, CancelledException {
+ throw new UnsupportedOperationException("Text data files do not support serial output");
+ }
+
+}
diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/PropertyFile.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/PropertyFile.java
index 85079a383e..abb5b7646e 100644
--- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/PropertyFile.java
+++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/PropertyFile.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,23 +15,22 @@
*/
package ghidra.util;
-import generic.stl.Pair;
-import ghidra.framework.store.FileSystem;
+import java.io.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.xml.sax.*;
+
+import ghidra.framework.store.local.ItemPropertyFile;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.xml.XmlUtilities;
import ghidra.xml.NonThreadedXmlPullParserImpl;
import ghidra.xml.XmlElement;
-import java.io.*;
-import java.util.HashMap;
-import java.util.Map.Entry;
-
-import org.xml.sax.*;
-
/**
- * Class that represents a file of property names and values. The file
- * extension used is PROPERTY_EXT.
- *
+ * {@link ItemPropertyFile} provides basic property storage. The file extension
+ * used is {@link #PROPERTY_EXT}.
*/
public class PropertyFile {
@@ -40,15 +39,17 @@ public class PropertyFile {
*/
public final static String PROPERTY_EXT = ".prp";
- private static final String FILE_ID = "FILE_ID";
-
protected File propertyFile;
protected String storageName;
- protected String parentPath;
- protected String name;
+ //@formatter:off
private static enum PropertyEntryType {
- INT_TYPE("int"), LONG_TYPE("long"), BOOLEAN_TYPE("boolean"), STRING_TYPE("string");
+ INT_TYPE("int"),
+ LONG_TYPE("long"),
+ BOOLEAN_TYPE("boolean"),
+ STRING_TYPE("string");
+ //@formatter:on
+
PropertyEntryType(String rep) {
this.rep = rep;
}
@@ -65,25 +66,25 @@ public class PropertyFile {
}
}
- private HashMap> map =
- new HashMap>();
+ private record PropertyMapEntry(PropertyEntryType entityType, String value) {
+ // no behaviors
+ }
+
+ private Map basicInfoMap = new HashMap();
/**
* Construct a new or existing PropertyFile.
- * This form ignores retained property values for NAME and PARENT path.
- * @param dir parent directory
+ * This constructor ignores retained property values for NAME and PARENT path.
+ * This constructor will not throw an exception if the file does not exist.
+ * @param dir native directory where this file is stored
* @param storageName stored property file name (without extension)
- * @param parentPath path to parent
- * @param name name of the property file
- * @throws IOException
+ * @throws InvalidObjectException if a file parse error occurs
+ * @throws IOException if an IO error occurs reading an existing file
*/
- public PropertyFile(File dir, String storageName, String parentPath, String name)
- throws IOException {
+ public PropertyFile(File dir, String storageName) throws IOException {
if (!dir.isAbsolute()) {
throw new IllegalArgumentException("dir must be specified by an absolute path");
}
- this.name = name;
- this.parentPath = parentPath;
this.storageName = storageName;
propertyFile = new File(dir, storageName + PROPERTY_EXT);
if (propertyFile.exists()) {
@@ -91,75 +92,33 @@ public class PropertyFile {
}
}
- /**
- * Return the name of this PropertyFile. A null value may be returned
- * if this is an older property file and the name was not specified at
- * time of construction.
- */
- public String getName() {
- return name;
+ protected boolean contains(String key) {
+ return basicInfoMap.containsKey(key);
}
/**
- * Returns true if file is writable
+ * {@return true if file is read-only as reported by underlying native file-system}
*/
public boolean isReadOnly() {
return !propertyFile.canWrite();
}
/**
- * Return the path to this PropertyFile. A null value may be returned
- * if this is an older property file and the name and parentPath was not specified at
- * time of construction.
+ * {@return the native parent storage directory containing this PropertyFile.}
*/
- public String getPath() {
- if (parentPath == null || name == null) {
- return null;
- }
- if (parentPath.length() == 1) {
- return parentPath + name;
- }
- return parentPath + FileSystem.SEPARATOR_CHAR + name;
- }
-
- /**
- * Return the path to the parent of this PropertyFile.
- */
- public String getParentPath() {
- return parentPath;
- }
-
- /**
- * Return the parent file to this PropertyFile.
- */
- public File getFolder() {
+ public File getParentStorageDirectory() {
return propertyFile.getParentFile();
}
/**
- * Return the storage name of this PropertyFile. This name does not include the property
+ * Return the native storage name for this PropertyFile. This name does not include the property
* file extension (.prp)
+ * @return native storage name
*/
public String getStorageName() {
return storageName;
}
- /**
- * Returns the FileID associated with this file.
- * @return FileID associated with this file
- */
- public String getFileID() {
- return getString(FILE_ID, null);
- }
-
- /**
- * Set the FileID associated with this file.
- * @param fileId
- */
- public void setFileID(String fileId) {
- putString(FILE_ID, fileId);
- }
-
/**
* Return the int value with the given propertyName.
* @param propertyName name of property that is an int
@@ -167,13 +126,12 @@ public class PropertyFile {
* @return int value
*/
public int getInt(String propertyName, int defaultValue) {
- Pair pair = map.get(propertyName);
- if (pair == null || pair.first != PropertyEntryType.INT_TYPE) {
+ PropertyMapEntry entry = basicInfoMap.get(propertyName);
+ if (entry == null || entry.entityType != PropertyEntryType.INT_TYPE) {
return defaultValue;
}
try {
- String value = pair.second;
- return Integer.parseInt(value);
+ return Integer.parseInt(entry.value);
}
catch (NumberFormatException e) {
return defaultValue;
@@ -186,8 +144,8 @@ public class PropertyFile {
* @param value value to set
*/
public void putInt(String propertyName, int value) {
- map.put(propertyName, new Pair(PropertyEntryType.INT_TYPE,
- Integer.toString(value)));
+ basicInfoMap.put(propertyName,
+ new PropertyMapEntry(PropertyEntryType.INT_TYPE, Integer.toString(value)));
}
/**
@@ -197,13 +155,12 @@ public class PropertyFile {
* @return long value
*/
public long getLong(String propertyName, long defaultValue) {
- Pair pair = map.get(propertyName);
- if (pair == null || pair.first != PropertyEntryType.LONG_TYPE) {
+ PropertyMapEntry entry = basicInfoMap.get(propertyName);
+ if (entry == null || entry.entityType != PropertyEntryType.LONG_TYPE) {
return defaultValue;
}
try {
- String value = pair.second;
- return Long.parseLong(value);
+ return Long.parseLong(entry.value);
}
catch (NumberFormatException e) {
return defaultValue;
@@ -216,8 +173,8 @@ public class PropertyFile {
* @param value value to set
*/
public void putLong(String propertyName, long value) {
- map.put(propertyName,
- new Pair(PropertyEntryType.LONG_TYPE, Long.toString(value)));
+ basicInfoMap.put(propertyName,
+ new PropertyMapEntry(PropertyEntryType.LONG_TYPE, Long.toString(value)));
}
/**
@@ -227,12 +184,11 @@ public class PropertyFile {
* @return string value
*/
public String getString(String propertyName, String defaultValue) {
- Pair pair = map.get(propertyName);
- if (pair == null || pair.first != PropertyEntryType.STRING_TYPE) {
+ PropertyMapEntry entry = basicInfoMap.get(propertyName);
+ if (entry == null || entry.entityType != PropertyEntryType.STRING_TYPE) {
return defaultValue;
}
- String value = pair.second;
- return value;
+ return entry.value;
}
/**
@@ -241,8 +197,13 @@ public class PropertyFile {
* @param value value to set
*/
public void putString(String propertyName, String value) {
- map.put(propertyName, new Pair(PropertyEntryType.STRING_TYPE,
- value));
+ if (value == null) {
+ basicInfoMap.remove(propertyName);
+ }
+ else {
+ basicInfoMap.put(propertyName,
+ new PropertyMapEntry(PropertyEntryType.STRING_TYPE, value));
+ }
}
/**
@@ -252,12 +213,11 @@ public class PropertyFile {
* @return boolean value
*/
public boolean getBoolean(String propertyName, boolean defaultValue) {
- Pair pair = map.get(propertyName);
- if (pair == null || pair.first != PropertyEntryType.BOOLEAN_TYPE) {
+ PropertyMapEntry entry = basicInfoMap.get(propertyName);
+ if (entry == null || entry.entityType != PropertyEntryType.BOOLEAN_TYPE) {
return defaultValue;
}
- String value = pair.second;
- return Boolean.parseBoolean(value);
+ return Boolean.parseBoolean(entry.value);
}
/**
@@ -266,20 +226,21 @@ public class PropertyFile {
* @param value value to set
*/
public void putBoolean(String propertyName, boolean value) {
- map.put(propertyName, new Pair(PropertyEntryType.BOOLEAN_TYPE,
- Boolean.toString(value)));
+ basicInfoMap.put(propertyName,
+ new PropertyMapEntry(PropertyEntryType.BOOLEAN_TYPE, Boolean.toString(value)));
}
/**
* Remove the specified property
- * @param propertyName
+ * @param propertyName name of property to be removed
*/
public void remove(String propertyName) {
- map.remove(propertyName);
+ basicInfoMap.remove(propertyName);
}
/**
- * Return the time of last modification in number of milliseconds.
+ * Return the time of last modification in number of milliseconds
+ * @return time of last modification
*/
public long lastModified() {
return propertyFile.lastModified();
@@ -290,15 +251,17 @@ public class PropertyFile {
* @throws IOException thrown if there was a problem writing the file
*/
public void writeState() throws IOException {
+ // NOTE: To avoid severe incompatibility with older versions of Ghidra this XML
+ // schema should not be changed.
PrintWriter writer = new PrintWriter(propertyFile);
try {
writer.println("");
writer.println("");
writer.println(" ");
- for (Entry> entry : map.entrySet()) {
+ for (Entry entry : basicInfoMap.entrySet()) {
String propertyName = entry.getKey();
- String propertyType = entry.getValue().first.rep;
- String propertyValue = entry.getValue().second;
+ String propertyType = entry.getValue().entityType.rep;
+ String propertyValue = entry.getValue().value;
writer.print(" (propertyType,
- propertyValue));
+ basicInfoMap.put(propertyName, new PropertyMapEntry(propertyType, propertyValue));
parser.end(state);
}
parser.end(basic_info);
parser.end(file_info);
}
catch (Exception e) {
- Msg.error(this, "Unexpected Exception: " + e.getMessage(), e);
- throw new InvalidObjectException("XML parse error in properties file");
+ String msg = "XML parse error in properties file";
+ Msg.error(this, msg + ": " + propertyFile);
+ throw new InvalidObjectException(msg);
}
finally {
if (parser != null) {
@@ -368,20 +333,19 @@ public class PropertyFile {
/**
* Move this PropertyFile to the newParent file.
- * @param newParent new parent of the file
- * @param newStorageName new storage name
- * @param newParentPath parent path of the new parent
- * @param newName new name for this PropertyFile
+ * @param newStorageParent new storage parent of the native file
+ * @param newStorageName new storage name for this property file
* @throws IOException thrown if there was a problem accessing the
* @throws DuplicateFileException thrown if a file with the newName
* already exists
*/
- public void moveTo(File newParent, String newStorageName, String newParentPath, String newName)
+ public void moveTo(File newStorageParent, String newStorageName)
throws DuplicateFileException, IOException {
- if (!newParent.equals(propertyFile.getParentFile()) || !newStorageName.equals(storageName)) {
- File newPropertyFile = new File(newParent, newStorageName + PROPERTY_EXT);
+ if (!newStorageParent.equals(propertyFile.getParentFile()) ||
+ !newStorageName.equals(storageName)) {
+ File newPropertyFile = new File(newStorageParent, newStorageName + PROPERTY_EXT);
if (newPropertyFile.exists()) {
- throw new DuplicateFileException(newName + " already exists");
+ throw new DuplicateFileException(newPropertyFile + " already exists");
}
if (!propertyFile.renameTo(newPropertyFile)) {
throw new IOException("move failed");
@@ -389,12 +353,11 @@ public class PropertyFile {
propertyFile = newPropertyFile;
storageName = newStorageName;
}
- parentPath = newParentPath;
- name = newName;
}
/**
* Return whether the file for this PropertyFile exists.
+ * @return true if this file exists
*/
public boolean exists() {
return propertyFile.exists();
@@ -409,10 +372,7 @@ public class PropertyFile {
@Override
public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + ((propertyFile == null) ? 0 : propertyFile.hashCode());
- return result;
+ return propertyFile.hashCode();
}
@Override
@@ -426,15 +386,8 @@ public class PropertyFile {
if (getClass() != obj.getClass()) {
return false;
}
- PropertyFile other = (PropertyFile) obj;
- if (propertyFile == null) {
- if (other.propertyFile != null) {
- return false;
- }
- }
- else if (!propertyFile.equals(other.propertyFile)) {
- return false;
- }
- return true;
+ ItemPropertyFile other = (ItemPropertyFile) obj;
+ return propertyFile.equals(other.propertyFile);
}
+
}
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 b65503a6ce..5ca2f5429e 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -232,8 +232,9 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
// Get storage name based upon data dir name ~.db
String storageName = dataDir.getName();
- storageName = storageName.substring(0,
- storageName.length() - LocalFolderItem.DATA_DIR_EXTENSION.length()).substring(1);
+ storageName = storageName
+ .substring(0, storageName.length() - LocalFolderItem.DATA_DIR_EXTENSION.length())
+ .substring(1);
File propertyFile =
new File(dataDir.getParentFile(), storageName + PropertyFile.PROPERTY_EXT);
assertTrue(propertyFile.isFile());
@@ -397,6 +398,12 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
fs.createFolder("/", "aaa");
+ // item's name and folder name may replicate each other
+ DataFileItem file = createItem(dataBytes, "/", "aaa");
+ assertNotNull(file);
+ assertTrue(fs.folderExists("/aaa"));
+ assertTrue(fs.fileExists("/", "aaa"));
+
createItem(dataBytes, "/aaa", "~)(%$#@!JGJ");
for (char cstart = 20; cstart < 255; cstart += fs.getMaxNameLength()) {
@@ -584,8 +591,9 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
// Get storage name based upon data dir name ~.db
String storageName = dataDir.getName();
- storageName = storageName.substring(0,
- storageName.length() - LocalFolderItem.DATA_DIR_EXTENSION.length()).substring(1);
+ storageName = storageName
+ .substring(0, storageName.length() - LocalFolderItem.DATA_DIR_EXTENSION.length())
+ .substring(1);
File propertyFile =
new File(dataDir.getParentFile(), storageName + PropertyFile.PROPERTY_EXT);
assertTrue(propertyFile.isFile());
@@ -615,8 +623,8 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
assertEquals("fred", items[1]);
assertEquals("greg", items[2]);
- assertEquals(LocalDataFile.class, fs.getItem("/abc", items[0]).getClass());
- assertEquals(LocalDataFile.class, fs.getItem("/abc", items[1]).getClass());
+ assertEquals(LocalDataFileItem.class, fs.getItem("/abc", items[0]).getClass());
+ assertEquals(LocalDataFileItem.class, fs.getItem("/abc", items[1]).getClass());
assertEquals(LocalDatabaseItem.class, fs.getItem("/abc", items[2]).getClass());
df = (DataFileItem) fs.getItem("/abc", items[0]);
@@ -649,7 +657,7 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
fs.moveItem("/abc", "fred", "/xyz", "bob");
assertNull(fs.getItem("/abc", "fred"));
- LocalDataFile df = (LocalDataFile) fs.getItem("/xyz", "bob");
+ LocalDataFileItem df = (LocalDataFileItem) fs.getItem("/xyz", "bob");
assertNotNull(df);
try (InputStream in = df.getInputStream()) {
diff --git a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/IndexedPropertyFileTest.java b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/IndexedPropertyFileTest.java
index 63d0217bf9..161bbe4180 100644
--- a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/IndexedPropertyFileTest.java
+++ b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/IndexedPropertyFileTest.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,8 +15,7 @@
*/
package ghidra.framework.store.local;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
import java.io.File;
import java.net.URLDecoder;
@@ -39,7 +38,7 @@ public class IndexedPropertyFileTest extends AbstractGenericTest {
String storageName = NamingUtilities.mangle(NAME);
- PropertyFile pf = new IndexedPropertyFile(parent, storageName, "/", NAME);
+ ItemPropertyFile pf = new IndexedPropertyFile(parent, storageName, "/", NAME);
assertEquals(storageName, pf.getStorageName());
assertEquals(NAME, pf.getName());
assertEquals("/", pf.getParentPath());
@@ -63,29 +62,44 @@ public class IndexedPropertyFileTest extends AbstractGenericTest {
pf.writeState();
- PropertyFile pf2 = new IndexedPropertyFile(parent, storageName, "/", NAME);
- pf2.readState();
+ pf = new IndexedPropertyFile(parent, storageName, "/X", "XXX");
+ // existing file ignores supplied name and parent path
+ assertEquals(storageName, pf.getStorageName());
+ assertEquals(NAME, pf.getName());
+ assertEquals("/", pf.getParentPath());
+ assertEquals("/" + NAME, pf.getPath());
+ assertTrue(pf.getBoolean("TestBooleanTrue", false));
+ assertTrue(!pf.getBoolean("TestBooleanFalse", true));
+ assertTrue(pf.getBoolean("TestBooleanBad", true));
+ assertEquals(1234, pf.getInt("TestInt", -1));
+ assertEquals(0x12345678, pf.getLong("TestLong", -1));
+ assertEquals(str, URLDecoder.decode(pf.getString("TestString", null), "UTF-8"));
- assertTrue(pf2.getBoolean("TestBooleanTrue", false));
- assertTrue(!pf2.getBoolean("TestBooleanFalse", true));
- assertTrue(pf2.getBoolean("TestBooleanBad", true));
- assertEquals(1234, pf2.getInt("TestInt", -1));
- assertEquals(0x12345678, pf2.getLong("TestLong", -1));
- assertEquals(str, URLDecoder.decode(pf2.getString("TestString", null), "UTF-8"));
+ pf = new IndexedPropertyFile(parent, storageName);
+ assertEquals(storageName, pf.getStorageName());
+ assertEquals(NAME, pf.getName());
+ assertEquals("/", pf.getParentPath());
+ assertEquals("/" + NAME, pf.getPath());
- PropertyFile pf3 =
- new IndexedPropertyFile(new File(parent, storageName + PropertyFile.PROPERTY_EXT));
- assertEquals(storageName, pf3.getStorageName());
- assertEquals(NAME, pf3.getName());
- assertEquals("/", pf3.getParentPath());
- assertEquals("/" + NAME, pf3.getPath());
+ assertTrue(pf.getBoolean("TestBooleanTrue", false));
+ assertTrue(!pf.getBoolean("TestBooleanFalse", true));
+ assertTrue(pf.getBoolean("TestBooleanBad", true));
+ assertEquals(1234, pf.getInt("TestInt", -1));
+ assertEquals(0x12345678, pf.getLong("TestLong", -1));
+ assertEquals(str, URLDecoder.decode(pf.getString("TestString", null), "UTF-8"));
- assertTrue(pf3.getBoolean("TestBooleanTrue", false));
- assertTrue(!pf3.getBoolean("TestBooleanFalse", true));
- assertTrue(pf3.getBoolean("TestBooleanBad", true));
- assertEquals(1234, pf3.getInt("TestInt", -1));
- assertEquals(0x12345678, pf3.getLong("TestLong", -1));
- assertEquals(str, URLDecoder.decode(pf3.getString("TestString", null), "UTF-8"));
+ pf = new IndexedPropertyFile(new File(parent, storageName + PropertyFile.PROPERTY_EXT));
+ assertEquals(storageName, pf.getStorageName());
+ assertEquals(NAME, pf.getName());
+ assertEquals("/", pf.getParentPath());
+ assertEquals("/" + NAME, pf.getPath());
+
+ assertTrue(pf.getBoolean("TestBooleanTrue", false));
+ assertTrue(!pf.getBoolean("TestBooleanFalse", true));
+ assertTrue(pf.getBoolean("TestBooleanBad", true));
+ assertEquals(1234, pf.getInt("TestInt", -1));
+ assertEquals(0x12345678, pf.getLong("TestLong", -1));
+ assertEquals(str, URLDecoder.decode(pf.getString("TestString", null), "UTF-8"));
}
diff --git a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/MangledLocalFileSystemTest.java b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/MangledLocalFileSystemTest.java
index b1b75a6281..895734c686 100644
--- a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/MangledLocalFileSystemTest.java
+++ b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/MangledLocalFileSystemTest.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -25,6 +25,7 @@ import java.util.List;
import org.junit.Test;
import ghidra.framework.store.DataFileItem;
+import utilities.util.FileUtilities;
public class MangledLocalFileSystemTest extends AbstractLocalFileSystemTest {
@@ -35,6 +36,10 @@ public class MangledLocalFileSystemTest extends AbstractLocalFileSystemTest {
@Test
public void testMigration() throws Exception {
+ File tmpProjectDir = new File(projectDir.getParentFile(),
+ LocalFileSystem.HIDDEN_DIR_PREFIX + '.' + projectDir.getName());
+ FileUtilities.deleteDir(tmpProjectDir);
+
testFilePaths();
List names = new ArrayList();
diff --git a/Ghidra/Framework/FileSystem/src/test/java/ghidra/framework/store/local/ItemPropertyFileTest.java b/Ghidra/Framework/FileSystem/src/test/java/ghidra/framework/store/local/ItemPropertyFileTest.java
new file mode 100644
index 0000000000..ce774ec4de
--- /dev/null
+++ b/Ghidra/Framework/FileSystem/src/test/java/ghidra/framework/store/local/ItemPropertyFileTest.java
@@ -0,0 +1,56 @@
+/* ###
+ * 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.store.local;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import ghidra.util.PropertyFileTest;
+
+public class ItemPropertyFileTest extends PropertyFileTest {
+
+ public ItemPropertyFileTest() {
+ super();
+ }
+
+ @Override
+ protected ItemPropertyFile getPropertyFile() throws IOException {
+ return new ItemPropertyFile(storageDir, storageName, "/", NAME);
+ }
+
+ @Test
+ public void testPropertyFileName() throws Exception {
+
+ ItemPropertyFile pf = getPropertyFile();
+ assertEquals(storageName, pf.getStorageName());
+ assertEquals(NAME, pf.getName());
+ assertEquals("/", pf.getParentPath());
+ assertEquals("/" + NAME, pf.getPath());
+ pf.writeState();
+
+ pf = getPropertyFile();
+ assertTrue(pf.exists());
+ assertEquals(storageName, pf.getStorageName());
+ assertEquals(NAME, pf.getName());
+ assertEquals("/", pf.getParentPath());
+ assertEquals("/" + NAME, pf.getPath());
+
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/test/java/ghidra/util/PropertyFileTest.java b/Ghidra/Framework/FileSystem/src/test/java/ghidra/util/PropertyFileTest.java
similarity index 77%
rename from Ghidra/Features/Base/src/test/java/ghidra/util/PropertyFileTest.java
rename to Ghidra/Framework/FileSystem/src/test/java/ghidra/util/PropertyFileTest.java
index 6597d69576..60d2eebbb6 100644
--- a/Ghidra/Features/Base/src/test/java/ghidra/util/PropertyFileTest.java
+++ b/Ghidra/Framework/FileSystem/src/test/java/ghidra/util/PropertyFileTest.java
@@ -18,37 +18,43 @@ package ghidra.util;
import static org.junit.Assert.*;
import java.io.File;
+import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
+import org.junit.Before;
import org.junit.Test;
import generic.test.AbstractGenericTest;
+import ghidra.util.NamingUtilities;
+import ghidra.util.PropertyFile;
public class PropertyFileTest extends AbstractGenericTest {
- private static String NAME = "Test";
+ protected static String NAME = "Test";
+
+ protected String storageName;
+ protected File storageDir;
- /**
- * Constructor for PropertyFileTest.
- * @param arg0
- */
public PropertyFileTest() {
super();
}
+ @Before
+ public void setUp() throws IOException {
+ storageDir = createTempDirectory(getName());
+ storageName = NamingUtilities.mangle(NAME);
+ }
+
+ protected PropertyFile getPropertyFile() throws IOException {
+ return new PropertyFile(storageDir, storageName);
+ }
+
@Test
public void testPropertyFile() throws Exception {
- String storageName = NamingUtilities.mangle(NAME);
-
- File parent = createTempDirectory(getName());
-
- PropertyFile pf = new PropertyFile(parent, storageName, "/", NAME);
+ PropertyFile pf = getPropertyFile();
assertEquals(storageName, pf.getStorageName());
- assertEquals(NAME, pf.getName());
- assertEquals("/", pf.getParentPath());
- assertEquals("/" + NAME, pf.getPath());
pf.putBoolean("TestBooleanTrue", true);
pf.putBoolean("TestBooleanFalse", false);
@@ -73,8 +79,8 @@ public class PropertyFileTest extends AbstractGenericTest {
pf.writeState();
- PropertyFile pf2 = new PropertyFile(parent, storageName, "/", NAME);
- pf2.readState();
+ PropertyFile pf2 = getPropertyFile();
+ assertTrue(pf2.exists()); // state will be read at construction time
assertTrue(pf2.getBoolean("TestBooleanTrue", false));
assertTrue(!pf2.getBoolean("TestBooleanFalse", true));
diff --git a/Ghidra/Framework/Gui/src/main/java/resources/MultiIcon.java b/Ghidra/Framework/Gui/src/main/java/resources/MultiIcon.java
index e78c5d5138..fc81345147 100644
--- a/Ghidra/Framework/Gui/src/main/java/resources/MultiIcon.java
+++ b/Ghidra/Framework/Gui/src/main/java/resources/MultiIcon.java
@@ -148,7 +148,7 @@ public class MultiIcon implements Icon {
if (disabled) {
// Alpha blend to background
- Color bgColor = c.getBackground();
+ Color bgColor = c != null ? c.getBackground() : Color.gray;
g.setColor(ColorUtils.withAlpha(bgColor, 128));
g.fillRect(x, y, width, height);
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/core/help/AboutDomainObjectUtils.java b/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/core/help/AboutDomainObjectUtils.java
index 164889a6c5..e4c342651b 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/core/help/AboutDomainObjectUtils.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/core/help/AboutDomainObjectUtils.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -19,7 +19,8 @@ import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.awt.event.*;
-import java.util.*;
+import java.util.Date;
+import java.util.Map;
import javax.swing.*;
import javax.swing.text.JTextComponent;
@@ -102,10 +103,11 @@ public class AboutDomainObjectUtils {
addInfo(aboutPanel, "Last Modified:", (new Date(lastModified)).toString());
}
addInfo(aboutPanel, "Readonly:", Boolean.toString(domainFile.isReadOnly()));
+ if (metadata.isEmpty() && domainFile.isLink()) {
+ addInfo(aboutPanel, "Link path/url:", domainFile.getLinkInfo().getLinkPath());
+ }
- Iterator it = metadata.keySet().iterator();
- while (it.hasNext()) {
- String key = it.next();
+ for (String key : metadata.keySet()) {
String value = metadata.get(key);
addInfo(aboutPanel, key + ":", value);
}
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 39b0c04fef..dd21fb0ebf 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
@@ -21,9 +21,7 @@ import javax.help.UnsupportedOperationException;
import javax.swing.Icon;
import ghidra.framework.model.*;
-import ghidra.framework.store.FileSystem;
-import ghidra.framework.store.FolderItem;
-import ghidra.framework.store.local.UnknownFolderItem;
+import ghidra.framework.store.*;
import ghidra.util.InvalidNameException;
import ghidra.util.classfinder.ExtensionPoint;
import ghidra.util.exception.CancelledException;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java
index f9023969ed..283a78cd7c 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java
@@ -19,6 +19,8 @@ import java.io.*;
import java.net.URL;
import java.util.*;
+import org.apache.commons.lang3.StringUtils;
+
import docking.widgets.OptionDialog;
import generic.timer.GhidraSwinglessTimer;
import ghidra.framework.client.*;
@@ -253,8 +255,7 @@ public class DefaultProjectData implements ProjectData {
*/
public static Properties readProjectProperties(File projectDir) {
try {
- PropertyFile pf =
- new PropertyFile(projectDir, PROPERTY_FILENAME, "/", PROPERTY_FILENAME);
+ PropertyFile pf = new PropertyFile(projectDir, PROPERTY_FILENAME);
if (pf.exists()) {
Properties properties = new Properties();
@@ -281,7 +282,7 @@ public class DefaultProjectData implements ProjectData {
throws IOException, LockException {
projectDir = localStorageLocator.getProjectDir();
- properties = new PropertyFile(projectDir, PROPERTY_FILENAME, "/", PROPERTY_FILENAME);
+ properties = new PropertyFile(projectDir, PROPERTY_FILENAME);
if (create) {
if (projectDir.exists()) {
throw new DuplicateFileException(
@@ -347,7 +348,7 @@ public class DefaultProjectData implements ProjectData {
}
String defaultMsg = "Unable to lock project! " + locator;
-
+
// in headless mode, just spit out an error
if (!allowInteractiveForce || SystemUtilities.isInHeadlessMode()) {
throw new LockException(defaultMsg);
@@ -358,8 +359,8 @@ public class DefaultProjectData implements ProjectData {
String lockInformation = lock.getExistingLockFileInformation();
if (!lock.canForceLock()) {
String msg = "Project is locked. You have another instance of Ghidra
" +
- "already running with this project open (locally or remotely).
" +
- projectStr + "
" + "Lock information: " + lockInformation;
+ "already running with this project open (locally or remotely).
" +
+ projectStr + "
" + "Lock information: " + lockInformation;
throw new LockException(msg);
}
@@ -586,35 +587,12 @@ public class DefaultProjectData implements ProjectData {
@Override
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 + "'");
- }
+ return getFolder(path, DomainFolderFilter.ALL_INTERNAL_FOLDERS_FILTER);
+ }
- DomainFolder folder = getRootFolder();
- String[] split = path.split(FileSystem.SEPARATOR);
- if (split.length == 0) {
- return folder;
- }
-
- 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
+ public DomainFolder getFolder(String path, DomainFolderFilter filter) {
+ return ProjectDataUtils.getDomainFolder(getRootFolder(), path, filter);
}
@Override
@@ -658,25 +636,32 @@ public class DefaultProjectData implements ProjectData {
@Override
public DomainFile getFile(String path) {
- int len = path.length();
- if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) {
+ return getFile(path, DomainFileFilter.ALL_INTERNAL_FILES_FILTER);
+ }
+
+ @Override
+ public DomainFile getFile(String path, DomainFileFilter filter) {
+ if (StringUtils.isBlank(path) || path.charAt(0) != FileSystem.SEPARATOR_CHAR) {
throw new IllegalArgumentException(
"Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'");
}
- else if (path.charAt(len - 1) == FileSystem.SEPARATOR_CHAR) {
+ else if (path.charAt(path.length() - 1) == FileSystem.SEPARATOR_CHAR) {
throw new IllegalArgumentException("Missing file name in path");
}
int ix = path.lastIndexOf(FileSystem.SEPARATOR);
DomainFolder folder;
if (ix > 0) {
- folder = getFolder(path.substring(0, ix));
+ folder = getFolder(path.substring(0, ix), filter);
}
else {
folder = getRootFolder();
}
if (folder != null) {
- return folder.getFile(path.substring(ix + 1));
+ DomainFile file = folder.getFile(path.substring(ix + 1));
+ if (file != null && filter.accept(file)) {
+ return file;
+ }
}
return null;
}
@@ -811,7 +796,9 @@ public class DefaultProjectData implements ProjectData {
ItemCheckoutStatus otherCheckoutStatus =
newRepository.getCheckout(df.getParent().getPathname(), df.getName(), checkoutId);
-
+ if (otherCheckoutStatus == null) {
+ return true;
+ }
if (!newRepository.getUser().getName().equals(otherCheckoutStatus.getUser())) {
return true;
}
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 3c558d0d20..d889af89be 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -247,24 +247,13 @@ public class DomainFileProxy implements DomainFile {
}
@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
- }
- }
+ public boolean isLink() {
return false;
}
@Override
- public DomainFolder followLink() {
- throw new UnsupportedOperationException();
+ public LinkFileInfo getLinkInfo() {
+ return null;
}
@Override
@@ -414,8 +403,8 @@ public class DomainFileProxy implements DomainFile {
}
@Override
- public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
- return null; // not supported by proxy file
+ public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
+ throw new UnsupportedOperationException();
}
@Override
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 2e5e1c832c..19fa3dbf2e 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
@@ -404,7 +404,7 @@ public abstract class DomainObjectAdapter implements DomainObject {
checkContentHandlerMaps();
ContentHandler> ch = contentHandlerTypeMap.get(contentType);
if (ch == null) {
- throw new IOException("Content handler not found for " + contentType);
+ throw new IOException("Content handler not found for content-type: " + contentType);
}
return ch;
}
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
index a58294c239..901258ba5a 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,18 +15,15 @@
*/
package ghidra.framework.data;
+import java.io.FileNotFoundException;
import java.io.IOException;
-import java.net.URL;
+import java.util.concurrent.atomic.AtomicReference;
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.Msg;
-import ghidra.util.exception.CancelledException;
-import ghidra.util.task.TaskMonitor;
/**
* {@code FolderLinkContentHandler} provide folder-link support.
@@ -39,16 +36,6 @@ public class FolderLinkContentHandler extends LinkHandler
+ * IMPORTANT: The use of external GhidraURL-based links is only supported in the context
+ * of a an active project which is used to manage the associated project view.
+ *
+ * If the link refers to a folder within the active project (i.e., path based), the resulting
+ * linked folder will be treated as part of that project, otherwise content will be treated
+ * as read-only.
+ *
* @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
+ * @throws IOException if an IO or folder item access error occurs or a linkage error
+ * exists.
*/
- public static LinkedGhidraFolder getReadOnlyLinkedFolder(DomainFile folderLinkFile)
- throws IOException {
+ public static LinkedGhidraFolder getLinkedFolder(DomainFile folderLinkFile) throws IOException {
- if (!FOLDER_LINK_CONTENT_TYPE.equals(folderLinkFile.getContentType())) {
+ LinkFileInfo linkInfo = folderLinkFile.getLinkInfo();
+ if (linkInfo == null || !linkInfo.isFolderLink()) {
return null;
}
- URL url = getURL(folderLinkFile);
+ AtomicReference linkStatus = new AtomicReference<>();
+ AtomicReference errMsg = new AtomicReference<>();
- Project activeProject = AppInfo.getActiveProject();
- if (activeProject == null) {
- Msg.error(FolderLinkContentHandler.class,
- "Use of Linked Folders requires active project.");
- return null;
+ // Following internal linkage will catch circular internal linkage
+ DomainFile folderLink = LinkHandler.followInternalLinkage(folderLinkFile,
+ s -> linkStatus.set(s), err -> errMsg.set(err));
+
+ LinkStatus s = linkStatus.get();
+ if (s == LinkStatus.BROKEN) {
+ String msg = errMsg.get();
+ if (msg == null) {
+ msg = "Unable to follow broken link";
+ }
+ // TODO: Should we just log warning instead?
+ throw new IOException(msg + ": " + folderLink);
}
- GhidraFolder parent = ((GhidraFile) folderLinkFile).getParent();
- return new LinkedGhidraFolder(activeProject, parent, folderLinkFile.getName(), url);
+
+ if (s == LinkStatus.EXTERNAL) {
+ Project activeProject = AppInfo.getActiveProject();
+ if (activeProject == null) {
+ Msg.error(FolderLinkContentHandler.class,
+ "Use of Linked Folders requires an active project.");
+ return null;
+ }
+ return new LinkedGhidraFolder(folderLink, getLinkURL(folderLink));
+ }
+
+ if (folderLink != null) {
+
+ ProjectData projectData;
+ DomainFolder parent = folderLink.getParent();
+ if (parent instanceof LinkedDomainFolder lf) {
+ try {
+ projectData = lf.getLinkedProjectData();
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Unexpected", e);
+ }
+ }
+ else {
+ projectData = parent.getProjectData();
+ }
+
+ String linkPath = LinkHandler.getAbsoluteLinkPath(folderLink);
+
+ DomainFolder linkedFolder = projectData.getFolder(linkPath);
+ if (linkedFolder != null) {
+ return new LinkedGhidraFolder(folderLinkFile, linkedFolder);
+ }
+ }
+
+ // TODO: Not sure if this can ever occur
+ throw new FileNotFoundException("Invalid folder-link: " + folderLinkFile);
}
}
-
-/**
- * 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 398c651354..6ac47181c5 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -25,13 +25,12 @@ import ghidra.framework.model.*;
import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.framework.store.Version;
import ghidra.framework.store.local.LocalFileSystem;
-import ghidra.util.*;
+import ghidra.util.InvalidNameException;
+import ghidra.util.ReadOnlyException;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
-public class GhidraFile implements DomainFile {
-
- // FIXME: This implementation assumes a single implementation of the DomainFile and DomainFolder interfaces
+public class GhidraFile implements DomainFile, LinkFileInfo {
protected DefaultProjectData projectData;
@@ -157,9 +156,9 @@ public class GhidraFile implements DomainFile {
}
@Override
- public boolean isLinkFile() {
+ public boolean isLink() {
try {
- return getFileData().isLinkFile();
+ return getFileData().isLink();
}
catch (IOException e) {
return false;
@@ -167,12 +166,38 @@ public class GhidraFile implements DomainFile {
}
@Override
- public DomainFolder followLink() {
+ public LinkFileInfo getLinkInfo() {
+ return isLink() ? this : null;
+ }
+
+ @Override
+ public DomainFile getFile() {
+ return this;
+ }
+
+ @Override
+ public String getLinkPath() {
try {
- return FolderLinkContentHandler.getReadOnlyLinkedFolder(this);
+ return getFileData().getLinkPath(false);
}
catch (IOException e) {
- Msg.error(this, "Failed to following folder-link: " + getPathname());
+ // ignore
+ }
+ return null;
+ }
+
+ @Override
+ public String getAbsoluteLinkPath() throws IOException {
+ return LinkHandler.getAbsoluteLinkPath(this);
+ }
+
+ @Override
+ public LinkedGhidraFolder getLinkedFolder() {
+ try {
+ return FolderLinkContentHandler.getLinkedFolder(this);
+ }
+ catch (IOException e) {
+ // Ignore
}
return null;
}
@@ -522,40 +547,40 @@ 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");
+
+ if (getParent().getProjectData() != newParent.getProjectData() || !isInWritableProject()) {
+ throw new IOException("Move only supported within the same writable project");
}
- GhidraFolder newGhidraParent = (GhidraFolder) newParent;
+
+ GhidraFolder newGhidraParent = GhidraFolder.getDestinationFolder(newParent);
+
return getFileData().moveTo(newGhidraParent.getFolderData());
}
@Override
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException {
- if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
- throw new UnsupportedOperationException("newParent does not support copyTo");
- }
- GhidraFolder newGhidraParent = (GhidraFolder) newParent;
+
+ GhidraFolder newGhidraParent = GhidraFolder.getDestinationFolder(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());
+ public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
+
+ GhidraFolder newGhidraParent = GhidraFolder.getDestinationFolder(newParent);
+
+ return getFileData().copyToAsLink(newGhidraParent.getFolderData(), relative);
}
@Override
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
throws IOException, CancelledException {
- if (!GhidraFolder.class.isAssignableFrom(destFolder.getClass())) {
- throw new UnsupportedOperationException("destFolder does not support copyVersionTo");
- }
- GhidraFolder destGhidraFolder = (GhidraFolder) destFolder;
+
+ GhidraFolder destGhidraFolder = GhidraFolder.getDestinationFolder(destFolder);
+
return getFileData().copyVersionTo(version, destGhidraFolder.getFolderData(),
monitor != null ? monitor : TaskMonitor.DUMMY);
}
@@ -636,10 +661,15 @@ public class GhidraFile implements DomainFile {
@Override
public boolean equals(Object obj) {
- if (!(obj instanceof GhidraFile)) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof GhidraFile other)) {
return false;
}
- GhidraFile other = (GhidraFile) obj;
if (projectData != other.projectData) {
return false;
}
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 072b9394a7..f718b1b098 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
@@ -81,6 +81,7 @@ public class GhidraFileData {
private GhidraFolderData parent;
private String name;
private String fileID;
+ private String linkPath;
private LocalFolderItem folderItem;
private FolderItem versionedFolderItem;
@@ -90,6 +91,7 @@ public class GhidraFileData {
private AtomicBoolean busy = new AtomicBoolean();
private boolean mergeInProgress = false;
+ private boolean openInProgress = false;
// TODO: Many of the old methods assumed that the state was up-to-date due to
// refreshing ... we are relying on non-refreshed data to be dropped from cache map and no
@@ -139,6 +141,7 @@ public class GhidraFileData {
}
void refresh(LocalFolderItem localFolderItem, FolderItem verFolderItem) {
+ linkPath = null;
icon = null;
disabledIcon = null;
@@ -155,6 +158,7 @@ public class GhidraFileData {
}
private boolean refresh() throws IOException {
+ linkPath = null;
String parentPath = parent.getPathname();
if (folderItem == null) {
folderItem = fileSystem.getItem(parentPath, name);
@@ -392,6 +396,11 @@ public class GhidraFileData {
if (parent.containsFile(newName)) {
throw new DuplicateFileException("File named " + newName + " already exists.");
}
+ if (isFolderLink() && parent.getFolderData(newName, false) != null) {
+ // Folder-link file name not permitted to conflict with Folder
+ throw new DuplicateFileException("Name conflict. Folder named " + name +
+ " already exists in " + parent.getPathname());
+ }
String oldName = name;
String folderPath = parent.getPathname();
@@ -425,6 +434,10 @@ public class GhidraFileData {
}
}
+ boolean isFolderLink() {
+ return FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(getContentType());
+ }
+
/**
* Returns content-type string for this file
* @return the file content type or a reserved content type {@link ContentHandler#MISSING_CONTENT}
@@ -432,7 +445,7 @@ public class GhidraFileData {
*/
String getContentType() {
synchronized (fileSystem) {
- FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
+ FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION);
// this can happen when we are trying to load a version file from
// a server to which we are not connected
if (item == null) {
@@ -450,7 +463,7 @@ public class GhidraFileData {
*/
ContentHandler> getContentHandler() throws IOException {
synchronized (fileSystem) {
- FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
+ FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION);
// this can happen when we are trying to load a version file from
// a server to which we are not connected
if (item == null) {
@@ -539,40 +552,61 @@ public class GhidraFileData {
FolderItem myFolderItem;
DomainObjectAdapter domainObj = null;
synchronized (fileSystem) {
- if (fileSystem.isReadOnly() || isLinkFile()) {
- return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor);
+ if (openInProgress) {
+ throw new IOException("Circular link reference detected: " + getPathname());
}
- domainObj = getOpenedDomainObject();
- if (domainObj != null) {
- if (!domainObj.addConsumer(consumer)) {
- domainObj = null;
- projectData.clearDomainObject(getPathname());
+ openInProgress = true;
+ try {
+ if (fileSystem.isReadOnly()) {
+ openInProgress = false;
+ return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor);
}
- else {
- return domainObj;
+ if (isLink()) {
+ String resolvedLinkPath = getLinkPath(true);
+ if (GhidraURL.isGhidraURL(resolvedLinkPath)) {
+ openInProgress = false;
+ return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION,
+ monitor);
+ }
+ DomainFile file = projectData.getFile(resolvedLinkPath);
+ if (file == null) {
+ throw new FileNotFoundException(
+ "Linked file not found: " + resolvedLinkPath);
+ }
+ return file.getDomainObject(consumer, okToUpgrade, okToRecover, monitor);
}
- }
- ContentHandler> contentHandler = getContentHandler();
- if (folderItem == null) {
- DomainObjectAdapter doa = contentHandler.getReadOnlyObject(versionedFolderItem,
- DomainFile.DEFAULT_VERSION, true, consumer, monitor);
- doa.setChanged(false);
- DomainFileProxy proxy = new DomainFileProxy(name, parent.getPathname(), doa,
- DomainFile.DEFAULT_VERSION, fileID, parent.getProjectLocator());
- proxy.setLastModified(getLastModifiedTime());
- return doa;
- }
- myFolderItem = folderItem;
+ domainObj = getOpenedDomainObject();
+ if (domainObj != null) {
+ if (!domainObj.addConsumer(consumer)) {
+ domainObj = null;
+ projectData.clearDomainObject(getPathname());
+ }
+ else {
+ return domainObj;
+ }
+ }
+ ContentHandler> contentHandler = getContentHandler();
+ if (folderItem == null) {
+ DomainObjectAdapter doa = contentHandler.getReadOnlyObject(versionedFolderItem,
+ DomainFile.DEFAULT_VERSION, true, consumer, monitor);
+ doa.setChanged(false);
+ DomainFileProxy proxy = new DomainFileProxy(name, parent.getPathname(), doa,
+ DomainFile.DEFAULT_VERSION, fileID, parent.getProjectLocator());
+ proxy.setLastModified(getLastModifiedTime());
+ return doa;
+ }
+ myFolderItem = folderItem;
- domainObj = contentHandler.getDomainObject(myFolderItem, parent.getUserFileSystem(),
- FolderItem.DEFAULT_CHECKOUT_ID, okToUpgrade, okToRecover, consumer, monitor);
- projectData.setDomainObject(getPathname(), domainObj);
+ domainObj = contentHandler.getDomainObject(myFolderItem, parent.getUserFileSystem(),
+ FolderItem.DEFAULT_CHECKOUT_ID, okToUpgrade, okToRecover, consumer, monitor);
+ projectData.setDomainObject(getPathname(), domainObj);
- // Notify file manager of in-use domain object.
- // A link-file object is indirect with tracking intiated by the URL-referenced file.
- if (!isLinkFile()) {
+ // Notify file manager of in-use domain object.
projectData.trackDomainFileInUse(domainObj);
}
+ finally {
+ openInProgress = false;
+ }
}
// Set domain file for newly opened domain object
@@ -622,23 +656,51 @@ public class GhidraFileData {
DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor)
throws VersionException, IOException, CancelledException {
synchronized (fileSystem) {
- FolderItem item =
- (folderItem != null && version == DomainFile.DEFAULT_VERSION) ? folderItem
- : versionedFolderItem;
- DomainObjectAdapter doa =
- getContentHandler().getReadOnlyObject(item, version, true, consumer, monitor);
- doa.setChanged(false);
-
- // Notify file manager of in-use domain object.
- // A link-file object is indirect with tracking intiated by the URL-referenced file.
- if (!isLinkFile()) {
- projectData.trackDomainFileInUse(doa);
+ if (openInProgress) {
+ throw new IOException("Circular link reference detected: " + getPathname());
}
+ openInProgress = true;
+ try {
+ FolderItem item = getFolderItem(version);
- DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), doa,
- version, fileID, parent.getProjectLocator());
- proxy.setLastModified(getLastModifiedTime());
- return doa;
+ DomainObjectAdapter doa;
+ ContentHandler> contentHandler = getContentHandler();
+ if (contentHandler instanceof LinkHandler linkHandler) {
+ String resolvedLinkPath = getLinkPath(true);
+
+ if (!GhidraURL.isGhidraURL(resolvedLinkPath)) {
+ DomainFile file = projectData.getFile(resolvedLinkPath);
+ if (file == null) {
+ throw new FileNotFoundException(
+ "Linked file not found: " + resolvedLinkPath);
+ }
+ return file.getReadOnlyDomainObject(consumer, version, monitor);
+ }
+
+ // Handle link to Ghidra URL
+ URL ghidraUrl = new URL(resolvedLinkPath);
+ doa = linkHandler.getObject(ghidraUrl, version, consumer, monitor, false);
+ }
+ else {
+ doa = contentHandler.getReadOnlyObject(item, version, true, consumer, monitor);
+ }
+
+ doa.setChanged(false);
+
+ // Notify file manager of in-use domain object.
+ // A link-file object is indirect with tracking intiated by the URL-referenced file.
+ if (!isLink()) {
+ projectData.trackDomainFileInUse(doa);
+ }
+
+ DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), doa,
+ version, fileID, parent.getProjectLocator());
+ proxy.setLastModified(getLastModifiedTime());
+ return doa;
+ }
+ finally {
+ openInProgress = false;
+ }
}
}
@@ -663,27 +725,48 @@ public class GhidraFileData {
DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor)
throws VersionException, IOException, CancelledException {
synchronized (fileSystem) {
- DomainObjectAdapter obj = null;
- ContentHandler> contentHandler = getContentHandler();
- if (versionedFolderItem == null ||
- (version == DomainFile.DEFAULT_VERSION && folderItem != null) || isHijacked()) {
- obj = contentHandler.getImmutableObject(folderItem, consumer, version, -1, monitor);
- }
- else {
- obj = contentHandler.getImmutableObject(versionedFolderItem, consumer, version, -1,
- monitor);
+ if (openInProgress) {
+ throw new IOException("Circular link reference detected: " + getPathname());
}
+ openInProgress = true;
+ try {
+ FolderItem item = getFolderItem(version);
+ DomainObjectAdapter doa;
+ ContentHandler> contentHandler = getContentHandler();
+ if (contentHandler instanceof LinkHandler linkHandler) {
+ String resolvedLinkPath = getLinkPath(true);
- // Notify file manager of in-use domain object.
- // A link-file object is indirect with tracking intiated by the URL-referenced file.
- if (!isLinkFile()) {
- projectData.trackDomainFileInUse(obj);
- }
+ if (!GhidraURL.isGhidraURL(resolvedLinkPath)) {
+ DomainFile file = projectData.getFile(resolvedLinkPath);
+ if (file == null) {
+ throw new FileNotFoundException(
+ "Linked file not found: " + resolvedLinkPath);
+ }
+ return file.getImmutableDomainObject(consumer, version, monitor);
+ }
- DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), obj,
- version, fileID, parent.getProjectLocator());
- proxy.setLastModified(getLastModifiedTime());
- return obj;
+ // Handle link to Ghidra URL
+ URL ghidraUrl = new URL(resolvedLinkPath);
+ doa = linkHandler.getObject(ghidraUrl, version, consumer, monitor, true);
+ }
+ else {
+ doa = contentHandler.getImmutableObject(item, consumer, version, -1, monitor);
+ }
+
+ // Notify file manager of in-use domain object.
+ // A link-file object is indirect with tracking intiated by the URL-referenced file.
+ if (!isLink()) {
+ projectData.trackDomainFileInUse(doa);
+ }
+
+ DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), doa,
+ version, fileID, parent.getProjectLocator());
+ proxy.setLastModified(getLastModifiedTime());
+ return doa;
+ }
+ finally {
+ openInProgress = false;
+ }
}
}
@@ -783,9 +866,9 @@ public class GhidraFileData {
}
synchronized (fileSystem) {
- boolean isLink = isLinkFile();
+ boolean isLink = isLink();
- FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
+ FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION);
Icon baseIcon = new TranslateIcon(getBaseIcon(item), 1, 1);
@@ -941,7 +1024,7 @@ public class GhidraFileData {
versionedFileSystem.isReadOnly()) {
return false;
}
- return !isLinkFile();
+ return !isLink();
}
catch (IOException e) {
return false;
@@ -1061,20 +1144,27 @@ public class GhidraFileData {
if (!versionedFileSystem.isOnline()) {
throw new NotConnectedException("Not connected to repository server");
}
+ if (folderItem == null) {
+ throw new FileNotFoundException("File not found");
+ }
+ if (!versionedFileSystem.isSupportedItemType(folderItem)) {
+ throw new IOException(folderItem.getClass().getSimpleName() +
+ " not supported by versioned filesystem/repository");
+ }
if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) {
throw new ReadOnlyException(
"versioning permitted within writeable project and repository only");
}
- if (folderItem == null) {
- throw new FileNotFoundException("File not found");
- }
+
if (folderItem.isCheckedOut() || versionedFolderItem != null) {
throw new IOException("File already versioned");
}
ContentHandler> contentHandler = getContentHandler();
- if (contentHandler instanceof LinkHandler linkHandler) {
- // must check local vs remote URL
- if (!GhidraURL.isServerRepositoryURL(LinkHandler.getURL(folderItem))) {
+ if (contentHandler == null) {
+ throw new IOException("Unsupported content-type: " + getContentType());
+ }
+ if (contentHandler instanceof LinkHandler) {
+ if (!LinkHandler.canShareLink(folderItem)) {
throw new IOException("Local project link-file may not be versioned");
}
}
@@ -1108,7 +1198,7 @@ public class GhidraFileData {
try {
inUseDomainObj = getAndLockInUseDomainObjectForMergeUpdate("checkin");
- if (isLinkFile()) {
+ if (isLink()) {
keepCheckedOut = false;
}
else if (inUseDomainObj != null && !keepCheckedOut) {
@@ -1121,8 +1211,7 @@ public class GhidraFileData {
String parentPath = parent.getPathname();
String user = ClientUtil.getUserName();
try {
- if (folderItem instanceof DatabaseItem) {
- DatabaseItem databaseItem = (DatabaseItem) folderItem;
+ if (folderItem instanceof DatabaseItem databaseItem) {
BufferFile bufferFile = databaseItem.open();
try {
versionedFolderItem = versionedFileSystem.createDatabase(parentPath,
@@ -1133,8 +1222,7 @@ public class GhidraFileData {
bufferFile.dispose();
}
}
- else if (folderItem instanceof DataFileItem) {
- DataFileItem dataFileItem = (DataFileItem) folderItem;
+ else if (folderItem instanceof DataFileItem dataFileItem) {
InputStream istream = dataFileItem.getInputStream();
try {
versionedFolderItem = versionedFileSystem.createDataFile(parentPath,
@@ -1144,6 +1232,11 @@ public class GhidraFileData {
istream.close();
}
}
+ else if (folderItem instanceof TextDataItem textDataItem) {
+ versionedFileSystem.createTextDataItem(parentPath, name,
+ folderItem.getFileID(), folderItem.getContentType(),
+ textDataItem.getTextData(), comment);
+ }
else {
throw new IOException(
"Unable to add unsupported content to version control");
@@ -1198,6 +1291,10 @@ public class GhidraFileData {
inUseDomainObj.domainObjectRestored();
}
}
+ catch (UnsupportedOperationException e) {
+ throw new IOException(
+ "The repository does not support file content. A newer server version may be required.");
+ }
finally {
unlockDomainObject(inUseDomainObj);
busy.set(false);
@@ -1235,7 +1332,7 @@ public class GhidraFileData {
if (!versionedFileSystem.isOnline()) {
throw new NotConnectedException("Not connected to repository server");
}
- if (isLinkFile()) {
+ if (isLink()) {
return false;
}
String user = ClientUtil.getUserName();
@@ -2013,8 +2110,8 @@ public class GhidraFileData {
}
// update checkout data within versioned repository
- versionedFolderItem.updateCheckoutVersion(checkoutId,
- folderItem.getCheckoutVersion(), ClientUtil.getUserName());
+ versionedFolderItem.updateCheckoutVersion(checkoutId, folderItem.getCheckoutVersion(),
+ ClientUtil.getUserName());
Msg.info(this, "Updated checkout completed for " + name);
@@ -2069,12 +2166,14 @@ public class GhidraFileData {
throw new ReadOnlyException("moveTo permitted within writeable project only");
}
if (getParent().getPathname().equals(newParent.getPathname())) {
- throw new IllegalArgumentException("newParent must differ from current parent");
+ throw new IllegalArgumentException(
+ "new parent must differ from current parent: " + newParent);
}
checkInUse();
+
GhidraFolderData oldParent = parent;
String oldName = name;
- String newName = newParent.getTargetName(name);
+ String newName = newParent.getUniqueFileName(name, isFolderLink());
try {
if (isHijacked()) {
fileSystem.moveItem(parent.getPathname(), name, newParent.getPathname(),
@@ -2113,6 +2212,18 @@ public class GhidraFileData {
}
}
+ /**
+ * Get the appropriate folder item (private or versioned) based upon the current state and
+ * targeted file version.
+ * @param version file version
+ * @return folder item to be used
+ */
+ private FolderItem getFolderItem(int version) {
+ return (folderItem != null && (version == DomainFile.DEFAULT_VERSION || isHijacked()))
+ ? folderItem
+ : versionedFolderItem;
+ }
+
/**
* 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
@@ -2120,34 +2231,74 @@ public class GhidraFileData {
* {@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.
- * The URL stored within the link-file may be read using {@link #getLinkFileURL()}.
+ * The link path or URL stored within the link-file may be read using {@link #getLinkPath(boolean)}.
* 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
*/
- boolean isLinkFile() {
- synchronized (fileSystem) {
- try {
- return LinkHandler.class.isAssignableFrom(getContentHandler().getClass());
- }
- catch (IOException e) {
- return false;
- }
+ boolean isLink() {
+ try {
+ return LinkHandler.class.isAssignableFrom(getContentHandler().getClass());
+ }
+ catch (IOException e) {
+ return false;
}
}
/**
- * Get URL associated with a link-file. The URL returned may reference either a folder
- * or a file within another project/repository.
- * @return link-file URL or null if not a link-file
- * @throws IOException if an IO error occurs
+ * If this is a {@link #isLink() link file} this method will return the link-path which
+ * may be either an absolute or relative path within the the project or a Ghidra URL.
+ *
+ * @param resolve if true relative paths will always be converted to an absolute path
+ * @return associated link path or null if not a link file
+ * @throws IOException if an IO error occurs or resolving a relative link-path produced
+ * an invalid path.
*/
- URL getLinkFileURL() throws IOException {
- if (!isLinkFile()) {
+ String getLinkPath(boolean resolve) throws IOException {
+ if (!isLink()) {
return null;
}
- FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
- return LinkHandler.getURL(item);
+
+ if (linkPath == null) {
+ FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION);
+ linkPath = LinkHandler.getLinkPath(item);
+ if (linkPath == null) {
+ linkPath = ""; // avoid repeated attempts
+ }
+ }
+
+ if (StringUtils.isBlank(linkPath)) {
+ return null;
+ }
+
+ if (!resolve) {
+ return linkPath;
+ }
+
+ String path = linkPath;
+ if (!GhidraURL.isGhidraURL(linkPath)) {
+ path = getAbsolutePath(linkPath);
+ }
+
+ return path;
+ }
+
+ private String getAbsolutePath(String path) throws IOException {
+ String absPath = path;
+ if (!path.startsWith(FileSystem.SEPARATOR)) {
+ absPath = getParent().getPathname();
+ if (!absPath.endsWith(FileSystem.SEPARATOR)) {
+ absPath += FileSystem.SEPARATOR;
+ }
+ absPath += path;
+ }
+ try {
+ absPath = FileSystem.normalizePath(absPath);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IOException("Invalid link path: " + linkPath);
+ }
+ return absPath;
}
/**
@@ -2177,16 +2328,19 @@ public class GhidraFileData {
* 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
+ * @param relative if true, and this file is contained within the same active
+ * {@link ProjectData} instance as {@code newParent}, an internal-project relative path
+ * file-link will be created.
* @return newly created domain file or null if content type does not support link use.
* @throws IOException if an IO or access error occurs.
*/
- DomainFile copyToAsLink(GhidraFolderData newParent) throws IOException {
+ DomainFile copyToAsLink(GhidraFolderData newParent, boolean relative) throws IOException {
synchronized (fileSystem) {
LinkHandler> lh = getContentHandler().getLinkHandler();
if (lh == null) {
return null;
}
- return newParent.copyAsLink(projectData, getPathname(), name, lh);
+ return newParent.createLinkFile(projectData, getPathname(), relative, name, lh);
}
}
@@ -2196,6 +2350,7 @@ public class GhidraFileData {
* @param monitor task monitor
* @return newly created domain file
* @throws FileInUseException if this file is in-use / checked-out.
+ * @throws DuplicateFileException if this file's name already exists in newParent
* @throws IOException if an IO or access error occurs.
* @throws CancelledException if task monitor cancelled operation.
*/
@@ -2208,7 +2363,7 @@ public class GhidraFileData {
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
String pathname = newParent.getPathname();
String contentType = item.getContentType();
- String targetName = newParent.getTargetName(name);
+ String targetName = newParent.getUniqueFileName(name, isFolderLink());
String user = ClientUtil.getUserName();
try {
if (item instanceof DatabaseItem) {
@@ -2222,6 +2377,36 @@ public class GhidraFileData {
bufferFile.dispose();
}
}
+ else if (item instanceof TextDataItem) {
+ ContentHandler> contentHandler =
+ DomainObjectAdapter.getContentHandler(contentType);
+ if (contentHandler instanceof LinkHandler) {
+ String lp = getLinkPath(true);
+ if (!GhidraURL.isGhidraURL(lp) &&
+ !parent.getProjectLocator().equals(newParent.getProjectLocator())) {
+ // Force use of external URL for link copy from another project
+ URL url = LinkHandler.getLinkURL(getDomainFile());
+ if (url != null) {
+ lp = url.toString();
+ }
+ }
+ else {
+ lp = LinkHandler.getLinkPath(item);
+ }
+ if (!StringUtils.isBlank(lp)) {
+ newParent.getLocalFileSystem()
+ .createTextDataItem(pathname, targetName,
+ FileIDFactory.createFileID(), contentType, lp, null);
+ }
+ else {
+ throw new IOException(
+ "Invalid link-file item in copyTo: " + item.getPathName());
+ }
+ }
+ else {
+ throw new IOException("Unsupported item in copyTo: " + item.getPathName());
+ }
+ }
else if (item instanceof DataFileItem) {
InputStream istream = ((DataFileItem) item).getInputStream();
try {
@@ -2234,7 +2419,7 @@ public class GhidraFileData {
}
}
else {
- throw new IOException("Unable to copy unsupported content");
+ throw new IOException("Unsupported item in copyTo: " + item.getPathName());
}
}
catch (InvalidNameException e) {
@@ -2260,6 +2445,9 @@ public class GhidraFileData {
if (destFolder.getLocalFileSystem().isReadOnly()) {
throw new ReadOnlyException("copyVersionTo permitted to writeable project");
}
+ if (isFolderLink()) {
+ throw new UnsupportedOperationException("Cannot copy folder-link version");
+ }
if (versionedFolderItem == null) {
return null; // NOTE: versioned file system may be offline
}
@@ -2268,7 +2456,7 @@ public class GhidraFileData {
}
String pathname = destFolder.getPathname();
String contentType = versionedFolderItem.getContentType();
- String targetName = destFolder.getTargetName(name + "_v" + version);
+ String targetName = destFolder.getUniqueFileName(name + "_v" + version, false);
String user = ClientUtil.getUserName();
try {
BufferFile bufferFile = ((DatabaseItem) versionedFolderItem).open(version);
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 f547eca92b..4d1cbc3e22 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -24,8 +24,7 @@ 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.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@@ -94,7 +93,7 @@ public class GhidraFolder implements DomainFolder {
/**
* Create folder hierarchy in local filesystem if it does not already exist
- * @param folderName
+ * @param folderName name of new folder
* @return folder data
* @throws IOException error while creating folder
*/
@@ -325,6 +324,19 @@ public class GhidraFolder implements DomainFolder {
monitor != null ? monitor : TaskMonitor.DUMMY);
}
+ @Override
+ public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname,
+ boolean makeRelative, String linkFilename, LinkHandler> lh) throws IOException {
+ return createFolderData().createLinkFile(sourceProjectData, pathname, makeRelative,
+ linkFilename, lh);
+ }
+
+ @Override
+ public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler> lh)
+ throws IOException {
+ return createFolderData().createLinkFile(ghidraUrl, linkFilename, lh);
+ }
+
@Override
public GhidraFolder createFolder(String folderName) throws InvalidNameException, IOException {
return createFolderData().createFolder(folderName).getDomainFolder();
@@ -340,43 +352,64 @@ public class GhidraFolder implements DomainFolder {
}
}
+ static GhidraFolder getDestinationFolder(DomainFolder newParent) throws IOException {
+
+ while (newParent instanceof LinkedDomainFolder linkedFolder) {
+
+ if (!linkedFolder.isInWritableProject()) {
+ throw new IOException("Destination folder is not within writable project");
+ }
+
+ // Find real folder - we may have multiple levels of linking
+ // This should only be done within the same writable project
+ newParent = linkedFolder.getRealFolder();
+
+ }
+
+ if (!newParent.isInWritableProject() || !(newParent instanceof GhidraFolder ghidraFolder)) {
+ throw new IOException("Destination folder is not within writable project");
+ }
+
+ return ghidraFolder;
+ }
+
@Override
public GhidraFolder moveTo(DomainFolder newParent) throws IOException {
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");
+
+ if (getProjectData() != newParent.getProjectData() || !isInWritableProject()) {
+ throw new IOException("Move only supported within the same writable project");
}
- GhidraFolderData folderData = getFolderData();
- GhidraFolder newGhidraParent = (GhidraFolder) newParent;
- return folderData.moveTo(newGhidraParent.getFolderData());
+
+ GhidraFolder newGhidraParent = getDestinationFolder(newParent);
+
+ return getFolderData().moveTo(newGhidraParent.getFolderData());
}
@Override
public GhidraFolder copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException {
- GhidraFolderData folderData = getFolderData();
- if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
- throw new UnsupportedOperationException("newParent does not support copyTo");
- }
- GhidraFolder newGhidraParent = (GhidraFolder) newParent;
- return folderData.copyTo(newGhidraParent.getFolderData(),
+
+ GhidraFolder newGhidraParent = getDestinationFolder(newParent);
+
+ return getFolderData().copyTo(newGhidraParent.getFolderData(),
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());
+ public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
+
+ GhidraFolder newGhidraParent = getDestinationFolder(newParent);
+
+ return getFolderData().copyToAsLink(newGhidraParent.getFolderData(), relative);
}
/**
- * used for testing
+ * ** Used for testing **
+ * Check for existance of private folder
+ * @return true if private folder exists else false
*/
boolean privateExists() {
try {
@@ -388,7 +421,9 @@ public class GhidraFolder implements DomainFolder {
}
/**
- * used for testing
+ * ** Used for testing **
+ * Check for existance of versioned/shared folder
+ * @return true if versioned/shared folder exists else false
*/
boolean sharedExists() {
try {
@@ -406,16 +441,56 @@ public class GhidraFolder implements DomainFolder {
@Override
public boolean equals(Object obj) {
- if (!(obj instanceof GhidraFolder)) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof GhidraFolder other)) {
return false;
}
- GhidraFolder other = (GhidraFolder) obj;
if (projectData != other.projectData) {
return false;
}
return getPathname().equals(other.getPathname());
}
+ @Override
+ public boolean isSameOrAncestor(DomainFolder folder) {
+
+ if (!getProjectLocator().equals(folder.getProjectLocator()) &&
+ !SystemUtilities.isEqual(projectData.getSharedProjectURL(),
+ folder.getProjectData().getSharedProjectURL())) {
+ // Containing project/repository appears to be unrelated
+ return false;
+ }
+
+ String pathname = getPathname();
+
+ DomainFolder f = folder;
+ while (f != null) {
+ if (f == this || pathname.equals(f.getPathname())) {
+ return true;
+ }
+ f = f.getParent();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isSame(DomainFolder folder) {
+
+ if (!getProjectLocator().equals(folder.getProjectLocator()) &&
+ !SystemUtilities.isEqual(projectData.getSharedProjectURL(),
+ folder.getProjectData().getSharedProjectURL())) {
+ // Containing project/repository appears to be unrelated
+ return false;
+ }
+
+ return getPathname().equals(folder.getPathname());
+ }
+
@Override
public int hashCode() {
return getPathname().hashCode();
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 6a5c786ea4..03c7736d37 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
@@ -17,16 +17,18 @@ package ghidra.framework.data;
import java.io.*;
import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
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.*;
import ghidra.framework.store.FileSystem;
-import ghidra.framework.store.FolderItem;
-import ghidra.framework.store.FolderNotEmptyException;
-import ghidra.framework.store.local.*;
+import ghidra.framework.store.local.LocalFileSystem;
+import ghidra.framework.store.local.LocalFolderItem;
import ghidra.util.*;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
@@ -211,11 +213,21 @@ class GhidraFolderData {
}
/**
- * Return this folder's name.
- * @return the name
+ * {@return this folder's name. The root folder will return the project or repository name}
*/
String getName() {
- return name;
+ // Allow root folder to use project/repository name
+ return parent != null ? name : projectData.getProjectLocator().getName();
+ }
+
+ private void checkFolderLinkConflict(String folderName) throws DuplicateFileException {
+ GhidraFile file = getDomainFile(folderName);
+ if (file != null && file.isFolderLink()) {
+ // Folder-link file name not permitted to conflict with Folder
+ // NOTE: There is still lthe possibility of conflicting with offline version filesystem
+ throw new DuplicateFileException("Name conflict. Folder-link named " + folderName +
+ " already exists in " + getPathname());
+ }
}
/**
@@ -224,7 +236,7 @@ class GhidraFolderData {
* @return renamed domain file (the original DomainFolder object becomes invalid since it is
* immutable)
* @throws InvalidNameException if newName contains illegal characters
- * @throws DuplicateFileException if a folder named newName
+ * @throws DuplicateFileException if a folder of folder-link named newName
* already exists in this files domain folder.
* @throws FileInUseException if any file within this folder or its descendants is
* in-use / checked-out.
@@ -233,11 +245,14 @@ class GhidraFolderData {
GhidraFolder setName(String newName) throws InvalidNameException, IOException {
synchronized (fileSystem) {
if (parent == null || fileSystem.isReadOnly()) {
- throw new UnsupportedOperationException("setName not permitted on this folder");
+ throw new IOException("setName not permitted on this folder");
}
+
updateExistenceState();
checkInUse();
+ parent.checkFolderLinkConflict(newName);
+
String oldName = name;
String parentPath = parent.getPathname();
@@ -650,7 +665,7 @@ class GhidraFolderData {
Msg.error(this,
"Project folder contains " + nullNameCount + " null items: " + getPathname());
}
- if (badItemCount != 0) {
+ if (unknownItemCount != 0) {
Msg.error(this, "Project folder contains " + unknownItemCount + " unsupported items: " +
getPathname());
}
@@ -1072,7 +1087,7 @@ class GhidraFolderData {
* Create a subfolder within this folder.
* @param folderName sub-folder name
* @return the new folder
- * @throws DuplicateFileException if a folder by this name already exists
+ * @throws DuplicateFileException if a folder or folder-link with this name already exists
* @throws InvalidNameException if name is an empty string of if it contains characters other
* than alphanumerics.
* @throws IOException if IO or access error occurs
@@ -1082,6 +1097,9 @@ class GhidraFolderData {
if (fileSystem.isReadOnly()) {
throw new AssertException("createFile permitted within writeable project only");
}
+
+ checkFolderLinkConflict(folderName);
+
fileSystem.createFolder(getPathname(), folderName);
folderChanged(folderName);
@@ -1102,6 +1120,9 @@ class GhidraFolderData {
if (fileSystem.isReadOnly()) {
throw new AssertException("delete permitted within writeable project only");
}
+ if (parent == null) {
+ throw new IOException("root folder may not be deleted");
+ }
checkInUse();
try {
fileSystem.deleteFolder(getPathname());
@@ -1150,19 +1171,26 @@ class GhidraFolderData {
GhidraFolder moveTo(GhidraFolderData newParent) throws IOException {
synchronized (fileSystem) {
if (newParent.getLocalFileSystem() != fileSystem || fileSystem.isReadOnly()) {
- throw new AssertException("moveTo permitted within writeable project only");
+ throw new IOException("moveTo permitted within writeable project only");
+ }
+ if (parent == null) {
+ throw new IOException("root folder may not be moved");
}
if (getPathname().equals(newParent.getPathname())) {
- throw new IllegalArgumentException("newParent must differ from current parent");
+ throw new IllegalArgumentException(
+ "new parent must differ from current parent: " + newParent);
}
checkInUse();
+ if (newParent.containsFolder(name)) {
+ throw new DuplicateFileException(
+ "Folder named " + name + " already exists in " + newParent);
+ }
+
+ newParent.checkFolderLinkConflict(name);
+
updateExistenceState();
try {
- if (newParent.containsFolder(name)) {
- throw new DuplicateFileException(
- "Folder named " + getName() + " already exists in " + newParent);
- }
if (folderExists) {
fileSystem.moveFolder(parent.getPathname(), name, newParent.getPathname());
@@ -1210,21 +1238,13 @@ class GhidraFolderData {
}
/**
- * Determine if the specified folder if an ancestor of this folder
- * (i.e., parent, grand-parent, etc.).
+ * {@return true if the specified folder is an ancestor of this folder
+ * (i.e., parent, grand-parent, etc.).}
* @param folderData folder to be checked
- * @return true if the specified folder if an ancestor of this folder
*/
boolean isAncestor(GhidraFolderData folderData) {
- if (!folderData.projectData.getProjectLocator().equals(projectData.getProjectLocator())) {
- // check if projects share a common repository
- RepositoryAdapter myRepository = projectData.getRepository();
- RepositoryAdapter otherRepository = folderData.projectData.getRepository();
- if (myRepository == null || otherRepository == null ||
- !myRepository.getServerInfo().equals(otherRepository.getServerInfo()) ||
- !myRepository.getName().equals(otherRepository.getName())) {
- return false;
- }
+ if (!hasSameProjectOrRepository(folderData)) {
+ return false;
}
GhidraFolderData checkParent = folderData;
while (checkParent != null) {
@@ -1236,6 +1256,37 @@ class GhidraFolderData {
return false;
}
+ /**
+ * {@return true if the specified folder is associated with the same project or repository as
+ * this folder.}
+ * @param folderData folder to be checked
+ */
+ boolean hasSameProjectOrRepository(GhidraFolderData folderData) {
+ if (folderData.projectData.getProjectLocator().equals(projectData.getProjectLocator())) {
+ return true;
+ }
+ // check if projects share a common repository
+ RepositoryAdapter myRepository = projectData.getRepository();
+ RepositoryAdapter otherRepository = folderData.projectData.getRepository();
+ if (myRepository == null || otherRepository == null ||
+ !myRepository.getServerInfo().equals(otherRepository.getServerInfo()) ||
+ !myRepository.getName().equals(otherRepository.getName())) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@return true if the specified folder is considered the same as this folder.}
+ * @param folderData folder to be checked
+ */
+ boolean isSame(GhidraFolderData folderData) {
+ if (!hasSameProjectOrRepository(folderData)) {
+ return false;
+ }
+ return getPathname().equals(folderData.getPathname());
+ }
+
/**
* Copy this folder into the newParent folder.
* @param newParent new parent folder
@@ -1255,16 +1306,19 @@ class GhidraFolderData {
if (isAncestor(newParent)) {
throw new IOException("self-referencing copy not permitted");
}
- GhidraFolderData newFolderData = newParent.getFolderData(name, false);
-
+ String folderName = getName();
+ GhidraFolderData newFolderData = newParent.getFolderData(folderName, false);
if (newFolderData == null) {
try {
- newFolderData = newParent.createFolder(name);
+ newFolderData = newParent.createFolder(folderName);
}
catch (InvalidNameException e) {
throw new AssertException("Unexpected error", e);
}
}
+ else if (isSame(newFolderData)) {
+ throw new IOException("self-referencing copy not permitted");
+ }
List files = getFileNames();
for (String file : files) {
monitor.checkCancelled();
@@ -1296,13 +1350,16 @@ class GhidraFolderData {
* managed project) the generated link will refer to the remote folder with a remote
* Ghidra URL, otherwise a local project storage path will be used.
* @param newParent new parent folder where link-file is to be created
- * @return newly created domain file (i.e., link-file) or null if link use not supported.
+ * @param relative if true, and this file is contained within the same active
+ * {@link ProjectData} instance as {@code newParent}, an internal-project relative path
+ * file-link will be created.
+ * @return newly created domain file which is a folder-link (i.e., link-file).
* @throws IOException if an IO or access error occurs.
*/
- DomainFile copyToAsLink(GhidraFolderData newParent) throws IOException {
+ DomainFile copyToAsLink(GhidraFolderData newParent, boolean relative) throws IOException {
synchronized (fileSystem) {
String linkFilename = name;
- if (linkFilename == null) {
+ if (linkFilename == null) { // create name for link to root folder
if (projectData instanceof TransientProjectData) {
linkFilename = projectData.getRepository().getName();
}
@@ -1310,66 +1367,74 @@ class GhidraFolderData {
linkFilename = projectData.getProjectLocator().getName();
}
}
- return newParent.copyAsLink(projectData, getPathname(), linkFilename,
+
+ return newParent.createLinkFile(projectData, getPathname(), relative, linkFilename,
FolderLinkContentHandler.INSTANCE);
}
}
/**
- * Create a link-file within this folder. The link-file may correspond to various types of
- * content (e.g., Program, Trace, Folder, etc.) based upon specified link handler.
+ * Create a link-file within this folder which references the specified file or folder
+ * {@code pathname} within the project specified by {@code sourceProjectData}. The link-file
+ * may correspond to various types of content (e.g., Program, Trace, Folder, etc.) based upon
+ * the specified {@link LinkHandler} instance.
+ *
* @param sourceProjectData referenced content project data within which specified path exists.
- * @param pathname path of referenced content with source project data
- * @param linkFilename name of link-file to be created within this folder.
- * @param lh link file handler used to create specific link file.
- * @return link-file
+ * If this differ's from this folder's project a Ghidra URL will be used, otherwise and internal
+ * link path reference will be used.
+ * @param pathname an absolute path of project folder or file within the specified source
+ * project data (a Ghidra URL is not permitted)
+ * @param makeRelative if true, and this file is contained within the same active
+ * {@link ProjectData} instance as {@code newParent}, an internal-project relative path
+ * link-file will be created.
+ * @param linkFilename name of link-file to be created within this folder. NOTE: This name may
+ * be modified to ensure uniqueness within this folder.
+ * @param lh link-file handler used to create specific link-file (see derived implementations
+ * of {@link LinkHandler} and their public static INSTANCE.
+ * @return newly created link-file
* @throws IOException if IO error occurs during link creation
*/
- DomainFile copyAsLink(ProjectData sourceProjectData, String pathname, String linkFilename,
- LinkHandler> lh) throws IOException {
+ DomainFile createLinkFile(ProjectData sourceProjectData, String pathname, boolean makeRelative,
+ String linkFilename, LinkHandler> lh) throws IOException {
synchronized (fileSystem) {
if (fileSystem.isReadOnly()) {
throw new ReadOnlyException("copyAsLink permitted to writeable project only");
}
- if (sourceProjectData == projectData) {
- // internal linking not yet supported
- Msg.error(this, "Internal file/folder links not yet supported");
- return null;
+ if (!pathname.startsWith(FileSystem.SEPARATOR)) {
+ throw new IllegalArgumentException("invalid pathname specified");
}
- URL ghidraUrl = null;
- if (sourceProjectData instanceof TransientProjectData) {
+ String linkPath;
+ if (sourceProjectData == projectData) {
+ if (makeRelative) {
+ linkPath = getRelativePath(pathname, getPathname());
+ }
+ else {
+ linkPath = pathname;
+ }
+ }
+ else if (sourceProjectData instanceof TransientProjectData) {
RepositoryAdapter repository = sourceProjectData.getRepository();
ServerInfo serverInfo = repository.getServerInfo();
- ghidraUrl = GhidraURL.makeURL(serverInfo.getServerName(),
+ URL ghidraUrl = GhidraURL.makeURL(serverInfo.getServerName(),
serverInfo.getPortNumber(), repository.getName(), pathname);
+ linkPath = ghidraUrl.toExternalForm();
}
else {
ProjectLocator projectLocator = sourceProjectData.getProjectLocator();
if (projectLocator.equals(projectData.getProjectLocator())) {
return null; // local internal linking not supported
}
- ghidraUrl = GhidraURL.makeURL(projectLocator, pathname, null);
+ URL ghidraUrl = GhidraURL.makeURL(projectLocator, pathname, null);
+ linkPath = ghidraUrl.toExternalForm();
}
- 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;
- }
+ // Force use of unique link-file name
+ String newName = getUniqueName(linkFilename);
try {
- lh.createLink(ghidraUrl, fileSystem, getPathname(), newName);
+ lh.createLink(linkPath, fileSystem, getPathname(), newName);
}
catch (InvalidNameException e) {
throw new IOException(e); // unexpected
@@ -1381,17 +1446,84 @@ class GhidraFolderData {
}
/**
- * Generate a non-conflicting file name for this folder based upon the specified preferred name.
+ * Create an external link-file within this folder which references the specified
+ * {@code ghidraUrl} and whose content is defined by the specified {@link LinkHandler lh}
+ * instance.
+ *
+ * @param ghidraUrl a Ghidra URL which corresponds to a file or a folder based on the designated
+ * {@link LinkHandler lh} instance. Only rudimentary URL checks are performed.
+ * @param linkFilename name of link-file to be created within this folder. NOTE: This name may
+ * be modified to ensure uniqueness within this folder.
+ * @param lh link-file handler used to create specific link-file (see derived implementations
+ * of {@link LinkHandler} and their public static INSTANCE.
+ * @return newly created link-file
+ * @throws IOException if IO error occurs during link creation
+ */
+ DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler> lh)
+ throws IOException {
+
+ URL url = new URL(ghidraUrl);
+ if (!GhidraURL.isLocalGhidraURL(ghidraUrl) && !GhidraURL.isServerRepositoryURL(url)) {
+ throw new IllegalArgumentException("Invalid Ghidra URL specified");
+ }
+
+ // Force use of unique link-file name
+ String newName = getUniqueName(linkFilename);
+
+ try {
+ lh.createLink(ghidraUrl, fileSystem, getPathname(), newName);
+ }
+ catch (InvalidNameException e) {
+ throw new IOException(e); // unexpected
+ }
+
+ fileChanged(newName);
+ return getDomainFile(newName);
+ }
+
+ private String getUniqueName(String name) throws IOException {
+ String newName = name;
+ int i = 1;
+ while (true) {
+ // Check for unique name considering both files and folders
+ // NOTE: If disconnected from repository, remote content is not considered
+ if (getFileData(newName, false) == null && getFolderData(newName, false) == null) {
+ return newName;
+ }
+ newName = name + "." + i;
+ ++i;
+ }
+ }
+
+ private static String getRelativePath(String referencedPathname, String linkParentPathname) {
+ Path referencedPath = Paths.get(referencedPathname);
+ Path linkParentPath = Paths.get(linkParentPathname);
+ Path relativePath = linkParentPath.relativize(referencedPath);
+ String path = relativePath.toString();
+ if (referencedPathname.endsWith(FileSystem.SEPARATOR) &&
+ !path.endsWith(FileSystem.SEPARATOR)) {
+ path += FileSystem.SEPARATOR;
+ }
+ return path;
+ }
+
+ /**
+ * Generate a non-conflicting file name for this destination folder based upon the specified
+ * preferred name.
* NOTE: This method is subject to race conditions where returned name could conflict by the
- * time it is actually used.
+ * time it is actually used or names already present in repository while disconnected, etc.
* @param preferredName preferred file name
+ * @param checkFilesAndFolders if true ensure name is unique relative to both files and folders,
+ * else unqiue among files only.
* @return non-conflicting file name
* @throws IOException if an IO error occurs during file checks
*/
- String getTargetName(String preferredName) throws IOException {
+ String getUniqueFileName(String preferredName, boolean checkFilesAndFolders)
+ throws IOException {
String newName = preferredName;
int i = 1;
- while (getFileData(newName, false) != null) {
+ while (getFileData(newName, false) != null ||
+ (checkFilesAndFolders && containsFolder(newName))) {
newName = preferredName + "." + i;
i++;
}
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
index 0951a7319c..5c321bfc0a 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -18,20 +18,24 @@ package ghidra.framework.data;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.Map;
+import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
import javax.swing.Icon;
+import org.apache.commons.lang3.StringUtils;
+
import generic.theme.GIcon;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.*;
-import ghidra.framework.store.FileSystem;
-import ghidra.framework.store.FolderItem;
+import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl;
+import ghidra.framework.store.*;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
+import utility.function.Dummy;
/**
* NOTE: ALL ContentHandler implementations MUST END IN "ContentHandler". If not,
@@ -42,59 +46,110 @@ import ghidra.util.task.TaskMonitor;
*
* @param {@link URLLinkObject} implementation class
*/
-public abstract class LinkHandler extends DBContentHandler {
+public abstract class LinkHandler implements ContentHandler {
+ /**
+ * Legacy linkPath metadata key for database storage
+ */
public static final String URL_METADATA_KEY = "link.url";
- // 16x16 link icon where link is placed in lower-left corner
+ /**
+ * 16x16 link icon where link is placed in lower-left corner
+ */
public static final Icon LINK_ICON = new GIcon("icon.content.handler.link.overlay");
+ /**
+ * {@link LinkStatus} provides a link evaluation for its ulimate type or if it is
+ * considered broken. See {@link LinkHandler#getLinkFileStatus(DomainFile, Consumer)}.
+ */
+ public enum LinkStatus {
+
+ /**
+ * The link-file specified does not refer to a valid file or content-type.
+ */
+ BROKEN,
+
+ /**
+ * The link-file ultimately refers to a file or folder path within the same project.
+ */
+ INTERNAL,
+
+ /**
+ * The link-file ultimately refers to an external project/repository path with a Ghidra URL.
+ */
+ EXTERNAL,
+
+ /**
+ * The specified file is not a link-file
+ */
+ NON_LINK;
+ }
+
+ @Override
+ public LinkHandler> getLinkHandler() {
+ return this; // allow links to the same type of link
+ }
+
/**
* Create a link file using the specified URL
- * @param ghidraUrl link URL (must be a Ghidra URL - see {@link GhidraURL}).
+ * @param linkPath link path or Ghidra URL.
* @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,
+ protected final void createLink(String linkPath, 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);
- }
+
+ fs.createTextDataItem(folderPath, linkFilename, FileIDFactory.createFileID(),
+ getContentType(), linkPath, null);
+ }
+
+ @Override
+ public final long createFile(FileSystem fs, FileSystem userfs, String path, String name,
+ DomainObject domainObject, TaskMonitor monitor)
+ throws IOException, InvalidNameException, CancelledException {
+ throw new UnsupportedOperationException("createLink must be used for link-file");
+ }
+
+ @Override
+ public final T getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
+ boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
+ throws IOException, CancelledException, VersionException {
+ throw new UnsupportedOperationException("getObject must be used for link-file");
}
@Override
public final T getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
- if (!okToUpgrade) {
- throw new UnsupportedOperationException("okToUpgrade must be true for link-file");
- }
- return getObject(item, version, consumer, monitor, false);
+ throw new UnsupportedOperationException("getObject must be used for link-file");
}
@Override
public T getImmutableObject(FolderItem item, Object consumer, int version, int minChangeVersion,
TaskMonitor monitor) throws IOException, CancelledException, VersionException {
- if (minChangeVersion != -1) {
- throw new UnsupportedOperationException("minChangeVersion must be -1 for link-file");
- }
- return getObject(item, version, consumer, monitor, true);
+ throw new UnsupportedOperationException("getObject must be used for link-file");
}
- private T getObject(FolderItem item, int version, Object consumer, TaskMonitor monitor,
- boolean immutable) throws IOException, VersionException, CancelledException {
+ /**
+ * Get immutable or read-only domain object based upon an initial external GhidraURL.
+ * @param ghidraUrl external URL
+ * @param version {@link DomainFile} version (ignored if URL end-point is a DomainFile since
+ * the {@link GhidraURLConnection} has no way to convey the version.
+ * @param consumer domain object consumer
+ * @param monitor task monitor
+ * @param immutable true if object is open immutable (no upgrade support), else read-only
+ * @return domain object
+ * @throws IOException if an IO error occurs
+ * @throws VersionException if a version exception prevents opening the file
+ * @throws CancelledException if task is cancelled
+ */
+ T getObject(URL ghidraUrl, int version, Object consumer, TaskMonitor monitor, boolean immutable)
+ throws IOException, VersionException, CancelledException {
- URL ghidraUrl = getURL(item);
+ // TODO: may not have insight into version associated with a link-file
Class> domainObjectClass = getDomainObjectClass();
if (domainObjectClass == null) {
@@ -103,32 +158,36 @@ public abstract class LinkHandler extends DBCon
AtomicReference verExcRef = new AtomicReference<>();
AtomicReference domainObjectRef = new AtomicReference<>();
- GhidraURLQuery.queryUrl(ghidraUrl, new GhidraURLResultHandlerAdapter(true) {
+ GhidraURLQuery.queryUrl(ghidraUrl, getDomainObjectClass(),
+ new GhidraURLResultHandlerAdapter(true) {
+ // GhidraURLQuery will perform the link-following
+ @Override
+ public void processResult(DomainFile domainFile, URL url, TaskMonitor m)
+ throws IOException, CancelledException {
+ if (!getDomainObjectClass()
+ .isAssignableFrom(domainFile.getDomainObjectClass())) {
+ throw new BadLinkException("Expected " + getDomainObjectClass() +
+ " but linked to " + domainFile.getDomainObjectClass());
+ }
+ try {
+ @SuppressWarnings("unchecked")
+ T linkedObject = immutable
+ ? (T) domainFile.getImmutableDomainObject(consumer, version,
+ monitor)
+ : (T) domainFile.getReadOnlyDomainObject(consumer, version,
+ monitor);
+ domainObjectRef.set(linkedObject);
+ }
+ catch (VersionException e) {
+ verExcRef.set(e);
+ }
+ }
- @Override
- public void processResult(DomainFile domainFile, URL url, TaskMonitor m)
- throws IOException, CancelledException {
- if (!getDomainObjectClass().isAssignableFrom(domainFile.getDomainObjectClass())) {
- throw new BadLinkException("Expected " + getDomainObjectClass() +
- " but linked to " + domainFile.getDomainObjectClass());
+ @Override
+ public void handleUnauthorizedAccess(URL url) throws IOException {
+ throw new IOException("Authorization failure");
}
- try {
- @SuppressWarnings("unchecked")
- T linkedObject = immutable
- ? (T) domainFile.getImmutableDomainObject(consumer, version, monitor)
- : (T) domainFile.getReadOnlyDomainObject(consumer, version, monitor);
- domainObjectRef.set(linkedObject);
- }
- catch (VersionException e) {
- verExcRef.set(e);
- }
- }
-
- @Override
- public void handleUnauthorizedAccess(URL url) throws IOException {
- throw new IOException("Authorization failure");
- }
- }, monitor);
+ }, LinkFileControl.FOLLOW_EXTERNAL, monitor);
VersionException versionException = verExcRef.get();
if (versionException != null) {
@@ -138,19 +197,11 @@ public abstract class LinkHandler extends DBCon
T domainObj = domainObjectRef.get();
if (domainObj == null) {
throw new IOException(
- "Failed to obtain linked object for unknown reason: " + item.getPathName());
+ "Failed to obtain linked object for unknown reason: " + ghidraUrl);
}
return domainObj;
}
- @Override
- public final T getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
- boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
- throws IOException, CancelledException, VersionException {
- // getReadOnlyObject or getImmutableObject should be used
- throw new UnsupportedOperationException("link-file does not support getDomainObject");
- }
-
@Override
public final ChangeSet getChangeSet(FolderItem versionedFolderItem, int olderVersion,
int newerVersion) throws VersionException, IOException {
@@ -168,49 +219,6 @@ public abstract class LinkHandler extends DBCon
throw new UnsupportedOperationException("Link file requires checking server vs local URL");
}
- /**
- * 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.
@@ -218,4 +226,322 @@ public abstract class LinkHandler extends DBCon
@Override
abstract public Icon getIcon();
+ //////////////////////////
+ // Static package methods
+ //////////////////////////
+
+ /**
+ * Determine if the contents of a link file can be shared (i.e., added to repository).
+ * Local project Ghidra-URL paths may not be shared.
+ *
+ * @param linkFile link file
+ * @return true if link may be shared
+ */
+ static boolean canShareLink(FolderItem linkFile) {
+ try {
+ String linkPath = getLinkPath(linkFile);
+ return !GhidraURL.isLocalGhidraURL(linkPath);
+ }
+ catch (IOException e) {
+ // ignore
+ }
+ return false;
+ }
+
+ /**
+ * Get the stored link-path or Ghidra-URL
+ *
+ * @param linkFile link file (see {@link DomainFile#isLink()}).
+ * @return stored link-path or Ghidra-URL
+ * @throws IOException if an IO error occurs or a valid link-file was not specified
+ */
+ static String getLinkPath(FolderItem linkFile) throws IOException {
+ String contentType = linkFile.getContentType();
+ ContentHandler> ch = DomainObjectAdapter.getContentHandler(contentType);
+ if (ch instanceof LinkHandler) {
+ String linkPath = null;
+ if (linkFile instanceof TextDataItem textItem) {
+ linkPath = textItem.getTextData();
+ }
+ if (linkPath == null) {
+ // Fallback to reading old database storage form as metadata
+ Map metadata = GhidraFileData.getMetadata(linkFile);
+ linkPath = metadata.get(URL_METADATA_KEY);
+ }
+ if (StringUtils.isBlank(linkPath)) {
+ throw new IOException("Invalid link-file: " + linkFile.getPathName());
+ }
+ return linkPath;
+ }
+ throw new IOException("Invalid link-file content: " + linkFile.getPathName());
+ }
+
+ //////////////////////////
+ // Static public methods
+ //////////////////////////
+
+ /**
+ * Get the link URL which corresponds to the specified link file's link-path.
+ * If link-path was originally specified as an internal path it will be transformed
+ * into a URL. See {@link DomainFile#isLink()}.
+ *
+ * @param linkFile link-file domain file which may correspond to a linked-folder or file.
+ * @return link URL or null if invalid link-URL or a non-link-file is specified
+ * @throws IOException if linkFile has an invalid relative link-path that failed to normalize
+ */
+ public static URL getLinkURL(DomainFile linkFile) throws IOException {
+
+ // TODO: link traversal not handled (e.g., path element is a linked folder)
+ // May have to follow incrementally
+
+ String linkPath = getAbsoluteLinkPath(linkFile);
+ if (linkPath == null) {
+ return null;
+ }
+
+ try {
+ if (!GhidraURL.isGhidraURL(linkPath)) {
+ ProjectData projectData = linkFile.getParent().getProjectData();
+ return GhidraURL.makeURL(projectData.getProjectLocator(), linkPath, null);
+ }
+ return new URL(linkPath);
+ }
+ catch (MalformedURLException | IllegalArgumentException e) {
+ // Bad URL from link path
+ throw new IOException("Failed to form URL from linkPath: " + linkPath, e);
+ }
+ }
+
+ /**
+ * Get the Ghidra URL or absolute normalized link-path from a link file.
+ * Path normalization eliminates any path element of "./" or "../".
+ * A local folder-link path will always end with a "/" path separator.
+ * Path normalization is not performed on Ghidra URLs.
+ *
+ * @param linkFile link file
+ * @return Ghidra URL or absolute normalized link-path from a link file
+ * @throws IOException if linkFile has an invalid relative link-path that failed to normalize
+ */
+ public static String getAbsoluteLinkPath(DomainFile linkFile) throws IOException {
+
+ LinkFileInfo linkInfo = linkFile.getLinkInfo();
+ if (linkInfo == null) {
+ return null;
+ }
+ String linkPath = linkInfo.getLinkPath();
+ if (StringUtils.isBlank(linkPath)) {
+ return null;
+ }
+
+ String path = linkPath;
+ if (!GhidraURL.isGhidraURL(path)) {
+ if (!linkPath.startsWith(FileSystem.SEPARATOR)) {
+ path = linkFile.getParent().getPathname();
+ if (!path.endsWith(FileSystem.SEPARATOR)) {
+ path += FileSystem.SEPARATOR;
+ }
+ path += linkPath;
+ }
+ try {
+ return FileSystem.normalizePath(path);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IOException("Invalid link path: " + linkPath);
+ }
+ }
+ return path;
+ }
+
+ /**
+ * Determine the link status for the specified {@link DomainFile#isLink() link-file}.
+ * If a status is {@link LinkStatus#BROKEN} and an {@code errorConsumer} has been specified
+ * the error details will be reported.
+ *
+ * @param file domain file
+ * @param errorConsumer broken link error consumer (may be null)
+ * @return link status
+ */
+ public static LinkStatus getLinkFileStatus(DomainFile file, Consumer errorConsumer) {
+ AtomicReference status = new AtomicReference<>();
+ followInternalLinkage(file, s -> status.set(s), errorConsumer);
+ return status.get();
+ }
+
+ /**
+ * Add real internal folder path for specified folder or folder-link and check for
+ * circular conflict.
+ * @param pathSet real path accumulator
+ * @param linkPath internal linkPath
+ * @return true if no path conflict detected, false if path conflict is detected
+ */
+ private static boolean addLinkPathPath(Set pathSet, String linkPath) {
+ // Must ensure that all paths end with '/' separator - even if path is endpoint
+ if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
+ linkPath += FileSystem.SEPARATOR;
+ }
+ for (String path : pathSet) {
+ if (path.startsWith(linkPath)) {
+ return false;
+ }
+ }
+ pathSet.add(linkPath);
+ return true;
+ }
+
+ /**
+ * Follow the internal linkage, if any, for the specified file. Any broken linkage details will
+ * be reported to the specified {@code errorConsumer}.
+ *
+ * @param file domain file to be checked
+ * @param statusConsumer link status consumer (required)
+ * @param errorConsumer broken link error consumer (may be null)
+ * @return the final {@link DomainFile} within the same project or null if file specified was
+ * not a link-file. A broken link will return the last valid link-file in chain.
+ */
+ public static DomainFile followInternalLinkage(DomainFile file,
+ Consumer statusConsumer, Consumer errorConsumer) {
+
+ Objects.requireNonNull(statusConsumer, "Status consumer is required");
+
+ errorConsumer = Dummy.ifNull(errorConsumer);
+
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ if (linkInfo == null) {
+ statusConsumer.accept(LinkStatus.NON_LINK);
+ return null;
+ }
+
+ Set linkPathsVisited = new HashSet<>();
+
+ ProjectData projectData;
+ DomainFolder parent = file.getParent();
+ if (parent instanceof LinkedDomainFolder lf) {
+ try {
+ projectData = lf.getLinkedProjectData();
+ addLinkPathPath(linkPathsVisited, lf.getLinkedPathname());
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Unexpected", e);
+ }
+ }
+ else {
+ projectData = parent.getProjectData();
+ addLinkPathPath(linkPathsVisited, file.getPathname());
+ }
+
+ String contentType = file.getContentType();
+ Class extends DomainObject> domainObjectClass = file.getDomainObjectClass();
+ boolean isFolderLink =
+ FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(contentType);
+
+ // Loop recurses through link-chain to arrive at final internal link-file
+ DomainFile nextLinkFile = file;
+
+ while (true) {
+
+ String linkPath = null;
+ try {
+ linkPath = LinkHandler.getAbsoluteLinkPath(nextLinkFile);
+ }
+ catch (IOException e) {
+ errorConsumer.accept(e.getMessage());
+ break;
+ }
+ if (linkPath == null) {
+ errorConsumer.accept("Invalid link-path storage");
+ break;
+ }
+
+ if (isFolderLink) {
+ String name = nextLinkFile.getName();
+ if (nextLinkFile.getParent().getFolder(name) != null) {
+ errorConsumer.accept(
+ "Folder name conflicts with this folder-link in same folder: " + name);
+ break;
+ }
+ }
+
+ if (GhidraURL.isGhidraURL(linkPath)) {
+ statusConsumer.accept(LinkStatus.EXTERNAL);
+ return nextLinkFile;
+ }
+
+ if (!addLinkPathPath(linkPathsVisited, linkPath)) {
+ errorConsumer.accept("Link has a circular reference");
+ break; // broken and can't continue
+ }
+
+ DomainFile linkedFile = null;
+ if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
+ linkedFile = projectData.getFile(linkPath);
+ }
+
+ if (isFolderLink) {
+ // Check for folder existence at linkPath
+ if (getNonLinkedFolder(projectData, linkPath) != null) {
+ // Check for folder-link that conflicts with folder found
+ if (linkedFile != null) {
+ LinkFileInfo linkedFileLinkInfo = linkedFile.getLinkInfo();
+ if (linkedFileLinkInfo != null && linkedFileLinkInfo.isFolderLink()) {
+ errorConsumer.accept(
+ "Referenced folder name conflicts with folder-link in the same folder: " +
+ linkPath);
+ break;
+ }
+ }
+ statusConsumer.accept(LinkStatus.INTERNAL);
+ return nextLinkFile;
+ }
+ }
+
+ if (linkedFile == null) {
+ String acceptableType = isFolderLink ? "folder" : "file";
+ errorConsumer.accept(
+ "Broken " + contentType + " - " + acceptableType + " not found: " + linkPath);
+ break;
+ }
+
+ if (!domainObjectClass.isAssignableFrom(linkedFile.getDomainObjectClass())) {
+ // NOTE: folder-links use NullFolderDomainObject
+ errorConsumer.accept(
+ "Broken " + contentType + " - incompatible content-type: " + linkPath);
+ break;
+ }
+
+ if (!linkedFile.isLink()) {
+ statusConsumer.accept(LinkStatus.INTERNAL);
+ return linkedFile;
+ }
+
+ nextLinkFile = linkedFile;
+ }
+
+ // Must be broken to end up here
+ statusConsumer.accept(LinkStatus.BROKEN);
+ return nextLinkFile;
+ }
+
+ private static DomainFolder getNonLinkedFolder(ProjectData projectData, 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 + "'");
+ }
+
+ DomainFolder folder = projectData.getRootFolder();
+ String[] split = path.split(FileSystem.SEPARATOR);
+ if (split.length == 0) {
+ return folder;
+ }
+
+ for (int i = 1; i < split.length; i++) {
+ DomainFolder subFolder = folder.getFolder(split[i]);
+ if (subFolder == null) {
+ return null;
+ }
+ folder = subFolder;
+ }
+ return folder;
+ }
+
}
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
index a298700753..aa17e5d2f1 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -30,14 +30,14 @@ import org.apache.commons.lang3.StringUtils;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.*;
-import ghidra.util.*;
+import ghidra.util.InvalidNameException;
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}.
+ * {@link LinkedGhidraSubFolder}.
*/
class LinkedGhidraFile implements LinkedDomainFile {
@@ -68,6 +68,25 @@ class LinkedGhidraFile implements LinkedDomainFile {
return fileName;
}
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof LinkedGhidraFile other)) {
+ return false;
+ }
+ return fileName.equals(other.fileName) && parent.equals(other.parent);
+ }
+
+ @Override
+ public int hashCode() {
+ return getPathname().hashCode();
+ }
+
@Override
public int compareTo(DomainFile df) {
return fileName.compareToIgnoreCase(df.getName());
@@ -86,7 +105,8 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override
public DomainFile setName(String newName) throws InvalidNameException, IOException {
- throw new ReadOnlyException("linked file is read only");
+ String name = getLinkedFile().setName(newName).getName();
+ return parent.getFile(name);
}
@Override
@@ -130,6 +150,7 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override
public ProjectLocator getProjectLocator() {
+ // TODO: Should this reflect real project?
return parent.getProjectLocator();
}
@@ -147,17 +168,21 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override
public ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException {
- return null;
+ return getLinkedFile().getChangesByOthersSinceCheckout();
}
@Override
public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
TaskMonitor monitor) throws VersionException, IOException, CancelledException {
- return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor);
+ return getLinkedFile().getDomainObject(consumer, okToUpgrade, okToRecover, monitor);
}
@Override
public DomainObject getOpenedDomainObject(Object consumer) {
+ DomainFile df = getLinkedFileNoError();
+ if (df != null) {
+ return df.getOpenedDomainObject(consumer);
+ }
return null;
}
@@ -195,7 +220,8 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override
public boolean isInWritableProject() {
- return false; // While project may be writeable this folder/file is not
+ // TODO: Is this correct?
+ return parent.isInWritableProject();
}
@Override
@@ -212,47 +238,56 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override
public boolean isCheckedOut() {
- return false;
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.isCheckedOut() : false;
}
@Override
public boolean isCheckedOutExclusive() {
- return false;
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.isCheckedOutExclusive() : false;
}
@Override
public boolean modifiedSinceCheckout() {
- return false;
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.modifiedSinceCheckout() : false;
}
@Override
public boolean canCheckout() {
- return false;
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.canCheckout() : false;
}
@Override
public boolean canCheckin() {
- return false;
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.canCheckin() : false;
}
@Override
public boolean canMerge() {
- return false;
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.canMerge() : false;
}
@Override
public boolean canAddToRepository() {
- return false;
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.canAddToRepository() : false;
}
@Override
public void setReadOnly(boolean state) throws IOException {
- // ignore
+ getLinkedFile().setReadOnly(state);
}
@Override
public boolean isReadOnly() {
- return true; // not reflected by icon
+ DomainFile df = getLinkedFileNoError();
+ // read-only state not reflected by icon
+ return df != null ? df.isReadOnly() : true;
}
@Override
@@ -263,7 +298,8 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override
public boolean isHijacked() {
- return false;
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.isHijacked() : false;
}
@Override
@@ -274,85 +310,84 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override
public boolean isLatestVersion() {
- return true;
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.isLatestVersion() : true;
}
@Override
public int getVersion() {
- // TODO: Do we want to reveal linked-local-project checkout details?
- return getLatestVersion();
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getVersion() : DomainFile.DEFAULT_VERSION;
}
@Override
public Version[] getVersionHistory() throws IOException {
- DomainFile df = getLinkedFileNoError();
+ DomainFile df = getLinkedFile();
return df != null ? df.getVersionHistory() : new Version[0];
}
@Override
public void addToVersionControl(String comment, boolean keepCheckedOut, TaskMonitor monitor)
throws IOException, CancelledException {
- throw new UnsupportedOperationException();
+ getLinkedFile().addToVersionControl(comment, keepCheckedOut, monitor);
}
@Override
public boolean checkout(boolean exclusive, TaskMonitor monitor)
throws IOException, CancelledException {
- throw new UnsupportedOperationException();
+ return getLinkedFile().checkout(exclusive, monitor);
}
@Override
public void checkin(CheckinHandler checkinHandler, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
- throw new UnsupportedOperationException();
+ getLinkedFile().checkin(checkinHandler, monitor);
}
@Override
public void merge(boolean okToUpgrade, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
- throw new UnsupportedOperationException();
+ getLinkedFile().merge(okToUpgrade, monitor);
}
@Override
public void undoCheckout(boolean keep) throws IOException {
- throw new UnsupportedOperationException();
+ getLinkedFile().undoCheckout(keep);
}
@Override
public void undoCheckout(boolean keep, boolean force) throws IOException {
- throw new UnsupportedOperationException();
+ getLinkedFile().undoCheckout(keep, force);
}
@Override
public void terminateCheckout(long checkoutId) throws IOException {
- throw new UnsupportedOperationException();
+ getLinkedFile().terminateCheckout(checkoutId);
}
@Override
public ItemCheckoutStatus[] getCheckouts() throws IOException {
- DomainFile df = getLinkedFileNoError();
- return df != null ? df.getCheckouts() : new ItemCheckoutStatus[0];
+ return getLinkedFile().getCheckouts();
}
@Override
public ItemCheckoutStatus getCheckoutStatus() throws IOException {
- // TODO: Do we want to reveal linked-local-project checkout details?
- return null;
+ return getLinkedFile().getCheckoutStatus();
}
@Override
public void delete() throws IOException {
- throw new ReadOnlyException("linked file is read only");
+ getLinkedFile().delete();
}
@Override
public void delete(int version) throws IOException {
- throw new ReadOnlyException("linked file is read only");
+ getLinkedFile().delete(version);
}
@Override
public DomainFile moveTo(DomainFolder newParent) throws IOException {
- throw new ReadOnlyException("linked file is read only");
+ return getLinkedFile().moveTo(newParent);
}
@Override
@@ -368,8 +403,8 @@ class LinkedGhidraFile implements LinkedDomainFile {
}
@Override
- public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
- return getLinkedFile().copyToAsLink(newParent);
+ public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
+ return getLinkedFile().copyToAsLink(newParent, relative);
}
@Override
@@ -385,17 +420,18 @@ class LinkedGhidraFile implements LinkedDomainFile {
@Override
public boolean isChanged() {
- return false;
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.isChanged() : false;
}
@Override
public boolean isOpen() {
- return false; // domain file proxy always used
+ return false; // real file may be but this is not
}
@Override
public boolean isBusy() {
- return false; // domain file proxy always used
+ return false; // real file may be but this is not
}
@Override
@@ -416,24 +452,30 @@ class LinkedGhidraFile implements LinkedDomainFile {
}
@Override
- public boolean isLinkFile() {
+ public boolean isLink() {
DomainFile df = getLinkedFileNoError();
- return df != null ? df.isLinkFile() : false;
+ return df != null ? df.isLink() : false;
}
@Override
- public DomainFolder followLink() {
- try {
- return FolderLinkContentHandler.getReadOnlyLinkedFolder(this);
- }
- catch (IOException e) {
- Msg.error(this, "Failed to following folder-link: " + getPathname());
- }
- return null;
+ public LinkFileInfo getLinkInfo() {
+ DomainFile df = getLinkedFileNoError();
+ return df != null ? df.getLinkInfo() : null;
+ }
+
+ @Override
+ public String getLinkedPathname() {
+ return parent.getLinkedPathname(fileName);
}
@Override
public String toString() {
- return "LinkedGhidraFile: " + getPathname();
+ String str = parent.toString();
+ if (!str.endsWith("/")) {
+ str += "/";
+ }
+ str += getName();
+ return str;
}
+
}
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
index 865038d3b6..d99d53830d 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,9 +22,12 @@ import java.net.URL;
import javax.swing.Icon;
import generic.theme.GIcon;
+import ghidra.framework.client.RepositoryAdapter;
+import ghidra.framework.main.AppInfo;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.FileSystem;
+import ghidra.util.InvalidNameException;
/**
* {@code LinkedGhidraFolder} provides the base {@link LinkedDomainFolder} implementation which
@@ -36,39 +39,88 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
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 final DomainFile folderLinkFile;
- private String linkedPathname;
+ // Linked folder established using either a URL or a folder
+ private final URL linkedFolderUrl;
+ private final DomainFolder linkedFolder;
+ private final String linkedPathname;
+ private final URL projectUrl;
- private URL projectUrl;
+ private boolean offline = false; // allow single failure
/**
- * 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
+ * Construct a linked-folder which is linked via a Ghidra URL.
+ *
+ * NOTE: An active project is required as conveyed by {@link AppInfo#getActiveProject()}
+ * which will take ownership of any project view which is required. This should be pre-checked
+ * since an error will occur if there is no active project at the time the link is followed.
+ *
+ * @param folderLinkFile link-file which corresponds to a linked-folder
+ * (see {@link LinkFileInfo#isFolderLink()}).
+ * @param linkedFolderUrl linked folder URL
*/
- LinkedGhidraFolder(Project activeProject, DomainFolder localParent, String linkFilename,
- URL folderUrl) {
- super(linkFilename);
+ LinkedGhidraFolder(DomainFile folderLinkFile, URL linkedFolderUrl) {
+ super(folderLinkFile.getName());
- if (!GhidraURL.isServerRepositoryURL(folderUrl) &&
- !GhidraURL.isLocalProjectURL(folderUrl)) {
- throw new IllegalArgumentException("Invalid Ghidra URL: " + folderUrl);
+ if (!GhidraURL.isServerRepositoryURL(linkedFolderUrl) &&
+ !GhidraURL.isLocalProjectURL(linkedFolderUrl)) {
+ throw new IllegalArgumentException("Invalid Ghidra URL: " + linkedFolderUrl);
}
- this.activeProject = activeProject;
- this.localParent = localParent;
- this.folderUrl = folderUrl;
+ this.folderLinkFile = folderLinkFile;
- linkedPathname = GhidraURL.getProjectPathname(folderUrl);
- if (linkedPathname.length() > 0 && linkedPathname.endsWith(FileSystem.SEPARATOR)) {
- linkedPathname = linkedPathname.substring(0, linkedPathname.length() - 1);
+ this.linkedFolderUrl = linkedFolderUrl;
+ this.linkedFolder = null;
+
+ String pathname = GhidraURL.getProjectPathname(linkedFolderUrl);
+ if (!FileSystem.SEPARATOR.equals(pathname) && pathname.endsWith(FileSystem.SEPARATOR)) {
+ // avoid trailing path separator except on root pathname
+ pathname = pathname.substring(0, pathname.length() - 1);
}
+ linkedPathname = pathname;
+ projectUrl = GhidraURL.getProjectURL(linkedFolderUrl);
+ }
+
+ /**
+ * Construct a linked-folder which is linked to another folder within the associated
+ * {@link #getProjectData() project data} instance.
+ *
+ * @param folderLinkFile link-file which corresponds to a linked-folder
+ * (see {@link LinkFileInfo#isFolderLink()}).
+ * @param linkedFolder locally-linked folder within same project
+ */
+ LinkedGhidraFolder(DomainFile folderLinkFile, DomainFolder linkedFolder) {
+ super(folderLinkFile.getName());
+
+ this.folderLinkFile = folderLinkFile;
+
+ this.linkedFolder = linkedFolder;
+ this.linkedFolderUrl = null;
+
+ linkedPathname = linkedFolder.getPathname();
+
+ projectUrl = linkedFolder.getProjectLocator().getURL();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof LinkedGhidraFolder other)) {
+ return false;
+ }
+ return linkedPathname.equals(other.linkedPathname) &&
+ folderLinkFile.equals(other.folderLinkFile);
+ }
+
+ @Override
+ public boolean isExternal() {
+ return linkedFolderUrl != null;
}
/**
@@ -76,9 +128,6 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
* @return Ghidra URL of the project/repository folder referenced by this object
*/
public URL getProjectURL() {
- if (projectUrl == null) {
- projectUrl = GhidraURL.getProjectURL(folderUrl);
- }
return projectUrl;
}
@@ -87,16 +136,67 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
return this;
}
- DomainFolder getLinkedFolder(String linkedPath) throws IOException {
+ @Override
+ public boolean isInWritableProject() {
+ return linkedFolder != null && linkedFolder.isInWritableProject();
+ }
- ProjectData projectData = activeProject.addProjectView(getProjectURL(), false);
- if (projectData == null) {
- throw new FileNotFoundException();
+ @Override
+ public ProjectData getLinkedProjectData() throws IOException {
+ // NOTE: The offline tracking is done to avoid repeatedly prompting for a connection
+ // password. Only one connect attempt per instance will be performed.
+ ProjectData projectData;
+ if (linkedFolder != null) {
+ projectData = linkedFolder.getProjectData();
}
+ else {
+ // Handle GhidraURL linkages
+ Project activeProject = AppInfo.getActiveProject();
+ if (activeProject == null) {
+ offline = true;
+ throw new IOException("active project not found");
+ }
+ URL url = getProjectURL();
+ projectData = activeProject.getProjectData(url);
+ if (projectData == null && !offline) {
+ offline = true;
+ projectData = activeProject.addProjectView(url, false);
+ if (projectData != null) {
+ offline = false;
+ RepositoryAdapter repository = projectData.getRepository();
+ if (repository != null && !repository.isConnected()) {
+ // User chose not to connect - don't force them
+ offline = true;
+ }
+ }
+ }
+ if (projectData == null) {
+ throw new FileNotFoundException("failed to add project view: " + url);
+ }
+ }
+ return projectData;
+ }
+
+ synchronized DomainFolder getRealFolder(String linkedPath) throws IOException {
+ ProjectData projectData = getLinkedProjectData();
DomainFolder folder = projectData.getFolder(linkedPath);
if (folder == null) {
- throw new FileNotFoundException(folderUrl.toExternalForm());
+ RepositoryAdapter repository = projectData.getRepository();
+ if (repository != null) {
+ if (!offline && !repository.isConnected()) {
+ repository.connect();
+ if (!repository.isConnected()) {
+ offline = true;
+ throw new FileNotFoundException("linked project/repository not connected");
+ }
+ folder = projectData.getFolder(linkedPath);
+ }
+ }
+ if (folder == null) {
+ String notConnectedMsg = offline ? " (not connected)" : "";
+ throw new FileNotFoundException("folder not found" + notConnectedMsg);
+ }
}
return folder;
}
@@ -106,24 +206,41 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
return linkedPathname;
}
+ @Override
+ public DomainFolder getRealFolder() throws IOException {
+ return getRealFolder(linkedPathname);
+ }
+
@Override
public ProjectLocator getProjectLocator() {
- return localParent.getProjectLocator();
+ return folderLinkFile.getProjectLocator();
}
@Override
public ProjectData getProjectData() {
- return localParent.getProjectData();
+ return folderLinkFile.getParent().getProjectData();
}
@Override
public DomainFolder getParent() {
- return localParent;
+ return folderLinkFile.getParent();
+ }
+
+ @Override
+ public DomainFolder setName(String newName) throws InvalidNameException, IOException {
+ DomainFile linkFile = folderLinkFile.setName(newName);
+ if (linkedFolder != null) {
+ return new LinkedGhidraFolder(linkFile, linkedFolder);
+ }
+ return new LinkedGhidraFolder(linkFile, linkedFolderUrl);
}
@Override
public String toString() {
- return "LinkedGhidraFolder: " + getPathname();
+ if (linkedFolder != null) {
+ return "->" + getLinkedPathname();
+ }
+ return "->" + linkedFolderUrl.toString();
}
@Override
@@ -135,4 +252,19 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
public boolean isLinked() {
return true;
}
+
+ /**
+ * Determine if this linked-folder corresponds to an external URL linkage and not an internal
+ * project linkage.
+ * @return true if linked based on external URL
+ */
+ public boolean isUrlLinked() {
+ if (linkedFolderUrl != null) {
+ return true;
+ }
+ if (linkedFolder instanceof LinkedGhidraFolder lf) {
+ return lf.isUrlLinked();
+ }
+ return false;
+ }
}
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
index 05c07f1aad..24d5813916 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -38,12 +38,21 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
private final LinkedGhidraSubFolder parent;
private final String folderName;
- LinkedGhidraSubFolder(String folderName) {
+ /**
+ * Construct root-linked-folder based on the name of a folder-link link-file.
+ * @param linkFileName name of link-file which represents a folder-link
+ */
+ LinkedGhidraSubFolder(String linkFileName) {
this.linkedRootFolder = getLinkedRootFolder();
this.parent = null; // must override getParent()
- this.folderName = folderName;
+ this.folderName = linkFileName;
}
+ /**
+ * Construct a linked-folder child
+ * @param parent parent folder within a linked-folder hierarchy
+ * @param folderName folder name
+ */
LinkedGhidraSubFolder(LinkedGhidraSubFolder parent, String folderName) {
this.linkedRootFolder = parent.getLinkedRootFolder();
this.parent = parent;
@@ -59,9 +68,14 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
return linkedRootFolder;
}
+ @Override
+ public boolean isExternal() {
+ return linkedRootFolder.isExternal();
+ }
+
@Override
public boolean isInWritableProject() {
- return false; // While project may be writeable this folder is not
+ return linkedRootFolder.isInWritableProject();
}
@Override
@@ -75,8 +89,27 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
}
@Override
- public DomainFolder getLinkedFolder() throws IOException {
- return linkedRootFolder.getLinkedFolder(getLinkedPathname());
+ public DomainFolder getRealFolder() throws IOException {
+ return linkedRootFolder.getRealFolder(getLinkedPathname());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof LinkedGhidraSubFolder other)) {
+ return false;
+ }
+ return folderName.equals(other.folderName) && parent.equals(other.parent);
+ }
+
+ @Override
+ public int hashCode() {
+ return getPathname().hashCode();
}
@Override
@@ -84,9 +117,50 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
return getName().compareToIgnoreCase(df.getName());
}
+ @Override
+ public boolean isSame(DomainFolder folder) {
+
+ // NOTE: This project check relates to the outermost containing project
+ // and not the project that may be referenenced by a link.
+ if (!getProjectLocator().equals(folder.getProjectLocator()) &&
+ !SystemUtilities.isEqual(getProjectData().getSharedProjectURL(),
+ folder.getProjectData().getSharedProjectURL())) {
+ // Containing project/repository appears to be unrelated
+ return false;
+ }
+
+ return getPathname().equals(folder.getPathname());
+ }
+
+ @Override
+ public boolean isSameOrAncestor(DomainFolder folder) {
+
+ // NOTE: This project check relates to the outermost containing project
+ // and not the project that may be referenenced by a link.
+ if (!getProjectLocator().equals(folder.getProjectLocator()) &&
+ !SystemUtilities.isEqual(getProjectData().getSharedProjectURL(),
+ folder.getProjectData().getSharedProjectURL())) {
+ // Containing project/repository appears to be unrelated
+ return false;
+ }
+
+ String pathname = getPathname();
+
+ DomainFolder f = folder;
+ while (f != null) {
+ if (f == this || pathname.equals(f.getPathname())) {
+ return true;
+ }
+ f = f.getParent();
+ }
+ return false;
+ }
+
@Override
public DomainFolder setName(String newName) throws InvalidNameException, IOException {
- throw new ReadOnlyException("linked folder is read only");
+ DomainFolder linkedFolder = getRealFolder();
+ String name = linkedFolder.setName(newName).getName();
+ return parent.getFolder(name);
}
@Override
@@ -125,6 +199,11 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
return parent.getProjectLocator();
}
+ @Override
+ public ProjectData getLinkedProjectData() throws IOException {
+ return linkedRootFolder.getLinkedProjectData();
+ }
+
@Override
public ProjectData getProjectData() {
return parent.getProjectData();
@@ -142,23 +221,24 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
return path;
}
- /**
- * Get the pathname of this folder within the linked-project/repository
- * @return absolute linked folder path within the linked-project/repository
- */
+ @Override
public String getLinkedPathname() {
- String path = parent.getLinkedPathname();
+ return parent.getLinkedPathname(folderName);
+ }
+
+ final String getLinkedPathname(String childName) {
+ String path = getLinkedPathname();
if (!path.endsWith(FileSystem.SEPARATOR)) {
path += FileSystem.SEPARATOR;
}
- path += folderName;
+ path += childName;
return path;
}
@Override
public LinkedGhidraSubFolder[] getFolders() {
try {
- DomainFolder linkedFolder = getLinkedFolder();
+ DomainFolder linkedFolder = getRealFolder();
DomainFolder[] folders = linkedFolder.getFolders();
LinkedGhidraSubFolder[] linkedSubFolders = new LinkedGhidraSubFolder[folders.length];
for (int i = 0; i < folders.length; i++) {
@@ -167,7 +247,7 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
return linkedSubFolders;
}
catch (IOException e) {
- Msg.error(this, "Linked folder failure: " + e.getMessage());
+ Msg.error(this, "Linked folder failure '" + this + "': " + e.getMessage());
return new LinkedGhidraSubFolder[0];
}
}
@@ -175,14 +255,14 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
@Override
public LinkedGhidraSubFolder getFolder(String name) {
try {
- DomainFolder linkedFolder = getLinkedFolder();
+ DomainFolder linkedFolder = getRealFolder();
DomainFolder f = linkedFolder.getFolder(name);
if (f != null) {
return new LinkedGhidraSubFolder(this, name);
}
}
catch (IOException e) {
- Msg.error(this, "Linked folder failure: " + e.getMessage());
+ Msg.error(this, "Linked folder failure '" + this + "': " + e.getMessage());
}
return null;
}
@@ -190,7 +270,7 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
@Override
public DomainFile[] getFiles() {
try {
- DomainFolder linkedFolder = getLinkedFolder();
+ DomainFolder linkedFolder = getRealFolder();
DomainFile[] files = linkedFolder.getFiles();
LinkedGhidraFile[] linkedSubFolders = new LinkedGhidraFile[files.length];
for (int i = 0; i < files.length; i++) {
@@ -199,7 +279,7 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
return linkedSubFolders;
}
catch (IOException e) {
- Msg.error(this, "Linked folder failure: " + e.getMessage());
+ Msg.error(this, "Linked folder failure '" + this + "': " + e.getMessage());
return new LinkedGhidraFile[0];
}
}
@@ -211,17 +291,17 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
*/
public DomainFile getLinkedFileNoError(String name) {
try {
- DomainFolder linkedFolder = getLinkedFolder();
+ DomainFolder linkedFolder = getRealFolder();
return linkedFolder.getFile(name);
}
catch (IOException e) {
- Msg.error(this, "Linked folder failure: " + e.getMessage());
+ // Ignore
}
return null;
}
DomainFile getLinkedFile(String name) throws IOException {
- DomainFolder linkedFolder = getLinkedFolder();
+ DomainFolder linkedFolder = getRealFolder();
DomainFile df = linkedFolder.getFile(name);
if (df == null) {
throw new FileNotFoundException("linked-file '" + name + "' not found");
@@ -238,11 +318,11 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
@Override
public boolean isEmpty() {
try {
- DomainFolder linkedFolder = getLinkedFolder();
+ DomainFolder linkedFolder = getRealFolder();
return linkedFolder.isEmpty();
}
catch (IOException e) {
- Msg.error(this, "Linked folder failure: " + e.getMessage());
+ Msg.error(this, "Linked folder failure '" + this + "': " + 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;
@@ -252,51 +332,83 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder {
@Override
public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
- throw new ReadOnlyException("linked folder is read only");
+ DomainFolder linkedFolder = getRealFolder();
+ return linkedFolder.createFile(name, obj, monitor);
}
@Override
public DomainFile createFile(String name, File packFile, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
- throw new ReadOnlyException("linked folder is read only");
+ DomainFolder linkedFolder = getRealFolder();
+ return linkedFolder.createFile(name, packFile, monitor);
+ }
+
+ @Override
+ public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname,
+ boolean makeRelative, String linkFilename, LinkHandler> lh) throws IOException {
+ DomainFolder linkedFolder = getRealFolder();
+ return linkedFolder.createLinkFile(sourceProjectData, pathname, makeRelative, linkFilename,
+ lh);
+ }
+
+ @Override
+ public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler> lh)
+ throws IOException {
+ DomainFolder linkedFolder = getRealFolder();
+ return linkedFolder.createLinkFile(ghidraUrl, linkFilename, lh);
}
@Override
public DomainFolder createFolder(String name) throws InvalidNameException, IOException {
- throw new ReadOnlyException("linked folder is read only");
+ DomainFolder linkedFolder = getRealFolder();
+ DomainFolder child = linkedFolder.createFolder(name);
+ return new LinkedGhidraSubFolder(parent, child.getName());
}
@Override
public void delete() throws IOException {
- throw new ReadOnlyException("linked folder is read only");
+ DomainFolder linkedFolder = getRealFolder();
+ linkedFolder.delete();
}
@Override
public DomainFolder moveTo(DomainFolder newParent) throws IOException {
- throw new ReadOnlyException("linked folder is read only");
+ DomainFolder linkedFolder = getRealFolder();
+ return linkedFolder.moveTo(newParent);
}
@Override
public DomainFolder copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException {
- DomainFolder linkedFolder = getLinkedFolder();
+ DomainFolder linkedFolder = getRealFolder();
return linkedFolder.copyTo(newParent, monitor);
}
@Override
- public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
- DomainFolder linkedFolder = getLinkedFolder();
- return linkedFolder.copyToAsLink(newParent);
+ public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
+ DomainFolder linkedFolder = getRealFolder();
+ return linkedFolder.copyToAsLink(newParent, relative);
}
@Override
public void setActive() {
- // do nothing
+ try {
+ DomainFolder linkedFolder = getRealFolder();
+ linkedFolder.setActive();
+ }
+ catch (IOException e) {
+ // ignore
+ }
}
@Override
public String toString() {
- return "LinkedGhidraSubFolder: " + getPathname();
+ String str = parent.toString();
+ if (!str.endsWith("/")) {
+ str += "/";
+ }
+ str += getName();
+ return str;
}
@Override
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/NullFolderDomainObject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/NullFolderDomainObject.java
new file mode 100644
index 0000000000..620f736309
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/NullFolderDomainObject.java
@@ -0,0 +1,37 @@
+/* ###
+ * 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;
+
+/**
+ * Dummy domain object to satisfy {@link FolderLinkContentHandler#getDomainObjectClass()}
+ */
+public 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/URLLinkObject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java
index c04cd40978..dc268c1712 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,61 +17,43 @@ 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
+ * {@link URLLinkObject} provides a link-file path/URL wrapper
+ * where the path/URL is intended to refer to a {@link DomainFile} within a 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.
+ *
+ * NOTE: This exists for backward compatibility and is no longer used for storing newly created
+ * link-files.
*/
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;
+ private String linkPath;
/**
- * 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)
+ * Constructs an existing 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 {
+ 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");
+ linkPath = metadata.get(LinkHandler.URL_METADATA_KEY);
+ if (linkPath == null) {
+ throw new IOException("Null link path/URL");
}
- url = new URL(urlText);
}
@Override
@@ -80,11 +62,11 @@ public class URLLinkObject extends DomainObjectAdapterDB {
}
/**
- * Get link URL
- * @return link URL
+ * Get the stored link path/URL
+ * @return link path/URL
*/
- public URL getLink() {
- return url;
+ public String getLinkPath() {
+ return linkPath;
}
@Override
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AcceptUrlContentTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AcceptUrlContentTask.java
index 355332770b..a45ab7b3c8 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AcceptUrlContentTask.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AcceptUrlContentTask.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,8 +20,8 @@ import java.io.IOException;
import java.net.URL;
import java.util.Set;
-import ghidra.framework.data.FolderLinkContentHandler;
import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl;
import ghidra.framework.protocol.ghidra.GhidraURLQueryTask;
import ghidra.util.Msg;
import ghidra.util.Swing;
@@ -31,8 +31,9 @@ public class AcceptUrlContentTask extends GhidraURLQueryTask {
private FrontEndPlugin plugin;
- public AcceptUrlContentTask(URL url, FrontEndPlugin plugin) {
- super("Accepting URL", url);
+ public AcceptUrlContentTask(URL url, boolean followExternalLinks, FrontEndPlugin plugin) {
+ super("Accepting URL", url, null, followExternalLinks ? LinkFileControl.FOLLOW_EXTERNAL
+ : LinkFileControl.FOLLOW_INTERNAL);
this.plugin = plugin;
}
@@ -65,8 +66,8 @@ public class AcceptUrlContentTask extends GhidraURLQueryTask {
}
Swing.runNow(() -> {
- if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE
- .equals(domainFile.getContentType())) {
+ LinkFileInfo linkInfo = domainFile.getLinkInfo();
+ if (linkInfo != null && linkInfo.isFolderLink()) {
// Simply select folder link-file within project - do not follow - let user do that.
if (isSameLocalProject(activeProject.getProjectLocator(),
domainFile.getProjectLocator())) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/BrokenLinkIcon.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/BrokenLinkIcon.java
new file mode 100644
index 0000000000..e797098513
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/BrokenLinkIcon.java
@@ -0,0 +1,67 @@
+/* ###
+ * 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 java.awt.*;
+
+import javax.swing.Icon;
+
+/**
+ * Icon class for for altering a baseIcon to render as a "broken" link-file icon.
+ */
+public class BrokenLinkIcon implements Icon {
+
+ private Icon baseIcon;
+
+ /**
+ * Constructs a "broken" link-file icon.
+ * @param baseIcon the base icon that will always be drawn first.
+ */
+ public BrokenLinkIcon(Icon baseIcon) {
+ this.baseIcon = baseIcon;
+ }
+
+ @Override
+ public int getIconHeight() {
+ return baseIcon.getIconHeight();
+ }
+
+ @Override
+ public int getIconWidth() {
+ return baseIcon.getIconWidth();
+ }
+
+ @Override
+ public void paintIcon(Component c, Graphics g, int x, int y) {
+ baseIcon.paintIcon(c, g, x, y);
+
+ Graphics2D g2d = (Graphics2D) g;
+
+ // Enable anti-aliasing
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+ g.setColor(Color.red);
+
+ int h = getIconHeight();
+ int halfh = h / 2;
+ int w = getIconWidth();
+ int halfw = w / 2;
+ //
+ g.drawLine(x, y + halfh - 1, x + halfw + 1, y + halfh - 3);
+ g.drawLine(x + halfw + 1, y + halfh - 3, x + halfw - 1, y + halfh + 1);
+ g.drawLine(x + halfw - 1, y + halfh + 1, x + w - 1, y + halfh - 1);
+ }
+}
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 8dbf28a051..0048dd7c8a 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
@@ -22,6 +22,7 @@ import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
import javax.swing.*;
import javax.swing.border.BevelBorder;
@@ -30,8 +31,7 @@ import docking.*;
import docking.action.DockingAction;
import docking.action.MenuData;
import docking.tool.ToolConstants;
-import docking.widgets.OkDialog;
-import docking.widgets.OptionDialog;
+import docking.widgets.*;
import docking.widgets.button.GButton;
import docking.widgets.dialogs.InputDialog;
import docking.widgets.filechooser.GhidraFileChooser;
@@ -41,7 +41,9 @@ import generic.theme.GIcon;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.GenericRunInfo;
import ghidra.framework.client.*;
-import ghidra.framework.data.*;
+import ghidra.framework.data.ContentHandler;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.LinkHandler.LinkStatus;
import ghidra.framework.main.datatable.ProjectDataTablePanel;
import ghidra.framework.main.datatree.*;
import ghidra.framework.main.projectdata.actions.*;
@@ -131,8 +133,10 @@ public class FrontEndPlugin extends Plugin
private ProjectDataCopyAction copyAction;
private ProjectDataPasteAction pasteAction;
private ProjectDataPasteLinkAction pasteLinkAction;
+ private ProjectDataPasteLinkAction pasteRelativeLinkAction;
private ProjectDataRenameAction renameAction;
private ProjectDataOpenDefaultToolAction openAction;
+ private ProjectDataFollowLinkAction followLinkAction;
private ProjectDataExpandAction expandAction;
private ProjectDataCollapseAction collapseAction;
private ProjectDataSelectAction selectAction;
@@ -153,6 +157,8 @@ public class FrontEndPlugin extends Plugin
private FindCheckoutsAction findCheckoutsAction;
private ToolChestChangeListener toolChestChangeListener;
+ private OptionDialogBuilder filterWarningBuilder;
+
/**
* Construct a new FrontEndPlugin. This plugin is constructed once when
* the Front end tool (Ghidra Project Window) is created. When a
@@ -212,6 +218,10 @@ public class FrontEndPlugin extends Plugin
private void createActions() {
String owner = getName();
+ // Top of popup menu actions - no group
+ openAction = new ProjectDataOpenDefaultToolAction(owner, null);
+ followLinkAction = new ProjectDataFollowLinkAction(this, null);
+
String groupName = "Cut/copy/paste/new1";
newFolderAction = new FrontEndProjectDataNewFolderAction(owner, groupName);
@@ -220,12 +230,12 @@ public class FrontEndPlugin extends Plugin
clearCutAction = new ClearCutAction(owner);
copyAction = new ProjectDataCopyAction(owner, groupName);
pasteAction = new ProjectDataPasteAction(owner, groupName);
- pasteLinkAction = new ProjectDataPasteLinkAction(owner, groupName);
+ pasteLinkAction = new ProjectDataPasteLinkAction(owner, groupName, false);
+ pasteRelativeLinkAction = new ProjectDataPasteLinkAction(owner, groupName, true);
groupName = "Delete/Rename";
renameAction = new ProjectDataRenameAction(owner, groupName);
deleteAction = new ProjectDataDeleteAction(owner, groupName);
- openAction = new ProjectDataOpenDefaultToolAction(owner, "Open");
groupName = "Expand/Collapse";
expandAction = new FrontEndProjectDataExpandAction(owner, groupName);
@@ -244,8 +254,10 @@ public class FrontEndPlugin extends Plugin
tool.addAction(copyAction);
tool.addAction(pasteAction);
tool.addAction(pasteLinkAction);
+ tool.addAction(pasteRelativeLinkAction);
tool.addAction(deleteAction);
tool.addAction(openAction);
+ tool.addAction(followLinkAction);
tool.addAction(renameAction);
tool.addAction(expandAction);
tool.addAction(collapseAction);
@@ -804,6 +816,7 @@ public class FrontEndPlugin extends Plugin
@Override
protected void dispose() {
+ projectDataPanel.setActiveProject(null); // force all project views to be disposed
dataTablePanel.dispose();
dataTreePanel.dispose();
projectActionManager.dispose();
@@ -1081,7 +1094,7 @@ public class FrontEndPlugin extends Plugin
}
public void openDomainFile(DomainFile domainFile) {
-
+
String contentType = domainFile.getContentType();
if (ContentHandler.UNKNOWN_CONTENT.equals(contentType)) {
Msg.showInfo(this, tool.getToolFrame(), "Cannot Find Tool",
@@ -1091,8 +1104,27 @@ public class FrontEndPlugin extends Plugin
return;
}
- if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(contentType)) {
- showLinkedFolderInViewedProject(domainFile);
+ if (domainFile.isLink() && domainFile.getLinkInfo().isFolderLink()) {
+
+ // Follow and check internal linkage
+ AtomicReference status = new AtomicReference<>();
+ DomainFile lastLink =
+ LinkHandler.followInternalLinkage(domainFile, s -> status.set(s), null);
+
+ try {
+ // Tree already handles opening folder-link while table does nothing
+ if (lastLink != null && status.get() == LinkStatus.EXTERNAL) {
+ showInViewedProject(LinkHandler.getLinkURL(lastLink), true);
+ }
+ else if (!dataTreePanel.isShowing()) {
+ // Filter table on absolute link path
+ String linkPath = LinkHandler.getAbsoluteLinkPath(domainFile);
+ dataTablePanel.setFilter(linkPath);
+ }
+ }
+ catch (IOException e) {
+ Msg.showError(this, tool.getActiveWindow(), "Link Error", e.getMessage());
+ }
return;
}
@@ -1120,64 +1152,32 @@ public class FrontEndPlugin extends Plugin
"opens this type of file");
}
- private void showLinkedFolderInViewedProject(DomainFile domainFile) {
+ void showInViewedProject(URL ghidraUrl, boolean isFolder) {
- try {
- LinkedGhidraFolder linkedFolder =
- FolderLinkContentHandler.getReadOnlyLinkedFolder(domainFile);
- if (linkedFolder == null) {
- return; // unsupported use
- }
-
- ProjectDataTreePanel dtp = projectDataPanel.openView(linkedFolder.getProjectURL());
- if (dtp == null) {
- return;
- }
-
- // Do not hang onto domainFile, linkedFolder or their underlying project data
-
- ProjectData viewedProjectData = dtp.getProjectData();
- DomainFolder domainFolder =
- viewedProjectData.getFolder(linkedFolder.getLinkedPathname());
-
- if (domainFolder != null) {
- // delayed to ensure tree is displayed
- Swing.runLater(() -> dtp.selectDomainFolder(domainFolder));
- }
+ // Check if active project can be used
+ URL activeProjectURL = activeProject.getProjectLocator().getURL();
+ URL viewProjectURL = GhidraURL.getProjectURL(ghidraUrl);
+ String path = GhidraURL.getProjectPathname(ghidraUrl);
+ boolean useActiveProject = activeProjectURL.equals(viewProjectURL);
+ if (!useActiveProject) {
+ // Check for shared repository match
+ useActiveProject =
+ viewProjectURL.equals(activeProject.getProjectData().getSharedProjectURL());
}
- catch (IOException e) {
- Msg.showError(this, projectDataPanel, "Linked-folder failure: " + domainFile.getName(),
- e);
+ if (useActiveProject) {
+ selectTreeNode(dataTreePanel, path, isFolder);
+ return;
}
- }
-
- void showInViewedProject(URL ghidraURL, boolean isFolder) {
-
- ProjectDataTreePanel dtp = projectDataPanel.openView(GhidraURL.getProjectURL(ghidraURL));
+ // Show in viewed project tree
+ ProjectDataTreePanel dtp = projectDataPanel.openView(GhidraURL.getProjectURL(ghidraUrl));
if (dtp == null) {
return;
}
Swing.runLater(() -> {
// delayed to ensure tree is displayed
-
- ProjectData viewedProjectData = dtp.getProjectData();
-
- String path = GhidraURL.getProjectPathname(ghidraURL);
-
- if (isFolder) {
- DomainFolder viewedProjectFolder = getViewProjectFolder(viewedProjectData, path);
- if (viewedProjectFolder != null) {
- dtp.selectDomainFolder(viewedProjectFolder);
- }
- }
- else {
- DomainFile viewedProjectFile = getViewProjectFile(viewedProjectData, path);
- if (viewedProjectFile != null) {
- dtp.selectDomainFile(viewedProjectFile);
- }
- }
+ selectTreeNode(dtp, path, isFolder);
});
}
@@ -1198,6 +1198,71 @@ public class FrontEndPlugin extends Plugin
return viewedProjectData.getFolder(path);
}
+ public void showInProjectTree(ProjectData projectData, String path, boolean isFolder) {
+ if (activeProject.getProjectData() == projectData) {
+ // Active project tree
+ selectTreeNode(dataTreePanel, path, isFolder);
+ return;
+ }
+
+ ProjectLocator projectLocator = projectData.getProjectLocator();
+ URL viewURL = projectLocator.getURL();
+ if (viewURL != null) {
+ ProjectDataTreePanel dtp = projectDataPanel.openView(viewURL);
+ // Found matching tree panel
+ selectTreeNode(dtp, path, isFolder);
+ return;
+ }
+
+ Msg.error(this, "Failed to open project tree: " + projectLocator.getName());
+ }
+
+ private void selectTreeNode(ProjectDataTreePanel dtp, String path, boolean isFolder) {
+
+ // NOTE: Would be nice to draw attention to the tree panel where the selection
+ // occurred since the selection may not change.
+
+ ProjectData viewedProjectData = dtp.getProjectData();
+ boolean foundIt = false;
+ if (isFolder) {
+ DomainFolder viewedProjectFolder = getViewProjectFolder(viewedProjectData, path);
+ if (viewedProjectFolder != null) {
+ if (viewedProjectFolder.isLinked()) {
+ isFolder = false; // linked-folder: must select as link-file node
+ }
+ else {
+ foundIt = true;
+ dtp.selectDomainFolder(viewedProjectFolder);
+ }
+ }
+ }
+
+ if (!isFolder) {
+ DomainFile viewedProjectFile = getViewProjectFile(viewedProjectData, path);
+ if (viewedProjectFile != null) {
+ foundIt = true;
+ dtp.selectDomainFile(viewedProjectFile);
+ }
+ }
+
+ DataTree dataTree = dtp.getDataTree();
+ if (!foundIt) {
+ Msg.showError(this, dataTree, "Invalid ",
+ "Referenced path not found or it conflicts with a link-file: " +
+ dataTree.getModelRoot().getName() + ":" + path);
+ }
+ else if (dataTree.isFiltered()) {
+ if (filterWarningBuilder == null) {
+ filterWarningBuilder =
+ new OptionDialogBuilder("Active Tree Filter: " + dtp.getName(),
+ "A project tree filter is currently active and may block the selection");
+ filterWarningBuilder.setMessageType(OptionDialog.WARNING_MESSAGE);
+ filterWarningBuilder.addDontShowAgainOption();
+ }
+ filterWarningBuilder.show(tool.getToolFrame());
+ }
+ }
+
private class MyToolChestChangeListener implements ToolChestChangeListener {
@Override
@@ -1216,4 +1281,5 @@ public class FrontEndPlugin extends Plugin
}
}
+
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java
index 90a733aaff..c2d89af22a 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java
@@ -184,7 +184,7 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
if (!GhidraURL.isLocalProjectURL(url) && !GhidraURL.isServerRepositoryURL(url)) {
return false;
}
- Swing.runLater(() -> execute(new AcceptUrlContentTask(url, plugin)));
+ Swing.runLater(() -> execute(new AcceptUrlContentTask(url, true, plugin)));
return true;
}
@@ -355,8 +355,8 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
"When enabled data buffers sent to Ghidra Server are compressed (see server " +
"configuration for other direction)");
- options.registerOption(BLINKING_CURSORS_OPTION_NAME, true, help, "This controls whether" +
- " text cursors blink when focused");
+ options.registerOption(BLINKING_CURSORS_OPTION_NAME, true, help,
+ "This controls whether" + " text cursors blink when focused");
options.registerOption(RESTORE_PREVIOUS_PROJECT_NAME, true, help,
"Restore the previous project when Ghidra starts.");
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/GetDomainObjectTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/GetDomainObjectTask.java
index c1ad49b0ec..8cf93f035d 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/GetDomainObjectTask.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/GetDomainObjectTask.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -37,7 +37,7 @@ import ghidra.util.task.TaskMonitor;
* A file open for read-only use will be upgraded if needed and is possible. Once open it is
* important that the specified consumer be released from the domain object when done using
* the open object (see {@link DomainObject#release(Object)}).
- */
+ */
public class GetDomainObjectTask extends Task {
private Object consumer;
@@ -46,7 +46,7 @@ public class GetDomainObjectTask extends Task {
private boolean immutable;
private DomainObject versionedObj;
-
+
/**
* Construct task open specified domainFile read only.
* An upgrade is performed if needed and is possible.
@@ -76,9 +76,9 @@ public class GetDomainObjectTask extends Task {
this.versionNumber = versionNumber;
this.immutable = immutable;
}
-
+
@Override
- public void run(TaskMonitor monitor) {
+ public void run(TaskMonitor monitor) {
String contentType = domainFile.getContentType();
try {
monitor.setMessage("Getting Version " + versionNumber + " for " + domainFile.getName());
@@ -97,7 +97,8 @@ public class GetDomainObjectTask extends Task {
catch (IOException e) {
ClientUtil.handleException(AppInfo.getActiveProject().getRepository(), e,
contentType + " Open", null);
- } catch (VersionException e) {
+ }
+ catch (VersionException e) {
if (immutable && e.isUpgradable()) {
String detailMessage =
e.getDetailMessage() == null ? "" : "\n" + e.getDetailMessage();
@@ -115,10 +116,10 @@ public class GetDomainObjectTask extends Task {
return;
}
VersionExceptionHandler.showVersionError(null, domainFile.getName(),
- domainFile.getContentType(), contentType + " Open", e);
+ domainFile.getContentType(), contentType + " Open", false, e);
}
}
-
+
/**
* Return the domain object instance.
* @return domain object which was opened or null if task cancelled or failed
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataFollowLinkAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataFollowLinkAction.java
new file mode 100644
index 0000000000..0690854008
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataFollowLinkAction.java
@@ -0,0 +1,111 @@
+/* ###
+ * 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 java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+
+import docking.action.MenuData;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.main.datatable.FrontendProjectTreeAction;
+import ghidra.framework.main.datatable.ProjectDataContext;
+import ghidra.framework.main.datatree.DataTree;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.ProjectData;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+import ghidra.util.HelpLocation;
+import ghidra.util.Msg;
+
+public class ProjectDataFollowLinkAction extends FrontendProjectTreeAction {
+
+ private FrontEndPlugin plugin;
+
+ public ProjectDataFollowLinkAction(FrontEndPlugin plugin, String group) {
+ super("Follow Link", plugin.getName());
+ this.plugin = plugin;
+ setPopupMenuData(new MenuData(new String[] { "Follow Link" }, group));
+ setHelpLocation(new HelpLocation("FrontEndPlugin", "Follow_Link"));
+ }
+
+ @Override
+ protected void actionPerformed(ProjectDataContext context) {
+
+ List selectedFiles = context.getSelectedFiles();
+ if (selectedFiles.size() != 1) {
+ return;
+ }
+ DomainFile file = selectedFiles.get(0);
+ if (!file.isLink()) {
+ return;
+ }
+
+ // Folder link may refer to another folder link
+ String linkPath;
+ try {
+ linkPath = LinkHandler.getAbsoluteLinkPath(file);
+ if (linkPath == null) {
+ Msg.showError(this, context.getComponent(), "Invalid Link",
+ "Link-file failed to provide link path: " + file);
+ return;
+ }
+ }
+ catch (IOException e) {
+ Msg.showError(this, context.getComponent(), "Invalid Link", e.getMessage());
+ return;
+ }
+
+ boolean isFolderLink = file.getLinkInfo().isFolderLink();
+ if (GhidraURL.isGhidraURL(linkPath)) {
+ // Follow URL using a project view
+ try {
+ plugin.showInViewedProject(new URL(linkPath), isFolderLink);
+ return;
+ }
+ catch (MalformedURLException e) {
+ Msg.error(this, "Invalid link URL: " + e.getMessage());
+ return;
+ }
+ }
+
+ // Check internal link
+ ProjectData projectData = context.getProjectData();
+ boolean isFolder = isFolderLink && projectData.getFolder(linkPath) != null;
+ if (!isFolder) {
+ DomainFile referencedFile = projectData.getFile(linkPath);
+ if (referencedFile == null) {
+ // referenced folder or file not found
+ return;
+ }
+ }
+
+ // Path is local to its project data tree
+ plugin.showInProjectTree(context.getProjectData(), linkPath, isFolder);
+ }
+
+ @Override
+ protected boolean isEnabledForContext(ProjectDataContext context) {
+ if (!(context.getComponent() instanceof DataTree)) {
+ return false;
+ }
+ if (context.getFolderCount() != 0 || context.getFileCount() != 1) {
+ return false;
+ }
+ DomainFile file = context.getSelectedFiles().get(0);
+ return file.isLink();
+ }
+}
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 2ebc410ce4..5eb3f06970 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -172,6 +172,7 @@ class ProjectDataPanel extends JSplitPane implements ProjectViewListener {
private void clearReadOnlyViews() {
readOnlyTab.removeAll();
+ readOnlyViews.values().forEach(ProjectDataTreePanel::dispose);
readOnlyViews.clear();
setViewsVisible(false);
}
@@ -214,6 +215,10 @@ class ProjectDataPanel extends JSplitPane implements ProjectViewListener {
if (projectData == null) {
return null; // repository connection may have been cancelled
}
+
+ // Force refresh to purge any stale data
+ projectData.refresh(true);
+
projectManager.rememberViewedProject(projectView);
String viewName = projectData.getProjectLocator().getName();
final ProjectDataTreePanel newPanel =
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java
index 186034e61c..c3e7c2f6df 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,13 +17,13 @@ package ghidra.framework.main;
import java.awt.BorderLayout;
import java.awt.CardLayout;
+import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import javax.swing.*;
-import javax.swing.event.ChangeListener;
import javax.swing.event.MouseInputAdapter;
import docking.ReusableDialogComponentProvider;
@@ -173,26 +173,22 @@ class RepositoryChooser extends ReusableDialogComponentProvider {
radioButtonPanel.getAccessibleContext().setAccessibleName("Radio Buttons");
radioButtonPanel.setBorder(BorderFactory.createTitledBorder("Repository Specification"));
- ChangeListener choiceListener = e -> {
- Object src = e.getSource();
- if (src instanceof JRadioButton) {
- JRadioButton choiceButton = (JRadioButton) src;
- choiceButton.getAccessibleContext().setAccessibleName("Choice");
- if (choiceButton.isSelected()) {
- choiceActivated(choiceButton);
- }
+ ItemListener choiceListener = e -> {
+ JRadioButton choiceButton = (JRadioButton) e.getSource();
+ if (choiceButton.isSelected()) {
+ choiceActivated(choiceButton);
}
};
serverInfoChoice = new GRadioButton("Ghidra Server");
serverInfoChoice.getAccessibleContext().setAccessibleName("Ghidra Server");
serverInfoChoice.setSelected(true);
- serverInfoChoice.addChangeListener(choiceListener);
+ serverInfoChoice.addItemListener(choiceListener);
radioButtonPanel.add(serverInfoChoice);
urlChoice = new GRadioButton("Ghidra URL");
- urlChoice.addChangeListener(choiceListener);
urlChoice.getAccessibleContext().setAccessibleName("Ghidra URL");
+ urlChoice.addItemListener(choiceListener);
radioButtonPanel.add(urlChoice);
ButtonGroup panelChoices = new ButtonGroup();
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java
index fceacc7439..e6fbdc1684 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java
@@ -21,7 +21,9 @@ import java.awt.dnd.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
import javax.swing.*;
@@ -33,6 +35,8 @@ import docking.dnd.*;
import docking.tool.ToolConstants;
import docking.util.image.ToolIconURL;
import docking.widgets.EmptyBorderButton;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.LinkHandler.LinkStatus;
import ghidra.framework.main.datatree.*;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool;
@@ -507,7 +511,33 @@ class ToolButton extends EmptyBorderButton implements Draggable, Droppable {
plugin.getActiveWorkspace().runTool(template);
}
else {
- PluginTool tool = toolServices.launchTool(template.getName(), domainFiles);
+ List files = new ArrayList<>();
+ domainFiles.forEach(file -> {
+ if (file.isLink()) {
+ if (file.getLinkInfo().isFolderLink()) {
+ return; // ignore folder links
+ }
+ AtomicReference errorMsg = new AtomicReference<>();
+ LinkStatus status =
+ LinkHandler.getLinkFileStatus(file, error -> errorMsg.set(error));
+ if (status == LinkStatus.BROKEN) {
+ String msg = errorMsg.get();
+ String pathname = file.getPathname();
+ if (!msg.contains(pathname)) {
+ msg += ": " + pathname;
+ }
+ Msg.showError(this, getParent(), "Failed to Open File",
+ msg + ": " + file.getPathname());
+ return;
+ }
+ }
+ files.add(file);
+ });
+ if (files.isEmpty()) {
+ return;
+ }
+
+ PluginTool tool = toolServices.launchTool(template.getName(), files);
if (tool == null) {
Msg.showError(this, getParent(), "Failed to Launch Tool",
"Failed to launch " + template.getName() + " tool.\nSee log for details.");
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java
index 726b7b56ed..16bc739fa5 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -19,19 +19,22 @@ import java.util.*;
import javax.swing.Icon;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.LinkHandler.LinkStatus;
+import ghidra.framework.main.BrokenLinkIcon;
+import ghidra.framework.main.datatree.DomainFileNode;
import ghidra.framework.model.DomainFile;
public class DomainFileInfo {
- // TODO: should not hang onto DomainFile since it may not track changes anymore
- // Think of DomainFile like a File object
-
private DomainFile domainFile;
private String name;
private String path;
private Map metadata;
private Date modificationDate;
private DomainFileType domainFileType;
+ private Boolean isBrokenLink;
+ private String toolTipText;
public DomainFileInfo(DomainFile domainFile) {
this.domainFile = domainFile;
@@ -84,14 +87,14 @@ public class DomainFileInfo {
return path;
}
- public Icon getIcon() {
- return domainFile.getIcon(false);
- }
-
public synchronized DomainFileType getDomainFileType() {
if (domainFileType == null) {
+ checkStatus();
String contentType = domainFile.getContentType();
Icon icon = domainFile.getIcon(false);
+ if (isBrokenLink) {
+ icon = new BrokenLinkIcon(icon);
+ }
boolean isVersioned = domainFile.isVersioned();
domainFileType = new DomainFileType(contentType, icon, isVersioned);
}
@@ -131,14 +134,15 @@ public class DomainFileInfo {
public synchronized void clearMetaCache() {
metadata = null;
modificationDate = null;
- domainFileType = null;
refresh();
}
public synchronized void refresh() {
- this.name = null;
- this.path = null;
-
+ domainFileType = null;
+ isBrokenLink = null;
+ toolTipText = null;
+ name = null;
+ path = null;
}
public String getMetaDataValue(String key) {
@@ -150,4 +154,26 @@ public class DomainFileInfo {
return domainFile.getName();
}
+ private void checkStatus() {
+ if (isBrokenLink == null) {
+ isBrokenLink = false;
+ List linkErrors = null;
+ if (domainFile.isLink()) {
+ List errors = new ArrayList<>();
+ LinkStatus linkStatus =
+ LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg));
+ isBrokenLink = (linkStatus == LinkStatus.BROKEN);
+ if (isBrokenLink) {
+ linkErrors = errors;
+ }
+ }
+ toolTipText = DomainFileNode.getToolTipText(domainFile, linkErrors);
+ }
+ }
+
+ public String getToolTip() {
+ checkStatus();
+ return toolTipText;
+ }
+
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataContext.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataContext.java
index 2d3778524e..97cd908a46 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataContext.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataContext.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -66,6 +66,10 @@ public class ProjectDataContext extends DefaultActionContext implements DomainFi
return (getFolderCount() + getFileCount()) == 1;
}
+ public boolean hasOneOrMoreFilesAndFolders() {
+ return getFolderCount() + getFileCount() > 0;
+ }
+
public int getFolderCount() {
if (selectedFolders == null) {
return 0;
@@ -101,10 +105,6 @@ public class ProjectDataContext extends DefaultActionContext implements DomainFi
return !projectData.getRootFolder().isInWritableProject();
}
- public boolean hasOneOrMoreFilesAndFolders() {
- return getFolderCount() + getFileCount() > 0;
- }
-
public boolean containsRootFolder() {
if (getFolderCount() == 0) {
return false;
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 d09c92b596..f3ca6b5efe 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
@@ -103,6 +103,7 @@ public class ProjectDataTablePanel extends JPanel {
.addListSelectionListener(e -> plugin.getTool().contextChanged(null));
gTable.setDefaultRenderer(Date.class, new DateCellRenderer());
gTable.setDefaultRenderer(DomainFileType.class, new TypeCellRenderer());
+ gTable.getColumn("Name").setCellRenderer(new NameCellRenderer());
// self-registering drag provider
new ProjectDataTableDragProvider();
@@ -125,6 +126,10 @@ public class ProjectDataTablePanel extends JPanel {
help.registerHelp(table, helpLocation);
}
+ public void setFilter(String filterText) {
+ table.setFiterText(filterText);
+ }
+
public void setSelectedDomainFiles(Set files) {
if (model.isBusy()) {
// we don't want to attempt to find the items to select while we the threaded
@@ -208,8 +213,7 @@ public class ProjectDataTablePanel extends JPanel {
public ActionContext getActionContext(ComponentProvider provider, MouseEvent e) {
int[] selectedRows = gTable.getSelectedRows();
if (selectedRows.length == 0) {
- return new ProjectDataContext(provider, projectData, gTable, null, null, gTable,
- true);
+ return new ProjectDataContext(provider, projectData, gTable, null, null, gTable, true);
}
List list = new ArrayList<>();
@@ -535,15 +539,41 @@ public class ProjectDataTablePanel extends JPanel {
JLabel renderer = (JLabel) super.getTableCellRendererComponent(data);
- Object value = data.getValue();
-
- renderer.setText("");
- if (value != null) {
- DomainFileType type = (DomainFileType) value;
- setToolTipText(type.getContentType());
- setText("");
- setIcon(type.getIcon());
+ DomainFileInfo info = (DomainFileInfo) data.getRowObject();
+ if (info != null) {
+ DomainFileType type = (DomainFileType) data.getValue();
+ renderer.setText(type.toString());
+ renderer.setIcon(type.getIcon());
+ String toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(info.getToolTip());
+ renderer.setToolTipText(toolTipText);
}
+ else {
+ renderer.setText("");
+ renderer.setToolTipText(null);
+ }
+
+ return renderer;
+ }
+ }
+
+ private class NameCellRenderer extends GTableCellRenderer {
+
+ @Override
+ public Component getTableCellRendererComponent(GTableCellRenderingData data) {
+
+ JLabel renderer = (JLabel) super.getTableCellRendererComponent(data);
+
+ DomainFileInfo info = (DomainFileInfo) data.getRowObject();
+ if (info != null) {
+ renderer.setText((String) data.getValue());
+ String toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(info.getToolTip());
+ renderer.setToolTipText(toolTipText);
+ }
+ else {
+ renderer.setText("");
+ renderer.setToolTipText(null);
+ }
+
return renderer;
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeContext.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeContext.java
index 3d9aca35cb..06739260e8 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeContext.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeContext.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -44,6 +44,16 @@ public interface ProjectTreeContext {
*/
public int getFileCount();
+ /**
+ * {@return true of only one file or folder has been selected, else false.}
+ */
+ public boolean hasExactlyOneFileOrFolder();
+
+ /**
+ * {@return true if one or more file and/or folders have been selected, else false.}
+ */
+ public boolean hasOneOrMoreFilesAndFolders();
+
/**
* Returns a list of {@link DomainFolder}s selected in the tree.
* @return a list of {@link DomainFolder}s selected in the tree.
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 8d62de971b..f3b5199ecd 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,69 +15,169 @@
*/
package ghidra.framework.main.datatree;
+import java.io.IOException;
import java.util.*;
+import java.util.function.Consumer;
+
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreePath;
import docking.widgets.tree.GTreeNode;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.main.datatree.DataTreeNode.NodeType;
import ghidra.framework.model.*;
/**
* Class to handle changes when a domain folder changes; updates the
* tree model to reflect added/removed/renamed nodes.
*/
-class ChangeManager implements DomainFolderChangeListener {
+class ChangeManager implements DomainFolderChangeListener, TreeModelListener {
private DomainFolderRootNode root;
+ private ProjectData projectData; // may be null
private ProjectDataTreePanel treePanel;
private DataTree tree;
+ //
+ // Link back-reference tree
+ // Associates file/folder-links with their referenced linked-files and folders.
+ // This tracking allows for rapid identification of link-related tree nodes which
+ // may be impacted by changes made to other files and folders.
+ //
+ private LinkedTreeNode linkTreeRoot = new LinkedTreeNode(null, null);
+
+ private boolean skipLinkUpdate = false; // updates within Swing event dispatch thread only
+
ChangeManager(ProjectDataTreePanel treePanel) {
this.treePanel = treePanel;
+ projectData = treePanel.getProjectData();
tree = treePanel.getDataTree();
root = (DomainFolderRootNode) tree.getModelRoot();
+ if (projectData != null) {
+ // Without a project this change manager does nothing (e.g., empty tree)
+ projectData.addDomainFolderChangeListener(this);
+ tree.addGTModelListener(this);
+ }
+ }
+
+ void dispose() {
+ if (projectData != null) {
+ projectData.removeDomainFolderChangeListener(this);
+ tree.removeGTModelListener(this);
+ projectData = null;
+ }
+ }
+
+ //
+ // File Changes
+ //
+
+ @Override
+ public void domainFileAdded(DomainFile file) {
+ boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
+ String fileName = file.getName();
+ DomainFolder parentFolder = file.getParent();
+ updateLinkedContent(parentFolder, p -> addFileNode(p, fileName, isFolderLink),
+ ltn -> ltn.refreshLinks(fileName));
+ DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
+ if (folderNode != null && folderNode.isLoaded()) {
+ addFileNode(folderNode, fileName, isFolderLink);
+ }
}
@Override
public void domainFileRemoved(DomainFolder parent, String name, String fileID) {
- updateFolderNode(parent);
+ updateLinkedContent(parent, null, ltn -> ltn.refreshLinks(name));
DomainFolderNode folderNode = findDomainFolderNode(parent, true);
- if (folderNode == null) {
- return;
+ if (folderNode != null) {
+ updateChildren(folderNode);
}
+ }
- List children = folderNode.getChildren();
- for (GTreeNode child : children) {
- if (child instanceof DomainFileNode) {
- if (child.getName().equals(name)) {
- folderNode.removeNode(child);
- }
- }
+ @Override
+ public void domainFileRenamed(DomainFile file, String oldName) {
+ boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink();
+ updateLinkedContent(file.getParent(), p -> {
+ updateChildren(p);
+ addFileNode(p, file.getName(), isFolderLink);
+ }, ltn -> {
+ ltn.refreshLinks(oldName);
+ ltn.refreshLinks(file.getName());
+ });
+ DomainFolder parent = file.getParent();
+ skipLinkUpdate = true;
+ try {
+ domainFileRemoved(parent, oldName, file.getFileID());
+ domainFileAdded(file);
+ }
+ finally {
+ skipLinkUpdate = false;
+ }
+ }
+
+ @Override
+ public void domainFileMoved(DomainFile file, DomainFolder oldParent, String oldName) {
+ domainFileRemoved(oldParent, oldName, null);
+ domainFileAdded(file);
+ }
+
+ @Override
+ public void domainFileStatusChanged(DomainFile file, boolean fileIDset) {
+ DomainFolder parentFolder = file.getParent();
+ updateLinkedContent(parentFolder, fn -> {
+ /* No folder update required */
+ }, ltn -> ltn.refreshLinks(file.getName()));
+ DomainFileNode fileNode = findDomainFileNode(file, true);
+ if (fileNode != null) {
+ fileNode.refresh();
+ }
+ treePanel.contextChanged();
+ }
+
+ //
+ // Folder Changes
+ //
+
+ @Override
+ public void domainFolderAdded(DomainFolder folder) {
+ String folderName = folder.getName();
+ DomainFolder parentFolder = folder.getParent();
+ updateLinkedContent(parentFolder, p -> addFolderNode(p, folderName),
+ ltn -> ltn.refreshLinks(folderName));
+ DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
+ if (folderNode != null && folderNode.isLoaded()) {
+ addFolderNode(folderNode, folderName);
}
}
@Override
public void domainFolderRemoved(DomainFolder parent, String name) {
- updateFolderNode(parent);
-
- ArrayList folderPath = new ArrayList();
- getFolderPath(parent, folderPath);
- folderPath.add(name);
-
- DomainFolderNode folderNode = findDomainFolderNode(folderPath, true);
+ updateLinkedContent(parent, null, ltn -> ltn.refreshLinks(name));
+ DomainFolderNode folderNode = findDomainFolderNode(parent, true);
if (folderNode != null) {
- folderNode.getParent().removeNode(folderNode);
+ updateChildren(folderNode);
}
}
@Override
public void domainFolderRenamed(DomainFolder folder, String oldName) {
- domainFolderRemoved(folder.getParent(), oldName);
- domainFolderAdded(folder);
- }
-
- @Override
- public void domainFileRenamed(DomainFile file, String oldName) {
- domainFileRemoved(file.getParent(), oldName, file.getFileID());
- domainFileAdded(file);
+ updateLinkedContent(folder.getParent(), p -> {
+ updateChildren(p);
+ addFolderNode(p, folder.getName());
+ }, ltn -> {
+ ltn.refreshLinks(oldName);
+ ltn.refreshLinks(folder.getName());
+ });
+ DomainFolder parent = folder.getParent();
+ skipLinkUpdate = true;
+ try {
+ domainFolderRemoved(parent, oldName);
+ domainFolderAdded(folder);
+ }
+ finally {
+ skipLinkUpdate = false;
+ }
}
@Override
@@ -86,52 +186,6 @@ class ChangeManager implements DomainFolderChangeListener {
domainFolderAdded(folder);
}
- @Override
- public void domainFileMoved(DomainFile file, DomainFolder oldParent, String oldName) {
- updateFolderNode(oldParent);
- domainFileAdded(file);
- }
-
- @Override
- public void domainFileAdded(DomainFile file) {
- DomainFileNode domainFileNode = findDomainFileNode(file, true);
- if (domainFileNode != null) {
- return;
- }
- DomainFolder parent = file.getParent();
- DomainFolderNode folderNode = findDomainFolderNode(parent, true);
- if (folderNode != null) {
- if (folderNode.isLoaded()) {
- DomainFileNode newNode = new DomainFileNode(file);
- addNode(folderNode, newNode);
- }
- }
- }
-
- static void addNode(GTreeNode parentNode, GTreeNode newNode) {
- List allChildren = parentNode.getChildren();
- int index = Collections.binarySearch(allChildren, newNode);
- if (index < 0) {
- index = -index - 1;
- }
- parentNode.addNode(index, newNode);
- }
-
- @Override
- public void domainFolderAdded(DomainFolder folder) {
- DomainFolderNode domainFolderNode = findDomainFolderNode(folder, true);
- if (domainFolderNode != null) {
- return;
- }
- DomainFolder parentFolder = folder.getParent();
- DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true);
- if (folderNode != null && folderNode.isLoaded()) {
- DomainFolderNode newNode =
- new DomainFolderNode(folder, folderNode.getDomainFileFilter());
- addNode(folderNode, newNode);
- }
- }
-
@Override
public void domainFolderSetActive(DomainFolder folder) {
DomainFolderNode folderNode = findDomainFolderNode(folder, false);
@@ -140,13 +194,58 @@ class ChangeManager implements DomainFolderChangeListener {
}
}
- @Override
- public void domainFileStatusChanged(DomainFile file, boolean fileIDset) {
- DomainFileNode fileNode = findDomainFileNode(file, true);
- if (fileNode != null) {
- fileNode.refresh();
+ //
+ // Helper methods
+ //
+
+ private DomainFolder getDomainFolder(DataTreeNode node) {
+ DomainFolder folder = null;
+ if (node instanceof DomainFileNode fileNode) {
+ folder = fileNode.getLinkedFolder(); // may return null
+ }
+ else if (node instanceof DomainFolderNode folderNode) {
+ folder = folderNode.getDomainFolder();
+ }
+ return folder;
+ }
+
+ private void addFileNode(DataTreeNode node, String fileName, boolean isFolderLink) {
+ if (node.isLeaf() || !node.isLoaded()) {
+ return;
+ }
+ // Check for existance of file by that name
+ DomainFileNode fileNode = (DomainFileNode) node.getChild(fileName,
+ isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
+ if (fileNode != null) {
+ domainFileStatusChanged(fileNode.getDomainFile(), false);
+ return;
+ }
+ DomainFolder folder = getDomainFolder(node);
+ if (folder != null) {
+ DomainFile file = folder.getFile(fileName);
+ if (file != null) {
+ DomainFileNode newNode = new DomainFileNode(file, root.getDomainFileFilter());
+ node.addNode(newNode);
+ }
+ }
+ }
+
+ private void addFolderNode(DataTreeNode node, String folderName) {
+ if (node.isLeaf() || !node.isLoaded()) {
+ return;
+ }
+ // Check for existance of folder by that name
+ if (node.getChild(folderName, NodeType.FOLDER) != null) {
+ return;
+ }
+ DomainFolder folder = getDomainFolder(node);
+ if (folder != null) {
+ DomainFolder f = folder.getFolder(folderName);
+ if (f != null) {
+ DomainFolderNode newNode = new DomainFolderNode(f, root.getDomainFileFilter());
+ node.addNode(newNode);
+ }
}
- treePanel.domainChange();
}
private void getFolderPath(DomainFolder df, List list) {
@@ -164,14 +263,12 @@ class ChangeManager implements DomainFolderChangeListener {
}
private DomainFolderNode findDomainFolderNode(List folderPath, boolean lazy) {
-
DomainFolderNode folderNode = root;
for (String name : folderPath) {
if (lazy && !folderNode.isLoaded()) {
return null; // not visited
}
- folderNode =
- (DomainFolderNode) folderNode.getChild(name, n -> (n instanceof DomainFolderNode));
+ folderNode = (DomainFolderNode) folderNode.getChild(name, NodeType.FOLDER);
if (folderNode == null) {
return null;
}
@@ -187,32 +284,333 @@ class ChangeManager implements DomainFolderChangeListener {
if (lazy && !folderNode.isLoaded()) {
return null; // not visited
}
-
+ boolean isFolderLink = domainFile.isLink() && domainFile.getLinkInfo().isFolderLink();
return (DomainFileNode) folderNode.getChild(domainFile.getName(),
- n -> (n instanceof DomainFileNode));
+ isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE);
}
- private void updateFolderNode(DomainFolder parent) {
- DomainFolderNode folderNode = findDomainFolderNode(parent, true);
- if (folderNode == null) {
+ /**
+ * Removes all children within the specified {@code parentNode} which no longer exist.
+ * @param parentNode parent node within tree
+ */
+ private void updateChildren(DataTreeNode parentNode) {
+
+ if (!parentNode.isLoaded()) {
return;
}
- DomainFolder folder = folderNode.getDomainFolder();
+
+ DomainFolder folder = null;
+ if (parentNode instanceof DomainFileNode fileNode) {
+ folder = fileNode.getLinkedFolder();
+ }
+ else if (parentNode instanceof DomainFolderNode folderNode) {
+ folder = folderNode.getDomainFolder();
+ }
+ if (folder == null) {
+ return;
+ }
+
// loop through children looking for nodes whose underlying model object
// does not have this folder as its parent;
- List children = folderNode.getChildren();
+ List children = parentNode.getChildren();
for (GTreeNode child : children) {
if (child instanceof DomainFileNode) {
if (folder.getFile(child.getName()) == null) {
- folderNode.removeNode(child);
+ parentNode.removeNode(child);
}
}
else if (child instanceof DomainFolderNode) {
if (folder.getFolder(child.getName()) == null) {
- folderNode.removeNode(child);
+ parentNode.removeNode(child);
}
}
}
}
+ //
+ // DataTree listener
+ //
+
+ @Override
+ public void treeStructureChanged(TreeModelEvent e) {
+
+ // This is used when an existing node is loaded to register all of its link-file children
+ // since the occurance of treeNodesChanged cannot be relied upon for notification of
+ // these existing children.
+
+ TreePath treePath = e.getTreePath();
+ if (treePath == null) {
+ return;
+ }
+ Object treeNode = treePath.getLastPathComponent();
+ if (!(treeNode instanceof DataTreeNode dataTreeNode)) {
+ return;
+ }
+ if (!dataTreeNode.isLoaded()) {
+ return;
+ }
+ // Register all visible link-file nodes
+ for (GTreeNode child : dataTreeNode.getChildren()) {
+ if (child instanceof DomainFileNode fileNode) {
+ if (fileNode.getDomainFile().isLink()) {
+ addLinkFile(fileNode);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void treeNodesChanged(TreeModelEvent e) {
+
+ // This is used to register link-file nodes which may be added to the tree as a result
+ // of changes to the associated project data.
+
+ Object treeNode = e.getTreePath().getLastPathComponent();
+ if (treeNode instanceof DomainFileNode fileNode) {
+ addLinkFile(fileNode);
+ }
+ }
+
+ @Override
+ public void treeNodesInserted(TreeModelEvent e) {
+ // Do nothing
+ }
+
+ @Override
+ public void treeNodesRemoved(TreeModelEvent e) {
+ // Do nothing
+ }
+
+ //
+ // Link tracking tree update support
+ //
+
+ /**
+ * Update link tree if the specified {@code domainFileNode} corresponds to an link-file
+ * which has an internal link-path which links to either a file or folder within the same
+ * project. Removal of obsolete link details within the link tree is done is a lazy
+ * fashion when refresh methods are invoked on a {@link LinkedTreeNode}.
+ *
+ * @param domainFileNode domain file tree node
+ */
+ void addLinkFile(DomainFileNode domainFileNode) {
+
+ DomainFile file = domainFileNode.getDomainFile();
+
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ if (linkInfo == null || linkInfo.isExternalLink()) {
+ return;
+ }
+
+ try {
+ String linkPath = LinkHandler.getAbsoluteLinkPath(file);
+ if (linkPath == null) {
+ return;
+ }
+ boolean isFolderLink = linkInfo.isFolderLink();
+ String[] pathElements = linkPath.split("/");
+ int lastFolderIndex = pathElements.length - 1;
+ if (!isFolderLink) {
+ --lastFolderIndex;
+ }
+ LinkedTreeNode folderLinkNode = linkTreeRoot;
+ for (int i = 1; i <= lastFolderIndex; i++) {
+ folderLinkNode = folderLinkNode.addFolder(pathElements[i]);
+ }
+
+ if (isFolderLink) {
+ folderLinkNode.addLinkedFolder(domainFileNode);
+ }
+ else {
+ folderLinkNode.addLinkedFile(pathElements[lastFolderIndex + 1], domainFileNode);
+ }
+ }
+ catch (IOException e) {
+ // ignore
+ }
+ }
+
+ /**
+ * Perform updates of linked tree content which relate to content within the specified
+ * {@code parentFolder}. All loaded folder linkages which include the specified
+ * {@code parentFolder} will be checked and the specified {@code folderNodeConsumer} will
+ * be invoked for each parent tree node which is a linked-reflection of it to facilitate
+ * specific updates. In addition, the specified {@code linkNodeConsumer} will be invoked
+ * once if a {@code LinkedTreeNode} is found which corresponds to the specified
+ * {@code parentFolder}. This allows targeted refresh of link-files.
+ *
+ * @param parentFolder a parent folder which relates to a change
+ * @param folderNodeConsumer optional consumer which will be invoked for each loaded parent
+ * tree node which is a linked-reflection of the specified {@code parentFolder}. If null is
+ * specified for this consumer a general update will be performed to remove any missing nodes.
+ * @param linkNodeConsumer optional consumer which will be invoked once if a {@code LinkedTreeNode}
+ * is found which corresponds to the specified {@code parentFolder}.
+ */
+ void updateLinkedContent(DomainFolder parentFolder, Consumer folderNodeConsumer,
+ Consumer linkNodeConsumer) {
+ if (skipLinkUpdate) {
+ return;
+ }
+ String pathname = parentFolder.getPathname();
+ String[] pathElements = pathname.split("/");
+ LinkedTreeNode folderLinkNode = linkTreeRoot;
+ folderLinkNode.updateLinkedContent(pathElements, 1, folderNodeConsumer);
+ for (int i = 1; i < pathElements.length; i++) {
+ folderLinkNode = folderLinkNode.folderMap.get(pathElements[i]);
+ if (folderLinkNode == null) {
+ return; // requested folder not contained within link-tree
+ }
+ folderLinkNode.updateLinkedContent(pathElements, i + 1, folderNodeConsumer);
+ }
+
+ // Requested folder was found in link-tree - invoke consumer to perform
+ // selective refresh
+ if (linkNodeConsumer != null) {
+ linkNodeConsumer.accept(folderLinkNode);
+ }
+ }
+
+ private class LinkedTreeNode {
+
+ private final LinkedTreeNode parent;
+ private final String name;
+
+ private Map folderMap = new HashMap<>();
+ private Set folderLinks = new HashSet<>();
+ private Map> linkedFilesMap = new HashMap<>();
+
+ LinkedTreeNode(LinkedTreeNode parent, String name) {
+ this.parent = parent;
+ this.name = name;
+ }
+
+ private void updateLinkedContent(String[] pathElements, int subFolderPathIndex,
+ Consumer folderNodeConsumer) {
+
+ // NOTE: This logic will not handle recursively linked-folders which is not supported.
+
+ boolean updateThisNode = subFolderPathIndex >= pathElements.length;
+
+ for (DomainFileNode folderLink : folderLinks) {
+
+ if (!folderLink.isLoaded()) {
+ continue;
+ }
+
+ if (updateThisNode) {
+ if (folderNodeConsumer != null) {
+ folderNodeConsumer.accept(folderLink);
+ }
+ else {
+ updateChildren(folderLink);
+ }
+ continue;
+ }
+
+ DomainFolderNode folderNode = null;
+ for (int ix = subFolderPathIndex; ix < pathElements.length; ++ix) {
+ folderNode =
+ (DomainFolderNode) folderLink.getChild(pathElements[ix], NodeType.FOLDER);
+ if (folderNode == null || !folderNode.isLoaded()) {
+ folderNode = null;
+ break;
+ }
+ }
+ if (folderNode != null) {
+ if (folderNodeConsumer != null) {
+ folderNodeConsumer.accept(folderNode);
+ }
+ else {
+ updateChildren(folderNode);
+ }
+ }
+ }
+
+ }
+
+ private void refreshLinks(String childName) {
+ // We are forced to refresh file-links and folder-links since a folder-link may be
+ // referencing another folder-link file and not the final referenced folder.
+ if (refreshFileLinks(childName) || refreshFolderLinks(childName)) {
+ purgeFolderWithoutLinks();
+ }
+ }
+
+ private boolean refreshFolderLinks(String folderName) {
+ LinkedTreeNode linkedTreeNode = folderMap.get(folderName);
+ if (linkedTreeNode != null) {
+ refresh(linkedTreeNode.folderLinks);
+ return linkedTreeNode.folderLinks.isEmpty();
+ }
+ return false;
+ }
+
+ private boolean refreshFileLinks(String fileName) {
+ Set linkFiles = linkedFilesMap.get(fileName);
+ if (linkFiles != null) {
+ refresh(linkFiles);
+ if (linkFiles.isEmpty()) {
+ linkedFilesMap.remove(fileName);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private LinkedTreeNode addFolder(String folderName) {
+ return folderMap.computeIfAbsent(folderName, n -> new LinkedTreeNode(this, n));
+ }
+
+ private void addLinkedFolder(DomainFileNode folderLink) {
+ folderLinks.add(folderLink);
+ }
+
+ private void addLinkedFile(String fileName, DomainFileNode fileLink) {
+ Set fileLinks =
+ linkedFilesMap.computeIfAbsent(fileName, n -> new HashSet<>());
+ fileLinks.add(fileLink);
+ }
+
+ private void purgeFolderWithoutLinks() {
+ if (parent != null && folderMap.isEmpty() && folderLinks.isEmpty() &&
+ linkedFilesMap.isEmpty()) {
+ parent.folderMap.remove(name);
+ parent.purgeFolderWithoutLinks();
+ }
+ }
+
+ private static void refresh(Set linkFiles) {
+ List purgeList = null;
+ for (DomainFileNode fileLink : linkFiles) {
+ DomainFile file = fileLink.getDomainFile();
+ // Perform lazy purge of missing link files
+ if (!file.isLink()) {
+ if (purgeList == null) {
+ purgeList = new ArrayList<>();
+ }
+ purgeList.add(fileLink);
+ }
+ else {
+ fileLink.refresh();
+ }
+ }
+ if (purgeList != null) {
+ linkFiles.removeAll(purgeList);
+ }
+ }
+
+ private String getPathname() {
+ if (parent == null) {
+ return "/";
+ }
+ return parent.getPathname() + name + "/";
+ }
+
+ @Override
+ public String toString() {
+ return getPathname();
+ }
+
+ }
+
}
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 15a1fd4c98..35c48efadc 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -95,7 +95,7 @@ public class CheckInTask extends VersionControlTask implements CheckinHandler {
}
catch (VersionException e) {
VersionExceptionHandler.showVersionError(parent, df.getName(),
- df.getContentType(), "Checkin", e);
+ df.getContentType(), "Check In", false, e);
}
if (myMonitor.isCancelled()) {
break;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/Cuttable.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/Cuttable.java
index d16b898679..3cf07922cd 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/Cuttable.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/Cuttable.java
@@ -1,13 +1,12 @@
/* ###
* IP: GHIDRA
- * REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -16,8 +15,20 @@
*/
package ghidra.framework.main.datatree;
+/**
+ * {@link Cuttable} associated with an element which supports cut/paste operation
+ */
public interface Cuttable {
- public void setIsCut(boolean b);
+
+ /**
+ * Set this node to be deleted so that it can be rendered as such.
+ * @param isCut true if node will be cut and moved
+ */
+ public void setIsCut(boolean isCut);
+
+ /**
+ * {@return true if node will be cut and moved}
+ */
public boolean isCut();
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java
index 1bfe373361..4aebd2fd41 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java
@@ -16,6 +16,7 @@
package ghidra.framework.main.datatree;
import java.awt.event.KeyEvent;
+import java.io.IOException;
import javax.swing.KeyStroke;
import javax.swing.ToolTipManager;
@@ -26,7 +27,9 @@ import docking.action.DockingAction;
import docking.actions.KeyBindingUtils;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode;
+import ghidra.framework.data.LinkHandler.LinkStatus;
import ghidra.framework.main.FrontEndTool;
+import ghidra.framework.model.*;
/**
* Tree that shows the folders and domain files in a Project
@@ -105,4 +108,72 @@ public class DataTree extends GTree {
public void stopEditing() {
getJTree().stopEditing();
}
+
+ /**
+ * Method returns either a {@link DomainFolder} within the node's project or null.
+ * The following cases indicate how the return value is established
+ * based on the specified {@link GTreeNode node}:
+ *
+ * - {@link DomainFolderNode} - the node's domain folder will be returned
+ * - {@link DomainFileNode} (folder-link content type) - the referenced folder within the node's
+ * project will be returned under the following conditions, otherwise null will be returned:
+ *
+ * - The file corresponds to a folder-link, and
+ * - the folder-link ultimately refers to a domain folder within the same project
+ * (i.e., a URL-based link path is not used and link status is {@link LinkStatus#INTERNAL}).
+ *
+ * - {@link DomainFileNode} (normal file or file-link) - the node's parent folder will be
+ * returned.
+ *
+ *
+ * Folder-links which reference other internal folder-links will be followed until a
+ * folder can be identified or the link-chain is considered is {@link LinkStatus#BROKEN}
+ * or {@link LinkStatus#EXTERNAL} in which case null will be returned.
+ *
+ * A {@link LinkedDomainFolder} will always be resolved to its real folder which it corresponds to.
+ *
+ *
+ * @param node Data Tree Node to be evaluated for its real internal folder
+ * @return internal project folder which corresponds to the specified node.
+ */
+ public static DomainFolder getRealInternalFolderForNode(GTreeNode node) {
+ DomainFolder folder = null;
+ if (node instanceof DomainFolderNode folderNode) {
+ folder = folderNode.getDomainFolder();
+ }
+ else if (node instanceof DomainFileNode fileNode) {
+ if (fileNode.isFolderLink()) {
+ // Handle case where file node corresponds to a folder-link.
+ // Folder-Link status needs to be checked to ensure it corresponds to a folder
+ // internal to the same project.
+ LinkFileInfo linkInfo = fileNode.getDomainFile().getLinkInfo();
+ if (linkInfo == null) {
+ return null; // unexpected
+ }
+ LinkStatus linkStatus = linkInfo.getLinkStatus(null);
+ if (linkStatus != LinkStatus.INTERNAL) {
+ return null;
+ }
+ // Get linked folder - status check ensures null will not be returned
+ folder = linkInfo.getLinkedFolder();
+ }
+ else {
+ // Handle normal file cases where we return node's parent folder
+ GTreeNode parent = node.getParent();
+ if (parent instanceof DomainFolderNode folderNode) {
+ folder = folderNode.getDomainFolder();
+ }
+ }
+ }
+ if (folder instanceof LinkedDomainFolder linkedFolder) {
+ // Resolve linked internal folder to its real folder
+ try {
+ folder = linkedFolder.getRealFolder();
+ }
+ catch (IOException e) {
+ folder = null;
+ }
+ }
+ return folder;
+ }
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java
index e21a238206..6e3798f48c 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -118,8 +118,7 @@ public class DataTreeDragNDropHandler implements GTreeDragNDropHandler {
if (ToolConstants.NO_ACTIVE_PROJECT.equals(destUserData.getName())) {
return false;
}
-
- return true;
+ return DataTree.getRealInternalFolderForNode(destUserData) != null;
}
@Override
@@ -164,30 +163,28 @@ public class DataTreeDragNDropHandler implements GTreeDragNDropHandler {
private List removeDuplicates(List allNodes) {
- List folderNodes = getDomainFolderNodes(allNodes);
+ List parentNodes = getDomainParentNodes(allNodes);
// if a file has a parent in the list, then it is not needed as a separate entry
return allNodes.stream()
- .filter(node -> !isChildOfFolders(folderNodes, node))
+ .filter(node -> !isChildOfParents(parentNodes, node))
.collect(Collectors.toList());
}
- private List getDomainFolderNodes(List nodeList) {
- List folderList = new ArrayList<>();
-
+ private List getDomainParentNodes(List nodeList) {
+ List parentList = new ArrayList<>();
for (GTreeNode node : nodeList) {
- if (node instanceof DomainFolderNode) {
- folderList.add(node);
+ if (!node.isLeaf()) {
+ parentList.add(node);
}
}
-
- return folderList;
+ return parentList;
}
- private boolean isChildOfFolders(List folderNodes, GTreeNode fileNode) {
+ private boolean isChildOfParents(List parentNodes, GTreeNode fileNode) {
GTreeNode node = fileNode.getParent();
while (node != null) {
- if (folderNodes.contains(node)) {
+ if (parentNodes.contains(node)) {
return true;
}
node = node.getParent();
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java
new file mode 100644
index 0000000000..a1537eae5f
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java
@@ -0,0 +1,313 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.framework.main.datatree;
+
+import java.util.*;
+
+import docking.widgets.tree.GTreeNode;
+import docking.widgets.tree.GTreeSlowLoadingNode;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.LinkHandler.LinkStatus;
+import ghidra.framework.model.*;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * {@link DataTreeNode} provides the base implementation for all node types contained within
+ * a {@link DataTree}.
+ */
+public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cuttable {
+
+ /**
+ * {@link NodeType} is used to aid the sorting/comparison of data tree node. The
+ * sort order is based upon the following comparisons in order of significance:
+ *
+ * - Node type weighting. Folder and Folder-Links have equal weighting.
+ * - Node comparison by name (see {@link DataTreeNode#compareNodeNames(String, String)}).
+ * - Node type ordinal (e.g., ensures that a Folder-Link with the same name as a Folder
+ * will be placed after the Folder.
+ *
+ */
+ enum NodeType {
+
+ FOLDER(1), FOLDER_LINK(1), FILE(2), OTHER(3);
+
+ int weight;
+
+ NodeType(int weight) {
+ this.weight = weight;
+ }
+
+ static NodeType getNodeType(GTreeNode node) {
+ if (node instanceof DomainFolderNode) {
+ return FOLDER;
+ }
+ if (node instanceof DomainFileNode fileNode) {
+ return fileNode.isFolderLink() ? FOLDER_LINK : FILE;
+ }
+ return OTHER;
+ }
+ }
+
+ /**
+ * Sort {@link Comparator} for use with sorting children and node comparison
+ */
+ static final Comparator DATA_NODE_SORT_COMPARATOR = new DataNodeSortComparator();
+
+ /**
+ * Search {@link Comparator} for use by {@link #getChild(String, NodeType)} only
+ */
+ private static final DataNodeSearchComparator DATA_NODE_SEARCH_COMPARATOR =
+ new DataNodeSearchComparator();
+
+ private volatile boolean isCut; // true if this node is marked as cut
+
+ @Override
+ public final void setIsCut(boolean isCut) {
+ if (isCut != this.isCut) {
+ this.isCut = isCut;
+ fireNodeChanged();
+ }
+ }
+
+ @Override
+ public final boolean isCut() {
+ return isCut;
+ }
+
+ /**
+ * Get the project data instance to which this file or folder belongs.
+ * @return project data instance
+ */
+ public abstract ProjectData getProjectData();
+
+ @Override
+ public abstract int compareTo(GTreeNode node);
+
+ @Override
+ public abstract boolean equals(Object obj);
+
+ @Override
+ public abstract int hashCode();
+
+ @Override
+ public void addNode(GTreeNode newNode) {
+ if (!isLoaded()) {
+ return;
+ }
+ List allChildren = getChildren();
+ int index = Collections.binarySearch(allChildren, newNode, DATA_NODE_SORT_COMPARATOR);
+ if (index < 0) {
+ index = -index - 1;
+ }
+ addNode(index, newNode);
+
+ if (newNode instanceof DomainFolderNode) {
+ // Refresh possible conflicting folder-link
+ DomainFileNode folderLink =
+ (DomainFileNode) getChild(newNode.getName(), NodeType.FOLDER_LINK);
+ if (folderLink != null) {
+ folderLink.refresh();
+ }
+ }
+ }
+
+ @Override
+ public void removeNode(GTreeNode node) {
+ if (!isLoaded()) {
+ return;
+ }
+ // NOTE: Remove node is not implemented in a manner where we can remove by index
+ // using a binary search.
+ super.removeNode(node);
+
+ if (node instanceof DomainFolderNode) {
+ // Refresh possible conflicting folder-link resolved
+ DomainFileNode folderLink =
+ (DomainFileNode) getChild(node.getName(), NodeType.FOLDER_LINK);
+ if (folderLink != null) {
+ folderLink.refresh();
+ }
+ }
+ }
+
+// NOTE: The use of this method should be blocked since it does not properly handle duplicate child
+// names within the same folder.
+// /**
+// * Domain folders and files may have the same name within a parent. This method should
+// * not be used.
+// */
+// @Override
+// public final GTreeNode getChild(String name) {
+// throw new UnsupportedOperationException("DataTree node names may not be unique");
+// }
+
+ /**
+ * Find a child using a binary-search approach.
+ *
+ * @param name name of child to find
+ * @param type node type
+ * @return matching tree node or null if not found
+ */
+ public abstract GTreeNode getChild(String name, NodeType type);
+
+ /**
+ * Find a child using a binary-search approach vs. the default brute-force search.
+ * Note that two supported node types may have the same name, one being a {@link DomainFolderNode}
+ * and the other being a {@link DomainFileNode}. Folders are always placed before Files,
+ * although such different node types with the same name are not adjacent. For this reason
+ * a binary search cannot be used with a arbitrary predicate.
+ *
+ * @param children children to be searched
+ * @param name name of child to find
+ * @param type node type
+ * @return matching tree node or null if not found
+ */
+ @SuppressWarnings("unchecked")
+ static GTreeNode getChild(List children, String name, NodeType type) {
+ ChildSearchRecord childSearchRecord = new ChildSearchRecord(name, type);
+ int index =
+ Collections.binarySearch(children, childSearchRecord, DATA_NODE_SEARCH_COMPARATOR);
+ return index >= 0 ? children.get(index) : null;
+ }
+
+ private record ChildSearchRecord(String name, NodeType type) {
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static class DataNodeSearchComparator implements Comparator {
+ @Override
+ public int compare(Object o1, Object o2) {
+
+ GTreeNode node = (GTreeNode) o1;
+ ChildSearchRecord childSearchRecord = (ChildSearchRecord) o2;
+
+ NodeType type1 = NodeType.getNodeType(node);
+ NodeType type2 = childSearchRecord.type;
+
+ int comp = type1.weight - type2.weight;
+ if (comp != 0) {
+ return comp;
+ }
+
+ // NOTE: This name comparison is consistent with the sort order and
+ // will provide a case-senstive name-match
+ comp = compareNodeNames(node.getName(), childSearchRecord.name);
+ if (comp == 0) {
+ return type1.ordinal() - type2.ordinal();
+ }
+ return comp;
+ }
+ }
+
+ private static class DataNodeSortComparator implements Comparator {
+ @Override
+ public int compare(GTreeNode o1, GTreeNode o2) {
+
+ //
+ // Goal is to have folders appear before files except for folder-links
+ // which should be grouped with folders but come after a folder with
+ // the same name
+
+ NodeType type1 = NodeType.getNodeType(o1);
+ NodeType type2 = NodeType.getNodeType(o2);
+
+ int comp = type1.weight - type2.weight;
+ if (comp != 0) {
+ return comp;
+ }
+
+ // NOTE: This name comparison is consistent with compareTo implementaions
+ comp = compareNodeNames(o1.getName(), o2.getName());
+ if (comp == 0) {
+ return type1.ordinal() - type2.ordinal();
+ }
+ return comp;
+ }
+ }
+
+ /**
+ * Name comparison to be used for DataTreeNode comparators and node comparison.
+ * @param n1 first name
+ * @param n2 second name
+ * @return comparison result consistent with {@link String#compareTo(String) n1.compareTo(n2)}
+ */
+ static int compareNodeNames(String n1, String n2) {
+ int c = n1.compareToIgnoreCase(n2);
+ if (c == 0) {
+ // disambiguate for deterministic sort
+ c = n1.compareTo(n2);
+ }
+ return c;
+ }
+
+ /**
+ * Generate filtered child nodes for a DomainFolder
+ * @param domainFolder folder
+ * @param filter filter
+ * @param monitor load task monitor
+ * @return list of filtered chidren
+ * @throws CancelledException if load task is cancelled
+ */
+ static List generateChildren(DomainFolder domainFolder, DomainFileFilter filter,
+ TaskMonitor monitor) throws CancelledException {
+
+ boolean hideFolderLinks = false;
+ boolean hideBroken = false;
+ boolean hideExternal = false;
+ if (filter != null) {
+ hideFolderLinks = filter.ignoreFolderLinks();
+ hideBroken = filter.ignoreBrokenLinks();
+ hideExternal = filter.ignoreExternalLinks();
+ }
+
+ List children = new ArrayList<>();
+ if (domainFolder != null) {
+
+ DomainFolder[] folders = domainFolder.getFolders();
+ for (DomainFolder folder : folders) {
+ monitor.checkCancelled();
+ children.add(new DomainFolderNode(folder, filter));
+ }
+
+ DomainFile[] files = domainFolder.getFiles();
+ for (DomainFile df : files) {
+ monitor.checkCancelled();
+ if (filter != null) {
+ boolean isFolderLink = df.isLink() && df.getLinkInfo().isFolderLink();
+ if (hideFolderLinks && isFolderLink) {
+ continue;
+ }
+ if ((hideBroken || hideExternal) && df.isLink()) {
+ LinkStatus linkStatus = LinkHandler.getLinkFileStatus(df, null);
+ if (hideBroken && linkStatus == LinkStatus.BROKEN) {
+ continue;
+ }
+ if (hideExternal && linkStatus == LinkStatus.EXTERNAL) {
+ continue;
+ }
+ }
+ if (!isFolderLink && !filter.accept(df)) {
+ continue;
+ }
+ }
+ children.add(new DomainFileNode(df, filter));
+ }
+ }
+ Collections.sort(children, DATA_NODE_SORT_COMPARATOR);
+ return children;
+ }
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DialogProjectTreeContext.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DialogProjectTreeContext.java
index 49166d8e93..fca389e279 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DialogProjectTreeContext.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DialogProjectTreeContext.java
@@ -39,9 +39,8 @@ public class DialogProjectTreeContext extends DialogActionContext implements Pro
private List selectedFolders;
private List selectedFiles;
- public DialogProjectTreeContext(ProjectData projectData,
- TreePath[] selectionPaths, List folderList, List fileList,
- DataTree tree) {
+ public DialogProjectTreeContext(ProjectData projectData, TreePath[] selectionPaths,
+ List folderList, List fileList, DataTree tree) {
super(getContextObject(selectionPaths), tree);
this.selectionPaths = selectionPaths;
this.selectedFolders = folderList;
@@ -98,6 +97,16 @@ public class DialogProjectTreeContext extends DialogActionContext implements Pro
return selectedFiles.size();
}
+ @Override
+ public boolean hasExactlyOneFileOrFolder() {
+ return (getFolderCount() + getFileCount()) == 1;
+ }
+
+ @Override
+ public boolean hasOneOrMoreFilesAndFolders() {
+ return getFolderCount() + getFileCount() > 0;
+ }
+
@Override
public GTreeNode getContextNode() {
return (GTreeNode) super.getContextObject();
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 785a8c93d2..cfad87ce7f 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -16,47 +16,64 @@
package ghidra.framework.main.datatree;
import java.io.IOException;
+import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
import javax.swing.Icon;
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.data.LinkHandler.LinkStatus;
+import ghidra.framework.main.BrokenLinkIcon;
+import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.util.*;
+import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
-import resources.ResourceManager;
+import ghidra.util.task.TaskMonitor;
/**
* Class to represent a node in the Data tree.
*/
-public class DomainFileNode extends GTreeNode implements Cuttable {
+public class DomainFileNode extends DataTreeNode {
private static final Icon UNKNOWN_FILE_ICON = new GIcon("icon.datatree.node.domain.file");
+ private static final String RIGHT_ARROW = "\u2b95";
private final DomainFile domainFile;
private volatile String displayName; // name displayed in the tree
private volatile Icon icon = UNKNOWN_FILE_ICON;
- private volatile Icon disabledIcon;
- protected volatile String toolTipText;
+ private volatile Icon cutIcon;
+ private volatile String toolTipText;
+ private AtomicInteger refreshCount = new AtomicInteger();
- private volatile boolean isCut; // true if this node is marked as cut
+ private boolean isLeaf = true;
+ private LinkFileInfo linkInfo;
+ private DomainFileFilter filter; // relavent when expand folder-link which is a file
- private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa");
+ private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa");
- DomainFileNode(DomainFile domainFile) {
+ DomainFileNode(DomainFile domainFile, DomainFileFilter filter) {
this.domainFile = domainFile;
+ this.linkInfo = domainFile.getLinkInfo();
+ this.filter = filter != null ? filter : DomainFileFilter.ALL_FILES_FILTER;
displayName = domainFile.getName();
refresh();
}
+ @Override
+ public boolean isAutoExpandPermitted() {
+ // Prevent auto-expansion through linked-folders
+ return false;
+ }
+
/**
* Get the domain file if this node represents a file object versus a folder; interface method
* for DomainDataTransfer.
@@ -69,7 +86,39 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
@Override
public boolean isLeaf() {
- return true;
+ return isLeaf;
+ }
+
+ @Override
+ public int getChildCount() {
+ if (isLeaf) {
+ // Optimization to avoid repeated attempts at following a bad link
+ return 0;
+ }
+ return super.getChildCount();
+ }
+
+ /**
+ * Determine if this file node corresponds to a folder-link
+ * @return true if file is a folder-link
+ */
+ public boolean isFolderLink() {
+ if (linkInfo != null) {
+ return linkInfo.isFolderLink();
+ }
+ return false;
+ }
+
+ /**
+ * Get linked folder which corresponds to this folder-link
+ * (see {@link #isFolderLink()}).
+ * @return linked folder or null if this is not a folder-link
+ */
+ LinkedDomainFolder getLinkedFolder() {
+ if (!isLeaf() && linkInfo != null) { // verifies that we are allowed to follow based upon filter
+ return linkInfo.getLinkedFolder();
+ }
+ return null;
}
@Override
@@ -93,24 +142,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
@Override
public int hashCode() {
- return System.identityHashCode(domainFile);
- }
-
- /**
- * Set this node to be deleted so that it can be rendered as such.
- */
- @Override
- public void setIsCut(boolean isCut) {
- this.isCut = isCut;
- fireNodeChanged();
- }
-
- /**
- * Returns whether this node is marked as deleted.
- */
- @Override
- public boolean isCut() {
- return isCut;
+ return domainFile.hashCode();
}
@Override
@@ -120,8 +152,8 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
@Override
public Icon getIcon(boolean expanded) {
- if (isCut) {
- return disabledIcon;
+ if (isCut()) {
+ return cutIcon;
}
return icon;
}
@@ -152,32 +184,80 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
@Override
protected DomainFileNode doInBackground() throws Exception {
- doRefresh();
+ try {
+ doRefresh();
+ }
+ finally {
+ refreshCount.decrementAndGet();
+ }
return DomainFileNode.this;
}
}
+ /**
+ * This method intended for test use only.
+ * {@return true if a pending refresh exists for this node}
+ */
+ public boolean hasPendingRefresh() {
+ return refreshCount.get() != 0;
+ }
+
/**
* Update the display name.
*/
void refresh() {
+ refreshCount.incrementAndGet();
DomainFileNodeSwingWorker worker = new DomainFileNodeSwingWorker();
worker.execute();
}
private void doRefresh() {
- //DomainFolderNode parent = (DomainFolderNode) getParent();
+ isLeaf = true;
+ linkInfo = null;
- String name = domainFile.getName();
- //domainFile = parent.getDomainFolder().getFile(name);
+ boolean brokenLink = false;
+ List linkErrors = null;
+ if (domainFile.isLink()) {
+ linkInfo = domainFile.getLinkInfo();
+ List errors = new ArrayList<>();
+ LinkStatus linkStatus =
+ LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg));
+ brokenLink = (linkStatus == LinkStatus.BROKEN);
+ if (brokenLink) {
+ linkErrors = errors;
+ }
+ else if (isFolderLink()) {
+ if (linkStatus == LinkStatus.INTERNAL) {
+ isLeaf = false;
+ }
+ else if (linkStatus == LinkStatus.EXTERNAL &&
+ filter.followExternallyLinkedFolders()) {
+ isLeaf = false;
+ }
+ }
+ }
- String newDisplayName = name;
+ if (isLeaf) {
+ unloadChildren();
+ }
+ displayName = getFormattedDisplayName();
+
+ toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(getToolTipText(domainFile, linkErrors));
+
+ refreshIcons(brokenLink);
+
+ fireNodeChanged();
+ }
+
+ private String getFormattedDisplayName() {
+
+ String newDisplayName = domainFile.getName();
if (domainFile.isHijacked()) {
newDisplayName += " (hijacked)";
}
- else if (domainFile.isVersioned() && !domainFile.isLinkFile()) {
+ else if (domainFile.isVersioned() && !domainFile.isLink()) {
int versionNumber = domainFile.getVersion();
String versionStr = "" + versionNumber;
@@ -200,58 +280,89 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
newDisplayName += " (" + versionStr + ")";
}
}
- displayName = newDisplayName;
- setToolTipText();
-
- icon = domainFile.getIcon(false);
- disabledIcon = ResourceManager.getDisabledIcon(icon);
-
- fireNodeChanged();
+ if (domainFile.isLink()) {
+ newDisplayName += " " + RIGHT_ARROW + " " + getFormattedLinkPath();
+ }
+ return newDisplayName;
}
- private void setToolTipText() {
- String newToolTipText = null;
- if (domainFile.isInWritableProject() && domainFile.isHijacked()) {
- newToolTipText = "Hijacked file should be deleted or renamed";
- }
- else {
- StringBuilder buf = new StringBuilder();
+ private String getFormattedLinkPath() {
+
+ String linkPath = linkInfo != null ? linkInfo.getLinkPath() : null;
+ if (GhidraURL.isGhidraURL(linkPath)) {
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 = "Checked out " +
- formatter.format(new Date(status.getCheckoutTime())) +
- "\n" + newToolTipText;
+ URL url = new URL(linkPath);
+ if (GhidraURL.isLocalGhidraURL(linkPath)) {
+ ProjectLocator loc = GhidraURL.getProjectStorageLocator(url);
+ if (loc != null) {
+ String projectPath = GhidraURL.getProjectPathname(url);
+ linkPath = loc.getName() + ":" + projectPath;
}
}
- catch (IOException e) {
- // just ignore and use the previously set tooltip
+ else if (GhidraURL.isServerURL(linkPath)) {
+ String host = url.getHost();
+ String repo = GhidraURL.getRepositoryName(url);
+ if (repo != null) {
+ String projectPath = GhidraURL.getProjectPathname(url);
+ linkPath = host + "[" + repo + "]:" + projectPath;
+ }
}
}
-
- if (domainFile.isReadOnly()) {
- newToolTipText += " (read only)";
+ catch (MalformedURLException e) {
+ // ignore - use original linkPath
}
- newToolTipText = HTMLUtilities.toLiteralHTML(newToolTipText, 0);
}
- toolTipText = newToolTipText;
+ return linkPath;
+ }
+
+ private void refreshIcons(boolean isBrokenLink) {
+
+ icon = domainFile.getIcon(false);
+ cutIcon = domainFile.getIcon(true);
+ if (isBrokenLink) {
+ icon = new BrokenLinkIcon(icon);
+ cutIcon = new BrokenLinkIcon(cutIcon);
+ }
+ }
+
+ public static String getToolTipText(DomainFile domainFile, List linkErrors) {
+ StringBuilder buf = new StringBuilder();
+ if (domainFile.isInWritableProject() && domainFile.isHijacked()) {
+ buf.append("Hijacked file should be deleted or renamed");
+ }
+
+ if (linkErrors != null) {
+ linkErrors.forEach(linkError -> appendLine(buf, linkError));
+ }
+
+ if (domainFile.isCheckedOut()) {
+ try {
+ ItemCheckoutStatus status = domainFile.getCheckoutStatus();
+ if (status != null) {
+ appendLine(buf,
+ "Checked out " + formatter.format(new Date(status.getCheckoutTime())));
+ }
+ }
+ catch (IOException e) {
+ // just ignore and use the previously set tooltip
+ }
+ }
+
+ long lastModified = domainFile.getLastModifiedTime();
+ appendLine(buf, "Last Modified " + formatter.format(new Date(lastModified)));
+
+ if (domainFile.isReadOnly()) {
+ appendLine(buf, "(read only)");
+ }
+ return buf.toString();
+ }
+
+ private static void appendLine(StringBuilder buf, String line) {
+ if (!buf.isEmpty()) {
+ buf.append('\n');
+ }
+ buf.append(line);
}
@Override
@@ -261,36 +372,7 @@ 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());
+ return DATA_NODE_SORT_COMPARATOR.compare(this, node);
}
@Override
@@ -318,4 +400,22 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
return getName();
}
+ @Override
+ public List generateChildren(TaskMonitor monitor) throws CancelledException {
+ if (isLeaf || linkInfo == null) {
+ return List.of();
+ }
+ return generateChildren(linkInfo.getLinkedFolder(), filter, monitor);
+ }
+
+ @Override
+ public GTreeNode getChild(String name, NodeType type) {
+ return getChild(children(), name, type);
+ }
+
+ @Override
+ public ProjectData getProjectData() {
+ return domainFile.getParent().getProjectData();
+ }
+
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFilesPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFilesPanel.java
index 763d68de17..a4e3af383f 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFilesPanel.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFilesPanel.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -79,6 +79,7 @@ class DomainFilesPanel extends JPanel {
/**
* Get the selected domain files.
+ * @return selected domain files
*/
DomainFile[] getSelectedDomainFiles() {
List list = new ArrayList<>();
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 eb3f2606b0..c7f48fa5c5 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
@@ -16,12 +16,11 @@
package ghidra.framework.main.datatree;
import java.io.IOException;
-import java.util.*;
+import java.util.List;
import javax.swing.Icon;
import docking.widgets.tree.GTreeNode;
-import docking.widgets.tree.GTreeSlowLoadingNode;
import ghidra.framework.model.*;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
@@ -31,7 +30,7 @@ import resources.ResourceManager;
/**
* Class to represent a node in the Data tree.
*/
-public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable {
+public class DomainFolderNode extends DataTreeNode {
private static final Icon ENABLED_OPEN_FOLDER = DomainFolder.OPEN_FOLDER_ICON;
private static final Icon ENABLED_CLOSED_FOLDER = DomainFolder.CLOSED_FOLDER_ICON;
@@ -42,7 +41,6 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable {
ResourceManager.getDisabledIcon(ENABLED_CLOSED_FOLDER);
private DomainFolder domainFolder;
- private boolean isCut;
private DomainFileFilter filter;
// variables that are accessed in with a lock on the filesystem in the underlying folder
@@ -55,8 +53,7 @@ public class DomainFolderNode extends GTreeSlowLoadingNode 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 = StringUtilities.trimMiddle(domainFolder.getPathname(), 120);
- toolTipText = HTMLUtilities.toLiteralHTML(toolTipText, 0);
+ setToolTipText();
isEditable = domainFolder.isInWritableProject();
}
}
@@ -84,33 +81,12 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable {
return false;
}
- /**
- * Set this node to be deleted so that it can be rendered as such.
- */
- @Override
- public void setIsCut(boolean isCut) {
- this.isCut = isCut;
- fireNodeChanged();
- }
-
- /**
- * Returns whether this node is marked as deleted.
- */
- @Override
- public boolean isCut() {
- return isCut;
- }
-
@Override
public Icon getIcon(boolean expanded) {
- if (domainFolder instanceof LinkedDomainFolder) {
- // NOTE: cut operation not supported
- return ((LinkedDomainFolder) domainFolder).getIcon(expanded);
+ if (isCut()) {
+ return expanded ? DISABLED_OPEN_FOLDER : DISABLED_CLOSED_FOLDER;
}
- if (expanded) {
- return isCut ? DISABLED_OPEN_FOLDER : ENABLED_OPEN_FOLDER;
- }
- return isCut ? DISABLED_CLOSED_FOLDER : ENABLED_CLOSED_FOLDER;
+ return expanded ? ENABLED_OPEN_FOLDER : ENABLED_CLOSED_FOLDER;
}
@Override
@@ -128,38 +104,20 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable {
return toolTipText;
}
+ private void setToolTipText() {
+ String newToolTipText;
+ if (domainFolder instanceof LinkedDomainFolder) {
+ newToolTipText = domainFolder.toString();
+ }
+ else {
+ newToolTipText = domainFolder.getPathname();
+ }
+ toolTipText = HTMLUtilities.toLiteralHTML(newToolTipText, 0);
+ }
+
@Override
public List generateChildren(TaskMonitor monitor) throws CancelledException {
-
- List children = new ArrayList<>();
- if (domainFolder == null || domainFolder.isEmpty()) {
- return children;
- }
-
- // NOTE: isEmpty() is used to avoid multiple failed connection attempts on this folder
-
- DomainFolder[] folders = domainFolder.getFolders();
- for (DomainFolder folder : folders) {
- monitor.checkCancelled();
- children.add(new DomainFolderNode(folder, filter));
- }
-
- DomainFile[] files = domainFolder.getFiles();
- for (DomainFile domainFile : files) {
- monitor.checkCancelled();
- 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));
- }
- }
- Collections.sort(children);
- return children;
+ return generateChildren(domainFolder, filter, monitor);
}
@Override
@@ -188,7 +146,7 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable {
@Override
public int hashCode() {
- return System.identityHashCode(domainFolder);
+ return domainFolder.hashCode();
}
public DomainFileFilter getDomainFileFilter() {
@@ -197,11 +155,7 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable {
@Override
public int compareTo(GTreeNode node) {
- if (node instanceof DomainFileNode) {
- // defer to DomainFileNode for comparison
- return -((DomainFileNode) node).compareTo(this);
- }
- return super.compareTo(node);
+ return DATA_NODE_SORT_COMPARATOR.compare(this, node);
}
@Override
@@ -222,4 +176,14 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable {
}
}
}
+
+ @Override
+ public GTreeNode getChild(String name, NodeType type) {
+ return getChild(children(), name, type);
+ }
+
+ @Override
+ public ProjectData getProjectData() {
+ return domainFolder.getProjectData();
+ }
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/FindCheckoutsTableModel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/FindCheckoutsTableModel.java
index 67a6207b2b..2f5ac81fab 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/FindCheckoutsTableModel.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/FindCheckoutsTableModel.java
@@ -98,6 +98,12 @@ class FindCheckoutsTableModel extends ThreadedTableModelStub {
if (monitor.isCancelled()) {
throw new CancelledException();
}
+ if (file.isLink()) {
+ // NOTE: We do not currently consider link-files whose referenced file
+ // is checked-out.
+ continue;
+ }
+
if (file.isCheckedOut()) {
try {
CheckoutInfo info = new CheckoutInfo(file);
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java
index 388a8a5e18..37458a911c 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -24,8 +24,7 @@ import java.util.List;
import docking.widgets.tree.GTreeNode;
import docking.widgets.tree.GTreeState;
import ghidra.app.util.FileOpenDataFlavorHandler;
-import ghidra.framework.model.DomainFile;
-import ghidra.framework.model.DomainFolder;
+import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
@@ -74,7 +73,12 @@ public final class LocalTreeNodeHandler
return false;
}
- CopyAllTask task = new CopyAllTask(list, destinationNode, dropAction);
+ DomainFolder destFolder = DataTree.getRealInternalFolderForNode(destinationNode);
+ if (destFolder == null || !destFolder.isInWritableProject()) {
+ return false;
+ }
+
+ CopyAllTask task = new CopyAllTask(list, destFolder, dropAction);
new TaskLauncher(task, dataTree, 1000);
if (treeState != null) { // is set to null if drag results in a task
@@ -87,38 +91,46 @@ public final class LocalTreeNodeHandler
return true;
}
- private void add(GTreeNode destNode, GTreeNode draggedNode, int dropAction,
+ private void add(DomainFolder destFolder, GTreeNode draggedNode, int dropAction,
TaskMonitor monitor) {
- DomainFolderNode folderNode = getDestinationFolderNode(destNode);
- if (!isValidDrag(folderNode, draggedNode)) {
+ if (destFolder instanceof LinkedDomainFolder linkedDomainFolder) {
+ try {
+ destFolder = linkedDomainFolder.getRealFolder();
+ }
+ catch (IOException e) {
+ Msg.error(this, "Unable to resolve linked-folder: " + destFolder.getName());
+ return;
+ }
+ }
+
+ if (!isValidDrag(destFolder, draggedNode)) {
return;
}
- DomainFolder destFolder = folderNode.getDomainFolder();
addDraggedTreeNode(destFolder, draggedNode, dropAction, monitor);
}
- private boolean isValidDrag(DomainFolderNode folderNode, GTreeNode draggedNode) {
- if (folderNode == draggedNode) {
- return false;
+ private boolean isValidDrag(DomainFolder destFolder, GTreeNode draggedNode) {
+ // NOTE: We may have issues since checks are not based on canonical paths
+ if (draggedNode instanceof DomainFolderNode folderNode) {
+ // This also checks cases where src/dest projects are using the same repository.
+ // Unfortunately, it will also prevent cases where shared-project folder
+ // does not contain versioned content and could actually be allowed.
+ DomainFolder folder = folderNode.getDomainFolder();
+ return !folder.isSameOrAncestor(destFolder);
}
- if (draggedNode.getParent() == folderNode) {
- return false; // dragging a node onto its parent has no effect
+ if (draggedNode instanceof DomainFileNode fileNode) {
+ DomainFolder folder = fileNode.getDomainFile().getParent();
+ DomainFile file = fileNode.getDomainFile();
+ if (file.isVersioned()) {
+ // This also checks cases where src/dest projects are using the same repository.
+ return !folder.isSame(destFolder);
+ }
+ DomainFile destFile = destFolder.getFile(file.getName());
+ return destFile == null || !destFile.equals(file);
}
-
- if (draggedNode instanceof DomainFolderNode) {
- return !draggedNode.isAncestor(folderNode);
- }
-
- return true;
- }
-
- private DomainFolderNode getDestinationFolderNode(GTreeNode destNode) {
- if (destNode instanceof DomainFolderNode) {
- return (DomainFolderNode) destNode;
- }
- return (DomainFolderNode) destNode.getParent();
+ return false;
}
private void addDraggedTreeNode(DomainFolder destFolder, GTreeNode data, int dropAction,
@@ -178,8 +190,8 @@ public final class LocalTreeNodeHandler
}
catch (DuplicateFileException dfe) {
Msg.showError(this, dataTree, "Error Moving Folder",
- "Destination folder already contains a folder named \"" + sourceFolder.getName() +
- "\"");
+ "Destination folder already contains a folder or folder-link named \"" +
+ sourceFolder.getName() + "\"");
}
catch (FileInUseException fiue) {
String message = fiue.getMessage();
@@ -201,13 +213,13 @@ public final class LocalTreeNodeHandler
private class CopyAllTask extends Task {
private List toCopy;
- private GTreeNode destination;
+ private DomainFolder destFolder;
private int dropAction;
- CopyAllTask(List toCopy, GTreeNode destination, int dropAction) {
+ CopyAllTask(List toCopy, DomainFolder destFolder, int dropAction) {
super("Copy Files", true, true, true);
this.toCopy = toCopy;
- this.destination = destination;
+ this.destFolder = destFolder;
this.dropAction = dropAction;
}
@@ -226,7 +238,7 @@ public final class LocalTreeNodeHandler
monitor.setMessage(
"Processing file " + (i + 1) + " of " + size + ": " + copyNode.getName());
- add(destination, copyNode, dropAction, subMonitors[i]);
+ add(destFolder, copyNode, dropAction, subMonitors[i]);
monitor.setProgress(i);
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalVersionInfoHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalVersionInfoHandler.java
index d86f249973..fcfebee170 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalVersionInfoHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalVersionInfoHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -35,8 +35,7 @@ public final class LocalVersionInfoHandler
VersionInfo info = (VersionInfo) obj;
DomainFile file = tool.getProject().getProjectData().getFile(info.getDomainFilePath());
- GetDomainObjectTask task =
- new GetDomainObjectTask(this, file, info.getVersionNumber());
+ GetDomainObjectTask task = new GetDomainObjectTask(this, file, info.getVersionNumber());
tool.execute(task, 250);
DomainObject versionedObj = task.getDomainObject();
if (versionedObj != null) {
@@ -53,7 +52,7 @@ public final class LocalVersionInfoHandler
@Override
public boolean handle(PluginTool tool, DataTree dataTree, GTreeNode destinationNode,
Object transferData, int dropAction) {
- DomainFolder folder = getDomainFolder(destinationNode);
+ DomainFolder folder = DataTree.getRealInternalFolderForNode(destinationNode);
VersionInfo info = (VersionInfo) transferData;
RepositoryAdapter rep = tool.getProject().getProjectData().getRepository();
@@ -77,14 +76,4 @@ public final class LocalVersionInfoHandler
return false;
}
- private DomainFolder getDomainFolder(GTreeNode destinationNode) {
- if (destinationNode instanceof DomainFolderNode) {
- return ((DomainFolderNode) destinationNode).getDomainFolder();
- }
- else if (destinationNode instanceof DomainFileNode) {
- DomainFolderNode parent = (DomainFolderNode) destinationNode.getParent();
- return parent.getDomainFolder();
- }
- return null;
- }
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/PasteFileTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/PasteFileTask.java
index 2fa3a358f3..0c90b77682 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/PasteFileTask.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/PasteFileTask.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -36,7 +36,7 @@ import ghidra.util.task.*;
*/
public class PasteFileTask extends Task {
- private DomainFolderNode destNode;
+ private DomainFolder destFolder;
private List list;
private boolean isCut;
private RepositoryAdapter repository; // null if project is not shared
@@ -46,13 +46,13 @@ public class PasteFileTask extends Task {
/**
* Constructor for PasteFileTask.
*
- * @param destNode destination folder
+ * @param destFolder destination folder
* @param list list of GTreeNodes being pasted
* @param isCut boolean flag, true means source nodes were cut instead of copied.
*/
- public PasteFileTask(DomainFolderNode destNode, List list, boolean isCut) {
+ public PasteFileTask(DomainFolder destFolder, List list, boolean isCut) {
super(list.size() > 1 ? "Paste Files" : "Paste File", true, true, true);
- this.destNode = destNode;
+ this.destFolder = destFolder;
this.list = list;
this.isCut = isCut;
repository = AppInfo.getActiveProject().getRepository();
@@ -70,13 +70,14 @@ public class PasteFileTask extends Task {
for (GTreeNode node : list) {
monitor.checkCancelled();
- if (node instanceof DomainFolderNode) {
+ if (node instanceof DomainFolderNode folderNode) {
monitor.setMessage("Pasting folder");
- pasteFolder(((DomainFolderNode) node).getDomainFolder(), subMonitor);
+ pasteFolder(folderNode.getDomainFolder(), subMonitor);
}
- else if (node instanceof DomainFileNode) {
+ else if (node instanceof DomainFileNode fileNode) {
monitor.setMessage("Pasting file");
- pasteFile(((DomainFileNode) node).getDomainFile(), subMonitor);
+ // NOTE: This may be a link-file
+ pasteFile(fileNode.getDomainFile(), subMonitor);
}
monitor.incrementProgress(1);
@@ -96,10 +97,10 @@ public class PasteFileTask extends Task {
*/
private void pasteFile(DomainFile file, TaskMonitor monitor) {
if (isCut) {
- moveFile(file, destNode.getDomainFolder());
+ moveFile(file, destFolder);
}
else {
- copyFile(file, destNode.getDomainFolder(), monitor);
+ copyFile(file, destFolder, monitor);
}
}
@@ -108,10 +109,10 @@ public class PasteFileTask extends Task {
*/
private void pasteFolder(DomainFolder folder, TaskMonitor monitor) {
if (isCut) {
- moveFolder(folder, destNode.getDomainFolder());
+ moveFolder(folder, destFolder);
}
else {
- copyFolder(folder, destNode.getDomainFolder(), monitor);
+ copyFolder(folder, destFolder, monitor);
}
}
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 cc34358872..a40283cbc8 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
@@ -48,6 +48,33 @@ public class ProjectDataTreePanel extends JPanel {
private static final String EXPANDED_PATHS_SEPARATOR = ":";
private static final int MAX_PROJECT_SIZE_TO_SEARCH = 1000;
+ private static final DomainFileFilter ALL_FILES_NO_EXTERNAL_FOLLOW = new DomainFileFilter() {
+ @Override
+ public boolean accept(DomainFile df) {
+ // Show all files
+ return true;
+ }
+
+ @Override
+ public boolean ignoreBrokenLinks() {
+ // Always show broken links in the main data tree
+ // A link file's status can change based on other changes to project data
+ return false;
+ }
+
+ @Override
+ public boolean ignoreExternalLinks() {
+ // Always show external links, but we do not allow expanding them.
+ return false;
+ }
+
+ @Override
+ public boolean followExternallyLinkedFolders() {
+ // Do not allow expanding external linked-folders.
+ return false;
+ }
+ };
+
private DataTree tree;
private ProjectData projectData;
private GTreeNode root;
@@ -60,21 +87,24 @@ public class ProjectDataTreePanel extends JPanel {
private FrontEndPlugin plugin;
/**
- * Construct an empty panel that is going to be used as the active panel
+ * Construct an empty data tree panel that is going to be used for the active project tree
+ * within the frontend tool.
+ *
* @param plugin front end plugin
*/
public ProjectDataTreePanel(FrontEndPlugin plugin) {
- this(null, true, plugin, null);
+ this(null, true, plugin, ALL_FILES_NO_EXTERNAL_FOLLOW);
}
/**
- * Constructor
+ * Constructor
*
* @param projectName name of project
* @param isActiveProject true if the project is active, and the
* data tree may be modified
* @param plugin front end plugin; will be null if the panel is used in a dialog
- * @param filter optional filter that is used to hide programs from view
+ * @param filter optional filter that is used to hide programs from view. If null is specified
+ * a default filter is employed which shows all domain files and link-files.
*/
public ProjectDataTreePanel(String projectName, boolean isActiveProject, FrontEndPlugin plugin,
DomainFileFilter filter) {
@@ -84,7 +114,8 @@ public class ProjectDataTreePanel extends JPanel {
this.tool = (FrontEndTool) plugin.getTool();
this.plugin = plugin;
}
- this.filter = filter;
+ this.filter =
+ filter != null ? filter : DomainFileFilter.ALL_FILES_NO_EXTERNAL_FOLDERS_FILTER;
create(projectName);
@@ -105,9 +136,8 @@ public class ProjectDataTreePanel extends JPanel {
if (this.projectData == projectData) {
return; // this can happen during setup if listeners get activated
}
-
- if (this.projectData != null) {
- this.projectData.removeDomainFolderChangeListener(changeMgr);
+ if (changeMgr != null) {
+ changeMgr.dispose();
}
this.projectData = projectData;
@@ -117,7 +147,7 @@ public class ProjectDataTreePanel extends JPanel {
oldRoot.dispose();
changeMgr = new ChangeManager(this);
- projectData.addDomainFolderChangeListener(changeMgr);
+
isActiveProject = projectData.getRootFolder().isInWritableProject();
tree.setProjectActive(isActiveProject);
}
@@ -144,6 +174,16 @@ public class ProjectDataTreePanel extends JPanel {
oldRoot.removeAll();
}
+ /**
+ * Generate a list of TreePaths which correspond to a set of {@link DomainFile domain files}.
+ *
+ * NOTE: The {@link DomainFileNode} included in the paths as the last component is not the same
+ * instance as may, or may not, be contained within the tree. This path is intended for
+ * generating a selection only and is not a reflection of the actual tree state.
+ *
+ * @param files set of domain files
+ * @return generated list of file tree paths
+ */
private List getTreePaths(Set files) {
List results = new ArrayList<>();
for (DomainFile file : files) {
@@ -152,8 +192,18 @@ public class ProjectDataTreePanel extends JPanel {
return results;
}
+ /**
+ * Generate a TreePath which corresponds to the specified {@link DomainFile}.
+ *
+ * NOTE: The {@link DomainFileNode} included in the path as the last component is not the same
+ * instance as may, or may not, be contained within the tree. This path is intended for
+ * generating a selection only and is not a reflection of the actual tree state.
+ *
+ * @param domainFile domain file
+ * @return generated file tree path
+ */
private TreePath getTreePath(DomainFile domainFile) {
- DomainFileNode node = new DomainFileNode(domainFile);
+ DomainFileNode node = new DomainFileNode(domainFile, filter);
DomainFolder parent = domainFile.getParent();
if (parent != null) {
return getTreePath(parent).pathByAddingChild(node);
@@ -161,13 +211,37 @@ public class ProjectDataTreePanel extends JPanel {
return new TreePath(node);
}
+ /**
+ * Generate a TreePath which corresponds to the specified {@link DomainFolder}.
+ *
+ * NOTE: The node included in the path as the last component is not the same instance as
+ * may, or may not, be contained within the tree. This path is intended for generating a
+ * selection only and is not a reflection of the actual tree state.
+ *
+ * NOTE: If the specified folder is a linked-folder which corresponds to a link-file
+ * (see {@link DomainFolder#isLinked()}) the returned path will correspond to a
+ * {@link DomainFileNode}, otherwise it will be a {@link DomainFolderNode}.
+ *
+ * @param domainFolder domain folder (may be a linked-folder)
+ * @return generated tree path
+ */
private TreePath getTreePath(DomainFolder domainFolder) {
DomainFolder parent = domainFolder.getParent();
if (parent != null) {
- return getTreePath(parent).pathByAddingChild(new DomainFolderNode(domainFolder, null));
+ if (domainFolder.isLinked()) {
+ // linked-folder: must handle as link-file node
+ DomainFile linkFile = parent.getFile(domainFolder.getName());
+ if (linkFile != null) {
+ return getTreePath(parent)
+ .pathByAddingChild(new DomainFileNode(linkFile, filter));
+ }
+ }
+ else {
+ return getTreePath(parent)
+ .pathByAddingChild(new DomainFolderNode(domainFolder, filter));
+ }
}
return new TreePath(root);
-
}
/**
@@ -187,6 +261,13 @@ public class ProjectDataTreePanel extends JPanel {
tree.expandAndSelectPaths(treePaths);
}
+ /**
+ * Select the specified domainFile if it exists in the tree.
+ *
+ * NOTE: The selection is performed in a delayed non-blocking fashion.
+ *
+ * @param domainFile domain file
+ */
public void selectDomainFile(DomainFile domainFile) {
if (domainFile != null) {
selectDomainFiles(Set.of(domainFile));
@@ -221,10 +302,7 @@ public class ProjectDataTreePanel extends JPanel {
*/
public DomainFolder getSelectedDomainFolder() {
GTreeNode node = tree.getLastSelectedPathComponent();
- if (node instanceof DomainFolderNode) {
- return ((DomainFolderNode) node).getDomainFolder();
- }
- return null;
+ return DataTree.getRealInternalFolderForNode(node);
}
/**
@@ -285,8 +363,9 @@ public class ProjectDataTreePanel extends JPanel {
}
public void dispose() {
- if (projectData != null) {
- projectData.removeDomainFolderChangeListener(changeMgr);
+ if (changeMgr != null) {
+ changeMgr.dispose();
+ changeMgr = null;
}
tree.dispose();
}
@@ -318,11 +397,12 @@ public class ProjectDataTreePanel extends JPanel {
for (TreePath treePath : selectionPaths) {
GTreeNode node = (GTreeNode) treePath.getLastPathComponent();
- if (node instanceof DomainFolderNode) {
- domainFolderList.add(((DomainFolderNode) node).getDomainFolder());
+ if (node instanceof DomainFolderNode folderNode) {
+ domainFolderList.add(folderNode.getDomainFolder());
}
- else if (node instanceof DomainFileNode) {
- domainFileList.add(((DomainFileNode) node).getDomainFile());
+ else if (node instanceof DomainFileNode fileNode) {
+ // NOTE: File may be a linked-folder. Treatment as folder or file depends on action
+ domainFileList.add(fileNode.getDomainFile());
}
}
@@ -406,6 +486,7 @@ public class ProjectDataTreePanel extends JPanel {
private GTreeNode findFolderNodeChild(GTreeNode node, String text) {
List children = node.getChildren();
+ // NOTE: Does not traverse link-files which may have children
for (GTreeNode child : children) {
if ((child instanceof DomainFolderNode) && child.getName().equals(text)) {
return child;
@@ -435,7 +516,7 @@ public class ProjectDataTreePanel extends JPanel {
tree.setProjectActive(isActiveProject);
}
- void domainChange() {
+ void contextChanged() {
if (plugin == null) {
return;
}
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 964519907b..26b05de100 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -64,7 +64,7 @@ public abstract class VersionControlTask extends Task {
VersionControlDialog vcDialog = new VersionControlDialog(addToVersionControl);
vcDialog.setCurrentFileName(file.getName());
vcDialog.setMultiFiles(list.size() > 1);
- if (file.isLinkFile()) {
+ if (file.isLink()) {
vcDialog.setKeepCheckboxEnabled(false, false, "Link file may not be Checked Out");
}
else {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java
index 5e2ac411c8..61ba49e411 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,8 +22,7 @@ import java.util.Set;
import docking.widgets.OptionDialog;
import docking.widgets.OptionDialogBuilder;
-import ghidra.framework.model.DomainFile;
-import ghidra.framework.model.DomainFolder;
+import ghidra.framework.model.*;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
@@ -105,6 +104,22 @@ public class DeleteProjectFilesTask extends Task {
private void deleteFolder(DomainFolder folder, TaskMonitor monitor) throws CancelledException {
+ while (folder instanceof LinkedDomainFolder linkedFolder) {
+ if (linkedFolder.isLinked()) {
+ throw new IllegalArgumentException(
+ "Linked-folder's originating file-link should have been removed instead: " +
+ linkedFolder.getPathname());
+ }
+ try {
+ folder = linkedFolder.getRealFolder();
+ }
+ catch (IOException e) {
+ Msg.error(this, "Error following linked-folder: " + e.getMessage() + "\n" +
+ folder.getPathname());
+ return;
+ }
+ }
+
for (DomainFolder subFolder : folder.getFolders()) {
monitor.checkCancelled();
if (!selectedFolders.contains(subFolder)) {
@@ -204,10 +219,10 @@ public class DeleteProjectFilesTask extends Task {
}
String msg =
- "The file \"" + file.getName() + "\" is a versioned file and if you continue, \n" +
+ "The file \"" + file.getName() + "\" is a versioned file and if you continue\n" +
"it (and all its versions) will be PERMANENTLY deleted!\n" +
- "If this is a shared project, it will be deleted on the server (if permitted)\n" +
- "for ALL users (if permitted)!" + "\nAre you sure you want to delete it?";
+ "If this is a shared project, it will be deleted on the server\n" +
+ "for ALL users (if permitted)!" + "\n\nAre you sure you want to delete it?";
versionedDialogBuilder.setMessage(msg);
return versionedDialogBuilder.show(parent);
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FindCheckoutsAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FindCheckoutsAction.java
index 467aeec546..4d31f451d7 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FindCheckoutsAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FindCheckoutsAction.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -26,13 +26,17 @@ import docking.widgets.OptionDialog;
import generic.theme.GIcon;
import ghidra.framework.client.*;
import ghidra.framework.main.datatable.ProjectTreeAction;
-import ghidra.framework.main.datatree.FindCheckoutsDialog;
-import ghidra.framework.main.datatree.FrontEndProjectTreeContext;
-import ghidra.framework.model.DomainFolder;
-import ghidra.framework.model.ProjectData;
+import ghidra.framework.main.datatree.*;
+import ghidra.framework.model.*;
import ghidra.framework.plugintool.Plugin;
import ghidra.util.HelpLocation;
+/**
+ * {@link FindCheckoutsAction} provide the ability to initiate the show checkout status for
+ * files selected within the {@link ProjectDataTreePanel}. Since link-files cannot be checked-out
+ * these files will never show checkouts and do not currently attempt to show checkout information
+ * for a referenced file.
+ */
public class FindCheckoutsAction extends ProjectTreeAction {
private static final Icon FIND_ICON = new GIcon("icon.projectdata.find.checkouts.search");
@@ -54,7 +58,20 @@ public class FindCheckoutsAction extends ProjectTreeAction {
@Override
protected void actionPerformed(FrontEndProjectTreeContext context) {
- DomainFolder domainFolder = context.getSelectedFolders().get(0);
+ DomainFolder domainFolder = null;
+ if (context.getFolderCount() == 1) {
+ domainFolder = context.getSelectedFolders().get(0);
+ }
+ else if (context.getFileCount() == 1) {
+ DomainFile domainFile = context.getSelectedFiles().get(0);
+ LinkFileInfo linkInfo = domainFile.getLinkInfo();
+ if (linkInfo != null && linkInfo.isFolderLink() && !linkInfo.isExternalLink()) {
+ domainFolder = linkInfo.getLinkedFolder();
+ }
+ }
+ if (domainFolder == null) {
+ return;
+ }
ProjectData projectData = domainFolder.getProjectData();
RepositoryAdapter repository = projectData.getRepository();
if (repository != null && !repository.isConnected()) {
@@ -81,14 +98,19 @@ public class FindCheckoutsAction extends ProjectTreeAction {
@Override
protected boolean isEnabledForContext(FrontEndProjectTreeContext context) {
- if (context.isReadOnlyProject()) {
+ if (context.isReadOnlyProject() || !context.hasExactlyOneFileOrFolder()) {
return false;
}
- return context.getFolderCount() == 1;
+ if (context.getFolderCount() == 1) {
+ return true;
+ }
+ // Only allow a local folder-link to be treated as a folder
+ DomainFile file = context.getSelectedFiles().get(0);
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ return linkInfo != null && linkInfo.isFolderLink() && !linkInfo.isExternalLink();
}
private void findCheckouts(DomainFolder folder, Component comp) {
-
FindCheckoutsDialog dialog = new FindCheckoutsDialog(plugin, folder);
plugin.getTool().showDialog(dialog, comp);
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCollapseAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCollapseAction.java
index 1e287cf7b1..6b2d0866bb 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCollapseAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCollapseAction.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -21,7 +21,7 @@ import docking.action.ContextSpecificAction;
import docking.action.MenuData;
import docking.widgets.tree.GTreeNode;
import ghidra.framework.main.datatable.ProjectTreeContext;
-import ghidra.framework.main.datatree.DataTree;
+import ghidra.framework.main.datatree.*;
public class ProjectDataCollapseAction
extends ContextSpecificAction {
@@ -39,14 +39,20 @@ public class ProjectDataCollapseAction
collapse(tree, paths[0]);
}
- @Override
- public boolean isAddToPopup(T context) {
- return context.getFolderCount() == 1 && context.getFileCount() == 0;
- }
-
@Override
protected boolean isEnabledForContext(T context) {
- return context.getFolderCount() == 1 && context.getFileCount() == 0;
+ if (!context.hasExactlyOneFileOrFolder()) {
+ return false;
+ }
+ TreePath[] paths = context.getSelectionPaths();
+ GTreeNode node = (GTreeNode) paths[0].getLastPathComponent();
+ if (node instanceof DomainFolderNode folderNode) {
+ return folderNode.isLoaded();
+ }
+ if (node instanceof DomainFileNode fileNode) {
+ return fileNode.isFolderLink() && !fileNode.isLeaf() && fileNode.isLoaded();
+ }
+ return false;
}
/**
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 092b89fe45..e705ef56f8 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -23,8 +23,9 @@ import javax.swing.tree.TreePath;
import docking.action.KeyBindingData;
import docking.action.MenuData;
import generic.theme.GIcon;
-import ghidra.framework.main.datatree.DataTreeClipboardUtils;
-import ghidra.framework.main.datatree.FrontEndProjectTreeContext;
+import ghidra.framework.main.AppInfo;
+import ghidra.framework.main.datatree.*;
+import ghidra.framework.model.Project;
import ghidra.util.HelpLocation;
public class ProjectDataCopyAction extends ProjectDataCopyCutBaseAction {
@@ -50,7 +51,10 @@ public class ProjectDataCopyAction extends ProjectDataCopyCutBaseAction {
if (!context.hasOneOrMoreFilesAndFolders()) {
return false;
}
-
+ Project activeProject = AppInfo.getActiveProject();
+ if (activeProject == null || !context.isInActiveProject()) {
+ return true;
+ }
return !context.containsRootFolder();
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCutAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCutAction.java
index 29f1a5c36e..61106dc85f 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCutAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCutAction.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -25,6 +25,7 @@ import docking.action.MenuData;
import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
import ghidra.framework.main.datatree.*;
+import ghidra.framework.model.DomainFolder;
import ghidra.util.HelpLocation;
public class ProjectDataCutAction extends ProjectDataCopyCutBaseAction {
@@ -48,15 +49,31 @@ public class ProjectDataCutAction extends ProjectDataCopyCutBaseAction {
@Override
protected boolean isEnabledForContext(FrontEndProjectTreeContext context) {
- if (!context.hasOneOrMoreFilesAndFolders()) {
+ if (!context.isInActiveProject() || !context.hasOneOrMoreFilesAndFolders()) {
return false;
}
+ return !context.containsRootFolder() && canMarkNodesCut(context.getSelectionPaths());
+ }
- if (!context.isInActiveProject()) {
- return false;
+ private boolean canMarkNodesCut(TreePath[] paths) {
+ for (TreePath treePath : paths) {
+ GTreeNode node = (GTreeNode) treePath.getLastPathComponent();
+ DomainFolder folder;
+ if (node instanceof DomainFileNode fileNode) {
+ folder = fileNode.getDomainFile().getParent();
+ }
+ else if (node instanceof DomainFolderNode folderNode) {
+ folder = folderNode.getDomainFolder();
+ }
+ else {
+ return false;
+ }
+ if (!folder.isInWritableProject()) {
+ // linked content may reside within a read-only project view
+ return false;
+ }
}
-
- return !context.containsRootFolder();
+ return true;
}
private void markNodesCut(TreePath[] paths) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java
index d29431c3c8..4aa8698fbd 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java
@@ -92,8 +92,9 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
if (fileCount == 1) {
if (!selectedFiles.isEmpty()) {
DomainFile file = CollectionUtils.any(selectedFiles);
- return "Are you sure you want to permanently delete \"" +
- HTMLUtilities.escapeHTML(file.getName()) + "\"?";
+ String type = file.isLink() ? "link" : "file";
+ return "Are you sure you want to permanently delete " + type +
+ " \"" + HTMLUtilities.escapeHTML(file.getName()) + "\"?";
}
// only folders are selected, but they contain files
@@ -108,6 +109,7 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction {
@Override
protected boolean isEnabledForContext(ProjectDataContext context) {
+ // NOTE: Folder-links are treated as files
if (!context.hasOneOrMoreFilesAndFolders()) {
return false;
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataExpandAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataExpandAction.java
index b824356c9f..4d42d9bc11 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataExpandAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataExpandAction.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -21,7 +21,7 @@ import docking.action.ContextSpecificAction;
import docking.action.MenuData;
import docking.widgets.tree.GTreeNode;
import ghidra.framework.main.datatable.ProjectTreeContext;
-import ghidra.framework.main.datatree.DataTree;
+import ghidra.framework.main.datatree.*;
public class ProjectDataExpandAction
extends ContextSpecificAction {
@@ -39,14 +39,20 @@ public class ProjectDataExpandAction
expand(tree, paths[0]);
}
- @Override
- public boolean isAddToPopup(T context) {
- return context.getFolderCount() == 1 && context.getFileCount() == 0;
- }
-
@Override
protected boolean isEnabledForContext(T context) {
- return context.getFolderCount() == 1 && context.getFileCount() == 0;
+ if (!context.hasExactlyOneFileOrFolder()) {
+ return false;
+ }
+ TreePath[] paths = context.getSelectionPaths();
+ GTreeNode node = (GTreeNode) paths[0].getLastPathComponent();
+ if (node instanceof DomainFolderNode) {
+ return true;
+ }
+ if (node instanceof DomainFileNode fileNode) {
+ return fileNode.isFolderLink() && !fileNode.isLeaf();
+ }
+ return false;
}
/**
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 12cd31839a..a56873c35f 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -24,8 +24,7 @@ import docking.action.MenuData;
import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
import ghidra.framework.main.datatable.ProjectTreeContext;
-import ghidra.framework.main.datatree.DataTree;
-import ghidra.framework.main.datatree.DomainFileNode;
+import ghidra.framework.main.datatree.*;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder;
import ghidra.util.InvalidNameException;
@@ -47,11 +46,6 @@ public class ProjectDataNewFolderAction
createNewFolder(context);
}
- @Override
- public boolean isAddToPopup(T context) {
- return (context.getFolderCount() + context.getFileCount()) == 1;
- }
-
@Override
protected boolean isEnabledForContext(T context) {
return getFolder(context).isInWritableProject();
@@ -91,11 +85,15 @@ public class ProjectDataNewFolderAction
private DomainFolder getFolder(T context) {
// the following code relies on the isAddToPopup to ensure that there is exactly one
// file or folder selected
- if (context.getFolderCount() > 0) {
+ if (context.getFolderCount() == 1 && context.getFileCount() == 0) {
return context.getSelectedFolders().get(0);
}
- DomainFile file = context.getSelectedFiles().get(0);
- return file.getParent();
+ if (context.getFileCount() == 1 && context.getFolderCount() == 0) {
+ DomainFile file = context.getSelectedFiles().get(0);
+ return file.getParent();
+ }
+ DomainFolderNode rootNode = (DomainFolderNode) context.getTree().getModelRoot();
+ return rootNode.getDomainFolder();
}
private GTreeNode getParentNode(T context) {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataOpenDefaultToolAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataOpenDefaultToolAction.java
index e1404cb494..355f57f0a2 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataOpenDefaultToolAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataOpenDefaultToolAction.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -29,7 +29,7 @@ public class ProjectDataOpenDefaultToolAction extends FrontendProjectTreeAction
public ProjectDataOpenDefaultToolAction(String owner, String group) {
super("Open File", owner);
- setPopupMenuData(new MenuData(new String[] { "Open in Default Tool" }));
+ setPopupMenuData(new MenuData(new String[] { "Open in Default Tool" }, group));
setKeyBindingData(new KeyBindingData(KeyEvent.VK_ENTER, 0));
markHelpUnnecessary();
}
@@ -37,11 +37,24 @@ public class ProjectDataOpenDefaultToolAction extends FrontendProjectTreeAction
@Override
protected void actionPerformed(ProjectDataContext context) {
List selectedFiles = context.getSelectedFiles();
+ // NOTE: Seems like we should confirm opening more than one file
AppInfo.getActiveProject().getToolServices().launchDefaultTool(selectedFiles);
}
@Override
protected boolean isEnabledForContext(ProjectDataContext context) {
- return context.getSelectedFiles().size() > 0 && context.getSelectedFolders().size() == 0;
+ if (!context.getSelectedFolders().isEmpty()) {
+ return false;
+ }
+ List selectedFiles = context.getSelectedFiles();
+ if (selectedFiles.isEmpty()) {
+ return false;
+ }
+ for (DomainFile file : context.getSelectedFiles()) {
+ if (file.isLink() && file.getLinkInfo().isFolderLink()) {
+ return false;
+ }
+ }
+ return true;
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteAction.java
index e45b44c9a1..4fbd1b720b 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteAction.java
@@ -16,6 +16,7 @@
package ghidra.framework.main.projectdata.actions;
import java.awt.event.InputEvent;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -26,6 +27,8 @@ import docking.action.MenuData;
import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
import ghidra.framework.main.datatree.*;
+import ghidra.framework.model.DomainFolder;
+import ghidra.framework.model.LinkedDomainFolder;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.task.TaskLauncher;
@@ -43,23 +46,20 @@ public class ProjectDataPasteAction extends ProjectDataCopyCutBaseAction {
@Override
protected void actionPerformed(FrontEndProjectTreeContext context) {
GTreeNode node = (GTreeNode) context.getContextObject();
- DomainFolderNode destNode = getFolderForNode(node);
-
- paste(context.getTree(), destNode);
+ DomainFolder destFolder = DataTree.getRealInternalFolderForNode(node);
+ if (destFolder != null) {
+ paste(context.getTree(), destFolder);
+ }
}
@Override
protected boolean isEnabledForContext(FrontEndProjectTreeContext context) {
- if (!context.hasExactlyOneFileOrFolder()) {
- return false;
- }
- if (!context.isInActiveProject()) {
+ if (!context.isInActiveProject() || !context.hasExactlyOneFileOrFolder()) {
return false;
}
GTreeNode node = (GTreeNode) context.getContextObject();
- GTreeNode destNode = getFolderForNode(node);
- return checkNodeForPaste(destNode);
-
+ DomainFolder destFolder = DataTree.getRealInternalFolderForNode(node);
+ return checkNodeForPaste(destFolder);
}
@Override
@@ -70,27 +70,30 @@ public class ProjectDataPasteAction extends ProjectDataCopyCutBaseAction {
return context.isInActiveProject();
}
- private DomainFolderNode getFolderForNode(GTreeNode node) {
- if (node instanceof DomainFolderNode) {
- return (DomainFolderNode) node;
- }
- return (DomainFolderNode) node.getParent();
- }
-
/**
* Check the destination node for whether clipboard data can be pasted there.
+ * Ancestry checks are performed for the node(s) in the clipboard against the
+ * specified destination folder.
*
- * @param destNode destination for paste operation
+ * @param destFolder destination for paste operation
* @return true if least one node can be pasted at destNode
*/
- private boolean checkNodeForPaste(GTreeNode destNode) {
+ static boolean checkNodeForPaste(DomainFolder destFolder) {
+
+ if (destFolder == null || !destFolder.isInWritableProject()) {
+ return false;
+ }
List list = DataTreeClipboardUtils.getDataTreeNodesFromClipboard();
for (GTreeNode node : list) {
- if (!node.isAncestor(destNode)) {
- // at least one node can be pasted from system clipboard
- return true;
+ if (node instanceof DomainFileNode fileNode && !fileNode.isFolderLink()) {
+ return true; // at least one good paste from clipboard
+ }
+ // Check folder-link or folder
+ DomainFolder folder = DataTree.getRealInternalFolderForNode(node);
+ if (folder != null && !folder.isSameOrAncestor(destFolder)) {
+ return true; // at least one good paste from clipboard
}
}
return false;
@@ -99,14 +102,14 @@ public class ProjectDataPasteAction extends ProjectDataCopyCutBaseAction {
/**
* Process a "paste" request from a menu action.
*/
- private void paste(DataTree tree, DomainFolderNode folderNode) {
+ private void paste(DataTree tree, DomainFolder destFolder) {
List list = DataTreeClipboardUtils.getDataTreeNodesFromClipboard();
boolean isCutOperation = isCutOperation(list);
- checkPasteList(tree, folderNode, list, isCutOperation);
+ checkPasteList(tree, destFolder, list, isCutOperation);
if (!list.isEmpty()) {
- PasteFileTask task = new PasteFileTask(folderNode, list, isCutOperation);
+ PasteFileTask task = new PasteFileTask(destFolder, list, isCutOperation);
new TaskLauncher(task, tree, 1000);
}
else {
@@ -121,74 +124,136 @@ public class ProjectDataPasteAction extends ProjectDataCopyCutBaseAction {
* Update the given list of nodes to paste if the corresponding file or
* folder cannot be pasted; remove it from the list and update the
* clipboard with the new list.
- * @param destNode destination node
+ * @param destFolder destination folder
* @param list list of nodes to paste
* @param isCutOperation true if this is a cut vs copy; for cut, files
* cannot be in use
*/
- private void checkPasteList(DataTree tree, GTreeNode destNode, List list,
+ private void checkPasteList(DataTree tree, DomainFolder destFolder, List list,
boolean isCutOperation) {
- if (list == null) {
+ if (list == null || list.isEmpty()) {
return;
}
- boolean listChanged = removeDescendantsFromList(list);
+ removeDescendantsFromList(list);
- boolean resetClipboard = false;
- StringBuffer sb = new StringBuffer();
+ StringBuilder msgBuffer = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
GTreeNode tnode = list.get(i);
- boolean removeNodeFromList = false;
-
- if (tnode.getParent() != null && isCutOperation && !destNode.equals(tnode)) {
- if (destNode == tnode.getParent()) {
- removeNodeFromList = true;
- sb.append(
- "File " + tnode.getName() + " already exists at " + tnode.getParent());
- }
- else if (tnode instanceof DomainFolderNode) {
- if (destNode.isAncestor(tnode)) {
- removeNodeFromList = true;
- }
- }
- }
- else if (tnode.getParent() == null || destNode == tnode) {
- removeNodeFromList = true;
- if (destNode == tnode) {
- sb.append("Cannot paste file to itself: " + destNode.getName());
- }
+ boolean removeNodeFromList = true;
+ if (tnode instanceof DataTreeNode dataTreeNode) {
+ removeNodeFromList =
+ !canCopyNode(dataTreeNode, destFolder, isCutOperation, msgBuffer);
}
if (removeNodeFromList) {
- list.remove(i);
- if (i > 0) {
- --i;
- }
- resetClipboard = true;
- if (tnode.getParent() != null) {
- if (tnode instanceof Cuttable) {
- ((Cuttable) tnode).setIsCut(false);
- }
+ // After removing the current 'tnode' from the list, decrement list index 'i'
+ // to compensate for the loop's index 'i' increment since the next node will
+ // reside at the same index position within the list.
+ list.remove(i--);
+ if (tnode instanceof Cuttable cuttable) {
+ cuttable.setIsCut(false);
}
}
}
- if (resetClipboard || listChanged) {
- if (sb.length() > 0) {
- String title = isCutOperation ? "Cannot Move File(s)" : "Cannot Copy File(s)";
- String action = isCutOperation ? "moved" : "copied";
+ if (msgBuffer.length() > 0) {
+ String title = isCutOperation ? "Cannot Move File(s)" : "Cannot Copy File(s)";
+ String action = isCutOperation ? "moved" : "copied";
+ Msg.showWarn(getClass(), tree, title,
+ "The following content could not be " + action + ":\n" + msgBuffer.toString());
+ }
+ }
- Msg.showWarn(getClass(), tree, title,
- "The following file(s) could not be " + action + ":\n" + sb.toString());
+ private void appendMsg(String msg, StringBuilder msgBuffer) {
+ if (!msg.isEmpty()) {
+ msgBuffer.append("\n");
+ }
+ msgBuffer.append(msg);
+ }
+
+ /**
+ * Determine if the specified node can be copied or moved to the specified destination folder.
+ * @param dataTreeNode copy/cut node
+ * @param destFolder destination folder
+ * @param isCutOperation true if node is being moved to {@code destFolder}
+ * @param msgBuffer error message buffer
+ * @return true if node copy/move is permitted, else false in which case {@code msgBuffer}
+ * may have messages.
+ */
+ private boolean canCopyNode(DataTreeNode dataTreeNode, DomainFolder destFolder,
+ boolean isCutOperation, StringBuilder msgBuffer) {
+ try {
+ String nodeType = (dataTreeNode instanceof DomainFolderNode) ? "Folder" : "File";
+ DomainFolder folder = getRealFolder(dataTreeNode);
+ if (isCutOperation) {
+ if (!folder.isInWritableProject()) {
+ appendMsg("Read-only project. " + nodeType + " '" + dataTreeNode.getName() +
+ "' cannot be moved", msgBuffer);
+ return false;
+ }
+ if (dataTreeNode.getParent() == null) {
+ return false; // ignore root node cut selection
+ }
+ DomainFolder checkFolder =
+ (dataTreeNode instanceof DomainFolderNode) ? folder.getParent() : folder;
+ if (destFolder.equals(checkFolder)) {
+ return false; // ignore move to same location
+ }
+ }
+
+ if (dataTreeNode instanceof DomainFolderNode) {
+ if (folder.isSameOrAncestor(destFolder)) {
+ appendMsg(
+ nodeType + " '" + dataTreeNode.getName() +
+ "' contains destination folder '" + destFolder.getName() + "'",
+ msgBuffer);
+ return false;
+ }
+ if (destFolder.getFolder(folder.getName()) != null) {
+ appendMsg("Folder '" + destFolder.getName() +
+ "' already contains a folder named '" + dataTreeNode.getName() + "'",
+ msgBuffer);
+ return false;
+ }
}
}
+ catch (IOException e) {
+ Msg.warn(this,
+ "Failed to resolve linked item: " + dataTreeNode.getName() + ": " + e.getMessage());
+ appendMsg("Failed to resolve linked item: " + dataTreeNode.getName(), msgBuffer);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@return the real folder which corresponds to a folder node or the parent of a file node}
+ * @param dataTreeNode file or folder data tree node
+ * @throws IOException if a linked-folder IO error occurs
+ */
+ private DomainFolder getRealFolder(DataTreeNode dataTreeNode) throws IOException {
+ DomainFolder folder = null;
+ if (dataTreeNode instanceof DomainFileNode fileNode) {
+ folder = fileNode.getDomainFile().getParent();
+ }
+ else if (dataTreeNode instanceof DomainFolderNode folderNode) {
+ folder = folderNode.getDomainFolder();
+ }
+ if (folder instanceof LinkedDomainFolder linkedFolder) {
+ // need real folder to simplify relationship checks
+ folder = linkedFolder.getRealFolder();
+ }
+ return folder;
}
/**
* Remove descendant nodes from the list; having the parent node
* is enough when folders are getting pasted.
*/
- private boolean removeDescendantsFromList(List list) {
+ private void removeDescendantsFromList(List list) {
+ // NOTE: This needs to be optimized and is not well suited
+ // for a large number of nodes
List newList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
GTreeNode destNode = list.get(i);
@@ -205,7 +270,6 @@ public class ProjectDataPasteAction extends ProjectDataCopyCutBaseAction {
for (int i = 0; i < newList.size(); i++) {
list.remove(newList.get(i));
}
- return newList.size() > 0;
}
private boolean isCutOperation(List list) {
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
index 9fd17d4014..271e4af8e2 100644
--- 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,11 +22,11 @@ import javax.swing.Icon;
import docking.action.MenuData;
import docking.widgets.tree.GTreeNode;
-import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.*;
+import ghidra.framework.main.AppInfo;
import ghidra.framework.main.datatable.ProjectTreeAction;
import ghidra.framework.main.datatree.*;
-import ghidra.framework.model.DomainFile;
-import ghidra.framework.model.DomainFolder;
+import ghidra.framework.model.*;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import resources.Icons;
@@ -35,10 +35,18 @@ import resources.MultiIcon;
public class ProjectDataPasteLinkAction extends ProjectTreeAction {
private static Icon baseIcon = Icons.PASTE_ICON;
- public ProjectDataPasteLinkAction(String owner, String group) {
- super("Paste Link", owner);
- setPopupMenuData(new MenuData(new String[] { "Paste as Link" }, getIcon(), group));
- setHelpLocation(new HelpLocation("FrontEndPlugin", "Create_File_Links"));
+ private boolean relative;
+
+ public ProjectDataPasteLinkAction(String owner, String group, boolean relative) {
+ super("Paste " + getLinkType(relative), owner);
+ this.relative = relative;
+ setPopupMenuData(
+ new MenuData(new String[] { "Paste as " + getLinkType(relative) }, getIcon(), group));
+ setHelpLocation(new HelpLocation("FrontEndPlugin", "Paste_Link"));
+ }
+
+ private static String getLinkType(boolean relative) {
+ return relative ? "Relative-Link" : "Link";
}
private static Icon getIcon() {
@@ -49,18 +57,22 @@ public class ProjectDataPasteLinkAction extends ProjectTreeAction {
@Override
protected void actionPerformed(FrontEndProjectTreeContext context) {
- GTreeNode node = (GTreeNode) context.getContextObject();
- DomainFolderNode destNode = getFolderForNode(node);
if (!isEnabledForContext(context)) {
+ return;
+ }
+
+ GTreeNode node = (GTreeNode) context.getContextObject();
+ DomainFolder destFolder = DataTree.getRealInternalFolderForNode(node);
+ if (destFolder == null) {
Msg.showWarn(getClass(), context.getTree(), "Unsupported Operation",
"Unsupported paste link condition");
}
GTreeNode copyNode = getFolderOrFileCopyNode();
- if (copyNode instanceof DomainFileNode) {
+ if (copyNode instanceof DomainFileNode fileNode) {
try {
- DomainFile domainFile = ((DomainFileNode) copyNode).getDomainFile();
- domainFile.copyToAsLink(destNode.getDomainFolder());
+ DomainFile domainFile = fileNode.getDomainFile();
+ domainFile.copyToAsLink(destFolder, relative);
}
catch (IOException e) {
Msg.showError(getClass(), context.getTree(), "Cannot Create Link",
@@ -70,7 +82,7 @@ public class ProjectDataPasteLinkAction extends ProjectTreeAction {
else {
try {
DomainFolder domainFolder = ((DomainFolderNode) copyNode).getDomainFolder();
- domainFolder.copyToAsLink(destNode.getDomainFolder());
+ domainFolder.copyToAsLink(destFolder, relative);
}
catch (IOException e) {
Msg.showError(getClass(), context.getTree(), "Cannot Create Link",
@@ -82,65 +94,52 @@ public class ProjectDataPasteLinkAction extends ProjectTreeAction {
@Override
protected boolean isEnabledForContext(FrontEndProjectTreeContext context) {
- if (!context.hasExactlyOneFileOrFolder()) {
- return false;
- }
- if (!context.isInActiveProject()) {
+ if (!context.isInActiveProject() || !context.hasExactlyOneFileOrFolder()) {
return false;
}
GTreeNode node = (GTreeNode) context.getContextObject();
- DomainFolderNode destNode = getFolderForNode(node);
-
- GTreeNode copyNode = getFolderOrFileCopyNode();
- if (copyNode == null || copyNode.getParent() == null) {
+ DomainFolder destFolder = DataTree.getRealInternalFolderForNode(node);
+ if (!ProjectDataPasteAction.checkNodeForPaste(destFolder)) {
return false;
}
-
- // local internal linking not supported
- if (destNode.getRoot() == copyNode.getRoot()) {
- return false;
+ Project activeProject = AppInfo.getActiveProject();
+ DataTreeNode copyNode = getFolderOrFileCopyNode();
+ if (copyNode != null) {
+ if (relative && copyNode.getProjectData() != activeProject.getProjectData()) {
+ return false;
+ }
+ if (copyNode instanceof DomainFileNode fileNode) {
+ // Only enable action if a LinkHandler exists for the file
+ DomainFile domainFile = fileNode.getDomainFile();
+ try {
+ ContentHandler> contentHandler =
+ DomainObjectAdapter.getContentHandler(domainFile.getContentType());
+ return contentHandler.getLinkHandler() != null;
+ }
+ catch (IOException e) {
+ return false;
+ }
+ }
+ return true;
}
-
- if (copyNode instanceof DomainFileNode) {
- DomainFile df = ((DomainFileNode) copyNode).getDomainFile();
- return df.isLinkingSupported();
- }
- return true;
+ return false;
}
- @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() {
+ private DataTreeNode getFolderOrFileCopyNode() {
+ // Null will be returned if single node is marked for cut operation
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 DomainFileNode fileNode) {
+ if (!fileNode.isCut()) {
+ return fileNode;
}
}
- if (copyNode instanceof DomainFolderNode) {
- if (!((DomainFolderNode) copyNode).isCut()) {
- return copyNode;
+ if (copyNode instanceof DomainFolderNode folderNode) {
+ if (!folderNode.isCut()) {
+ return folderNode;
}
}
return null;
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataRefreshAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataRefreshAction.java
index 9c5a0ecc53..7364eedefb 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataRefreshAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataRefreshAction.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -42,6 +42,11 @@ public class ProjectDataRefreshAction extends FrontendProjectTreeAction {
markHelpUnnecessary();
}
+ @Override
+ protected boolean isEnabledForContext(ProjectDataContext context) {
+ return context.hasOneOrMoreFilesAndFolders();
+ }
+
@Override
protected void actionPerformed(ProjectDataContext context) {
refresh(context.getProjectData(), context.getComponent());
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataSelectAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataSelectAction.java
index 5190105dbd..54941d9576 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataSelectAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataSelectAction.java
@@ -15,6 +15,7 @@
*/
package ghidra.framework.main.projectdata.actions;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -23,9 +24,11 @@ import javax.swing.tree.TreePath;
import docking.action.MenuData;
import docking.widgets.tree.GTreeNode;
import docking.widgets.tree.tasks.GTreeExpandAllTask;
+import ghidra.framework.data.LinkHandler;
import ghidra.framework.main.datatable.ProjectTreeAction;
-import ghidra.framework.main.datatree.DataTree;
-import ghidra.framework.main.datatree.FrontEndProjectTreeContext;
+import ghidra.framework.main.datatree.*;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.store.FileSystem;
public class ProjectDataSelectAction extends ProjectTreeAction {
@@ -40,12 +43,42 @@ public class ProjectDataSelectAction extends ProjectTreeAction {
DataTree tree = context.getTree();
TreePath[] paths = context.getSelectionPaths();
GTreeNode node = (GTreeNode) paths[0].getLastPathComponent();
+
selectAllChildren(tree, node);
}
@Override
public boolean isAddToPopup(FrontEndProjectTreeContext context) {
- return context.getFolderCount() == 1 && context.getFileCount() == 0;
+ if (!context.hasExactlyOneFileOrFolder()) {
+ return false;
+ }
+ if (context.getFolderCount() == 1) {
+ return true;
+ }
+ DomainFile folderLinkFile = context.getSelectedFiles().get(0);
+ return canTraverseFolderLinkFile(folderLinkFile);
+ }
+
+ private static boolean canTraverseFolderLinkFile(DomainFile file) {
+ if (file.isLink() && file.getLinkInfo().isFolderLink()) {
+ // Prevent selection of folder-link which is contained within referenced link-path.
+ // Cycle prevention in tree should prevent this from being an issue
+ String filePath = file.getPathname() + FileSystem.SEPARATOR;
+ String linkPath;
+ try {
+ linkPath = LinkHandler.getAbsoluteLinkPath(file);
+ if (!linkPath.endsWith(FileSystem.SEPARATOR)) {
+ linkPath += FileSystem.SEPARATOR;
+ }
+ if (!filePath.startsWith(linkPath)) {
+ return true;
+ }
+ }
+ catch (IOException e) {
+ // ignore
+ }
+ }
+ return false;
}
/**
@@ -70,9 +103,30 @@ public class ProjectDataSelectAction extends ProjectTreeAction {
* Select all descendants starting at node.
*/
private void getAllTreePaths(GTreeNode node, List paths) {
- paths.add(node.getTreePath());
+
+ // Origin node is intentionally not included in selection since the origin node
+ // is not a child of itself. Including the root node can present problems as well.
+
List children = node.getChildren();
for (GTreeNode child : children) {
+ // Limit recursion through folder-links which may be self-referencing
+ if (child instanceof DomainFileNode fileNode) {
+
+ if (fileNode.isLeaf()) {
+ // add individual child
+ paths.add(child.getTreePath());
+ continue;
+ }
+
+ // We should only get here is file is a internal folder link
+ // which needs to be checked for possible circular ancestry issue
+ if (!canTraverseFolderLinkFile(fileNode.getDomainFile())) {
+ continue;
+ }
+ }
+
+ // recurse and add child with its children
+ paths.add(child.getTreePath());
getAllTreePaths(child, paths);
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlViewCheckOutAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlViewCheckOutAction.java
index 38549f0d66..603c47429e 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlViewCheckOutAction.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlViewCheckOutAction.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -61,7 +61,8 @@ public class VersionControlViewCheckOutAction extends VersionControlAction {
}
DomainFile domainFile = domainFiles.get(0);
- return domainFile.isVersioned();
+ // Link files do not support checkout
+ return !domainFile.isLink() && domainFile.isVersioned();
}
/**
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DefaultDomainFileFilter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DefaultDomainFileFilter.java
new file mode 100644
index 0000000000..f00ce34b97
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DefaultDomainFileFilter.java
@@ -0,0 +1,58 @@
+/* ###
+ * 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;
+
+/**
+ * {@link DefaultDomainFileFilter} provides a simple default domain file filter which accepts
+ * files for a specified domain object interface class.
+ */
+public class DefaultDomainFileFilter implements DomainFileFilter {
+
+ private final Class extends DomainObject> domainObjectClass;
+ private final boolean ignoreExternalLinks;
+
+ /**
+ * Construct a {@link DomainFileFilter} which accepts a specific domain object interface
+ * class and either shows or hides external links. If external links are not ignored
+ * the filter will allow following external folder-links into other projects or server
+ * repositories. Note that this should be enabled carefully since it may required
+ * proper repository authentication support to facilitate access.
+ * Broken links are always ignored and all internal linked-folders and linked-files will be
+ * followed/processed.
+ *
+ * @param domainObjectClass domain object interface class. May be null to disallow all files
+ * (i.e., only folders and folder-links are shown).
+ * @param ignoreExternalLinks true to ignore/skip external links, else they will be
+ * shown/processed and opening/following such links will be supported.
+ */
+ public DefaultDomainFileFilter(Class extends DomainObject> domainObjectClass,
+ boolean ignoreExternalLinks) {
+ this.domainObjectClass = domainObjectClass;
+ this.ignoreExternalLinks = ignoreExternalLinks;
+ }
+
+ @Override
+ public boolean accept(DomainFile file) {
+ return domainObjectClass != null &&
+ domainObjectClass.isAssignableFrom(file.getDomainObjectClass());
+ }
+
+ @Override
+ public boolean ignoreExternalLinks() {
+ return ignoreExternalLinks;
+ }
+
+}
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 b9fdae05f8..4b14f99c83 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -31,10 +31,57 @@ import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
/**
- * DomainFile provides a storage interface for project files. A
- * DomainFile is an immutable reference to file contained within a project. The state
- * of a DomainFile object does not track name/parent changes made to the referenced
- * project file.
+ * {@link DomainFile} provides a storage interface for a project file. A domain file
+ * provides an immutable reference to a stored file contained within a project. The state
+ * of a file object does not track name/parent changes made to the referenced project file.
+ * An up-to-date object may be obtained from {@link ProjectData#getFile(String)},
+ * {@link ProjectData#getFileByID(String)}, or may be returned by any method used to move or rename
+ * it. The project data object for the active
+ * {@link Project} may be obtained via {@link Project#getProjectData()}.
+ *
+ * Link Files
+ *
+ * Link files may exist or be created within a project where the methods {@link #isLink()} and
+ * {@link #getLinkInfo()} may be used to obtain more details about a link and in the case of a
+ * linked-folder can facilitate obtainining the referenced {@link LinkedGhidraFolder}. This
+ * information object can also be used to determine if the referenced file or folder is external
+ * to this file's project.
+ *
+ * A link-file can become "broken" if its reference has one of the following conditions
+ * occur:
+ *
+ * - A referenced internal file or folder does not exist, or
+ * - the nature/content-type of the referenced file does not match the designated type when the
+ * link was created, or
+ * - the link has a circular reference path within this file's project.
+ *
+ *
+ * The method {@link LinkFileInfo#getLinkStatus(java.util.function.Consumer)} may be used to
+ * determine if a link is "broken". Use of a broken link may result in an IOException or other
+ * failure. The domain object for a file-link (e.g., ProgramLink) may be obtained in the same
+ * manner as a normal file (e.g., {@link #getDomainObject(Object, boolean, boolean, TaskMonitor)}.
+ * However, as with any file it is recommended that {@link #getDomainObjectClass()} first be used
+ * to ensure the file corresponds to the expected/supported content type.
+ *
+ * NOTE: Using external links to shared projects or
+ * repositories may result in required authentication; which in headless situations may be
+ * limited by the active authentication handler (see {@link LinkFileInfo#isExternalLink()} and
+ * {@link LinkFileInfo#getLinkStatus(java.util.function.Consumer)} for more details).
+ *
+ * Link files can facilitate a link to either a folder or another file of a specific content type
+ * within a Ghidra project. Here's why someone might choose to use them:
+ *
+ * - File Organization: links allow users to organize files and folders in a way that makes
+ * sense for their workflow without duplicating data. A single file can appear to exist in multiple
+ * locations without taking up additional space.
+ * - Dynamic Updates: If the original file or folder is modified, the changes are automatically
+ * reflected wherever the link is used, ensuring consistency without manual updates.
+ * - Shared Resources: links can be used to establish shortcuts to files stored in different
+ * repositories, projects or directories, enabling easy access without navigating deeply nested folder
+ * structures or replicating stored data.
+ * - System Configuration: links can be used to link different versions of programs or libraries
+ * without changing paths.
+ *
*/
public interface DomainFile extends Comparable {
@@ -575,20 +622,21 @@ public interface DomainFile extends Comparable {
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.
+ * Copy this file into the newParent folder as a file-link. A file-link references another
+ * file without actually copying all of its content. If this file is associated with a
+ * temporary transient project (i.e., not a locally managed project) the generated link will
+ * refer to the this file with a Ghidra URL. If this file is contained within the
+ * same active {@link ProjectData} instance as {@code newParent} an internal link reference
+ * will be made.
+ *
* @param newParent new parent folder
+ * @param relative if true, and this file is contained within the same active
+ * {@link ProjectData} instance as {@code newParent}, an internal-project relative path
+ * file-link will be created.
* @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;
+ public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException;
/**
* Determine if this file's content type supports linking.
@@ -650,25 +698,35 @@ public interface DomainFile extends Comparable {
public long length() throws IOException;
/**
- * Determine if this file is a link file which corresponds to either a file or folder link.
+ * Determine if this file is a link-file which corresponds to either a file or folder link.
+ * See {@link #getLinkInfo()} to obtain link information.
+ *
+ * If the link-file is a {@link LinkFileInfo#isFolderLink() folder-link} the method
+ * {@link LinkFileInfo#getLinkedFolder()} can be used to get the linked domain folder where the
+ * resulting folder's {@link DomainFolder#isLinked()} indicates that it was the result of
+ * following a folder-link.
+ *
+ * The associated link path/URL may be obtained with {@link LinkFileInfo#getLinkPath()}.
+ *
+ * The content type (see {@link #getContentType()} of a link-file will differ from that of the
+ * linked object (e.g., "LinkedProgram" vs "Program"). It is highly recommended that the
+ * {@link #getDomainObjectClass()} method be used instead since it will return the same value
+ * for a normal file or link-file that corresponds to the same {@link DomainObject} implementation.
+ *
* 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").
+ * to obtain a read-only instance. These methods should not be used on a folder-link since
+ * an {@link UnsupportedOperationException} will be thrown.
+ *
* @return true if link file else false for a normal domain file
*/
- public boolean isLinkFile();
+ public boolean isLink();
/**
- * If this is a folder-link file get the corresponding linked folder.
- * @return a linked domain folder or null if not a folder-link.
+ * If this file is a {@link #isLink() link-file} the link information will be returned.
+ * @return link information or null if this is not a link-file
*/
- public DomainFolder followLink();
+ public LinkFileInfo getLinkInfo();
}
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 9ceb871e8f..303bef66f7 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,28 +15,131 @@
*/
package ghidra.framework.model;
-/**
- * Interface to indicate whether a domain file should be included in a list or
- * set of domain files.
- */
-public interface DomainFileFilter {
+import ghidra.framework.data.LinkHandler;
- /**
- * Tests whether or not the specified domain file should be
- * included in a domain file list.
- *
- * @param df The domain file to be tested
- * @return true if and only if df
- *
- */
- public boolean accept(DomainFile df);
+/**
+ * {@link DomainFileFilter} interface to indicate whether a domain file should be included in a
+ * list or set of domain files. This interface extends {@link DomainFolderFilter} which also
+ * controls the following of linked-folders.
+ *
+ * Without specific overrides the default behavior:
+ *
+ * - {@link #ignoreBrokenLinks()} (true) Ignores all broken links
+ * - {@link #ignoreExternalLinks()} (true) Ignores all external links
+ * - {@link #ignoreFolderLinks()} (false) Will follow folder-links
+ * - {@link #followExternallyLinkedFolders()} Based on
+ * NOT-{@link #ignoreExternalLinks()} AND NOT-{@link #ignoreFolderLinks()}
+ *
+ *
+ * The specific handling of link-files is determined by the consumer of this filter.
+ */
+public interface DomainFileFilter extends DomainFolderFilter {
/**
- * 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.
+ * File filter which accepts all files, including all external file-links,
+ * and allows opening/expanding of external folder-links. All broken links are ignored.
*/
- public default boolean followLinkedFolders() {
- return true;
+ DomainFileFilter ALL_FILES_FILTER = new DomainFileFilter() {
+ @Override
+ public boolean accept(DomainFile df) {
+ return true;
+ }
+
+ @Override
+ public boolean ignoreExternalLinks() {
+ return false;
+ }
+ };
+
+ /**
+ * File filter which accepts all files, including all external file-links,
+ * but does not allow opening/expanding of external folder-links. All broken links are ignored.
+ */
+ DomainFileFilter ALL_FILES_NO_EXTERNAL_FOLDERS_FILTER = new DomainFileFilter() {
+ @Override
+ public boolean accept(DomainFile df) {
+ return true;
+ }
+
+ @Override
+ public boolean ignoreExternalLinks() {
+ return false;
+ }
+
+ @Override
+ public boolean followExternallyLinkedFolders() {
+ return false;
+ }
+ };
+
+ /**
+ * File filter which allows all internal folders and files.
+ * All external and broken links are ignored. This filter is useful when
+ * selecting a file with an arbitrary content type. If targeting a specific file content
+ * type the use of {@link DefaultDomainFileFilter} may be preferred.
+ */
+ DomainFileFilter ALL_INTERNAL_FILES_FILTER = new DomainFileFilter() {
+ @Override
+ public boolean accept(DomainFile df) {
+ return true;
+ }
+ };
+
+ /**
+ * File filter which allows all non-linked internal folders and files.
+ * All links are ignored. This filter is useful if code does not handle some of the
+ * implications of following links such as:
+ *
+ * - External repository authentication
+ * - Processing the same project content more than once or lack of support for link-files
+ *
+ * If targeting a specific file content type the use of {@link DefaultDomainFileFilter} may
+ * be preferred.
+ */
+ public static DomainFileFilter NON_LINKED_FILE_FILTER = new DomainFileFilter() {
+
+ @Override
+ public boolean accept(DomainFile df) {
+ // Accept all domain files which are not a link-file.
+ // Processing of link-files may result in the same file being returned by the
+ // iterator more than once.
+ return !df.isLink();
+ }
+
+ @Override
+ public boolean ignoreFolderLinks() {
+ return true;
+ }
+ };
+
+ /**
+ * Tests whether or not the specified domain file should be included in a domain file list.
+ * Since link-files will also be subject to this constraint the ability to handle or follow
+ * such links must be considered.
+ *
+ * NOTE: File-links have the same {@link DomainFile#getDomainObjectClass()} as the file they
+ * refer to, while their {@link DomainFile#getContentType()} is specific to their
+ * {@link LinkHandler} implementation.
+ *
+ * @param df The domain file to be tested
+ * @return true if and only if df
+ */
+ public boolean accept(DomainFile df);
+
+ /**
+ * Check if the children of an externally-linked folder should be loaded/processed.
+ *
+ * If this method is not implemented the value returned is
+ * NOT-{@link #ignoreExternalLinks()} AND NOT-{@link #ignoreFolderLinks()}.
+ *
+ * NOTE: Following an external link utilizes the application's active project to retain
+ * and external project as one of it's viewed-projects. In the process of accessing a
+ * viewed-project the user may be required to authenticate to a remote server.
+ *
+ * @return true if children of an externally-linked folder should be traversed or displayed
+ * (subject to a successful connection to the referenced project or server-based repository).
+ */
+ public default boolean followExternallyLinkedFolders() {
+ return !ignoreExternalLinks() && !ignoreFolderLinks();
}
}
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 c68de7fe3e..1196cabe2b 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,19 +22,70 @@ import java.net.URL;
import javax.swing.Icon;
import generic.theme.GIcon;
+import ghidra.framework.data.LinkHandler;
import ghidra.framework.store.FolderNotEmptyException;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
/**
- * DomainFolder provides a storage interface for project folders. A
- * DomainFolder is an immutable reference to a folder contained within a project. The
- * state of a DomainFolder object does not track name/parent changes made to the
- * referenced project folder.
+ * {@link DomainFolder} provides a storage interface for a project folder. A
+ * domain folder is an immutable reference to a folder contained within a project. Provided the
+ * corresponding path exists within the project it may continue to be used to create and access
+ * its files and sub-folders. The state of a folder object does not track name/parent changes made
+ * to the referenced project file. An up-to-date instance may be obtained from
+ * {@link ProjectData#getFolder(String)} or may be returned by any method used to move or rename it.
+ * The project data object for the active {@link Project} may be obtained via
+ * {@link Project#getProjectData()}.
+ *
+ * Link Files
+ *
+ * Link files may exist or be created within a folder. See {@link DomainFile} for more information.
+ *
+ * Obtaining the folder which corresponds to a linked-folder is done indirectly via a link file.
+ * There are different ways to arrive on a linked-folder:
+ *
+ * - Obtained directly via a folder request by path, or
+ * - discovered by the presence of a link file which corresponds to a linked-folder.
+ *
+ *
+ * Example #1, where folder path is known (external links will be followed):
+ *
+ * DomainFolder folder = projectData.getFolder("/A/B/linkedFolder");
+ * if (folder != null) {
+ * if (folder.isLinked())
+ * LinkedDomainFolder linkedFolder = (LinkedDomainFolder) folder;
+ * // linkedFolder behaves the same as a normal folder
+ * }
+ * DomainFile[] files = folder.getFiles();
+ * }
+ *
+ *
+ * Example #2, where we locate via a link file:
+ *
+ * DomainFile file = ...
+ * LinkFileInfo linkInfo = file.getLinkInfo();
+ * if (linkInfo != null && linkInfo.isFolderLink()) {
+ * LinkStatus status = linkInfo.getLinkStatus(null);
+ * if (status != LinkStatus.INTERNAL) {
+ * return; // Only consider internally linked-folder, skip broken or external link
+ * }
+ * LinkedDomainFolder linkedFolder = linkInfo.getLinkedFolder();
+ * if (linkedFolder != null) {
+ * DomainFile[] files = linkedFolder.getFiles();
+ * }
+ * }
+ *
+ *
+ * The utility method {@link ProjectDataUtils#descendantFiles(DomainFolder, DomainFileFilter)}
+ * may also come in handy to iterate over folder contents while restricting treatment of
+ * linked content.
*/
public interface DomainFolder extends Comparable {
+ // TODO: Need more robust folder icon support to allow repository connection state
+ // for root node to be reflected in icon (GP-5333)
+
public static final Icon OPEN_FOLDER_ICON = new GIcon("icon.datatree.node.domain.folder.open");
public static final Icon CLOSED_FOLDER_ICON =
@@ -88,6 +139,31 @@ public interface DomainFolder extends Comparable {
*/
public String getPathname();
+ /**
+ * Returns true if the given folder is the same as this folder based on path
+ * and underlying project/repository. Unlike the {@link #equals(Object)} check, this method
+ * handles cases where the folder provided may correspond to another project instance
+ * which is considered the same as the project that this folder is contained within.
+ *
+ * @param folder the potential same or descendant folder to check
+ * @return true if the given folder is the same or a child of this folder or
+ * one of its decendents.
+ */
+ public boolean isSame(DomainFolder folder);
+
+ /**
+ * Returns true if the given folder is the same or a child of this folder or
+ * one of its decendents based on path and underlying project/repository. Unlike the
+ * {@link #equals(Object)} check, this method
+ * handles cases where the folder provided may correspond to another project instance
+ * which is considered the same as the project that this folder is contained within.
+ *
+ * @param folder the potential same or descendant folder to check
+ * @return true if the given folder is the same or a child of this folder or
+ * one of its decendents.
+ */
+ public boolean isSameOrAncestor(DomainFolder folder);
+
/**
* Get a remote Ghidra URL for this domain folder if available within an associated shared
* project repository. URL path will end with "/". A null value will be returned if shared
@@ -125,6 +201,7 @@ public interface DomainFolder extends Comparable {
/**
* Return the folder for the given name.
+ * Folder link-files are ignored.
* @param name of folder to retrieve
* @return folder or null if there is no folder by the given name.
*/
@@ -182,6 +259,47 @@ public interface DomainFolder extends Comparable {
public DomainFile createFile(String name, File packFile, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException;
+ /**
+ * Create a link-file within this folder which references the specified file or folder
+ * {@code pathname} within the project specified by {@code sourceProjectData}. The link-file
+ * may correspond to various types of content (e.g., Program, Trace, Folder, etc.) based upon
+ * the specified {@link LinkHandler} instance.
+ *
+ * @param sourceProjectData referenced content project data within which specified path exists.
+ * If this differ's from this folder's project a Ghidra URL will be used, otherwise and internal
+ * link path reference will be used.
+ * @param pathname an absolute path of project folder or file within the specified source
+ * project data (a Ghidra URL is not permitted)
+ * @param makeRelative if true, and this file is contained within the same active
+ * {@link ProjectData} instance as {@code newParent}, an internal-project relative path
+ * link-file will be created.
+ * @param linkFilename name of link-file to be created within this folder. NOTE: This name may
+ * be modified to ensure uniqueness within this folder.
+ * @param lh link-file handler used to create specific link-file (see derived implementations
+ * of {@link LinkHandler} and their public static INSTANCE.
+ * @return newly created link-file
+ * @throws IOException if IO error occurs during link creation
+ */
+ public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname,
+ boolean makeRelative, String linkFilename, LinkHandler> lh) throws IOException;
+
+ /**
+ * Create an external link-file within this folder which references the specified
+ * {@code ghidraUrl} and whose content is defined by the specified {@link LinkHandler lh}
+ * instance.
+ *
+ * @param ghidraUrl a Ghidra URL which corresponds to a file or a folder based on the designated
+ * {@link LinkHandler lh} instance. Only rudimentary URL checks are performed.
+ * @param linkFilename name of link-file to be created within this folder. NOTE: This name may
+ * be modified to ensure uniqueness within this folder.
+ * @param lh link-file handler used to create specific link-file (see derived implementations
+ * of {@link LinkHandler} and their public static INSTANCE.
+ * @return newly created link-file
+ * @throws IOException if IO error occurs during link creation
+ */
+ public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler> lh)
+ throws IOException;
+
/**
* Create a subfolder within this folder.
* @param folderName sub-folder name
@@ -229,20 +347,21 @@ public interface DomainFolder extends Comparable {
throws IOException, CancelledException;
/**
- * Create a new link-file in the specified newParent which will reference this folder
- * (i.e., linked-folder). 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 folder with a remote
- * Ghidra URL, otherwise a local project storage path will be used.
+ * Copy this folder into the newParent folder as a folder-link. A folder-link references another
+ * folder without actually copying all of its children. If this folder is associated with a
+ * temporary transient project (i.e., not a locally managed project) the generated link will
+ * refer to the this folder with a Ghidra URL. If this folder is contained within the
+ * same active {@link ProjectData} instance as {@code newParent} an internal link reference
+ * will be made.
+ *
* @param newParent new parent folder where link-file is to be created
- * @return newly created domain file (i.e., link-file) or null if link use not supported.
+ * @param relative if true, and this folder is contained within the same active
+ * {@link ProjectData} instance as {@code newParent}, an internal-project relative path
+ * folder-link will be created.
+ * @return newly created domain file which is a folder-link (i.e., link-file).
* @throws IOException if an IO or access error occurs.
*/
- public DomainFile copyToAsLink(DomainFolder newParent) throws IOException;
+ public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException;
/**
* Allows the framework to react to a request to make this folder the "active" one.
@@ -250,7 +369,11 @@ public interface DomainFolder extends Comparable {
public void setActive();
/**
- * Determine if this folder corresponds to a linked-folder.
+ * Determine if this folder corresponds to a linked-folder which directly corresponds to a
+ * folder-link file. While this method is useful for identify a linked-folder root, in some
+ * cases it may be preferrable to simply check for instanceof {@link LinkedDomainFolder} which
+ * applies to the linked-folder root as well as its child sub-folders.
+ *
* @return true if folder corresponds to a linked-folder, else false.
*/
public default boolean isLinked() {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolderFilter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolderFilter.java
new file mode 100644
index 0000000000..987d308f42
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolderFilter.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.model;
+
+/**
+ * {@link DomainFolderFilter} interface to controls the following of linked-folders.
+ *
+ * Without specific overrides the default behavior:
+ *
+ * - {@link #ignoreBrokenLinks()} (true) Ignores all broken links
+ * - {@link #ignoreExternalLinks()} (true) Ignore external folder-links
+ * - {@link #ignoreFolderLinks()} (false) Will follow internal folder-links
+ *
+ */
+public interface DomainFolderFilter {
+
+ /**
+ * Folder filter which accepts all folders and will follow all linked folders.
+ * All broken links are ignored.
+ */
+ DomainFolderFilter ALL_FOLDERS_FILTER = new DomainFolderFilter() {
+
+ @Override
+ public boolean ignoreExternalLinks() {
+ return false;
+ }
+ };
+
+ /**
+ * File filter which allows only folders and internal folder-links.
+ * All external and broken links are ignored. This filter is useful when
+ * selecting a folder when creating/saving a file to the active project.
+ * If targeting a specific file content type for creation or saving use of
+ * {@link DefaultDomainFileFilter} may be preferred.
+ *
+ * It is the consumer of this filter who is responsible for following folder-links.
+ */
+ DomainFolderFilter ALL_INTERNAL_FOLDERS_FILTER = new DomainFolderFilter() {
+ // Default bahaviors
+ };
+
+ /**
+ * Folder filter which accepts only real folders and ignores all folder-links.
+ * All broken links are ignored.
+ */
+ DomainFolderFilter NON_LINKED_FOLDER_FILTER = new DomainFolderFilter() {
+
+ @Override
+ public boolean ignoreFolderLinks() {
+ return true;
+ }
+ };
+
+ /**
+ * Check if folder-links should be ignored (includes internal and external).
+ *
+ * @return true if all folder-links should be ignored (i.e., not followed/displayed)
+ */
+ public default boolean ignoreFolderLinks() {
+ return false;
+ }
+
+ /**
+ * Check if link-files should be ignored if the link is external (i.e., Ghidra-URL).
+ * Multi-level internal links are followed within the same project before a determination is made.
+ *
+ * If this method is not implemented the default behavior will ignore external links.
+ * This method should be ignored for folder-links if {@link #ignoreFolderLinks()} returns true.
+ *
+ * @return true if external links should be ignored (i.e., not displayed)
+ */
+ public default boolean ignoreExternalLinks() {
+ return true;
+ }
+
+ /**
+ * Check if link-files should be ignored if the link is broken. Multi-level internal links
+ * are followed within the same project before a determination is made.
+ *
+ * If this method is not implemented the default behavior will ignore broken links.
+ *
+ * @return true if broken links should be ignored (i.e., not followed/displayed)
+ */
+ public default boolean ignoreBrokenLinks() {
+ return true;
+ }
+
+}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkFileInfo.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkFileInfo.java
new file mode 100644
index 0000000000..22c401096a
--- /dev/null
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkFileInfo.java
@@ -0,0 +1,102 @@
+/* ###
+ * 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 java.util.function.Consumer;
+
+import ghidra.framework.data.*;
+import ghidra.framework.data.LinkHandler.LinkStatus;
+import ghidra.framework.protocol.ghidra.GhidraURL;
+
+/**
+ * {@link LinkFileInfo} provides access to link details for a {@link DomainFile} which is
+ * a link-file.
+ */
+public interface LinkFileInfo {
+
+ /**
+ * {@return the file that is associated with this link information.}
+ */
+ public DomainFile getFile();
+
+ /**
+ * Determine if the link "directly" refers to an external resource
+ * (i.e., URL-based {@link #getLinkPath() link path}).
+ *
+ * NOTE: It is important to understand that if this method returns {@code false} it
+ * may link to another link that is external. If the a file external status is required
+ * an {@link LinkStatus#EXTERNAL} status should be checked using {@link #getLinkStatus(Consumer)}.
+ *
+ * @return true if link-path is URL-based, else false
+ */
+ public default boolean isExternalLink() {
+ return GhidraURL.isGhidraURL(getLinkPath());
+ }
+
+ /**
+ * {@return true if this file is a folder-link, else false.}
+ */
+ public default boolean isFolderLink() {
+ return FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(getFile().getContentType());
+ }
+
+ /**
+ * If this is a folder-link file get the corresponding linked folder. Invoking this
+ * method on an {@link #isExternalLink() external-link} will cause the associated
+ * project or repository to be opened and associated with the active project as a
+ * a viewed-project. The resulting folder instance will return true to the method
+ * {@link DomainFolder#isLinked()}. This method will recurse all internal folder-links
+ * which may be chained together.
+ *
+ * @return a linked domain folder or null if not a valid folder-link.
+ */
+ public LinkedGhidraFolder getLinkedFolder();
+
+ /**
+ * Get the stored link-path. This may be either be an absolute or relative path within the
+ * link-file's project or a Ghidra URL.
+ *
+ * If you want to ensure that a project path returned is absolute and normalized, then
+ * {@link #getAbsoluteLinkPath()} may be used.
+ *
+ * @return associated link path
+ */
+ public String getLinkPath();
+
+ /**
+ * Get the stored link-path as a Ghidra URL or absolute normalized link-path from a link file.
+ * Path normalization eliminates any path element of "./" or "../".
+ * A local folder-link path will always end with a "/" path separator.
+ * Path normalization is not performed on Ghidra URLs.
+ *
+ * @return Ghidra URL or absolute normalized link-path from a link file
+ * @throws IOException if linkFile has an invalid relative link-path that failed to normalize
+ */
+ public String getAbsoluteLinkPath() throws IOException;
+
+ /**
+ * Determine the link status. If a status is {@link LinkStatus#BROKEN} and an
+ * {@code errorConsumer} has been specified the error details will be reported.
+ *
+ * @param errorConsumer broken link error consumer (may be null)
+ * @return link status
+ */
+ public default LinkStatus getLinkStatus(Consumer errorConsumer) {
+ return LinkHandler.getLinkFileStatus(getFile(), errorConsumer);
+ }
+
+}
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
index dab005fc34..bfc8df833c 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -23,6 +23,13 @@ import java.io.IOException;
*/
public interface LinkedDomainFile extends DomainFile {
+ /**
+ * Get the project file pathname relative to the linked-folder root.
+ * NOTE: It may be a link-file path.
+ * @return project pathname
+ */
+ public String getLinkedPathname();
+
/**
* Get the real domain file which corresponds to this file contained within a linked-folder.
* @return domain file
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
index 5ea918735e..f7158040ce 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,6 +15,7 @@
*/
package ghidra.framework.model;
+import java.io.FileNotFoundException;
import java.io.IOException;
import javax.swing.Icon;
@@ -28,17 +29,51 @@ import ghidra.framework.data.FolderLinkContentHandler;
public interface LinkedDomainFolder extends DomainFolder {
/**
- * Get the real domain folder which corresponds to this linked-folder.
- * @return domain folder
+ * Get the project data that corresponds to the linked-project and contains the
+ * {@link #getLinkedPathname()} which corresponds to this folder.
+ *
+ * @return linked project data
* @throws IOException if an IO error occurs
*/
- public DomainFolder getLinkedFolder() throws IOException;
+ public ProjectData getLinkedProjectData() throws IOException;
+
+ /**
+ * Get the project folder/file pathname for this this linked-folder relative to the
+ * linked-folder root.
+ *
+ * @return project pathname
+ */
+ public String getLinkedPathname();
+
+ /**
+ * Get the real domain folder which corresponds to this linked-folder.
+ * In the process of resolving the real folder a remote project or repository may be
+ * required.
+ *
+ * @return domain folder
+ * @throws FileNotFoundException if folder does not exist (could occur due to connection issue)
+ * @throws IOException if an IO error occurs while connecting/accessing the associated
+ * project or repository.
+ */
+ public DomainFolder getRealFolder() 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);
+ /**
+ * Determine if this folder resides within an external project or repository. The
+ * term "external" means the actual folder does not reside within the same project
+ * as the folder-link that referenced it and which was used to produce this
+ * linked folder instance.
+ *
+ * @return true if linked-folder is external to the link file which was used to access,
+ * else false if internal to the same project.
+ */
+ public boolean isExternal();
+
}
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 f25900cf61..c065e25bde 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
@@ -33,43 +33,42 @@ import ghidra.framework.options.SaveState;
public interface Project extends AutoCloseable, Iterable {
/**
- * Convenience method to get the name of this project.
+ * {@return the name of this project}
*/
public String getName();
/**
- * Get the project locator for this project.
+ * {@return the project locator for this project}
*/
public ProjectLocator getProjectLocator();
/**
- * Returns the project manager of this project.
- * @return the project manager of this project.
+ * {@return the project manager of this project}
*/
public ProjectManager getProjectManager();
/**
- * Return the tool manager for this project.
+ * {@return the tool manager for this project}
*/
public ToolManager getToolManager();
/**
- * Return the tool services for this project.
+ * {@return the tool services for this project}
*/
public ToolServices getToolServices();
/**
- * Return whether the project configuration has changed.
+ * {@return whether the project configuration has changed}
*/
public boolean hasChanged();
/**
- * Returns whether this project instance has been closed
+ * {@return whether this project instance has been closed}
*/
public boolean isClosed();
/**
- * Return the local tool chest for the user logged in.
+ * {@return the local tool chest for the user logged in}
*/
public ToolChest getLocalToolChest();
@@ -83,13 +82,14 @@ public interface Project extends AutoCloseable, Iterable {
/**
* 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.
+ * project. If the URL corresponds to this project its ProjectData will be returned.
* @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 IOException if this project is closed, an invalid URL is specified, or failed to
+ * open/connect to project/repository.
*/
public ProjectData addProjectView(URL projectURL, boolean visible) throws IOException;
@@ -100,7 +100,7 @@ public interface Project extends AutoCloseable, Iterable {
public void removeProjectView(URL projectURL);
/**
- * Return the list of visible project views in this project.
+ * {@return the list of visible project views in this project}
*/
public ProjectLocator[] getProjectViews();
@@ -143,13 +143,16 @@ public interface Project extends AutoCloseable, Iterable {
/**
* Allows the user to store data related to the project.
- * @param key A value used to store and lookup saved data
+ * See {@link #getSaveableData(String)} for future retieval of data.
+ * @param key a value used to store and lookup saved data
* @param saveState a container of data that will be written out when persisted
*/
public void setSaveableData(String key, SaveState saveState);
/**
- * The analog for {@link #setSaveableData(String, SaveState)}.
+ * {@return the user data previously stored to the project}
+ * See {@link #setSaveableData(String, SaveState)}.
+ * @param key a value used to store and lookup saved data
*/
public SaveState getSaveableData(String key);
@@ -160,16 +163,28 @@ public interface Project extends AutoCloseable, Iterable {
public List getOpenData();
/**
- * Get the root domain data folder in the project.
+ * {@return the root domain data folder in the project}
*/
public ProjectData getProjectData();
/**
* Returns the Project Data for the given Project locator. The Project locator must
* be either the current active project or an currently open project view.
+ * The returned view may not be visible.
+ * @param projectLocator project locator object used to open project
+ * @return requested project data
*/
public ProjectData getProjectData(ProjectLocator projectLocator);
+ /**
+ * Returns the Project Data for the given Project URL. The Project URL must
+ * be either the current active project or a currently open project view.
+ * The returned view may not be visible.
+ * @param projectURL identifier for the project view (ghidra protocol only).
+ * @return project data for this view or null
+ */
+ public ProjectData getProjectData(URL projectURL);
+
/**
* Get the project data for visible viewed projects that are
* managed by this project.
@@ -195,9 +210,14 @@ public interface Project extends AutoCloseable, Iterable {
*/
public void removeProjectViewListener(ProjectViewListener listener);
+ /**
+ * Return a {@link DomainFile} iterator over all non-link files within this project's data store.
+ * If links should be followed use an appropropriate static method from {@link ProjectDataUtils}.
+ * @return domain file iterator
+ */
@Override
public default Iterator iterator() {
- return new ProjectDataUtils.DomainFileIterator(this);
+ return getProjectData().iterator();
}
}
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 767820a6a3..d3fe7fc423 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
@@ -30,6 +30,10 @@ import ghidra.util.task.TaskMonitor;
/**
* The ProjectData interface provides access to all the data files and folders
* in a project.
+ *
+ * NOTE: Iterating over this project data instance will ignore all link-files. If links should
+ * be handled please instantiate {@link ProjectDataUtils#descendantFiles(DomainFolder, DomainFileFilter)}
+ * with a suitable {@link DomainFileFilter}.
*/
public interface ProjectData extends Iterable {
@@ -45,12 +49,36 @@ public interface ProjectData extends Iterable {
public DomainFolder getRootFolder();
/**
- * Get domain folder specified by an absolute data path.
- * @param path the absolute path of domain folder relative to the data folder.
+ * Get domain folder specified by an absolute data path. All internal folder-links will be followed.
+ * All path elements must refer to a valid internal non-conflicting folder or folder-link.
+ * Internal folder-links will be resolved to their corresponding linked-folder.
+ *
+ * External links are not followed. If external links should be followed the
+ * {@link #getFolder(String, DomainFolderFilter)} method should be used with an appropriate filter.
+ *
+ * NOTE: Absolute paths do not include the project name which may be shown in the project
+ * data tree in place of the root folder node {@code "/"}.
+ *
+ * @param path the absolute path of a domain folder within the project.
* @return domain folder or null if folder not found
*/
public DomainFolder getFolder(String path);
+ /**
+ * Get domain folder specified by an absolute data path. If path refers to a
+ * non-conflicting folder-link the specified filter will determine if it should be
+ * followed to the linked-folder. All folder path elements must satisfy the filter restrictions.
+ *
+ * NOTE: Absolute paths do not include the project name which may be shown in the project
+ * data tree in place of the root folder node {@code "/"}.
+ *
+ * @param path the absolute path of a domain folder within the project.
+ * @param filter domain folder filter which constrains returned folder and following of
+ * folder-links.
+ * @return domain folder or null if folder not found
+ */
+ public DomainFolder getFolder(String path, DomainFolderFilter filter);
+
/**
* Get the approximate number of files contained within the project. The number
* may be reduced if not connected to the shared repository. Only the newer
@@ -68,15 +96,41 @@ public interface ProjectData extends Iterable {
public int getFileCount();
/**
- * Get domain file specified by an absolute data path.
- * @param path the absolute path of domain file relative to the root folder.
+ * Get domain file specified by an absolute data path. All internal folder-links will be followed.
+ * The returned file may be a link-file and {@link DomainFile#getLinkInfo()} result and/or
+ * {@link DomainFile#getDomainObjectClass()} / {@link DomainFile#getContentType()} should be
+ * checked if needed.
+ *
+ * External links are not followed. If external links should be followed the
+ * {@link #getFile(String, DomainFileFilter)} method should be used with an appropriate filter.
+ *
+ * NOTE: Absolute path does not include the project name which may be shown in the project
+ * data tree in place of the root folder node {@code "/"}.
+ *
+ * @param path the absolute path of domain file within the project.
* @return domain file or null if file not found
*/
public DomainFile getFile(String path);
/**
- * Finds all open domain files and appends
- * them to the specified list.
+ * Get domain file specified by an absolute data path which satisfies the specified filter.
+ * If permitted by the filter the returned file may be a link-file. This may occur if filter
+ * constrains based upon {@link DomainFile#getDomainObjectClass()} instead of
+ * {@link DomainFile#getContentType()}. {@link DomainFile#getLinkInfo()} result can be checked
+ * if needed.
+ *
+ * NOTE: Absolute path does not include the project name which may be shown in the project
+ * data tree in place of the root folder node {@code "/"}.
+ *
+ * @param path the absolute path of domain file within the project.
+ * @param filter domain file filter which constrains returned file and following of folder-links
+ * and file-links.
+ * @return domain file or null if file not found
+ */
+ public DomainFile getFile(String path, DomainFileFilter filter);
+
+ /**
+ * Finds all open domain files and appends them to the specified list.
* @param list the list to receive the open domain files
*/
public void findOpenFiles(List list);
@@ -106,7 +160,7 @@ public interface ProjectData extends Iterable {
throws IOException, CancelledException;
/**
- * Get domain file specified by its unique fileID.
+ * Get domain file specified by its unique fileID. Link following is not performed.
* @param fileID domain file ID
* @return domain file or null if file not found
*/
@@ -116,7 +170,7 @@ public interface ProjectData extends Iterable {
* 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.
* NOTE: Uniqueness of name within the intended target folder is not considered.
- * @param name
+ * @param name original name to be sanitized
* @return valid name or "unknown" if no valid characters exist within name provided
*/
public String makeValidName(String name);
@@ -225,8 +279,13 @@ public interface ProjectData extends Iterable {
*/
public URL getLocalProjectURL();
+ /**
+ * Return a {@link DomainFile} iterator over all non-link files within this project data store.
+ * If links should be followed use an appropropriate static method from {@link ProjectDataUtils}.
+ * @return domain file iterator
+ */
@Override
public default Iterator iterator() {
- return new ProjectDataUtils.DomainFileIterator(getRootFolder());
+ return ProjectDataUtils.descendantFiles(getRootFolder()).iterator();
}
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectDataUtils.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectDataUtils.java
index c6d01bd97d..d6d3843140 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectDataUtils.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectDataUtils.java
@@ -17,141 +17,92 @@ package ghidra.framework.model;
import java.io.IOException;
import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
-import ghidra.util.InvalidNameException;
+import org.apache.commons.lang3.StringUtils;
+
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.LinkHandler.LinkStatus;
+import ghidra.framework.data.LinkedGhidraFolder;
+import ghidra.framework.store.FileSystem;
+import ghidra.util.*;
public class ProjectDataUtils {
- /**
- * A not-thread-safe {@link DomainFile} iterator that recursively walks a
- * {@link ProjectData project's data} and returns each {@code DomainFile} that is
- * found.
- */
- public static class DomainFileIterator implements Iterator {
-
- private Deque fileQueue = new LinkedList<>();
- private Deque folderQueue = new LinkedList<>();
-
- /**
- * Recursively traverse a {@link Project} starting in its root folder.
- *
- * @param project
- */
- public DomainFileIterator(Project project) {
- this(project.getProjectData().getRootFolder());
- }
-
- /**
- * Recursively traverse the {@link DomainFile}s under a specific {@link DomainFolder}.
- *
- * @param startFolder
- */
- public DomainFileIterator(DomainFolder startFolder) {
- folderQueue.add(startFolder);
- }
-
- private void queueNextFiles() {
- DomainFolder folder;
- while (fileQueue.isEmpty() && (folder = folderQueue.poll()) != null) {
- DomainFolder[] folders = folder.getFolders();
- for (int i = folders.length - 1; i >= 0; i--) {
- DomainFolder subfolder = folders[i];
- folderQueue.addFirst(subfolder);
- }
- for (DomainFile subfile : folder.getFiles()) {
- fileQueue.addLast(subfile);
- }
- }
- }
-
- @Override
- public boolean hasNext() {
- queueNextFiles();
- return !fileQueue.isEmpty();
- }
-
- @Override
- public DomainFile next() {
- return fileQueue.poll();
- }
- }
/**
- * A not-thread-safe {@link DomainFolder} iterator that recursively walks a
- * {@link ProjectData project's data} and returns each {@code DomainFolder} that is
- * found.
- */
- public static class DomainFolderIterator implements Iterator {
-
- private Deque folderQueue = new LinkedList<>();
- private DomainFolder nextFolder;
-
- /**
- * Recursively traverse a {@link Project} starting in its root folder.
- *
- * @param project
- */
- public DomainFolderIterator(Project project) {
- this(project.getProjectData().getRootFolder());
- }
-
- /**
- * Recursively traverse the {@link DomainFolder}s under a specific {@link DomainFolder}.
- *
- * @param startFolder
- */
- public DomainFolderIterator(DomainFolder startFolder) {
- folderQueue.add(startFolder);
- }
-
- private void queueNextFiles() {
- if (nextFolder == null && !folderQueue.isEmpty()) {
- nextFolder = folderQueue.poll();
- DomainFolder[] folders = nextFolder.getFolders();
- for (int i = folders.length - 1; i >= 0; i--) {
- DomainFolder subfolder = folders[i];
- folderQueue.addFirst(subfolder);
- }
- }
- }
-
- @Override
- public boolean hasNext() {
- queueNextFiles();
- return nextFolder != null;
- }
-
- @Override
- public DomainFolder next() {
- DomainFolder tmp = nextFolder;
- nextFolder = null;
- return tmp;
- }
- }
-
- /**
- * Returns a {@link Iterable} sequence of all the {@link DomainFile}s that exist under
- * the specified {@link DomainFolder folder}.
+ * Returns a {@link Iterable} of {@link DomainFile}s that exist under
+ * the specified {@link DomainFolder folder} including all sub-folder content.
+ * All folder-links and file-links will be ignored and files of all content-types will
+ * be returned by the iterator.
+ *
+ * Use {@link ProjectDataUtils#descendantFiles(DomainFolder, DomainFileFilter)} for
+ * finer-grained control over returned files.
*
- * @param folder
- * @return
+ * @param folder domain folder
+ * @return domain file iterator
*/
public static Iterable descendantFiles(DomainFolder folder) {
- return () -> new DomainFileIterator(folder);
+ return new DomainFileIterator(folder, DomainFileFilter.NON_LINKED_FILE_FILTER);
}
/**
- * Returns a {@link Iterable} sequence of all the {@link DomainFolder}s that exist under
- * the specified {@link DomainFolder folder}.
- * @param folder
- * @return
+ * Returns a {@link Iterable} of {@link DomainFile}s that exist under
+ * the specified {@link DomainFolder folder}, including all sub-folder content,
+ * which satisfy the specified filter restrictions.
+ *
+ * NOTE: Care must be taken in the presence of folder-links and file-links since such links can
+ * result in the same files being returned by the iterator multiple times. In
+ * general it is recommended that all links (see {@link DomainFile#isLink()}) be ignored
+ * when iterating over an entire project. When restricting content-type it is highly recommended
+ * that the method {@link DomainFile#getDomainObjectClass()} since both linked and non-linked
+ * files for the same content will specify the same {@link DomainObject} class
+ * (e.g., {@code Program.class}).
+ *
+ * @param folder domain folder
+ * @param filter the filter which determines which files should be returned by the
+ * iterator and what links should be followed.
+ * @return domain file iterator
+ */
+ public static Iterable descendantFiles(DomainFolder folder,
+ DomainFileFilter filter) {
+ return new DomainFileIterator(folder, filter);
+ }
+
+ /**
+ * Returns a {@link Iterable} of {@link DomainFolder}s that exist under
+ * the specified {@link DomainFolder folder} including all sub-folders.
+ * All folder-links will be ignored.
+ *
+ * Use {@link ProjectDataUtils#descendantFolders(DomainFolder, boolean, boolean)} if
+ * folder-links should be followed.
+ *
+ * @param folder domain folder
+ * @return domain folder iterator
*/
public static Iterable descendantFolders(DomainFolder folder) {
- return () -> new DomainFolderIterator(folder);
+ return descendantFolders(folder, true, true);
+ }
+
+ /**
+ * Returns a {@link Iterable} of {@link DomainFolder}s that exist under
+ * the specified {@link DomainFolder folder} including all sub-folders.
+ * subject to the specified folder-link restrictions. All broken folder-links encountered
+ * will be logged and skipped.
+ *
+ * @param folder domain folder
+ * @param ignoreFolderLinks true if all folder-links should be ignored
+ * @param ignoreExternalLinks true if all external-links should be ignored
+ * (ignored if ignoreFolderLinks is true)
+ * @return domain folder iterator
+ */
+ public static Iterable descendantFolders(DomainFolder folder,
+ boolean ignoreFolderLinks, boolean ignoreExternalLinks) {
+ return new DomainFolderIterator(folder, ignoreFolderLinks, ignoreExternalLinks);
}
/**
* Returns a Ghidra {@link DomainFolder} with the matching path, creating
- * any missing parent folders as needed.
+ * any missing parent folders as needed. Broken folder-links will always be ignored.
*
* @param currentFolder starting {@link DomainFolder}.
* @param path relative path to the desired DomainFolder, using forward slashes
@@ -159,54 +110,147 @@ public class ProjectDataUtils {
* trailing slashes ignored.
* @return {@link DomainFolder} that the path points to.
* @throws InvalidNameException if bad name
- * @throws IOException if problem when creating folder
+ * @throws ReadOnlyException if unable to create a folder within a read-only project
+ * @throws IOException if problem when creating folder or a conflicting/broken folder/folder-link
+ * encountered.
*/
public static DomainFolder createDomainFolderPath(DomainFolder currentFolder, String path)
throws InvalidNameException, IOException {
- String[] pathElements = path.split("/");
+ if (!currentFolder.isInWritableProject()) {
+ throw new ReadOnlyException("Folder is read-only: " + currentFolder);
+ }
+
+ if (StringUtils.isBlank(path)) {
+ return currentFolder;
+ }
+
+ DomainFolder folder = currentFolder;
+
+ String[] pathElements = path.split(FileSystem.SEPARATOR);
for (String pathElement : pathElements) {
- pathElement = pathElement.trim();
+
+ // pathElement = pathElement.trim(); // NOTE: Seems too forgiving
if (pathElement.isEmpty()) {
continue;
}
- DomainFolder nextFolder = currentFolder.getFolder(pathElement);
- if (nextFolder == null) {
- // TODO: race condition between getFolder() and createFolder()
- nextFolder = currentFolder.createFolder(pathElement);
+
+ DomainFolder subFolder = folder.getFolder(pathElement);
+
+ // Check for folder link-file
+ DomainFile file = folder.getFile(pathElement);
+ if (file != null && file.isLink()) {
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ if (linkInfo.isFolderLink()) {
+ if (subFolder != null) {
+ throw new IOException(
+ "Folder and folder-link name conflict encountered: " + file);
+ }
+ // May only follow non-external and non-broken folder-links
+ if (linkInfo.isExternalLink()) {
+ throw new IOException("May not follow external folder-link: " + file);
+ }
+ if (LinkHandler.getLinkFileStatus(file, null) == LinkStatus.BROKEN) {
+ throw new IOException("May not follow broken folder-link: " + file);
+ }
+ subFolder = linkInfo.getLinkedFolder();
+ }
}
- currentFolder = nextFolder;
+ if (subFolder == null) {
+ subFolder = folder.createFolder(pathElement);
+ }
+ folder = subFolder;
}
- return currentFolder;
+
+ return folder;
+ }
+
+ /**
+ * Returns a Ghidra {@link DomainFolder} with the matching path within the baseFolder's
+ * project, or null if not found. Broken and external folder-links will be ignored.
+ *
+ * @param baseFolder Base {@link DomainFolder} for relativePath
+ * @param relativePath path relative to the specified DomainFolder, using forward slashes
+ * as separators. Empty string ok, multiple slashes in a row treated as single slash,
+ * leading and trailing slashes ignored.
+ * @return {@link DomainFolder} that the path points to or null if not found.
+ */
+ public static DomainFolder getDomainFolder(DomainFolder baseFolder, String relativePath) {
+ return getDomainFolder(baseFolder, relativePath,
+ DomainFolderFilter.ALL_INTERNAL_FOLDERS_FILTER);
}
/**
* Returns a Ghidra {@link DomainFolder} with the matching path, or null if not found.
*
- * @param currentFolder starting {@link DomainFolder}.
- * @param path relative path to the desired DomainFolder, using forward slashes
+ * @param baseFolder Base {@link DomainFolder} for relativePath
+ * @param relativePath path relative to the specified DomainFolder, using forward slashes
* as separators. Empty string ok, multiple slashes in a row treated as single slash,
- * trailing slashes ignored.
- * @return {@link DomainFolder} that the path points to or null if not found.
+ * leading and trailing slashes ignored.
+ * @param filter domain folder filter which constrains returned folder and following of
+ * folder-links. Broken links will always be ignored.
+ * @return {@link DomainFolder} that the path points to or null if not found or path contains
+ * a broken folder-link.
*/
- public static DomainFolder lookupDomainPath(DomainFolder currentFolder, String path) {
+ public static DomainFolder getDomainFolder(DomainFolder baseFolder, String relativePath,
+ DomainFolderFilter filter) {
- String[] pathElements = path.split("/");
+ if (StringUtils.isBlank(relativePath)) {
+ return baseFolder;
+ }
+
+ DomainFolder folder = baseFolder;
+
+ String[] pathElements = relativePath.split(FileSystem.SEPARATOR);
for (String pathElement : pathElements) {
- pathElement = pathElement.trim();
+
+ // pathElement = pathElement.trim(); // NOTE: Seems too forgiving
if (pathElement.isEmpty()) {
continue;
}
- currentFolder = currentFolder.getFolder(pathElement);
- if (currentFolder == null) {
- break;
+
+ DomainFolder subFolder = folder.getFolder(pathElement);
+
+ // Check for folder link-file
+ // NOTE: if real folder name matches folder-link-file name it will fail
+ // to resolve folder - either folder or link should be renamed.
+ DomainFile file = folder.getFile(pathElement);
+ if (file != null && file.isLink()) {
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ if (linkInfo.isFolderLink()) {
+ if (filter.ignoreFolderLinks()) {
+ return null;
+ }
+ if (subFolder != null) {
+ Msg.error(ProjectDataUtils.class,
+ "Folder and folder-link name conflict encountered: " + file);
+ return null; // conflicting folder and folder-link
+ }
+ if (linkInfo.isExternalLink() && filter.ignoreExternalLinks()) {
+ return null;
+ }
+ if (LinkHandler.getLinkFileStatus(file, null) == LinkStatus.BROKEN) {
+ Msg.warn(ProjectDataUtils.class,
+ "Skipping broken folder-link: " + file.getPathname());
+ return null;
+ }
+ subFolder = linkInfo.getLinkedFolder();
+ }
}
+
+ if (subFolder == null) {
+ return null; // folder path element not found
+ }
+ folder = subFolder;
}
- return currentFolder;
+
+ return folder;
}
/**
- * Returns a unique name in a Ghidra {@link DomainFolder}.
+ * Returns a unique folder/file name within the specified {@link DomainFolder folder}.
+ * The specified {@code baseName} will be used as the basis for the name returned with an
+ * appended number.
*
* @param folder {@link DomainFolder} to check for child name collisions.
* @param baseName String base name of the file or folder
@@ -226,4 +270,219 @@ public class ProjectDataUtils {
}
return null;
}
+
+ /**
+ * A non-thread-safe {@link DomainFile} iterator that recursively walks a
+ * {@link ProjectData project's data} and returns each {@code DomainFile} that is
+ * found.
+ *
+ * This iterator will never return a folder-link as a file. If a folder-link is not ignored
+ * its children will be processed.
+ */
+ private static class DomainFileIterator implements Iterator, Iterable {
+
+ private Deque fileQueue = new LinkedList<>();
+ private Deque folderQueue = new LinkedList<>();
+
+ private DomainFileFilter filter;
+
+ /**
+ * Recursively traverse the {@link DomainFile}s under a specific {@link DomainFolder}.
+ *
+ * NOTE: Care must be taken in the presence of folder-links and file-links since such links
+ * can result in the same files being returned by the iterator multiple times. In
+ * general it is recommended that all links (see {@link DomainFile#isLink()}) be ignored
+ * when iterating over an entire project. When restricting content-type it is highly recommended
+ * that the method {@link DomainFile#getDomainObjectClass()} since both linked and non-linked
+ * files for the same content will specify the same {@link DomainObject} class
+ * (e.g., {@code Program.class}).
+ *
+ * @param startFolder folder to start from
+ * @param filter the filter which determines which files should be returned by the
+ * iterator and what links should be followed. Following of external links is blocked
+ * and takes precendence over specified filter.
+ */
+ DomainFileIterator(DomainFolder startFolder, DomainFileFilter filter) {
+ Objects.requireNonNull(startFolder, "folder not specified");
+ Objects.requireNonNull(filter, "domain file filter not specified");
+ folderQueue.add(startFolder);
+ this.filter = filter;
+ }
+
+ private void queueNextFiles() {
+ DomainFolder folder;
+ while (fileQueue.isEmpty() && (folder = folderQueue.poll()) != null) {
+ DomainFolder[] folders = folder.getFolders();
+ for (int i = folders.length - 1; i >= 0; i--) {
+ DomainFolder subfolder = folders[i];
+ folderQueue.addFirst(subfolder);
+ }
+ for (DomainFile df : folder.getFiles()) {
+ if (df.isLink()) {
+ AtomicReference linkStatus = new AtomicReference<>();
+ if (skipLinkFile(df, linkStatus)) {
+ continue;
+ }
+ if (df.getLinkInfo().isFolderLink()) {
+ LinkedGhidraFolder linkedFolder =
+ resolveFolderLink(df, linkStatus.get());
+ if (linkedFolder != null) {
+ // queue folder for subsequent processing
+ folderQueue.addFirst(linkedFolder);
+ }
+ continue;
+ }
+ // A file-link may drop-through (e.g., ProgramLink) but will be
+ // subject to filter.accept method below.
+ }
+ if (filter.accept(df)) {
+ fileQueue.addLast(df);
+ }
+ }
+ }
+ }
+
+ private LinkedGhidraFolder resolveFolderLink(DomainFile folderLinkFile, LinkStatus status) {
+ if (status == LinkStatus.BROKEN) {
+ Msg.warn(this, "Skipping broken folder-link: " + folderLinkFile.getPathname());
+ return null;
+ }
+ if (status == LinkStatus.EXTERNAL && !filter.followExternallyLinkedFolders()) {
+ return null;
+ }
+ return folderLinkFile.getLinkInfo().getLinkedFolder();
+ }
+
+ /**
+ * Check linkFile against filter to see if it should be skipped.
+ * @param linkFile link file to be checked
+ * @param returnedLinkStatus if method returns false this will be updated with status
+ * @return true if linkFile should be skipped, else false
+ */
+ private boolean skipLinkFile(DomainFile linkFile,
+ AtomicReference returnedLinkStatus) {
+ LinkFileInfo linkInfo = linkFile.getLinkInfo();
+ boolean isFolderLink = linkInfo.isFolderLink();
+ if (isFolderLink && filter.ignoreFolderLinks()) {
+ return true;
+ }
+ LinkStatus linkStatus = LinkHandler.getLinkFileStatus(linkFile, null);
+ if (linkStatus == LinkStatus.BROKEN && filter.ignoreBrokenLinks()) {
+ return true;
+ }
+ if (linkStatus == LinkStatus.EXTERNAL) {
+ return true;
+ }
+ if (linkStatus == LinkStatus.BROKEN) {
+ // Filter did not ignore broken link so we will simply report it and continue
+ Msg.warn(this, "Skipping broken link-file: " + linkFile.getPathname());
+ return true;
+ }
+ returnedLinkStatus.set(linkStatus);
+ return false;
+ }
+
+ @Override
+ public boolean hasNext() {
+ queueNextFiles();
+ return !fileQueue.isEmpty();
+ }
+
+ @Override
+ public DomainFile next() {
+ return fileQueue.poll();
+ }
+
+ @Override
+ public Iterator iterator() {
+ return this;
+ }
+ }
+
+ /**
+ * A non-thread-safe {@link DomainFolder} iterator that recursively walks a
+ * {@link ProjectData project's data} and returns each {@code DomainFolder} that is
+ * found. Non-broken folder-links will be followed based upon specified constraints.
+ */
+ private static class DomainFolderIterator
+ implements Iterator, Iterable {
+
+ private Deque folderQueue = new LinkedList<>();
+ private DomainFolder nextFolder;
+
+ private boolean ignoreFolderLinks;
+ private boolean ignoreExternalLinks;
+
+ /**
+ * Recursively traverse the {@link DomainFolder}s under a specific {@link DomainFolder}
+ * subject to the specified folder-link restrictions. All broken folder-links encountered
+ * will be logged and skipped.
+ *
+ * @param startFolder domain folder
+ * @param ignoreFolderLinks true if all folder-links should be ignored
+ * @param ignoreExternalLinks true if all external-links should be ignored
+ * (ignored if ignoreFolderLinks is true)
+ */
+ DomainFolderIterator(DomainFolder startFolder, boolean ignoreFolderLinks,
+ boolean ignoreExternalLinks) {
+ folderQueue.add(startFolder);
+ this.ignoreFolderLinks = ignoreFolderLinks;
+ this.ignoreExternalLinks = ignoreExternalLinks;
+ }
+
+ private void queueNextFiles() {
+ if (nextFolder == null && !folderQueue.isEmpty()) {
+ nextFolder = folderQueue.poll();
+ DomainFolder[] folders = nextFolder.getFolders();
+ for (int i = folders.length - 1; i >= 0; i--) {
+ DomainFolder subfolder = folders[i];
+ folderQueue.addFirst(subfolder);
+ }
+ if (!ignoreFolderLinks) {
+ for (DomainFile df : nextFolder.getFiles()) {
+ LinkedGhidraFolder linkedFolder = resolveFolderLink(df);
+ if (linkedFolder != null) {
+ // queue folder for subsequent processing
+ folderQueue.addFirst(linkedFolder);
+ }
+ }
+ }
+ }
+
+ }
+
+ private LinkedGhidraFolder resolveFolderLink(DomainFile file) {
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ if (linkInfo == null || !linkInfo.isFolderLink()) {
+ return null;
+ }
+ LinkStatus linkStatus = LinkHandler.getLinkFileStatus(file, null);
+ if (linkStatus == LinkStatus.BROKEN) {
+ Msg.warn(this, "Skipping broken folder-link: " + file.getPathname());
+ return null;
+ }
+ if (linkStatus == LinkStatus.EXTERNAL && ignoreExternalLinks) {
+ return null;
+ }
+ return linkInfo.getLinkedFolder();
+ }
+
+ @Override
+ public boolean hasNext() {
+ queueNextFiles();
+ return nextFolder != null;
+ }
+
+ @Override
+ public DomainFolder next() {
+ DomainFolder tmp = nextFolder;
+ nextFolder = null;
+ return tmp;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return this;
+ }
+ }
}
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 227a4071f1..26668731c1 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
@@ -59,6 +59,10 @@ public class ProjectLocator {
* @throws IllegalArgumentException if an absolute path is not specified or invalid project name
*/
public ProjectLocator(String path, String name) {
+ this(path, name, null);
+ }
+
+ protected ProjectLocator(String path, String name, URL url) {
if (name.contains("/") || name.contains("\\")) {
throw new IllegalArgumentException("name contains path separator character: " + name);
}
@@ -71,7 +75,7 @@ public class ProjectLocator {
path = Application.getUserTempDirectory().getAbsolutePath();
}
this.location = checkAbsolutePath(path);
- url = GhidraURL.makeURL(location, name);
+ this.url = url != null ? url : GhidraURL.makeURL(location, name);
}
/**
@@ -216,12 +220,12 @@ public class ProjectLocator {
return false;
}
ProjectLocator projectLocator = (ProjectLocator) obj;
- return name.equals(projectLocator.name) && location.equals(projectLocator.location);
+ return url.equals(projectLocator.getURL());
}
@Override
public int hashCode() {
- return name.hashCode() + location.hashCode();
+ return url.hashCode();
}
@Override
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
index 9a65ec9ef8..584a27a1db 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,7 +20,7 @@ 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 {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolChest.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolChest.java
index 25e7596ee8..859832d463 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolChest.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolChest.java
@@ -1,13 +1,12 @@
/* ###
* IP: GHIDRA
- * REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,71 +19,70 @@ package ghidra.framework.model;
* Interface to define methods to manage tools in a central location.
*/
public interface ToolChest {
-
-
- /**
- * Get the tool template for the given tool name.
- * @param toolName name of tool
- * @return null if there is no tool template for the given
- * toolName.
- */
- public ToolTemplate getToolTemplate(String toolName);
- /**
- * Get the tool templates from the tool chest.
- * @return list of tool template
- */
- public ToolTemplate[] getToolTemplates();
+ /**
+ * Get the tool template for the given tool name.
+ * @param toolName name of tool
+ * @return null if there is no tool template for the given
+ * toolName.
+ */
+ public ToolTemplate getToolTemplate(String toolName);
- /**
- * Add a listener to be notified when the tool chest is changed.
- * @param l listener to add
- */
- public void addToolChestChangeListener(ToolChestChangeListener l);
+ /**
+ * Get the tool templates from the tool chest.
+ * @return list of tool template
+ */
+ public ToolTemplate[] getToolTemplates();
- /**
- *
- * Remove a listener that is listening to when the tool chest is changed.
- * @param l to remove
- */
- public void removeToolChestChangeListener(ToolChestChangeListener l);
+ /**
+ * Add a listener to be notified when the tool chest is changed.
+ * @param l listener to add
+ */
+ public void addToolChestChangeListener(ToolChestChangeListener l);
- /**
- * Add tool template to the tool chest.
- *
- * Note: If the given tool template name already exists in the project, then the name will
- * be altered by appending an underscore and a one-up value. The template
- * parameter's name is also updated with then new name.
- *
- * To simply replace a tool with without changing its name, call
- * {@link #replaceToolTemplate(ToolTemplate)}
- *
- * @param template tool template to add
- */
- public boolean addToolTemplate(ToolTemplate template);
+ /**
+ *
+ * Remove a listener that is listening to when the tool chest is changed.
+ * @param l to remove
+ */
+ public void removeToolChestChangeListener(ToolChestChangeListener l);
- /**
- * Remove entry (toolTemplate or toolSet) from the tool chest.
- *
- * @param toolName name of toolConfig or toolSet to remove
- * @return true if the toolConfig or toolset was
- * successfully removed from the tool chest.
- */
- public boolean remove(String toolName);
-
- /**
- * Get the number of tools in this tool chest.
- * @return tool count.
- */
- public int getToolCount();
+ /**
+ * Add tool template to the tool chest.
+ *
+ * Note: If the given tool template name already exists in the project, then the name will
+ * be altered by appending an underscore and a one-up value. The template
+ * parameter's name is also updated with then new name.
+ *
+ * To simply replace a tool with without changing its name, call
+ * {@link #replaceToolTemplate(ToolTemplate)}
+ *
+ * @param template tool template to add
+ */
+ public boolean addToolTemplate(ToolTemplate template);
- /**
- * Performs the same action as calling {@link #remove(String)} and then
- * {@link #addToolTemplate(ToolTemplate)}. However, calling this method prevents state from
- * being lost in the transition, such as position in the tool chest and default tool status.
- *
- * @param template The template to add to the tool chest, replacing any tools with the same name.
- * @return True if the template was added.
- */
- public boolean replaceToolTemplate(ToolTemplate template);
+ /**
+ * Remove entry (toolTemplate or toolSet) from the tool chest.
+ *
+ * @param toolName name of toolConfig or toolSet to remove
+ * @return true if the toolConfig or toolset was
+ * successfully removed from the tool chest.
+ */
+ public boolean remove(String toolName);
+
+ /**
+ * Get the number of tools in this tool chest.
+ * @return tool count.
+ */
+ public int getToolCount();
+
+ /**
+ * Performs the same action as calling {@link #remove(String)} and then
+ * {@link #addToolTemplate(ToolTemplate)}. However, calling this method prevents state from
+ * being lost in the transition, such as position in the tool chest and default tool status.
+ *
+ * @param template The template to add to the tool chest, replacing any tools with the same name.
+ * @return True if the template was added.
+ */
+ public boolean replaceToolTemplate(ToolTemplate template);
}
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 626c274921..7c3b2e36bb 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -285,6 +285,10 @@ public class DefaultProject implements Project {
throw new IOException("Invalid Ghidra URL specified: " + url);
}
+ if (url.equals(projectLocator.getURL())) {
+ return projectData;
+ }
+
ProjectData viewedProjectData = otherViewsMap.get(url);
if (viewedProjectData == null) {
viewedProjectData = openProjectView(url);
@@ -298,6 +302,18 @@ public class DefaultProject implements Project {
}
}
+ @Override
+ public ProjectData getProjectData(URL url) {
+
+ if (url.equals(projectLocator.getURL())) {
+ return projectData;
+ }
+
+ synchronized (otherViewsMap) {
+ return otherViewsMap.get(url);
+ }
+ }
+
/**
* Remove the view from this project.
*/
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java
index bb9afd9b80..655463045f 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java
@@ -141,6 +141,7 @@ public class DefaultProjectManager implements ProjectManager {
try {
currentProject = new DefaultProject(this, projectLocator, resetOwner);
+ AppInfo.setActiveProject(currentProject);
if (doRestore) {
currentProject.restore();
}
@@ -164,7 +165,6 @@ public class DefaultProjectManager implements ProjectManager {
throw e;
}
finally {
- AppInfo.setActiveProject(currentProject);
if (currentProject == null) {
File dirFile = projectLocator.getProjectDir();
if (!dirFile.exists() || !dirFile.isDirectory()) {
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 d56d7db157..01e757d58c 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -243,8 +243,8 @@ class ToolServicesImpl implements ToolServices {
@Override
public PluginTool launchDefaultToolWithURL(URL ghidraUrl) throws IllegalArgumentException {
String contentType = getContentType(ghidraUrl);
- if (contentType == null) {
- return null;
+ if (contentType == null || ContentHandler.UNKNOWN_CONTENT.equals(contentType)) {
+ return null; // assume folder, non-existent, or unsupported content
}
ToolTemplate template = getDefaultToolTemplate(contentType);
return defaultLaunch(template, t -> {
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java
index 11714b6f50..5a266c6bc7 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java
@@ -19,6 +19,7 @@ import java.net.URL;
import ghidra.framework.data.ContentHandler;
import ghidra.framework.model.DomainFile;
+import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl;
import ghidra.util.task.TaskMonitor;
/**
@@ -35,7 +36,7 @@ public class ContentTypeQueryTask extends GhidraURLQueryTask {
* (see {@link GhidraURL}).
*/
public ContentTypeQueryTask(URL ghidraUrl) {
- super("Query URL Content Type", ghidraUrl);
+ super("Query URL Content Type", ghidraUrl, null, LinkFileControl.NO_FOLLOW);
}
/**
@@ -54,4 +55,5 @@ public class ContentTypeQueryTask extends GhidraURLQueryTask {
public void processResult(DomainFile domainFile, URL url, TaskMonitor monitor) {
contentType = domainFile.getContentType();
}
+
}
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 53cb117531..c90c8be0f1 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
@@ -250,14 +250,14 @@ public class GhidraURL {
path = "/" + path;
}
else {
- throw new IllegalArgumentException("absolute path required");
+ throw new IllegalArgumentException("Absolute project path required");
}
scanIndex = 3;
}
else if (len >= 3 && hasDriveLetter(path, 1)) {
if (len < 4 || path.charAt(3) != '/') {
// path such as "/c:" not permitted
- throw new IllegalArgumentException("absolute path required");
+ throw new IllegalArgumentException("Absolute project path required");
}
scanIndex = 4;
}
@@ -439,7 +439,7 @@ public class GhidraURL {
return "/";
}
- throw new IllegalArgumentException("not project/repository URL");
+ throw new IllegalArgumentException("Not a project/repository URL");
}
/**
@@ -569,11 +569,13 @@ public class GhidraURL {
}
/**
- * Create a URL which refers to a local Ghidra project with optional project file and ref
+ * Create a URL which refers to a local Ghidra project with optional project folder/file path
+ * and optional reference
* @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)
+ * @param projectFilePath an absolute folder or file path within the project (e.g., /a/b/c, may be null)
+ * @param ref optional location reference (may be null) which is appended to URL with a '#'
+ * delimiter.
* @return local Ghidra project URL
* @throws IllegalArgumentException if an absolute projectLocation path is not specified
*/
@@ -593,7 +595,7 @@ public class GhidraURL {
if (!StringUtils.isBlank(projectFilePath)) {
if (!projectFilePath.startsWith("/") || projectFilePath.contains("\\")) {
- throw new IllegalArgumentException("Invalid project file path");
+ throw new IllegalArgumentException("Absolute path required using '/' delimiter");
}
buf.append("?");
buf.append(projectFilePath);
@@ -611,8 +613,9 @@ public class GhidraURL {
}
/**
- * Create a URL which refers to a local Ghidra project with optional project file and ref
- * @param projectLocator local project locator
+ * Create a URL which refers to a Ghidra project with optional project file and ref.
+ * If project locator corresponds to a transient project a server URL form will be returned.
+ * @param projectLocator project locator (local or transient)
* @param projectFilePath file path (e.g., /a/b/c, may be null)
* @param ref location reference (may be null)
* @return local Ghidra project URL
@@ -620,6 +623,31 @@ public class GhidraURL {
* instantion fails.
*/
public static URL makeURL(ProjectLocator projectLocator, String projectFilePath, String ref) {
+
+ if (projectLocator.isTransient()) {
+
+ // Transient project corresponds to server-based repository
+ String serverUrl = projectLocator.getURL().toExternalForm();
+ if (projectFilePath != null) {
+ if (!projectFilePath.startsWith("/")) {
+ throw new IllegalArgumentException(
+ "Absolute path required using '/' delimiter");
+ }
+ serverUrl += projectFilePath;
+ }
+ if (ref != null) {
+ serverUrl += "#";
+ serverUrl += ref;
+ }
+ try {
+ return new URL(serverUrl);
+ }
+ catch (MalformedURLException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ // Handle local project case
return makeURL(projectLocator.getLocation(), projectLocator.getName(), projectFilePath,
ref);
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java
index cd034c1063..44d5538694 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,9 +17,14 @@ package ghidra.framework.protocol.ghidra;
import java.io.IOException;
import java.net.URL;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
-import ghidra.framework.model.DomainFile;
-import ghidra.framework.model.DomainFolder;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.LinkHandler.LinkStatus;
+import ghidra.framework.data.NullFolderDomainObject;
+import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
@@ -30,46 +35,127 @@ import ghidra.util.task.TaskMonitor;
* queries for processing either a {@link DomainFile} or {@link DomainFolder} that a
* Ghidra URL may reference.
*/
-public abstract class GhidraURLQuery {
+public class GhidraURLQuery {
+
+ /**
+ * {@link LinkFileControl} setting control how link-files will be followed.
+ */
+ public enum LinkFileControl {
+
+ /**
+ * No links are followed and only a single file/folder which corresponds to the URL
+ * will be queried.
+ */
+ NO_FOLLOW,
+
+ /**
+ * All links will be followed to arrive at an end-point
+ */
+
+ FOLLOW_EXTERNAL,
+ /**
+ * Beyond the initial URL only internal links local to the corresponding project or
+ * repository will be followed.
+ */
+ FOLLOW_INTERNAL;
+ }
+
+ /**
+ * When recuring through link-files we must keep track of URLs considered and ensure
+ * we don't encounter a link cycle.
+ */
+ private static final ThreadLocal> linkedUrlSet = ThreadLocal.withInitial(() -> null);
+
+ private final URL ghidraUrl;
+ private final boolean readOnly;
+ private final GhidraURLResultHandler resultHandler;
+ private final LinkFileControl linkFileControl;
+
+ private Class extends DomainObject> contentClass;
+
+ private boolean cleanupUrlSetUponReturn = false;
+
+ private GhidraURLQuery(URL ghidraUrl, Class extends DomainObject> contentClass,
+ boolean readOnly, LinkFileControl linkFileControl,
+ GhidraURLResultHandler resultHandler) {
+ this.ghidraUrl = ghidraUrl;
+ this.contentClass = contentClass;
+ this.readOnly = readOnly;
+ this.resultHandler = resultHandler;
+ this.linkFileControl = linkFileControl;
+ }
/**
* Perform read-only query using specified GhidraURL and process result.
* Both local project and remote repository URLs are supported.
* This method is intended to be invoked from within a {@link Task} or for headless operations.
* @param ghidraUrl local or remote Ghidra URL
+ * @param contentClass expected content class or null. If a folder is expected
+ * {@link NullFolderDomainObject} class should be specified.
* @param resultHandler query result handler
+ * @param linkFileControl controls how or if link files will be followed
* @param monitor task monitor
* @throws IOException if an IO error occurs which was re-thrown by {@code resultHandler}
* @throws CancelledException if task is cancelled
*/
- public static void queryUrl(URL ghidraUrl, GhidraURLResultHandler resultHandler,
+ public static void queryUrl(URL ghidraUrl, Class extends DomainObject> contentClass,
+ GhidraURLResultHandler resultHandler, LinkFileControl linkFileControl,
TaskMonitor monitor) throws IOException, CancelledException {
- doQueryUrl(ghidraUrl, true, resultHandler, monitor);
+ GhidraURLQuery ghidraUrlQuery =
+ new GhidraURLQuery(ghidraUrl, contentClass, true, linkFileControl, resultHandler);
+ ghidraUrlQuery.query(monitor);
}
/**
* Perform query using specified GhidraURL and process result.
* Both local project and remote repository URLs are supported.
- * This method is intended to be invoked from within a {@link Task} or for headless operations.
- * @param ghidraUrl local or remote Ghidra URL
+ * This method is intended to be invoked from within a {@link Task} or for headless operations.
+ * @param ghidraUrl local or remote folder-level Ghidra URL
* @param readOnly allows update/commit (false) or read-only (true) access.
* @param resultHandler query result handler
+ * @param linkFileControl controls how or if link files will be followed
* @param monitor task monitor
* @throws IOException if an IO error occurs which was re-thrown by {@code resultHandler}
* @throws CancelledException if task is cancelled
*/
public static void queryRepositoryUrl(URL ghidraUrl, boolean readOnly,
- GhidraURLResultHandler resultHandler, TaskMonitor monitor)
- throws IOException, CancelledException {
+ GhidraURLResultHandler resultHandler, LinkFileControl linkFileControl,
+ TaskMonitor monitor) throws IOException, CancelledException {
if (!GhidraURL.isServerRepositoryURL(ghidraUrl)) {
throw new IllegalArgumentException("Unsupported repository URL: " + ghidraUrl);
}
- doQueryUrl(ghidraUrl, readOnly, resultHandler, monitor);
+ GhidraURLQuery ghidraUrlQuery = new GhidraURLQuery(ghidraUrl, NullFolderDomainObject.class,
+ readOnly, linkFileControl, resultHandler);
+ ghidraUrlQuery.query(monitor);
}
- private static void doQueryUrl(URL ghidraUrl, boolean readOnly,
- GhidraURLResultHandler resultHandler, TaskMonitor monitor)
- throws IOException, CancelledException {
+ private void query(TaskMonitor monitor) throws IOException, CancelledException {
+
+ try {
+ doQuery(monitor);
+ }
+ finally {
+ if (cleanupUrlSetUponReturn) {
+ // cleanup thread local URL set
+ linkedUrlSet.set(null);
+ }
+ cleanupUrlSetUponReturn = false;
+ }
+ }
+
+ private void doQuery(TaskMonitor monitor) throws IOException, CancelledException {
+
+ URL normalizedUrl = GhidraURL.getNormalizedURL(ghidraUrl);
+
+ Set urls = linkedUrlSet.get();
+ if (urls == null) {
+ urls = new HashSet<>();
+ linkedUrlSet.set(urls);
+ cleanupUrlSetUponReturn = true;
+ }
+ if (!urls.add(normalizedUrl)) {
+ throw new IOException("Circular link reference detected: " + ghidraUrl);
+ }
GhidraURLConnection c;
Object obj = null;
@@ -133,18 +219,12 @@ public abstract class GhidraURLQuery {
return;
}
+ // NOTE: We cannot handle ambiguous folder vs folder URL. A folder-link
+ // may refer to another folder-link or a folder. If duplicate name exists
+ // a failure may occur.
+
monitor.checkCancelled();
- if (content instanceof DomainFile file) {
- resultHandler.processResult(file, ghidraUrl, monitor);
- }
- else if (content instanceof DomainFolder folder) {
- resultHandler.processResult(folder, ghidraUrl, monitor);
- }
- else {
- // unexpected condition
- resultHandler.handleError("Unsupported Content",
- "Content class: " + content.getClass().getName(), ghidraUrl, null);
- }
+ processContent(content, monitor);
}
finally {
if (content != null) {
@@ -154,4 +234,114 @@ public abstract class GhidraURLQuery {
}
}
+ private void processContent(Object content, TaskMonitor monitor)
+ throws IOException, CancelledException {
+ if (content instanceof DomainFile file) {
+
+ if (!checkContentClass(file)) {
+ return;
+ }
+
+ if (linkFileControl != LinkFileControl.NO_FOLLOW && file.isLink()) {
+
+ // Establish content class if not specified to pickup on link inconsistencies
+ if (contentClass == null) {
+ contentClass = file.getDomainObjectClass();
+ }
+
+ // Following link may return null on error or if external link already handled
+ file = followLink(file, monitor);
+ if (file == null) {
+ return;
+ }
+
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ if (linkInfo != null && linkInfo.isFolderLink()) {
+ // Handle folder link as folder
+ DomainFolder folder = linkInfo.getLinkedFolder();
+ if (folder == null) {
+ resultHandler.handleError("Link Resolution Failed",
+ "Unable to follow invalid folder-link", ghidraUrl, null);
+ }
+ else {
+ resultHandler.processResult(folder, ghidraUrl, monitor);
+ }
+ return;
+ }
+ }
+
+ // process file result
+ resultHandler.processResult(file, ghidraUrl, monitor);
+ }
+ else if (content instanceof DomainFolder folder) {
+ if (contentClass != null && contentClass != NullFolderDomainObject.class) {
+ URL url = folder.getLocalProjectURL();
+ if (url == null) {
+ url = folder.getSharedProjectURL();
+ }
+ resultHandler.handleError("Unexpected Content", "Unexpected folder", url, null);
+ }
+ else {
+ // process folder result
+ resultHandler.processResult(folder, ghidraUrl, monitor);
+ }
+ }
+ else {
+ // unexpected condition
+ resultHandler.handleError("Unsupported Content",
+ "Content class: " + content.getClass().getName(), ghidraUrl, null);
+ }
+ }
+
+ private boolean checkContentClass(DomainFile file) throws IOException {
+ Class extends DomainObject> domainObjectClass = file.getDomainObjectClass();
+ if (contentClass != null && !contentClass.isAssignableFrom(file.getDomainObjectClass())) {
+ URL url = file.getLocalProjectURL(null);
+ if (url == null) {
+ url = file.getSharedProjectURL(null);
+ }
+ resultHandler.handleError("Unexpected Content",
+ "File content is " + domainObjectClass.getSimpleName(), url, null);
+ return false;
+ }
+ return true;
+ }
+
+ private DomainFile followLink(DomainFile file, TaskMonitor monitor)
+ throws CancelledException, IOException {
+
+ AtomicReference linkStatus = new AtomicReference<>();
+ AtomicReference errMsg = new AtomicReference<>();
+
+ // Following internal linkage will catch circular internal linkage
+ file =
+ LinkHandler.followInternalLinkage(file, s -> linkStatus.set(s), err -> errMsg.set(err));
+
+ LinkStatus s = linkStatus.get();
+ if (s == LinkStatus.BROKEN) {
+ String msg = errMsg.get();
+ if (msg == null) {
+ msg = "Unable to follow broken link";
+ }
+ resultHandler.handleError("Link Resolution Failed", msg, ghidraUrl, null);
+ return null;
+ }
+
+ if (s == LinkStatus.EXTERNAL) {
+ // file is expected to be an external link-file
+ if (linkFileControl == LinkFileControl.FOLLOW_EXTERNAL) {
+ URL linkURL = LinkHandler.getLinkURL(file);
+ // continue recursion with external link
+ queryUrl(linkURL, contentClass, resultHandler, linkFileControl, monitor);
+ return null;
+ }
+
+ // cannot follow external link
+ resultHandler.externalLinkIgnored(ghidraUrl);
+ return null;
+ }
+
+ return file;
+ }
+
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java
index 9bcb535399..7ad6a250e3 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -19,8 +19,9 @@ import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.URL;
-import ghidra.framework.model.DomainFile;
-import ghidra.framework.model.DomainFolder;
+import ghidra.framework.data.NullFolderDomainObject;
+import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.*;
@@ -42,6 +43,8 @@ import ghidra.util.task.*;
public abstract class GhidraURLQueryTask extends Task implements GhidraURLResultHandler {
private final URL ghidraUrl;
+ private final Class extends DomainObject> contentClass;
+ private final LinkFileControl linkFileControl;
private boolean done = false;
@@ -49,16 +52,22 @@ public abstract class GhidraURLQueryTask extends Task implements GhidraURLResult
* Construct a Ghidra URL read-only query task.
* @param title task dialog title
* @param ghidraUrl Ghidra URL (local or remote)
+ * @param contentClass expected content class or null. If a folder is expected
+ * {@link NullFolderDomainObject} class should be specified.
+ * @param linkFileControl controls how or if link files will be followed
* @throws IllegalArgumentException if specified URL is not a Ghidra URL
* (see {@link GhidraURL}).
*/
- protected GhidraURLQueryTask(String title, URL ghidraUrl) {
+ protected GhidraURLQueryTask(String title, URL ghidraUrl,
+ Class extends DomainObject> contentClass, LinkFileControl linkFileControl) {
super(title, true, false, true);
if (!GhidraURL.isLocalProjectURL(ghidraUrl) &&
!GhidraURL.isServerRepositoryURL(ghidraUrl)) {
throw new IllegalArgumentException("Unsupported URL: " + ghidraUrl);
}
this.ghidraUrl = ghidraUrl;
+ this.contentClass = contentClass;
+ this.linkFileControl = linkFileControl;
}
/**
@@ -77,7 +86,7 @@ public abstract class GhidraURLQueryTask extends Task implements GhidraURLResult
monitor.addCancelledListener(cancelledListener);
try {
- GhidraURLQuery.queryUrl(ghidraUrl, this, monitor);
+ GhidraURLQuery.queryUrl(ghidraUrl, contentClass, this, linkFileControl, monitor);
}
catch (InterruptedIOException e) {
// ignore - assume cancelled
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java
index 61e579318e..ebfd2abcd7 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -76,4 +76,13 @@ public interface GhidraURLResultHandler {
default void handleUnauthorizedAccess(URL url) throws IOException {
// do nothing - assume user has already been notified or issue has been logged
}
+
+ /**
+ * Handle an external link URL which is not followed.
+ * @param url connection URL
+ * @throws IOException may be thrown if handler decides to propogate error
+ */
+ default void externalLinkIgnored(URL url) throws IOException {
+ // do nothing
+ }
}
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 8a435f06cb..4ddc9a5dfc 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,7 +17,6 @@ package ghidra.framework.protocol.ghidra;
import java.io.File;
import java.io.IOException;
-import java.net.URL;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@@ -192,15 +191,10 @@ public class TransientProjectManager {
private final RepositoryInfo repositoryInfo;
TransientProjectStorageLocator(String path, String name, RepositoryInfo repositoryInfo) {
- super(path, name);
+ super(path, name, repositoryInfo.repositoryURL);
this.repositoryInfo = repositoryInfo;
}
- @Override
- public URL getURL() {
- return repositoryInfo.repositoryURL;
- }
-
@Override
public String getName() {
return repositoryInfo.repositoryURL.toExternalForm();
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/util/VersionExceptionHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/util/VersionExceptionHandler.java
index 0e58cf9abc..6b6d82064c 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/util/VersionExceptionHandler.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/util/VersionExceptionHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -18,6 +18,7 @@ package ghidra.util;
import java.awt.Component;
import docking.widgets.OptionDialog;
+import ghidra.framework.data.LinkHandler;
import ghidra.framework.model.DomainFile;
import ghidra.util.exception.VersionException;
@@ -25,10 +26,28 @@ public class VersionExceptionHandler {
public static boolean isUpgradeOK(Component parent, DomainFile domainFile, String actionName,
VersionException ve) {
+
String contentType = domainFile.getContentType();
- if (domainFile.isReadOnly() || ve.getVersionIndicator() != VersionException.OLDER_VERSION ||
- !ve.isUpgradable()) {
- showVersionError(parent, domainFile.getName(), contentType, actionName, ve);
+ if (ve.getVersionIndicator() != VersionException.OLDER_VERSION || !ve.isUpgradable()) {
+ showVersionError(parent, domainFile.getName(), contentType, actionName, false, ve);
+ return false;
+ }
+
+ boolean linkUsed = domainFile.isLink();
+ if (linkUsed) {
+ DomainFile file = LinkHandler.followInternalLinkage(domainFile, s -> {
+ /* ignore */ }, null);
+ if (file.isLink() && file.getLinkInfo().isExternalLink()) {
+ VersionExceptionHandler.showVersionError(null, domainFile.getName(),
+ domainFile.getContentType(), actionName, true, ve);
+ return false;
+ }
+ // redirect error handling to linked file
+ domainFile = file;
+ }
+
+ if (domainFile.isReadOnly() || !domainFile.isInWritableProject()) {
+ showVersionError(parent, domainFile.getName(), contentType, actionName, true, ve);
return false;
}
String filename = domainFile.getName();
@@ -90,8 +109,20 @@ public class VersionExceptionHandler {
OptionDialog.WARNING_MESSAGE);
}
+ /**
+ * Show a version error in response to a content {@link VersionException}.
+ * @param parent popup message parent
+ * @param filename name of file
+ * @param contentType file content type
+ * @param actionName action name (e.g., "Open")
+ * @param readOnly true if read-only, else false. Specify false if not a factor to presenting
+ * the error.
+ * @param ve version exception
+ */
+
public static void showVersionError(final Component parent, final String filename,
- final String contentType, final String actionName, VersionException ve) {
+ final String contentType, final String actionName, boolean readOnly,
+ VersionException ve) {
int versionIndicator = ve.getVersionIndicator();
final String versionType;
@@ -111,7 +142,8 @@ public class VersionExceptionHandler {
}
Msg.showError(VersionExceptionHandler.class, parent, actionName + " Failed!",
- "Unable to " + actionName + " " + contentType + ": " + filename + "\n \n" +
- "File was created with a" + versionType + " version of Ghidra" + upgradeMsg);
+ "Unable to " + actionName + " " + (readOnly ? " read-only " : "") + contentType + ": " +
+ filename + "\n \n" + "File was created with a" + versionType +
+ " version of Ghidra" + upgradeMsg);
}
}
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
index 6722c55c15..9f468b724f 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -27,4 +27,8 @@ public class BadLinkException extends IOException {
super(msg);
}
+ public BadLinkException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
}
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 9a676ed1bf..94dd9180c2 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
@@ -25,12 +25,13 @@ import javax.swing.Icon;
import org.apache.commons.lang3.StringUtils;
-import ghidra.framework.data.CheckinHandler;
+import ghidra.framework.Application;
+import ghidra.framework.data.*;
import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.framework.store.Version;
import ghidra.util.InvalidNameException;
-import ghidra.util.exception.CancelledException;
-import ghidra.util.exception.VersionException;
+import ghidra.util.classfinder.ClassSearcher;
+import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
/**
@@ -38,7 +39,7 @@ import ghidra.util.task.TaskMonitor;
*
* @see TestDummyDomainFolder
*/
-public class TestDummyDomainFile implements DomainFile {
+public class TestDummyDomainFile implements DomainFile, LinkFileInfo {
private String name;
private TestDummyDomainFolder parent;
@@ -47,9 +48,47 @@ public class TestDummyDomainFile implements DomainFile {
private boolean isVersioned;
private boolean isInUse;
+ private String contentType;
+ private Class extends DomainObject> domainObjectClass;
+
+ /**
+ * Construct test file with unknown content-type.
+ *
+ * @param parent parent folder
+ * @param name file name
+ */
public TestDummyDomainFile(TestDummyDomainFolder parent, String name) {
+ this(parent, name, null);
+ }
+
+ /**
+ * Construct test file with a specified content-type. When a content-type other than
+ * {@link ContentHandler#UNKNOWN_CONTENT} is specified the corresponding {@link ContentHandler}
+ * must be available which will require the {@link ClassSearcher} to be active with
+ * appropriate {@link Application} initialization.
+ *
+ * NOTE: Support for a link-file will require a derived implementation.
+ *
+ * @param parent parent folder
+ * @param name file name
+ * @param fileContentType {@link DomainObject} content-type as specified by corresponding
+ * {@link ContentHandler} implementation class.
+ */
+ public TestDummyDomainFile(TestDummyDomainFolder parent, String name, String fileContentType) {
this.parent = parent;
this.name = name;
+ contentType = fileContentType != null ? fileContentType : ContentHandler.UNKNOWN_CONTENT;
+ domainObjectClass = DomainObject.class;
+ if (!ContentHandler.UNKNOWN_CONTENT.equals(contentType)) {
+ try {
+ ContentHandler> ch = DomainObjectAdapter.getContentHandler(contentType);
+ domainObjectClass = ch.getDomainObjectClass();
+ }
+ catch (IOException e) {
+ // Ensure corresponding content-handler has been found by ClassSearcher.
+ throw new AssertException("Unsupported content type: " + contentType);
+ }
+ }
}
public void setInUse() {
@@ -111,24 +150,36 @@ public class TestDummyDomainFile implements DomainFile {
throw new UnsupportedOperationException();
}
+ ContentHandler> getContentHandler() throws IOException {
+ if (contentType == null) {
+ throw new UnsupportedOperationException();
+ }
+ return DomainObjectAdapter.getContentHandler(contentType);
+ }
+
@Override
public String getContentType() {
- throw new UnsupportedOperationException();
+ return contentType;
}
@Override
- public boolean isLinkFile() {
- return false;
+ public boolean isLink() {
+ try {
+ return getContentHandler() instanceof LinkHandler;
+ }
+ catch (IOException e) {
+ return false; // unknown content
+ }
}
@Override
- public DomainFolder followLink() {
- throw new UnsupportedOperationException();
+ public LinkFileInfo getLinkInfo() {
+ return isLink() ? this : null;
}
@Override
public Class extends DomainObject> getDomainObjectClass() {
- throw new UnsupportedOperationException();
+ return domainObjectClass;
}
@Override
@@ -347,7 +398,7 @@ public class TestDummyDomainFile implements DomainFile {
}
@Override
- public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
+ public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
throw new UnsupportedOperationException();
}
@@ -405,4 +456,28 @@ public class TestDummyDomainFile implements DomainFile {
}
return name;
}
+
+ //
+ // LinkFileInfo methods
+ //
+
+ @Override
+ public DomainFile getFile() {
+ return this;
+ }
+
+ @Override
+ public LinkedGhidraFolder getLinkedFolder() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getLinkPath() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getAbsoluteLinkPath() throws IOException {
+ throw new UnsupportedOperationException();
+ }
}
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 82052b474e..dadf407eea 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -23,6 +23,7 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils;
+import ghidra.framework.data.*;
import ghidra.framework.store.FolderNotEmptyException;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.CancelledException;
@@ -54,6 +55,16 @@ public class TestDummyDomainFolder implements DomainFolder {
throw new UnsupportedOperationException();
}
+ @Override
+ public boolean isSame(DomainFolder folder) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isSameOrAncestor(DomainFolder folder) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public synchronized String getName() {
return folderName;
@@ -95,7 +106,7 @@ public class TestDummyDomainFolder implements DomainFolder {
@Override
public boolean isInWritableProject() {
- throw new UnsupportedOperationException();
+ return parent != null ? parent.isInWritableProject() : false;
}
@Override
@@ -131,7 +142,14 @@ public class TestDummyDomainFolder implements DomainFolder {
@Override
public synchronized DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
- DomainFile file = new TestDummyDomainFile(this, name);
+
+ String contentType = ContentHandler.UNKNOWN_CONTENT;
+ if (obj != null) {
+ ContentHandler> ch = DomainObjectAdapter.getContentHandler(obj);
+ contentType = ch.getContentType();
+ }
+
+ DomainFile file = new TestDummyDomainFile(this, name, contentType);
files.add(file);
return file;
}
@@ -143,9 +161,20 @@ public class TestDummyDomainFolder implements DomainFolder {
}
@Override
- public synchronized DomainFolder createFolder(String name)
- throws InvalidNameException, IOException {
- DomainFolder folder = new TestDummyDomainFolder(this, name);
+ public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname,
+ boolean makeRelative, String linkFilename, LinkHandler> lh) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler> lh)
+ throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public synchronized TestDummyDomainFolder createFolder(String name) {
+ TestDummyDomainFolder folder = new TestDummyDomainFolder(this, name);
subFolders.add(folder);
return folder;
}
@@ -174,7 +203,7 @@ public class TestDummyDomainFolder implements DomainFolder {
}
@Override
- public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
+ public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException {
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 32921a18bd..5ff2a78e7a 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
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -42,6 +42,12 @@ public class TestDummyProjectData implements ProjectData {
@Override
public DomainFolder getFolder(String path) {
+ // stub
+ return getFolder(path, DomainFolderFilter.ALL_INTERNAL_FOLDERS_FILTER);
+ }
+
+ @Override
+ public DomainFolder getFolder(String path, DomainFolderFilter filter) {
// stub
return null;
}
@@ -54,6 +60,12 @@ public class TestDummyProjectData implements ProjectData {
@Override
public DomainFile getFile(String path) {
+ // stub
+ return getFile(path, DomainFileFilter.ALL_INTERNAL_FILES_FILTER);
+ }
+
+ @Override
+ public DomainFile getFile(String path, DomainFileFilter filter) {
// stub
return null;
}
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
index 7c64fdf1ac..bebd6b64f0 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,32 +15,16 @@
*/
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";
+ public static DataTypeArchiveLinkContentHandler INSTANCE =
+ new DataTypeArchiveLinkContentHandler();
- @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);
- }
+ public static final String ARCHIVE_LINK_CONTENT_TYPE = "ArchiveLink";
@Override
public String getContentType() {
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
index 09d02a94c1..a5f7ee1e3c 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,32 +15,15 @@
*/
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";
+ public static ProgramLinkContentHandler INSTANCE = new ProgramLinkContentHandler();
- @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);
- }
+ public static final String PROGRAM_LINK_CONTENT_TYPE = "ProgramLink";
@Override
public String getContentType() {
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/CodeManager.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/CodeManager.java
index 1055d9d138..e63f4ce315 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/CodeManager.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/CodeManager.java
@@ -1160,7 +1160,7 @@ public class CodeManager implements ErrorHandler, ManagerDB {
* Get an iterator that contains the code units which have the specified property type defined.
* Only code units starting within the address set specified will be returned by the iterator.
* If the address set is null then check the entire program.
- *
+ *
* Standard property types are defined in the CodeUnit class. The property types are:
*
* - REFERENCE_PROPERTY
diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ExporterPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ExporterPluginScreenShots.java
index 3326e43179..f7994f80c7 100644
--- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ExporterPluginScreenShots.java
+++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ExporterPluginScreenShots.java
@@ -15,6 +15,7 @@
*/
package help.screenshot;
+import java.awt.Dialog;
import java.io.IOException;
import javax.swing.JComboBox;
@@ -27,6 +28,7 @@ import ghidra.app.util.exporter.Exporter;
import ghidra.framework.model.*;
import ghidra.framework.preferences.Preferences;
import ghidra.program.model.listing.Program;
+import ghidra.util.Swing;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor;
@@ -40,10 +42,15 @@ public class ExporterPluginScreenShots extends GhidraScreenShotGenerator {
Preferences.setProperty(Preferences.LAST_EXPORT_DIRECTORY, "/path");
DomainFile df = createDomainFile();
- ExporterDialog dialog = new ExporterDialog(tool, df);
- runSwing(() -> tool.showDialog(dialog), false);
+
+ runSwing(() -> ExporterDialog.show(tool, df), false);
+
+ Dialog dialog = waitForJDialog("Export Program_A");
+
waitForSwing();
captureDialog(dialog);
+
+ Swing.runNow(() -> dialog.dispose());
}
@Test
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 b960285efc..6fe25c5e00 100644
--- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java
+++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java
@@ -37,8 +37,7 @@ import docking.wizard.WizardDialog;
import generic.theme.GThemeDefaults.Colors;
import ghidra.app.plugin.core.archive.RestoreDialog;
import ghidra.framework.Application;
-import ghidra.framework.data.DefaultProjectData;
-import ghidra.framework.data.GhidraFileData;
+import ghidra.framework.data.*;
import ghidra.framework.main.*;
import ghidra.framework.main.wizard.project.*;
import ghidra.framework.model.*;
@@ -56,10 +55,14 @@ import ghidra.util.exception.CancelledException;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.task.TaskMonitor;
import resources.MultiIcon;
+import resources.icons.TranslateIcon;
public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
+
+ private static final String RIGHT_ARROW = "\u2b95";
private static final String OTHER_PROJECT = "Other_Project";
- Icon icon = (Icon) getInstanceField("CONVERT_ICON", ProjectChooseRepositoryWizardModel.class);
+ private Icon icon =
+ (Icon) getInstanceField("CONVERT_ICON", ProjectChooseRepositoryWizardModel.class);
public FrontEndPluginScreenShots() {
super();
@@ -139,8 +142,7 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
TestDummyWizardModel panelMgr =
new TestDummyWizardModel(panel, false, true, false,
- "Change Shared Project Information", 600, 375,
- new ProjectWizardData(), icon);
+ "Change Shared Project Information", 600, 375, new ProjectWizardData(), icon);
WizardDialog wizard = new WizardDialog(panelMgr, false);
wizard.show();
@@ -156,9 +158,8 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
ProjectWizardData data = new ProjectWizardData();
data.setServerInfo(new ServerInfo("server1", 13100));
- TestDummyWizardModel panelMgr =
- new TestDummyWizardModel<>(panel, false, true, false,
- "Change Shared Project Information", 600, 180, data, icon);
+ TestDummyWizardModel panelMgr = new TestDummyWizardModel<>(panel, false,
+ true, false, "Change Shared Project Information", 600, 180, data, icon);
WizardDialog wizard = new WizardDialog(panelMgr, false);
wizard.show();
@@ -326,6 +327,40 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
captureIconAndText(multiIcon, "Example");
}
+ @Test
+ public void testAbsoluteFileLinkIcon() {
+ Icon programIcon = ProgramContentHandler.PROGRAM_ICON;
+ MultiIcon multiIcon = new MultiIcon(programIcon);
+ multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1));
+ captureIconAndText(multiIcon, "Example " + RIGHT_ARROW + " /data/Example");
+ }
+
+ @Test
+ public void testAbsoluteBrokenFileLinkIcon() {
+ Icon programIcon = ProgramContentHandler.PROGRAM_ICON;
+ MultiIcon multiIcon = new MultiIcon(programIcon);
+ multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1));
+ Icon linkIcon = new BrokenLinkIcon(multiIcon);
+ captureIconAndText(linkIcon, "Example " + RIGHT_ARROW + " /data/Example");
+ }
+
+ @Test
+ public void testAbsoluteFolderLinkIcon() {
+ Icon folderIcon = DomainFolder.CLOSED_FOLDER_ICON;
+ MultiIcon multiIcon = new MultiIcon(folderIcon);
+ multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1));
+ captureIconAndText(multiIcon, "Example " + RIGHT_ARROW + " /data/Example");
+ }
+
+ @Test
+ public void testAbsoluteBrokenFolderLinkIcon() {
+ Icon folderIcon = DomainFolder.CLOSED_FOLDER_ICON;
+ MultiIcon multiIcon = new MultiIcon(folderIcon);
+ multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1));
+ Icon linkIcon = new BrokenLinkIcon(multiIcon);
+ captureIconAndText(linkIcon, "Example " + RIGHT_ARROW + " /data/Example");
+ }
+
@Test
public void testProjectDataTable()
throws CancelledException, IOException, InvalidNameException {
@@ -337,8 +372,7 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
FrontEndPlugin plugin = getPlugin(tool, FrontEndPlugin.class);
JComponent projectDataPanel = (JComponent) getInstanceField("projectDataPanel", plugin);
- JTabbedPane tabbedPane =
- (JTabbedPane) getInstanceField("projectTab", projectDataPanel);
+ JTabbedPane tabbedPane = (JTabbedPane) getInstanceField("projectTab", projectDataPanel);
tabbedPane.setSelectedIndex(1);
setToolSize(800, 600);
captureComponent(projectDataPanel);
@@ -407,8 +441,7 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
TestDummyWizardModel panelMgr =
new TestDummyWizardModel(panel, false, true, false,
- "Specify Repository Name on Server1", 600, 375,
- new ProjectWizardData(), icon);
+ "Specify Repository Name on Server1", 600, 375, new ProjectWizardData(), icon);
WizardDialog wizard = new WizardDialog(panelMgr, false);
@@ -694,13 +727,12 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
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);
+ DomainFile otherFile = ProjectTestUtils.createProgramFile(otherProject, "Program1",
+ language, language.getDefaultCompilerSpec(), null);
ProjectTestUtils.createProgramFile(otherProject, "Program2", language,
language.getDefaultCompilerSpec(), null);
- otherFile.copyToAsLink(projectData.getRootFolder());
+ otherFile.copyToAsLink(projectData.getRootFolder(), false);
otherProject.close();
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginOpenProgramActionsTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginOpenProgramActionsTest.java
new file mode 100644
index 0000000000..a6053e3a06
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginOpenProgramActionsTest.java
@@ -0,0 +1,411 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.framework.main.datatree;
+
+import static org.junit.Assert.*;
+
+import java.awt.Rectangle;
+import java.awt.Window;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.*;
+import javax.swing.tree.TreePath;
+
+import org.junit.*;
+
+import docking.ActionContext;
+import docking.ComponentProvider;
+import docking.action.DockingActionIf;
+import docking.test.AbstractDockingTest;
+import docking.tool.ToolConstants;
+import docking.widgets.tree.GTreeNode;
+import ghidra.framework.main.FrontEndTool;
+import ghidra.framework.main.datatable.ProjectDataContext;
+import ghidra.framework.model.*;
+import ghidra.framework.options.ToolOptions;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.program.model.listing.Program;
+import ghidra.test.*;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * Tests for opening files.
+ */
+public class FrontEndPluginOpenProgramActionsTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private FrontEndTool frontEndTool;
+ private TestEnv env;
+ private DataTree tree;
+ private DomainFolder rootFolder;
+ private GTreeNode rootNode;
+
+ @Before
+ public void setUp() throws Exception {
+ env = new TestEnv();
+ env.resetDefaultTools();
+
+ frontEndTool = env.getFrontEndTool();
+ env.showFrontEndTool();
+ setErrorGUIEnabled(false);
+
+ tree = findComponent(frontEndTool.getToolFrame(), DataTree.class);
+ rootFolder = env.getProject().getProjectData().getRootFolder();
+ Program p = ToyProgramBuilder.buildSimpleProgram("sample", this);
+ rootFolder.createFile("sample", p, TaskMonitor.DUMMY);
+ p.release(this);
+
+ p = ToyProgramBuilder.buildSimpleProgram("x", this);
+ rootFolder.createFile("X", p, TaskMonitor.DUMMY);
+ p.release(this);
+
+ rootNode = tree.getViewRoot();
+
+ waitForSwing();
+ tree.expandPath(rootNode.getTreePath());
+ waitForTree();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ waitForTree();
+ env.dispose();
+ }
+
+ @Test
+ public void testOpenActionsEnabled() throws Exception {
+ setSelectionPath(rootNode.getTreePath());
+ DockingActionIf openAction = getAction("Open File");
+ assertTrue(!openAction.isEnabledForContext(getDomainFileActionContext(rootNode)));
+
+ ToolChest tc = env.getProject().getLocalToolChest();
+ ToolTemplate[] configs = tc.getToolTemplates();
+ for (ToolTemplate config : configs) {
+ DockingActionIf action = getAction("Open" + config.getName());
+ assertTrue(!action.isEnabledForContext(getDomainFileActionContext(rootNode)));
+ assertTrue(!openAction.isEnabledForContext(getDomainFileActionContext(rootNode)));
+ }
+ }
+
+ @Test
+ public void testOpenInDefaultTool() throws Exception {
+ //Open File
+ GTreeNode npNode = rootNode.getChild("sample");
+ setSelectionPath(npNode.getTreePath());
+ waitForTree();
+ DockingActionIf openAction = getAction("Open File");
+ performAction(openAction, getFrontEndContext(), true);
+ verifyToolExistsAndCloseTool();
+ }
+
+ @Test
+ public void testOpenInDefaultToolMultipleNewTool() throws Exception {
+
+ ToolOptions options = frontEndTool.getOptions(ToolConstants.TOOL_OPTIONS);
+ options.setEnum(FrontEndTool.DEFAULT_TOOL_LAUNCH_MODE, DefaultLaunchMode.NEW_TOOL);
+
+ //Open 1st File
+ DomainFile sampleDf = openInDefaultTool("sample");
+ PluginTool[] runningTools = env.getProject().getToolManager().getRunningTools();
+ assertEquals(1, runningTools.length);
+ assertOpenFiles(runningTools[0], sampleDf);
+
+ //Open 2nd File in new tool
+ DomainFile xDf = openInDefaultTool("X");
+
+ // NOTE: runningTools order may vary
+ runningTools = env.getProject().getToolManager().getRunningTools();
+ assertEquals(2, runningTools.length);
+ DomainFile[] domainFiles0 = runningTools[0].getDomainFiles();
+ assertEquals(1, domainFiles0.length);
+ DomainFile[] domainFiles1 = runningTools[1].getDomainFiles();
+ assertEquals(1, domainFiles1.length);
+ if (sampleDf.equals(domainFiles0[0])) {
+ assertEquals(xDf, domainFiles1[0]);
+ }
+ else if (sampleDf.equals(domainFiles1[0])) {
+ assertEquals(xDf, domainFiles0[0]);
+ }
+ else {
+ fail("Unexpected open domain files");
+ }
+
+ exitTools(runningTools);
+ }
+
+ @Test
+ public void testOpenInDefaultToolMultipleReuseTool() throws Exception {
+
+ ToolOptions options = frontEndTool.getOptions(ToolConstants.TOOL_OPTIONS);
+ options.setEnum(FrontEndTool.DEFAULT_TOOL_LAUNCH_MODE, DefaultLaunchMode.REUSE_TOOL);
+
+ //Open 1st File
+ DomainFile sampleDf = openInDefaultTool("sample");
+ PluginTool[] runningTools = env.getProject().getToolManager().getRunningTools();
+ assertEquals(1, runningTools.length);
+ assertOpenFiles(runningTools[0], sampleDf);
+
+ //Open 2nd File in same tool
+ DomainFile xDf = openInDefaultTool("X");
+ runningTools = env.getProject().getToolManager().getRunningTools();
+ assertEquals(1, runningTools.length);
+ assertOpenFiles(runningTools[0], sampleDf, xDf);
+
+ exitTools(runningTools);
+ }
+
+ @Test
+ public void testOpenMultipleNewTool() throws Exception {
+
+ Program p = ToyProgramBuilder.buildSimpleProgram("y", this);
+ rootFolder.createFile("Y", p, TaskMonitor.DUMMY);
+ p.release(this);
+
+ ToolOptions options = frontEndTool.getOptions(ToolConstants.TOOL_OPTIONS);
+ options.setEnum(FrontEndTool.DEFAULT_TOOL_LAUNCH_MODE, DefaultLaunchMode.NEW_TOOL);
+
+ //Open 1st File
+ DomainFile sampleDf = openInDefaultTool("sample");
+ PluginTool[] runningTools = env.getProject().getToolManager().getRunningTools();
+ assertEquals(1, runningTools.length);
+ assertOpenFiles(runningTools[0], sampleDf);
+
+ String toolName = runningTools[0].getName();
+
+ //Open two additional files in new tool
+ openInTool(toolName, "X", "Y");
+
+ // NOTE: runningTools order may vary
+ runningTools = env.getProject().getToolManager().getRunningTools();
+ assertEquals(2, runningTools.length);
+
+ DomainFile[] domainFiles0 = runningTools[0].getDomainFiles();
+ DomainFile[] domainFiles1 = runningTools[1].getDomainFiles();
+ assertEquals(3, domainFiles0.length + domainFiles1.length);
+
+ exitTools(runningTools);
+ }
+
+ @Test
+ public void testOpenMultipleReuseTool() throws Exception {
+
+ Program p = ToyProgramBuilder.buildSimpleProgram("y", this);
+ rootFolder.createFile("Y", p, TaskMonitor.DUMMY);
+ p.release(this);
+
+ ToolOptions options = frontEndTool.getOptions(ToolConstants.TOOL_OPTIONS);
+ options.setEnum(FrontEndTool.DEFAULT_TOOL_LAUNCH_MODE, DefaultLaunchMode.REUSE_TOOL);
+
+ //Open 1st File
+ DomainFile sampleDf = openInDefaultTool("sample");
+ PluginTool[] runningTools = env.getProject().getToolManager().getRunningTools();
+ assertEquals(1, runningTools.length);
+ assertOpenFiles(runningTools[0], sampleDf);
+
+ String toolName = runningTools[0].getName();
+
+ //Open two additional files in same tool
+ openInTool(toolName, "X", "Y");
+
+ runningTools = env.getProject().getToolManager().getRunningTools();
+ assertEquals(1, runningTools.length);
+
+ DomainFile[] domainFiles0 = runningTools[0].getDomainFiles();
+ assertEquals(3, domainFiles0.length);
+
+ exitTools(runningTools);
+ }
+
+ @Test
+ public void testOpenWith() throws Exception {
+
+ GTreeNode npNode = rootNode.getChild("sample");
+ setSelectionPath(npNode.getTreePath());
+ waitForTree();
+
+ ToolChest tc = env.getProject().getLocalToolChest();
+ ToolTemplate[] configs = tc.getToolTemplates();
+
+ DockingActionIf action = getAction("Open" + configs[0].getName());
+ performAction(action, getFrontEndContext(), true);
+ verifyToolExistsAndCloseTool();
+ }
+
+ @Test
+ public void testOpenWithDoubleClick() throws Exception {
+ // make sure that the Code Browser tool is the default
+ ToolChest tc = env.getProject().getLocalToolChest();
+ ToolTemplate[] configs = tc.getToolTemplates();
+ ToolTemplate codeBrowserConfig = null;
+ for (ToolTemplate config : configs) {
+ if ("CodeBrowser".equals(config.getName())) {
+ codeBrowserConfig = config;
+ }
+ }
+
+ if (codeBrowserConfig == null) {
+ Assert.fail("Unable to find the Code Browser config file.");
+ }
+
+ // double click on the program node
+ GTreeNode npNode = rootNode.getChild("sample");
+ JTree jTree = (JTree) invokeInstanceMethod("getJTree", tree);
+ Rectangle rect = jTree.getPathBounds(npNode.getTreePath());
+ setSelectionPath(npNode.getTreePath());
+ waitForTree();
+
+ clickMouse(jTree, MouseEvent.BUTTON1, rect.x, rect.y, 2, 0);
+
+ // make sure that the tool is loaded and processes all of the tasks it launches
+ Window window = waitForToolLaunch();
+
+ // DEBUG:
+ if (window == null) {
+ // see if any tools have been launched
+ PluginTool[] runningTools = frontEndTool.getToolServices().getRunningTools();
+ for (PluginTool tool : runningTools) {
+ System.err.println("\t\"" + tool.getName() + "\"");
+ JFrame toolFrame = tool.getToolFrame();
+ System.err.println("\t\twith window: " + toolFrame.getTitle());
+ }
+
+ System.err.println("Open Windows: ");
+ System.err.println(getOpenWindowsAsString());
+ }
+
+ assertNotNull(window);
+ waitForBusyTool(env.getProject().getToolManager().getRunningTools()[0]);
+ waitForTasks();
+
+ verifyToolExistsAndCloseTool();
+ }
+
+ private Window waitForToolLaunch() {
+
+ waitForSwing();
+
+ long start = System.currentTimeMillis();
+ int tryCount = 0;
+ Window window = null;
+ while (window == null && tryCount < 5) {
+ ++tryCount;
+ window = waitForValueWithoutFailing(() -> {
+ return getWindowByTitleContaining(null,
+ "CodeBrowser: " + PROJECT_NAME + ":/sample");
+ });
+ }
+
+ long total = System.currentTimeMillis() - start;
+ assertNotNull("Timed-out waiting for tool - " + total + " ms", window);
+ return window;
+ }
+
+//==================================================================================================
+// Private Methods
+//==================================================================================================
+
+ private ActionContext getFrontEndContext() {
+ ComponentProvider provider = env.getFrontEndProvider();
+ return runSwing(() -> provider.getActionContext(null));
+ }
+
+ private DomainFile openInDefaultTool(String fileName) throws Exception {
+ GTreeNode node = rootNode.getChild(fileName);
+ assertTrue("Expected domain file node", node instanceof DomainFileNode);
+ DomainFileNode fileNode = (DomainFileNode) node;
+ DomainFile domainFile = fileNode.getDomainFile();
+ setSelectionPath(fileNode.getTreePath());
+ waitForTree();
+ DockingActionIf openAction = getAction("Open File");
+ performAction(openAction, getFrontEndContext(), true);
+ return domainFile;
+ }
+
+ private List openInTool(String toolName, String... fileNames) throws Exception {
+
+ ToolServices toolServices = env.getProject().getToolServices();
+
+ ArrayList domainFiles = new ArrayList<>();
+ for (String fileName : fileNames) {
+ DomainFile df = rootFolder.getFile(fileName);
+ assertNotNull(df);
+ domainFiles.add(df);
+ }
+
+ runSwing(() -> toolServices.launchTool(toolName, domainFiles));
+ waitForSwing();
+ return domainFiles;
+ }
+
+ private void assertOpenFiles(PluginTool tool, DomainFile... expectedDomainFiles) {
+ DomainFile[] domainFiles = tool.getDomainFiles();
+ assertArrayEquals(expectedDomainFiles, domainFiles);
+ }
+
+ private void exitTools(PluginTool... tools) {
+ runSwing(() -> {
+ for (PluginTool t : tools) {
+ t.close();
+ }
+ });
+ }
+
+ private void verifyToolExistsAndCloseTool() {
+ PluginTool[] runningTools = env.getProject().getToolManager().getRunningTools();
+ assertEquals(1, runningTools.length);
+ exitTools(runningTools[0]);
+ }
+
+ private ActionContext getDomainFileActionContext(GTreeNode... nodes) {
+ List fileList = new ArrayList<>();
+ List folderList = new ArrayList<>();
+ for (GTreeNode node : nodes) {
+ if (node instanceof DomainFileNode fileNode) {
+ fileList.add(fileNode.getDomainFile());
+ }
+ else if (node instanceof DomainFolderNode folderNode) {
+ folderList.add(folderNode.getDomainFolder());
+ }
+ }
+
+ return new ProjectDataContext(null, null, nodes[0], folderList, fileList, tree, true);
+
+ }
+
+ private DockingActionIf getAction(String actionName) {
+ DockingActionIf action =
+ AbstractDockingTest.getAction(frontEndTool, "FrontEndPlugin", actionName);
+ return action;
+ }
+
+ private void setSelectionPath(final TreePath path) throws Exception {
+ SwingUtilities.invokeAndWait(() -> tree.setSelectionPath(path));
+ }
+
+ private void waitForTree() {
+ waitForSwing();
+ while (tree.isBusy()) {
+ try {
+ Thread.sleep(10);
+ }
+ catch (InterruptedException e) {
+ // don't care
+ }
+ }
+ waitForSwing();
+ }
+}
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteFromRepositoryTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteFromRepositoryTest.java
new file mode 100644
index 0000000000..6cd77609b6
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteFromRepositoryTest.java
@@ -0,0 +1,674 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.framework.main.datatree;
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+
+import org.junit.*;
+
+import docking.ActionContext;
+import docking.action.DockingActionIf;
+import ghidra.framework.client.ClientUtil;
+import ghidra.framework.data.FolderLinkContentHandler;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.LinkHandler.LinkStatus;
+import ghidra.framework.model.*;
+import ghidra.framework.protocol.ghidra.*;
+import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl;
+import ghidra.program.database.ProgramDB;
+import ghidra.program.database.ProgramLinkContentHandler;
+import ghidra.server.remote.ServerTestUtil;
+import ghidra.test.*;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+import utilities.util.FileUtilities;
+
+public class ProjectCopyPasteFromRepositoryTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private String testDirPath;
+ private File serverRoot;
+ private URL viewURL;
+
+ private FrontEndTestEnv env;
+
+ @Before
+ public void setUp() throws Exception {
+ testDirPath = getTestDirectoryPath();
+
+ env = new FrontEndTestEnv();
+
+ startServer();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ env.dispose();
+
+ killServer();
+
+ ClientUtil.clearRepositoryAdapter("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT);
+ }
+
+ private void killServer() {
+
+ if (serverRoot == null) {
+ return;
+ }
+
+ ServerTestUtil.disposeServer();
+
+ FileUtilities.deleteDir(serverRoot);
+ }
+
+ private void startServer() throws Exception {
+
+ // Authorized user "test" is predefined within TestServer.zip
+ ServerTestUtil.setLocalUser("test");
+
+ // Create server instance
+ serverRoot = new File(testDirPath, "TestServer");
+
+ ServerTestUtil.createPopulatedTestServer(serverRoot.getAbsolutePath());
+
+ ServerTestUtil.startServer(serverRoot.getAbsolutePath(),
+ ServerTestUtil.GHIDRA_TEST_SERVER_PORT, -1, false, false, false);
+
+ viewURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT, "Test");
+
+ addLinkedServerContent();
+ }
+
+ private void addRepoView() throws IOException {
+
+ Project project = env.getFrontEndTool().getProject();
+ ProjectData projectData = project.addProjectView(viewURL, true);
+ assertNotNull(projectData);
+ assertEquals(viewURL, projectData.getProjectLocator().getURL());
+
+ // validate the view was added to project
+ ProjectLocator[] projViews = project.getProjectViews();
+ assertEquals(1, projViews.length);
+ }
+
+ private void addLinkedServerContent() throws Exception {
+
+ /**
+ * Initial server files:
+ * /foo
+ * /notepad
+ * /f1/bash
+ */
+
+ GhidraURLQuery.queryRepositoryUrl(viewURL, false, new GhidraURLResultHandlerAdapter() {
+ @Override
+ public void processResult(DomainFolder serverRootFolder, URL url, TaskMonitor monitor)
+ throws IOException, CancelledException {
+
+ //
+ // Add folder link: /f1Link -> f1
+ //
+ DomainFile linkFile =
+ serverRootFolder.createLinkFile(serverRootFolder.getProjectData(), "/f1", true,
+ "f1Link", FolderLinkContentHandler.INSTANCE);
+ assertNotNull(linkFile);
+ assertTrue(linkFile.isLink() && linkFile.getLinkInfo().isFolderLink());
+ assertEquals("f1", linkFile.getLinkInfo().getLinkPath());
+ linkFile.addToVersionControl("Add Folder Link", false, monitor);
+
+ //
+ // Add file link: /bashLink -> f1/bash
+ //
+ linkFile = serverRootFolder.createLinkFile(serverRootFolder.getProjectData(),
+ "/f1/bash", true, "bashLink", ProgramLinkContentHandler.INSTANCE);
+ assertNotNull(linkFile);
+ assertTrue(linkFile.isLink() && !linkFile.getLinkInfo().isFolderLink());
+ assertEquals("f1/bash", linkFile.getLinkInfo().getLinkPath());
+ linkFile.addToVersionControl("Add File Link", false, monitor);
+ }
+ }, LinkFileControl.NO_FOLLOW, TaskMonitor.DUMMY);
+
+ }
+
+ @Test
+ public void testCopyPasteExternalFile() throws Exception {
+
+ env.getRootFolder().createFolder("xyz");
+
+ addRepoView();
+
+ env.waitForTree();
+
+ //
+ // Select foo file from viewed repository and Copy
+ //
+ DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm());
+ assertNotNull("repo data tree view not found", viewTreeHelper);
+
+ DomainFileNode fooFile = viewTreeHelper.waitForFileNode("/foo");
+
+ final ActionContext copyActionContext = viewTreeHelper.getDomainFileActionContext(fooFile);
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ //
+ // Select xyz folder and perform Paste
+ //
+ DomainFolderNode xyzNode = env.waitForFolderNode("/xyz");
+
+ final ActionContext pasteActionContext = env.getDomainFileActionContext(xyzNode);
+
+ DockingActionIf pasteAction = getAction(env.getFrontEndTool(), "Paste");
+ assertNotNull("Paste action not found", pasteAction);
+
+ assertTrue(pasteAction.isAddToPopup(pasteActionContext));
+ assertTrue(pasteAction.isEnabledForContext(pasteActionContext));
+ runSwing(() -> pasteAction.actionPerformed(pasteActionContext));
+
+ //
+ // Verify paste of external file from repository to active project
+ //
+ DomainFileNode fooCopyNode = env.waitForFileNode("/xyz/foo");
+ DomainFile file = fooCopyNode.getDomainFile();
+ assertTrue(file.exists());
+ assertFalse(file.isLink());
+
+ assertEquals(LinkStatus.NON_LINK, LinkHandler.getLinkFileStatus(file, null));
+ }
+
+ @Test
+ public void testCopyPasteExternalLinkFile() throws Exception {
+
+ env.getRootFolder().createFolder("xyz");
+
+ addRepoView();
+
+ env.waitForTree();
+
+ //
+ // Select bashLink link-file from viewed repository and Copy
+ //
+ DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm());
+ assertNotNull("repo data tree view not found", viewTreeHelper);
+
+ DomainFileNode bashLinkFile = viewTreeHelper.waitForFileNode("/bashLink");
+
+ final ActionContext copyActionContext =
+ viewTreeHelper.getDomainFileActionContext(bashLinkFile);
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ //
+ // Select xyz folder and perform Paste
+ //
+ DomainFolderNode xyzNode = env.waitForFolderNode("/xyz");
+
+ final ActionContext pasteActionContext = env.getDomainFileActionContext(xyzNode);
+
+ DockingActionIf pasteAction = getAction(env.getFrontEndTool(), "Paste");
+ assertNotNull("Paste action not found", pasteAction);
+
+ assertTrue(pasteAction.isAddToPopup(pasteActionContext));
+ assertTrue(pasteAction.isEnabledForContext(pasteActionContext));
+ runSwing(() -> pasteAction.actionPerformed(pasteActionContext));
+
+ //
+ // Verify paste of external link-file from repository to active project
+ //
+ DomainFileNode bashLinkCopyNode = env.waitForFileNode("/xyz/bashLink");
+ DomainFile file = bashLinkCopyNode.getDomainFile();
+ assertTrue(file.exists());
+ assertTrue(file.isLink());
+
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(file, null));
+ assertFalse(file.getLinkInfo().isFolderLink());
+
+ //
+ // Verify external URL to the link referenced file is applied with normal copy
+ //
+ assertEquals(viewURL + "/f1/bash", file.getLinkInfo().getLinkPath());
+ }
+
+ @Test
+ public void testCopyPasteExternalFileAsLink() throws Exception {
+
+ env.getRootFolder().createFolder("abc");
+
+ addRepoView();
+
+ env.waitForTree();
+
+ //
+ // Select /foo file from viewed project and Copy
+ //
+ DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm());
+ assertNotNull("repo data tree view not found", viewTreeHelper);
+
+ DomainFileNode fooFile = viewTreeHelper.waitForFileNode("/foo");
+
+ final ActionContext copyActionContext = viewTreeHelper.getDomainFileActionContext(fooFile);
+
+ URL sharedFileURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
+ "Test", "/foo", null);
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ //
+ // Select /abc folder and perform Paste Link
+ //
+ DomainFolderNode abcNode = env.waitForFolderNode("/abc");
+
+ final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(abcNode);
+
+ DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), "Paste Link");
+ assertNotNull("Paste Link action not found", pasteLinkAction);
+
+ assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext));
+ assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext));
+ runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext));
+
+ //
+ // Verify external file paste as link
+ //
+ DomainFileNode fooCopyNode = env.waitForFileNode("/abc/foo");
+ DomainFile file = fooCopyNode.getDomainFile();
+ assertTrue(file.exists());
+ assertTrue(file.isLink());
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ assertFalse(linkInfo.isFolderLink());
+ assertTrue(linkInfo.isExternalLink());
+
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(file, null));
+
+ assertEquals(sharedFileURL.toExternalForm(), linkInfo.getLinkPath());
+
+ assertEquals(sharedFileURL, fooFile.getDomainFile().getSharedProjectURL(null));
+
+ //
+ // Verify link open follows into repository to open domain object database
+ //
+ DomainObject dobj = null;
+ try {
+ dobj = file.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ assertTrue(dobj instanceof ProgramDB);
+ assertFalse(dobj.canSave());
+ assertTrue(dobj.isChangeable());
+ }
+ finally {
+ if (dobj != null) {
+ dobj.release(this);
+ }
+ }
+ }
+
+ @Test
+ public void testCopyPasteExternalLinkFileAsLink() throws Exception {
+
+ env.getRootFolder().createFolder("abc");
+
+ addRepoView();
+
+ env.waitForTree();
+
+ //
+ // Select /bashLink file from viewed project and Copy
+ //
+ DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm());
+ assertNotNull("repo data tree view not found", viewTreeHelper);
+
+ DomainFileNode bashLinkFile = viewTreeHelper.waitForFileNode("/bashLink");
+
+ final ActionContext copyActionContext =
+ viewTreeHelper.getDomainFileActionContext(bashLinkFile);
+
+ URL sharedFileURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
+ "Test", "/bashLink", null);
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ //
+ // Select /abc folder and perform Paste Link
+ //
+ DomainFolderNode abcNode = env.waitForFolderNode("/abc");
+
+ final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(abcNode);
+
+ DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), "Paste Link");
+ assertNotNull("Paste Link action not found", pasteLinkAction);
+
+ assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext));
+ assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext));
+ runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext));
+
+ //
+ // Verify external link-file paste as link
+ //
+ DomainFileNode bashLinkCopyNode = env.waitForFileNode("/abc/bashLink");
+ DomainFile file = bashLinkCopyNode.getDomainFile();
+ assertTrue(file.exists());
+ assertTrue(file.isLink());
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ assertFalse(linkInfo.isFolderLink());
+ assertTrue(linkInfo.isExternalLink());
+
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(file, null));
+
+ assertEquals(sharedFileURL.toExternalForm(), linkInfo.getLinkPath());
+
+ assertEquals(sharedFileURL, bashLinkFile.getDomainFile().getSharedProjectURL(null));
+
+ //
+ // Verify link open follows double-hop into repository to open domain object database
+ //
+ DomainObject dobj = null;
+ try {
+ dobj = file.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ assertTrue(dobj instanceof ProgramDB);
+ assertFalse(dobj.canSave());
+ assertTrue(dobj.isChangeable());
+ }
+ finally {
+ if (dobj != null) {
+ dobj.release(this);
+ }
+ }
+ }
+
+ @Test
+ public void testCopyPastExternalFolder() throws Exception {
+
+ env.getRootFolder().createFolder("xyz");
+
+ addRepoView();
+
+ env.waitForTree();
+
+ //
+ // Select /f1 folder from viewed project and Copy
+ //
+ DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm());
+ assertNotNull("repo data tree view not found", viewTreeHelper);
+
+ DomainFolderNode f1Folder = viewTreeHelper.waitForFolderNode("/f1");
+
+ final ActionContext copyActionContext = viewTreeHelper.getDomainFileActionContext(f1Folder);
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ //
+ // Select xyz folder and perform Paste
+ //
+ DomainFolderNode xyzNode = env.waitForFolderNode("/xyz");
+
+ final ActionContext pasteActionContext = env.getDomainFileActionContext(xyzNode);
+
+ DockingActionIf pasteAction = getAction(env.getFrontEndTool(), "Paste");
+ assertNotNull("Paste action not found", pasteAction);
+
+ assertTrue(pasteAction.isAddToPopup(pasteActionContext));
+ assertTrue(pasteAction.isEnabledForContext(pasteActionContext));
+ runSwing(() -> pasteAction.actionPerformed(pasteActionContext));
+
+ //
+ // Verify external folder paste (full folder copy) with its content file
+ //
+ DomainFolderNode f1CopyNode = env.waitForFolderNode("/xyz/f1");
+ DomainFolder folder = f1CopyNode.getDomainFolder();
+ assertTrue(!folder.isEmpty());
+
+ DomainFile file = folder.getFile("bash");
+ assertNotNull(file);
+ assertTrue(file.exists());
+
+ assertEquals(LinkStatus.NON_LINK, LinkHandler.getLinkFileStatus(file, null));
+ }
+
+ @Test
+ public void testCopyPastExternalFolderAsLink() throws Exception {
+
+ env.getRootFolder().createFolder("abc");
+
+ addRepoView();
+
+ env.waitForTree();
+
+ //
+ // Select f1 folder from viewed project and Copy
+ //
+ DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm());
+ assertNotNull("repo data tree view not found", viewTreeHelper);
+
+ DomainFolderNode f1Folder = viewTreeHelper.waitForFolderNode("/f1");
+
+ final ActionContext copyActionContext = viewTreeHelper.getDomainFileActionContext(f1Folder);
+
+ URL sharedFolderURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
+ "Test", "/f1/", null);
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ //
+ // Select abc folder and perform Paste Link
+ //
+ DomainFolderNode abcNode = env.waitForFolderNode("/abc");
+
+ final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(abcNode);
+
+ DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), "Paste Link");
+ assertNotNull("Paste Link action not found", pasteLinkAction);
+
+ assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext));
+ assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext));
+ runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext));
+
+ //
+ // Verify external folder paste as link
+ //
+ DomainFileNode abcCopyNode = env.waitForFileNode("/abc/f1");
+ DomainFile file = abcCopyNode.getDomainFile();
+ assertTrue(file.exists());
+ assertTrue(file.isLink());
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ assertTrue(linkInfo.isFolderLink());
+ assertTrue(linkInfo.isExternalLink());
+
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(file, null));
+
+ //
+ // Folder link-paths intentionally omit the trailing / so they can adapt to use
+ // of folder or another folder-link-file at the referenced location
+ //
+ String urlPath = sharedFolderURL.toExternalForm(); // will end with '/'
+ urlPath = urlPath.substring(0, urlPath.length() - 1); // strip trailing '/'
+
+ assertEquals(urlPath, linkInfo.getLinkPath());
+
+ LinkedDomainFolder linkedFolder = linkInfo.getLinkedFolder();
+ assertNotNull(linkedFolder);
+ assertTrue(linkedFolder.isLinked());
+ assertEquals(f1Folder.getDomainFolder(), linkedFolder.getRealFolder());
+
+ ProjectData projectData = env.getFrontEndTool().getProject().getProjectData();
+
+ //
+ // Verify stored folder and its indirect folder content access via ProjectData
+ //
+ DomainFolder remoteFolder = projectData.getFolder("/abc/f1");
+ assertNull(remoteFolder); // must use filter to allow externals
+ remoteFolder = projectData.getFolder("/abc/f1", DomainFolderFilter.ALL_FOLDERS_FILTER);
+ assertEquals(linkedFolder, remoteFolder);
+ assertEquals(sharedFolderURL, remoteFolder.getSharedProjectURL());
+
+ DomainFile remoteFile = projectData.getFile("/abc/f1/bash");
+ assertNull(remoteFile); // must use filter to allow externals
+ remoteFile = projectData.getFile("/abc/f1/bash", DomainFileFilter.ALL_FILES_FILTER);
+ assertNotNull(remoteFile);
+ assertTrue(remoteFile.exists());
+ URL sharedFileURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
+ "Test", "/f1", "bash", null);
+ assertEquals(sharedFileURL, remoteFile.getSharedProjectURL(null));
+
+ //
+ // Verify ability to open linked-folder content
+ //
+ DomainObject dobj = null;
+ try {
+ dobj = remoteFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ assertTrue(dobj instanceof ProgramDB);
+ assertFalse(dobj.canSave());
+ assertTrue(dobj.isChangeable());
+ }
+ finally {
+ if (dobj != null) {
+ dobj.release(this);
+ }
+ }
+ }
+
+ @Test
+ public void testCopyPastExternalFolderLinkAsLink() throws Exception {
+
+ env.getRootFolder().createFolder("abc");
+
+ addRepoView();
+
+ env.waitForTree();
+
+ //
+ // Select f1Link folder-link from viewed project and Copy
+ //
+ DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm());
+ assertNotNull("repo data tree view not found", viewTreeHelper);
+
+ DomainFileNode f1LinkFile = viewTreeHelper.waitForFileNode("/f1Link");
+
+ final ActionContext copyActionContext =
+ viewTreeHelper.getDomainFileActionContext(f1LinkFile);
+
+ URL sharedFolderURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
+ "Test", "/f1Link", null);
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ //
+ // Select abc folder and perform Paste Link
+ //
+ DomainFolderNode abcNode = env.waitForFolderNode("/abc");
+
+ final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(abcNode);
+
+ DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), "Paste Link");
+ assertNotNull("Paste Link action not found", pasteLinkAction);
+
+ assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext));
+ assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext));
+ runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext));
+
+ DomainFileNode f1LinkCopyNode = env.waitForFileNode("/abc/f1Link");
+ DomainFile file = f1LinkCopyNode.getDomainFile();
+ assertTrue(file.exists());
+ assertTrue(file.isLink());
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ assertTrue(linkInfo.isFolderLink());
+ assertTrue(linkInfo.isExternalLink());
+
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(file, null));
+
+ //
+ // Folder link-paths intentionally omit the trailing / so they can adapt to use
+ // of folder or another folder-link-file at the referenced location
+ //
+ String urlPath = sharedFolderURL.toExternalForm();
+
+ assertEquals(urlPath, linkInfo.getLinkPath());
+
+ LinkedDomainFolder linkedFolder = linkInfo.getLinkedFolder();
+ assertNotNull(linkedFolder);
+ assertTrue(linkedFolder.isLinked());
+
+ assertEquals("/f1Link", linkedFolder.getLinkedPathname());
+
+ assertNotNull("Linked folder content not found", linkedFolder.getFile("bash"));
+
+ //
+ // Verify stored folder and its double-hop indirect folder content access via ProjectData
+ //
+ ProjectData projectData = env.getFrontEndTool().getProject().getProjectData();
+ DomainFile remoteFile =
+ projectData.getFile("/abc/f1Link/bash", DomainFileFilter.ALL_FILES_FILTER);
+ assertNotNull(remoteFile);
+ assertTrue(remoteFile.exists());
+ URL sharedFileURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT,
+ "Test", "/f1Link", "bash", null);
+ assertEquals(sharedFileURL, remoteFile.getSharedProjectURL(null));
+
+ //
+ // Verify ability to open double-hop linked-folder content
+ //
+ DomainObject dobj = null;
+ try {
+ dobj = remoteFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ assertTrue(dobj instanceof ProgramDB);
+ assertFalse(dobj.canSave());
+ assertTrue(dobj.isChangeable());
+ }
+ finally {
+ if (dobj != null) {
+ dobj.release(this);
+ }
+ }
+ }
+
+}
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteTest.java
new file mode 100644
index 0000000000..8c312a8692
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteTest.java
@@ -0,0 +1,400 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.framework.main.datatree;
+
+import static org.junit.Assert.*;
+
+import org.junit.*;
+
+import docking.ActionContext;
+import docking.action.DockingActionIf;
+import ghidra.framework.client.ClientUtil;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.LinkHandler.LinkStatus;
+import ghidra.framework.model.*;
+import ghidra.program.database.ProgramDB;
+import ghidra.program.model.listing.Program;
+import ghidra.server.remote.ServerTestUtil;
+import ghidra.test.*;
+import ghidra.util.task.TaskMonitor;
+
+public class ProjectCopyPasteTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private FrontEndTestEnv env;
+
+ private DomainFolder abcFolder;
+ private DomainFile programFile;
+
+ @Before
+ public void setUp() throws Exception {
+
+ env = new FrontEndTestEnv();
+
+ /**
+ /abc (folder)
+ foo (program file)
+ /xyz (empty folder)
+ **/
+
+ DomainFolder rootFolder = env.getRootFolder();
+
+ abcFolder = rootFolder.createFolder("abc");
+ rootFolder.createFolder("xyz");
+
+ Program p = ToyProgramBuilder.buildSimpleProgram("foo", this);
+ programFile = abcFolder.createFile("foo", p, TaskMonitor.DUMMY);
+ p.release(this);
+
+ env.waitForTree();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ env.dispose();
+
+ ClientUtil.clearRepositoryAdapter("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT);
+ }
+
+ @Test
+ public void testCopyPasteFile() throws Exception {
+
+ // Select /abc/foo file and Copy
+
+ DomainFileNode fooFile = env.waitForFileNode("/abc/foo");
+
+ final ActionContext copyActionContext = env.getDomainFileActionContext(fooFile);
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ // Select /xyz folder and perform Paste
+
+ DomainFolderNode xyzNode = env.waitForFolderNode("/xyz");
+
+ final ActionContext pasteActionContext = env.getDomainFileActionContext(xyzNode);
+
+ DockingActionIf pasteAction = getAction(env.getFrontEndTool(), "Paste");
+ assertNotNull("Paste action not found", pasteAction);
+
+ assertTrue(pasteAction.isAddToPopup(pasteActionContext));
+ assertTrue(pasteAction.isEnabledForContext(pasteActionContext));
+ runSwing(() -> pasteAction.actionPerformed(pasteActionContext));
+
+ DomainFileNode fooCopyNode = env.waitForFileNode("/xyz/foo");
+ DomainFile file = fooCopyNode.getDomainFile();
+ assertTrue(file.exists());
+ assertFalse(file.isLink());
+
+ assertEquals(LinkStatus.NON_LINK, LinkHandler.getLinkFileStatus(file, null));
+ }
+
+ @Test
+ public void testCopyPastInternalAbsoluteFileLink() throws Exception {
+ testCopyPastInternalFileLink("Paste Link");
+ }
+
+ @Test
+ public void testCopyPastInternalRelativeFileLink() throws Exception {
+ testCopyPastInternalFileLink("Paste Relative-Link");
+ }
+
+ private void testCopyPastInternalFileLink(String pastActionName) throws Exception {
+
+ /**
+ /abc
+ foo (copied)
+ /xyz (pasted into)
+ foo -> (direct link)
+ foo.1 -> (link to direct link)
+ **/
+
+ boolean isRelative = pastActionName.contains("Relative");
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ // Select /abc/foo file and perform Copy
+
+ DomainFileNode fooNode = env.waitForFileNode("/abc/foo");
+ final ActionContext copyActionContext = env.getDomainFileActionContext(fooNode);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), pastActionName);
+ assertNotNull(pastActionName + " action not found", pasteLinkAction);
+
+ // Select /xyz folder and perform Paste as Link
+
+ DomainFolderNode xyzNode = env.waitForFolderNode("/xyz");
+ final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(xyzNode);
+
+ assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext));
+ assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext));
+ runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext));
+
+ DomainFileNode fooLinkNode = env.waitForFileNode("/xyz/foo");
+ DomainFile file = fooLinkNode.getDomainFile();
+ assertTrue(file.exists());
+ assertTrue(file.isLink());
+ LinkFileInfo linkInfo = file.getLinkInfo();
+ assertFalse(linkInfo.isFolderLink());
+ assertFalse(linkInfo.isExternalLink());
+ assertEquals(isRelative ? "../abc/foo" : "/abc/foo", linkInfo.getLinkPath());
+ assertNull(linkInfo.getLinkedFolder());
+
+ ProjectData projectData = env.getFrontEndTool().getProject().getProjectData();
+
+ DomainFile fooLinkFile = projectData.getFile("/xyz/foo");
+ assertNotNull(fooLinkFile);
+ assertTrue(fooLinkFile.exists());
+
+ assertEquals(LinkStatus.INTERNAL, LinkHandler.getLinkFileStatus(fooLinkFile, null));
+
+ DomainObject dobj = fooLinkFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ try {
+ assertTrue(dobj instanceof ProgramDB);
+ assertTrue(dobj.canSave());
+ assertTrue(dobj.isChangeable());
+ assertEquals(programFile, dobj.getDomainFile());
+ }
+ finally {
+ if (dobj != null) {
+ dobj.release(this);
+ }
+ }
+
+ // Select /xyz/foo file and perform Copy
+
+ final ActionContext copy2ActionContext = env.getDomainFileActionContext(fooLinkNode);
+
+ assertTrue(copyAction.isAddToPopup(copy2ActionContext));
+ assertTrue(copyAction.isEnabledForContext(copy2ActionContext));
+ runSwing(() -> copyAction.actionPerformed(copy2ActionContext));
+
+ // Select /xyz folder and perform Paste as Link
+
+ assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext));
+ assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext));
+ runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext));
+
+ fooLinkNode = env.waitForFileNode("/xyz/foo.1");
+ file = fooLinkNode.getDomainFile();
+ assertTrue(file.exists());
+ assertTrue(file.isLink());
+ linkInfo = file.getLinkInfo();
+ assertFalse(linkInfo.isFolderLink());
+ assertFalse(linkInfo.isExternalLink());
+ assertEquals(isRelative ? "foo" : "/xyz/foo", linkInfo.getLinkPath());
+ assertNull(linkInfo.getLinkedFolder());
+
+ fooLinkFile = projectData.getFile("/xyz/foo.1");
+ assertNotNull(fooLinkFile);
+ assertTrue(fooLinkFile.exists());
+
+ assertEquals(LinkStatus.INTERNAL, LinkHandler.getLinkFileStatus(fooLinkFile, null));
+
+ dobj = fooLinkFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ try {
+ assertTrue(dobj instanceof ProgramDB);
+ assertTrue(dobj.canSave());
+ assertTrue(dobj.isChangeable());
+ assertEquals(programFile, dobj.getDomainFile());
+ }
+ finally {
+ if (dobj != null) {
+ dobj.release(this);
+ }
+ }
+ }
+
+ @Test
+ public void testCopyPastFolder() throws Exception {
+
+ // Select /abc file from viewed project and Copy
+
+ DomainFolderNode abcFolderNode = env.waitForFolderNode("/abc");
+
+ final ActionContext copyActionContext = env.getDomainFileActionContext(abcFolderNode);
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ // Select /xyz folder and perform Paste
+
+ DomainFolderNode xyzNode = env.waitForFolderNode("/xyz");
+
+ final ActionContext pasteActionContext = env.getDomainFileActionContext(xyzNode);
+
+ DockingActionIf pasteAction = getAction(env.getFrontEndTool(), "Paste");
+ assertNotNull("Paste action not found", pasteAction);
+
+ assertTrue(pasteAction.isAddToPopup(pasteActionContext));
+ assertTrue(pasteAction.isEnabledForContext(pasteActionContext));
+ runSwing(() -> pasteAction.actionPerformed(pasteActionContext));
+
+ DomainFolderNode abcCopyNode = env.waitForFolderNode("/xyz/abc");
+ DomainFolder folder = abcCopyNode.getDomainFolder();
+ assertTrue(!folder.isEmpty());
+
+ DomainFile file = folder.getFile("foo");
+ assertNotNull(file);
+ assertTrue(file.exists());
+
+ }
+
+ @Test
+ public void testCopyPastInternalAbsoluteFolderLink() throws Exception {
+ testCopyPastInternalFolderLink("Paste Link");
+ }
+
+ @Test
+ public void testCopyPastInternalRelativeFolderLink() throws Exception {
+ testCopyPastInternalFolderLink("Paste Relative-Link");
+ }
+
+ private void testCopyPastInternalFolderLink(String pastActionName) throws Exception {
+
+ /**
+ /abc (copied)
+ foo
+ /xyz (pasted into)
+ abc -> (direct link)
+ abc.1 -> (link to direct link)
+ **/
+
+ boolean isRelative = pastActionName.contains("Relative");
+
+ DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy");
+ assertNotNull("Copy action not found", copyAction);
+
+ // Select /abc folder and perform Copy
+
+ DomainFolderNode abcNode = env.waitForFolderNode("/abc");
+ final ActionContext copyActionContext = env.getDomainFileActionContext(abcNode);
+
+ assertTrue(copyAction.isAddToPopup(copyActionContext));
+ assertTrue(copyAction.isEnabledForContext(copyActionContext));
+ runSwing(() -> copyAction.actionPerformed(copyActionContext));
+
+ // Select /xyz folder and perform Paste as Link /xyz/abc
+
+ DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), pastActionName);
+ assertNotNull(pastActionName + " action not found", pasteLinkAction);
+
+ DomainFolderNode xyzNode = env.waitForFolderNode("/xyz");
+ final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(xyzNode);
+
+ assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext));
+ assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext));
+ runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext));
+
+ final DomainFileNode xyzAbcLinkNode = env.waitForFileNode("/xyz/abc");
+ final DomainFile xyzAbcLinkFile = xyzAbcLinkNode.getDomainFile();
+ assertTrue(xyzAbcLinkFile.exists());
+ assertTrue(xyzAbcLinkFile.isLink());
+ LinkFileInfo xyzAbcLinkInfo = xyzAbcLinkFile.getLinkInfo();
+ assertTrue(xyzAbcLinkInfo.isFolderLink());
+ assertFalse(xyzAbcLinkInfo.isExternalLink());
+ assertEquals(isRelative ? "../abc" : "/abc", xyzAbcLinkInfo.getLinkPath());
+
+ assertEquals(LinkStatus.INTERNAL, LinkHandler.getLinkFileStatus(xyzAbcLinkFile, null));
+
+ final LinkedDomainFolder xyzAbcLinkedFolder = xyzAbcLinkInfo.getLinkedFolder();
+ assertNotNull(xyzAbcLinkedFolder);
+ assertTrue(xyzAbcLinkedFolder.isLinked());
+ assertEquals(abcFolder, xyzAbcLinkedFolder.getRealFolder());
+
+ ProjectData projectData = env.getFrontEndTool().getProject().getProjectData();
+
+ DomainFile fooFile = projectData.getFile("/xyz/abc/foo");
+ assertNotNull(fooFile);
+ assertTrue(fooFile.exists());
+
+ DomainObject dobj = fooFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ try {
+ assertTrue(dobj instanceof ProgramDB);
+ assertTrue(dobj.canSave());
+ assertTrue(dobj.isChangeable());
+ assertEquals(programFile, dobj.getDomainFile());
+ }
+ finally {
+ if (dobj != null) {
+ dobj.release(this);
+ }
+ }
+
+ // Select /xyz/abc linked-folder and perform Copy
+
+ final ActionContext copy2ActionContext = env.getDomainFileActionContext(xyzAbcLinkNode);
+
+ assertTrue(copyAction.isAddToPopup(copy2ActionContext));
+ assertTrue(copyAction.isEnabledForContext(copy2ActionContext));
+ runSwing(() -> copyAction.actionPerformed(copy2ActionContext));
+
+ // Select /xyz and perform Paste as Link /xyz/abc.1
+
+ assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext));
+ assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext));
+ runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext));
+
+ final DomainFileNode xyzAbc1CopyNode = env.waitForFileNode("/xyz/abc.1");
+ DomainFile xyzAbc1LinkFile = xyzAbc1CopyNode.getDomainFile();
+ assertTrue(xyzAbc1LinkFile.exists());
+ assertTrue(xyzAbc1LinkFile.isLink());
+ LinkFileInfo xyzAbc1LinkInfo = xyzAbc1LinkFile.getLinkInfo();
+ assertTrue(xyzAbc1LinkInfo.isFolderLink());
+ assertFalse(xyzAbc1LinkInfo.isExternalLink());
+ assertEquals(isRelative ? "abc" : "/xyz/abc", xyzAbc1LinkInfo.getLinkPath());
+
+ assertEquals(LinkStatus.INTERNAL, LinkHandler.getLinkFileStatus(xyzAbc1LinkFile, null));
+
+ final LinkedDomainFolder xyzAbc1LinkedFolder = xyzAbc1LinkInfo.getLinkedFolder();
+ assertNotNull(xyzAbc1LinkedFolder);
+ assertTrue(xyzAbc1LinkedFolder.isLinked());
+
+ assertEquals(xyzAbcLinkedFolder.getRealFolder(), xyzAbc1LinkedFolder.getRealFolder());
+
+ fooFile = projectData.getFile("/xyz/abc.1/foo");
+ assertNotNull(fooFile);
+ assertTrue(fooFile.exists());
+
+ dobj = fooFile.getDomainObject(this, false, false, TaskMonitor.DUMMY);
+ try {
+ assertTrue(dobj instanceof ProgramDB);
+ assertTrue(dobj.canSave());
+ assertTrue(dobj.isChangeable());
+ assertEquals(programFile, dobj.getDomainFile());
+ }
+ finally {
+ if (dobj != null) {
+ dobj.release(this);
+ }
+ }
+
+ }
+
+}
diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTest.java
new file mode 100644
index 0000000000..722997904f
--- /dev/null
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTest.java
@@ -0,0 +1,439 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.framework.main.datatree;
+
+import static org.junit.Assert.*;
+
+import java.util.function.BooleanSupplier;
+
+import org.junit.*;
+
+import ghidra.framework.client.ClientUtil;
+import ghidra.framework.data.FolderLinkContentHandler;
+import ghidra.framework.data.LinkHandler;
+import ghidra.framework.data.LinkHandler.LinkStatus;
+import ghidra.framework.model.DomainFile;
+import ghidra.framework.model.DomainFolder;
+import ghidra.program.database.DataTypeArchiveDB;
+import ghidra.program.database.ProgramLinkContentHandler;
+import ghidra.program.model.listing.Program;
+import ghidra.server.remote.ServerTestUtil;
+import ghidra.test.*;
+import ghidra.util.exception.DuplicateFileException;
+import ghidra.util.task.TaskMonitor;
+
+public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private FrontEndTestEnv env;
+
+ private DomainFolder abcFolder;
+ private DomainFolder xyzFolder;
+ private DomainFile programFile;
+
+ @Before
+ public void setUp() throws Exception {
+
+ env = new FrontEndTestEnv();
+
+ /**
+ /abc/ (folder)
+ abc -> /xyz/abc (circular)
+ foo (program file)
+ /xyz/
+ abc -> /abc (folder link)
+ abc -> (circular)
+ foo
+ foo -> /abc/foo (program link)
+ **/
+
+ DomainFolder rootFolder = env.getRootFolder();
+
+ abcFolder = rootFolder.createFolder("abc");
+ xyzFolder = rootFolder.createFolder("xyz");
+ DomainFile abcLinkFile = abcFolder.copyToAsLink(xyzFolder, false);
+ abcLinkFile.copyToAsLink(abcFolder, false);
+
+ Program p = ToyProgramBuilder.buildSimpleProgram("foo", this);
+ programFile = abcFolder.createFile("foo", p, TaskMonitor.DUMMY);
+ p.release(this);
+
+ programFile.copyToAsLink(xyzFolder, false);
+
+ env.waitForTree();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (env != null) {
+ env.dispose();
+ }
+ ClientUtil.clearRepositoryAdapter("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT);
+ }
+
+ @Test
+ public void testNonFileLink() throws Exception {
+ DomainFileNode fileNode = env.waitForFileNode("/abc/foo");
+ assertEquals(LinkStatus.NON_LINK,
+ LinkHandler.getLinkFileStatus(fileNode.getDomainFile(), null));
+ }
+
+ @Test
+ public void testExternalFileLink() throws Exception {
+
+ //
+ // Create external program file-link /abc/A to remote repository
+ //
+ DomainFile linkFile = abcFolder.createLinkFile("ghidra://localhost/Test/A", "A",
+ ProgramLinkContentHandler.INSTANCE);
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ //
+ // Verify /abc/A external program file-link exists with correct status and display name
+ //
+ DomainFileNode nodeA = waitForFileNode("/abc/A");
+ assertFalse(nodeA.isFolderLink());
+ assertEquals(linkFile, nodeA.getDomainFile());
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null));
+ String displayName = runSwing(() -> nodeA.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName,
+ displayName.contains("localhost[Test]:/A"));
+
+ //
+ // Create external program file-link /abc/B to local project
+ //
+ linkFile = abcFolder.createLinkFile("ghidra:/x/y/Test?/B", "B",
+ ProgramLinkContentHandler.INSTANCE);
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ //
+ // Verify /abc/B external program file-link exists with correct status and display name
+ //
+ DomainFileNode nodeB = waitForFileNode("/abc/B");
+ assertFalse(nodeB.isFolderLink());
+ assertEquals(linkFile, nodeB.getDomainFile());
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null));
+ displayName = runSwing(() -> nodeB.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.contains("Test:/B"));
+
+ //
+ // Remove /abc/foo file
+ //
+ DomainFile fooFile = abcFolder.getFile("foo");
+ assertNotNull(fooFile);
+ fooFile.delete();
+
+ //
+ // Replace deleted file with external program file-link to local project
+ // which sets-up indirect link path from /xyz/foo -> /abc/foo -> local project file
+ //
+ linkFile = abcFolder.createLinkFile("ghidra:/x/y/Test?/foo", "foo",
+ ProgramLinkContentHandler.INSTANCE);
+
+ waitForSwing(); // give a chance for ChangeManager to be notified
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ //
+ // Verify /abc/foo external program file-link exists with correct status and display name
+ //
+ DomainFileNode fooNode = waitForFileNode("/abc/foo");
+ assertFalse(fooNode.isFolderLink());
+ assertEquals(linkFile, fooNode.getDomainFile());
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null));
+ displayName = runSwing(() -> fooNode.getDisplayText());
+ if (!displayName.contains("Test:/foo")) {
+ int junk = 0;
+ }
+ assertTrue("Unexpected node display name: " + displayName,
+ displayName.contains("Test:/foo"));
+
+ //
+ // Check pre-existing file-link /xyz/foo reflects external status
+ //
+ DomainFileNode fooLinkNode = waitForFileNode("/xyz/foo");
+ assertEquals(LinkStatus.EXTERNAL,
+ LinkHandler.getLinkFileStatus(fooLinkNode.getDomainFile(), null));
+ }
+
+ @Test
+ public void testExternalFolderLink() throws Exception {
+
+ // NOTE: Only refer to root repo folder with remote URL to avoid unwanted connection attempt
+
+ //
+ // Create external folder-link /abc/A to remote repository
+ //
+ DomainFile linkFile = abcFolder.createLinkFile("ghidra://localhost/Test/", "A",
+ FolderLinkContentHandler.INSTANCE);
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ //
+ // Verify /abc/A external folder-link exists with correct status and display name
+ //
+ DomainFileNode nodeA = waitForFileNode("/abc/A");
+ assertTrue(nodeA.isFolderLink());
+ assertEquals(linkFile, nodeA.getDomainFile());
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null));
+ String displayName = runSwing(() -> nodeA.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName,
+ displayName.contains("localhost[Test]:/"));
+
+ //
+ // Create external folder-link /abc/B to local project
+ //
+ linkFile =
+ abcFolder.createLinkFile("ghidra:/x/y/Test?/B", "B", FolderLinkContentHandler.INSTANCE);
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ //
+ // Verify /abc/B external folder-link exists with correct status and display name
+ //
+ DomainFileNode nodeB = waitForFileNode("/abc/B");
+ assertTrue(nodeB.isFolderLink());
+ assertEquals(linkFile, nodeB.getDomainFile());
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null));
+ displayName = runSwing(() -> nodeB.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.contains("Test:/B"));
+
+ //
+ // Remove /abc folder and its children
+ //
+ DomainFolder rootFolder = abcFolder.getParent();
+ abcFolder.getFile("abc").delete();
+ abcFolder.getFile("foo").delete();
+ abcFolder.getFile("A").delete();
+ abcFolder.getFile("B").delete();
+ abcFolder.delete();
+
+ //
+ // Remove /xyz/foo file to avoid remote access attempt to ghidra://localhost/Test/foo
+ // after /abc is replaced in the next step
+ //
+ DomainFile fooFile = xyzFolder.getFile("foo");
+ assertNotNull(fooFile);
+ fooFile.delete();
+
+ //
+ // Replace deleted folder with external folder-link to local project
+ // which sets-up indirect link path from /xyz/abc -> /abc -> local project root folder
+ //
+ linkFile = rootFolder.createLinkFile("ghidra://localhost/Test/", "abc",
+ FolderLinkContentHandler.INSTANCE);
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ //
+ // Verify /abc external folder-link exists with correct status and display name
+ //
+ DomainFileNode abcLinkNode = waitForFileNode("/abc");
+ assertTrue(abcLinkNode.isFolderLink());
+ assertEquals(linkFile, abcLinkNode.getDomainFile());
+ assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null));
+ displayName = runSwing(() -> abcLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName,
+ displayName.contains("localhost[Test]:/"));
+
+ //
+ // Check pre-existing folder-link /xyz/abc reflects external status
+ //
+ DomainFileNode abcLinkNode2 = waitForFileNode("/xyz/abc");
+ assertEquals(LinkStatus.EXTERNAL,
+ LinkHandler.getLinkFileStatus(abcLinkNode2.getDomainFile(), null));
+ }
+
+ @Test
+ public void testBrokenFolderLink() throws Exception {
+
+ //
+ // Verify broken folder-link status for /abc/abc which has circular reference
+ //
+ DomainFileNode abcAbcLinkNode = waitForFileNode("/abc/abc");
+ assertTrue(abcAbcLinkNode.isFolderLink());
+ String displayName = runSwing(() -> abcAbcLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName,
+ displayName.endsWith(" /xyz/abc"));
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(abcAbcLinkNode.getDomainFile(), null));
+ String tooltip = abcAbcLinkNode.getToolTip().replace(" ", " ");
+ assertTrue(tooltip.contains("circular"));
+
+ //
+ // Verify good folder-link internal status for /xyz/abc which has circular reference
+ //
+ DomainFileNode xyzAbcLinkNode = waitForFileNode("/xyz/abc");
+ assertTrue(xyzAbcLinkNode.isFolderLink());
+ displayName = runSwing(() -> xyzAbcLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" /abc"));
+ assertEquals(LinkStatus.INTERNAL,
+ LinkHandler.getLinkFileStatus(xyzAbcLinkNode.getDomainFile(), null));
+
+ //
+ // Verify broken folder-link status for /xyz/abc/abc which has circular reference
+ //
+ DomainFileNode abcLinkedNode = waitForFileNode("/xyz/abc/abc");
+ assertTrue(abcLinkedNode.isFolderLink());
+ displayName = runSwing(() -> abcLinkedNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName,
+ displayName.endsWith(" /xyz/abc"));
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(abcLinkedNode.getDomainFile(), null));
+ tooltip = abcLinkedNode.getToolTip().replace(" ", " ");
+ assertTrue(tooltip.contains("circular"));
+
+ //
+ // Rename folder /abc to /ABC causing folder-link /xyz/abc to become broken
+ //
+ abcFolder = abcFolder.setName("ABC");
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ // Verify /abc node not found
+ assertNull(env.getRootNode().getChild("abc"));
+
+ //
+ // Verify broken folder-link status for /ABC/abc which has circular reference
+ //
+ DomainFileNode ABCAbcLinkNode = waitForFileNode("/ABC/abc");
+ assertTrue(ABCAbcLinkNode.isFolderLink());
+ displayName = runSwing(() -> ABCAbcLinkNode.getDisplayText());
+ assertTrue("Unexpected node display name: " + displayName,
+ displayName.endsWith(" /xyz/abc"));
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(ABCAbcLinkNode.getDomainFile(), null));
+ tooltip = ABCAbcLinkNode.getToolTip().replace(" ", " ");
+ assertTrue(tooltip.contains("folder not found: /abc"));
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ //
+ // Verify that folder-link /xyz/abc is broken due to missing /abc
+ //
+ DomainFileNode n = waitForFileNode("/xyz/abc"); // wait for refresh
+ assertTrue(n == xyzAbcLinkNode);
+ assertTrue(xyzAbcLinkNode.isFolderLink());
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(xyzAbcLinkNode.getDomainFile(), null));
+ tooltip = xyzAbcLinkNode.getToolTip().replace(" ", " ");
+ assertTrue("Unexpected tooltip: " + tooltip, tooltip.contains("folder not found: /abc"));
+
+ //
+ // Attempt conflicting folder-link creation
+ //
+ DomainFile linkFile = abcFolder.getParent()
+ .createLinkFile("ghidra://localhost/Test/ABC", "ABC",
+ FolderLinkContentHandler.INSTANCE);
+ assertEquals("ABC.1", linkFile.getName()); // link forced to have unqiue name
+
+ //
+ // Try to force folder vs folder-link name conflict
+ // While it won't be allowed it could occur in-the-wild due to shared project content
+ // (case not tested here)
+ //
+
+ try {
+ linkFile.setName("ABC"); // trigger folder name conflict for folder-link
+ fail("Expected DuplicateFileException");
+ }
+ catch (DuplicateFileException e) {
+ // expected for link file
+ }
+
+ try {
+ abcFolder.setName("ABC.1");
+ fail("Expected DuplicateFileException");
+ }
+ catch (DuplicateFileException e) {
+ // expected for link file
+ }
+
+ }
+
+ @Test
+ public void testBrokenFileLink() throws Exception {
+
+ //
+ // Verify good internal file-link status for /xyz/foo -> /abc/foo
+ //
+ DomainFileNode linkNode = waitForFileNode("/xyz/foo");
+ assertEquals(LinkStatus.INTERNAL,
+ LinkHandler.getLinkFileStatus(linkNode.getDomainFile(), null));
+
+ //
+ // Copy program file /abc/foo as relative file-link /abc/foo.1 and verify good internal file-link status
+ //
+ DomainFile relativeProgramLink = programFile.copyToAsLink(abcFolder, true);
+ assertEquals("/abc/foo.1", relativeProgramLink.getPathname());
+ assertEquals(LinkStatus.INTERNAL, LinkHandler.getLinkFileStatus(relativeProgramLink, null));
+
+ //
+ // Delete program file /abc/foo and verify that file-link /abc/foo.1 becomes broken
+ //
+ programFile.delete();
+ assertEquals(LinkStatus.BROKEN, LinkHandler.getLinkFileStatus(relativeProgramLink, null));
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ //
+ // Verify broken /xyz/foo file link status due to deleted file /abc/foo
+ //
+ linkNode = waitForFileNode("/xyz/foo");
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(linkNode.getDomainFile(), null));
+ String tooltip = linkNode.getToolTip().replace(" ", " ");
+ assertTrue(tooltip.contains("file not found: /abc/foo"));
+
+ //
+ // Create DataTypeArchive project file /abc/foo
+ //
+ DataTypeArchiveDB dtm = new DataTypeArchiveDB(abcFolder, "foo", this);
+ dtm.save(null, TaskMonitor.DUMMY);
+ dtm.release(this);
+
+ env.waitForTree(); // give time for ChangeManager to update
+
+ //
+ // Verify that Program file-link is now broken due to incompatible content for /abc/foo
+ //
+ linkNode = waitForFileNode("/xyz/foo");
+ assertEquals(LinkStatus.BROKEN,
+ LinkHandler.getLinkFileStatus(linkNode.getDomainFile(), null));
+ env.waitForSwing();
+ tooltip = linkNode.getToolTip().replace(" ", " ");
+ assertTrue("Unexpected tooltip: " + tooltip,
+ tooltip.contains("incompatible content-type: /abc/foo"));
+
+ }
+
+ private DomainFileNode waitForFileNode(String path) {
+ DomainFileNode fileNode = env.waitForFileNode(path);
+ waitForRefresh(fileNode);
+ return fileNode;
+ }
+
+ private void waitForRefresh(DomainFileNode fileNode) {
+ waitFor(new BooleanSupplier() {
+ @Override
+ public boolean getAsBoolean() {
+ return !fileNode.hasPendingRefresh();
+ }
+ });
+ }
+
+}
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
index d3a5c3e683..1947134da3 100644
--- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java
+++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -130,11 +130,11 @@ public class DataTreeHelper {
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());
+ if (node instanceof DomainFileNode fileNode) {
+ fileList.add(fileNode.getDomainFile());
}
- else if (node instanceof DomainFolderNode) {
- folderList.add(((DomainFolderNode) node).getDomainFolder());
+ else if (node instanceof DomainFolderNode folderNode) {
+ folderList.add(folderNode.getDomainFolder());
}
}