diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java index e556bd5631..01fd9f49d1 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/DebuggerCoordinates.java @@ -27,6 +27,7 @@ import ghidra.framework.data.ProjectFileManager; import ghidra.framework.model.*; import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.store.LockException; import ghidra.trace.database.DBTraceContentHandler; import ghidra.trace.model.Lifespan; import ghidra.trace.model.Trace; @@ -643,6 +644,7 @@ public class DebuggerCoordinates { ProjectData projData = tool.getProject().getProjectData(projLoc); if (projData == null) { try { + // FIXME! orphaned instance - transient in nature projData = new ProjectFileManager(projLoc, false, false); } catch (NotOwnerException e) { @@ -650,7 +652,7 @@ public class DebuggerCoordinates { "Not project owner: " + projLoc + "(" + pathname + ")"); return null; } - catch (IOException e) { + catch (IOException | LockException e) { Msg.showError(DebuggerCoordinates.class, tool.getToolFrame(), "Trace Open Failed", "Project error: " + e.getMessage()); return null; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java index d91aaacd57..3a81e82f92 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java @@ -368,7 +368,18 @@ public class DebuggerTraceManagerServicePlugin extends Plugin if (traceChooserDialog != null) { return traceChooserDialog; } - DomainFileFilter filter = df -> Trace.class.isAssignableFrom(df.getDomainObjectClass()); + DomainFileFilter filter = new DomainFileFilter() { + + @Override + public boolean accept(DomainFile df) { + return Trace.class.isAssignableFrom(df.getDomainObjectClass()); + } + + @Override + public boolean followLinkedFolders() { + return false; + } + }; // TODO regarding the hack note below, I believe this issue ahs been fixed, but not sure how to test return traceChooserDialog = diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/url/ProjectExperimentsTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/url/ProjectExperimentsTest.java index 7234a74d4f..e0c6d45e19 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/url/ProjectExperimentsTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/url/ProjectExperimentsTest.java @@ -176,7 +176,7 @@ public class ProjectExperimentsTest extends AbstractGhidraHeadedIntegrationTest assertNotNull(proj2 = pm.openProject(loc2, false, false)); - ProjectData data1 = proj2.addProjectView(loc1.getURL()); + ProjectData data1 = proj2.addProjectView(loc1.getURL(), true); assertNotNull(data1); // It's a cryin' shame. I don't get *any* callbacks. _ANY!_ diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceContentHandler.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceContentHandler.java index 8030c22464..f3018fbae1 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceContentHandler.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceContentHandler.java @@ -18,11 +18,13 @@ package ghidra.trace.database; import java.io.IOException; import javax.swing.Icon; +import javax.swing.ImageIcon; import db.DBHandle; import db.buffers.BufferFile; import db.buffers.ManagedBufferFile; -import ghidra.framework.data.*; +import ghidra.framework.data.DBWithUserDataContentHandler; +import ghidra.framework.data.DomainObjectMergeManager; import ghidra.framework.model.ChangeSet; import ghidra.framework.model.DomainObject; import ghidra.framework.store.*; @@ -34,9 +36,16 @@ import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; -public class DBTraceContentHandler extends DBContentHandler { +public class DBTraceContentHandler extends DBWithUserDataContentHandler { public static final String TRACE_CONTENT_TYPE = "Trace"; + public static ImageIcon TRACE_ICON = Trace.TRACE_ICON; + + static final Class TRACE_DOMAIN_OBJECT_CLASS = DBTrace.class; + static final String TRACE_CONTENT_DEFAULT_TOOL = "Debugger"; + + private static final DBTraceLinkContentHandler linkHandler = new DBTraceLinkContentHandler(); + @Override public long createFile(FileSystem fs, FileSystem userfs, String path, String name, DomainObject obj, TaskMonitor monitor) @@ -48,7 +57,7 @@ public class DBTraceContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version, + public DBTrace getImmutableObject(FolderItem item, Object consumer, int version, int minChangeVersion, TaskMonitor monitor) throws IOException, CancelledException, VersionException { String contentType = item.getContentType(); @@ -96,7 +105,7 @@ public class DBTraceContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, + public DBTrace getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, Object consumer, TaskMonitor monitor) throws IOException, VersionException, CancelledException { String contentType = item.getContentType(); @@ -146,7 +155,7 @@ public class DBTraceContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, + public DBTrace getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, boolean okToUpgrade, boolean recover, Object consumer, TaskMonitor monitor) throws IOException, CancelledException, VersionException { String contentType = item.getContentType(); @@ -296,8 +305,8 @@ public class DBTraceContentHandler extends DBContentHandler { } @Override - public Class getDomainObjectClass() { - return DBTrace.class; + public Class getDomainObjectClass() { + return TRACE_DOMAIN_OBJECT_CLASS; } @Override @@ -312,12 +321,12 @@ public class DBTraceContentHandler extends DBContentHandler { @Override public String getDefaultToolName() { - return "Debugger"; + return TRACE_CONTENT_DEFAULT_TOOL; } @Override public Icon getIcon() { - return Trace.TRACE_ICON; + return TRACE_ICON; } @Override @@ -331,4 +340,9 @@ public class DBTraceContentHandler extends DBContentHandler { // TODO: return null; } + + @Override + public DBTraceLinkContentHandler getLinkHandler() { + return linkHandler; + } } diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceLinkContentHandler.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceLinkContentHandler.java new file mode 100644 index 0000000000..f64526be0a --- /dev/null +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceLinkContentHandler.java @@ -0,0 +1,71 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.trace.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 DBTraceLinkContentHandler extends LinkHandler { + + public static final String TRACE_LINK_CONTENT_TYPE = "TraceLink"; + + @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, TRACE_LINK_CONTENT_TYPE, fs, path, name, + monitor); + } + + @Override + public String getContentType() { + return TRACE_LINK_CONTENT_TYPE; + } + + @Override + public String getContentTypeDisplayString() { + return TRACE_LINK_CONTENT_TYPE; + } + + @Override + public Class getDomainObjectClass() { + // return linked content class + return DBTraceContentHandler.TRACE_DOMAIN_OBJECT_CLASS; + } + + @Override + public Icon getIcon() { + return DBTraceContentHandler.TRACE_ICON; + } + + @Override + public String getDefaultToolName() { + return DBTraceContentHandler.TRACE_CONTENT_DEFAULT_TOOL; + } + +} diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index d35661f6a9..195fbaad40 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -351,6 +351,7 @@ src/main/help/help/topics/FrontEndPlugin/images/DeleteProject.png||GHIDRA||||END src/main/help/help/topics/FrontEndPlugin/images/EditPluginPath.png||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/EditProjectAccessList.png||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/EditProjectAccessPanel.png||GHIDRA||||END| +src/main/help/help/topics/FrontEndPlugin/images/LinkOtherProject.png||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/MemoryUsage.png||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/NonSharedProjectInfo.png||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/OpenProject.png||GHIDRA||||END| @@ -1089,7 +1090,6 @@ src/main/resources/images/layout_add.png||FAMFAMFAM Icons - CC 2.5|||famfamfam s src/main/resources/images/ledgreen.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| src/main/resources/images/ledred.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| src/main/resources/images/ledyellow.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| -src/main/resources/images/link.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| src/main/resources/images/lock.gif||GHIDRA||||END| src/main/resources/images/magnifier.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/media-flash.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| diff --git a/Ghidra/Features/Base/ghidra_scripts/CallAnotherScriptForAllPrograms.java b/Ghidra/Features/Base/ghidra_scripts/CallAnotherScriptForAllPrograms.java index 4832a4be91..2394c1f664 100644 --- a/Ghidra/Features/Base/ghidra_scripts/CallAnotherScriptForAllPrograms.java +++ b/Ghidra/Features/Base/ghidra_scripts/CallAnotherScriptForAllPrograms.java @@ -17,6 +17,8 @@ // NOTE: Script will only process unversioned and checked-out files. //@category Examples +import java.io.IOException; + import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraState; import ghidra.framework.model.*; @@ -25,8 +27,6 @@ import ghidra.program.model.listing.Program; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; -import java.io.IOException; - public class CallAnotherScriptForAllPrograms extends GhidraScript { // The script referenced in the following line should be replaced with the script to be called @@ -59,6 +59,10 @@ public class CallAnotherScriptForAllPrograms extends GhidraScript { } private void processDomainFile(DomainFile domainFile) throws CancelledException, IOException { + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. If program links should be considered + // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())" + // should be used. if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domainFile.getContentType())) { return; // skip non-Program files } diff --git a/Ghidra/Features/Base/ghidra_scripts/RepositoryFileUpgradeScript.java b/Ghidra/Features/Base/ghidra_scripts/RepositoryFileUpgradeScript.java index db4c0acaec..617aab04a8 100644 --- a/Ghidra/Features/Base/ghidra_scripts/RepositoryFileUpgradeScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/RepositoryFileUpgradeScript.java @@ -143,6 +143,8 @@ public class RepositoryFileUpgradeScript extends GhidraScript { } private boolean performProgramUpgrade(DomainFile df) throws IOException, CancelledException { + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(df.getContentType())) { return false; } diff --git a/Ghidra/Features/Base/ghidra_scripts/VersionControl_AddAll.java b/Ghidra/Features/Base/ghidra_scripts/VersionControl_AddAll.java index c1f2f84d62..63db21645d 100644 --- a/Ghidra/Features/Base/ghidra_scripts/VersionControl_AddAll.java +++ b/Ghidra/Features/Base/ghidra_scripts/VersionControl_AddAll.java @@ -48,7 +48,10 @@ public class VersionControl_AddAll extends GhidraScript { if (monitor.isCancelled()) { break; } - + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. If program links should be considered + // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())" + // should be used. It may also be appropriate to handle other content types. if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(file.getContentType()) || file.isVersioned()) { continue;// skip diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html b/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html index 288af811a4..acb36fbbd4 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html +++ b/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html @@ -22,13 +22,15 @@ hyperlink.

-

The following text shows the syntax of the URL annotation:

+

The following text shows the syntax of a sample URL annotation:


- {@url "http://www.google.com"}
+ {@url "http://www.google.com" "Search Web"}

The bold text is required for all annotations. The italicized text is required but is - specific to the annotation being used (see the table below).

+ specific to the annotation being used (see the table below). The optional rendered display text + "Search Web" will be displayed in listing. If the optional display test is omitted, the URL + will be displayed. Quotes around display text are optional.

Examples

@@ -47,10 +49,21 @@ Rendered URL Annotation Example

When the URL text (e.g., "http://www.google.com") in the above image is clicked from within - Ghidra, a web browser is launched and attempts to load the corresponding web page. If the URL text - is of the form ghidra://<host>[:<port>]/<repository-name>/<program-path>[#<address-or-symbol-ref>] - an attempt will be made to open the corresponding program from the referenced Ghidra Server (e.g., - ghidra://myserver/Repo/notepad.exe#entry).

+ Ghidra, a web browser is launched and attempts to load the corresponding web page.

+ +

If the URL text corresponds to a Ghidra URL and attempt will be made to open the referenced + Program file within the Code Browser. Such a URL may refer to a Program file from a + local project or Ghidra Server. The Ghidra URL forms supported include:

+ +

Remote Ghidra Server file
+ ghidra://<host>[:<port>]/<repository-name>/<program-path>[#<address-or-symbol-ref>]
+ Example: ghidra://hostname/Repo/notepad.exe#entry +

+ +

Local Ghidra project file
+ ghidra:/[<project-path>/]<project-name>?/<program-path>[#<address-or-symbol-ref>]
+ Example: ghidra:/share/MyProject?/notepad.exe#entry +

Valid Annotations

@@ -174,9 +187,10 @@ Displays the given URL has a hyperlink. This annotation optionally takes display text so that the hyperlink may be displayed with text other than that of the URL.

- References to ghidra://, which refer to a program within a Ghidra Server repository, - will be opened within the Listing display, while all other URL protocols (e.g., http://, https://, - file://, etc.) will be launched via an external web browser (see + References to a program file on a Ghidra Server (ghidra://<host...) or + local project (ghidra:/<project-...) will be opened within the Listing display, + while all other URL forms (e.g., http://, https://, file://, etc.) will be + launched via an external web browser (see command configuration for Processor Manuals). @@ -216,6 +230,12 @@
  • {@url "ghidra://myserver/Repo/notepad.exe#entry"}
  • {@url "ghidra://myserver/Repo/notepad.exe" "see notepad.exe"}
  • + +
  • {@url "ghidra:/share/MyProject?/notepad.exe#entry"}
  • + +
  • {@url "ghidra:/share/MyProject?/notepad.exe" "see notepad.exe"}
  • + + @@ -224,8 +244,9 @@ Program
    - Displays a hyperlink to the given Ghidra program name that - will open that program in a new Listing tab when clicked.
    + Displays a hyperlink to the given Ghidra program pathname + with the current project. Referenced program + will open in a new Listing tab when clicked.
    You may optionally provide an address or symbol to be displayed when the program is opened by appending to the program name an '@' character, followed by an address or symbol name. diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm index db73da2d09..8ba831bb37 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm @@ -688,6 +688,54 @@

    The tabbed pane for read-only Project data is removed from the Project Window.

    + +

    Create Linked Folder or File

    + +
    +

    This feature allows you to create a folder or file link in your active project to a + corresponding folder or file within a read-only viewed project. + This is done using a Ghidra URL which references the + file in its local or remote storage location. If the viewed project corresponds to a + viewed repository a remote URL will be used, while other cases will refer to the + locally viewed project. It is possible for links to become broken if the referenced + repository, local project or file location are changed.

    +
      +
    1. Select a single folder or file from a viewed READ-ONLY Project Data tree.
    2. +
    3. Right mouse click on the selected tree node and choose the Copy option.
    4. +
    5. Select a destination folder in the active project tree.
    6. +
    7. Right mouse click on the folder and choose the Paste as Link option. +

      Currently, linked-file types are + currently limited to Program and Data Type Archive files + only. The Past as Link menu item will be disabled for + unsupported file content types or for other unsupported situations such as internal + linking within the same project.

      +
    8. +
    +

    A linked-file may be opened in a tool via the project window in the same fashion that + a normal file is opened (e.g., double-left-mouse-click or drag-n-drop onto a tool box icon). + Such a project file may also be opened within a Tool using its File->Open... action + and selected from the resulting project file selection dilaog. + Clicking on a linked-folder in the active project window will open that location in a + READ-ONLY Project Data tree. The user may be prompted for a shared repository + connection password when accessing a linked folder or file.

    +

    Within a project file chooser dialog a linked-folder may be expanded in a similar fashion + to local folders provided any neccessary repository connection can be completed.

    +

    Add to Version Control... is supported + for repository folder and file links only and will be disabled for links to a + local project.

    +

    Currently, linked-files only provide access + to the latest file version and do not facilitate access to older file versions.

    +

    The project window below shows a Program file-link "Program1" which is linked to the + same file in the viewed project. Hovering the mouse over a linked-file will show the URL + of the linked file or folder. The chain-link icon decoration indicates such a linked + file or folder.

    + +
    + +
    + +
    +

    Workspaces

    diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/LinkOtherProject.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/LinkOtherProject.png new file mode 100644 index 0000000000..15d4c8bcaa Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/LinkOtherProject.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/ViewOtherProjects.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/ViewOtherProjects.png index 4f7e849d8f..95518a3f13 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/ViewOtherProjects.png and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/ViewOtherProjects.png differ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallTreeProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallTreeProvider.java index 52ee26081e..af9625d203 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallTreeProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/calltree/CallTreeProvider.java @@ -1266,19 +1266,24 @@ public class CallTreeProvider extends ComponentProviderAdapter implements Domain } @Override - protected void expandNode(GTreeNode node, TaskMonitor monitor) throws CancelledException { + protected void expandNode(GTreeNode node, boolean force, TaskMonitor monitor) + throws CancelledException { TreePath treePath = node.getTreePath(); Object[] path = treePath.getPath(); if (path.length > maxDepth) { return; } + if (!force && !node.isAutoExpandPermitted()) { + return; + } + CallNode callNode = (CallNode) node; if (callNode.functionIsInPath()) { return; // this path hit a function that is already in the path } - super.expandNode(node, monitor); + super.expandNode(node, false, monitor); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clear/ClearFlowAndRepairCmd.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clear/ClearFlowAndRepairCmd.java index 6188600b22..b767ed8591 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clear/ClearFlowAndRepairCmd.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/clear/ClearFlowAndRepairCmd.java @@ -106,9 +106,33 @@ public class ClearFlowAndRepairCmd extends BackgroundCommand { CodeUnit cu = cuIter.next(); if (cu instanceof Instruction) { Instruction instr = (Instruction) cu; + + // check for function on delay slot + if (listing.getFunctionAt(instr.getMinAddress()) != null) { + continue; // skip since it will be picked-up by flow if appropriate + } + + // check for fallthrough to instruction Address ffAddr = instr.getFallFrom(); if (ffAddr != null && startAddrs.contains(ffAddr)) { - continue; // skip since it will be picked-up by flow + continue; // skip since it will be picked-up by flow if appropriate + } + + // check for flow into delay slot + if (instr.isInDelaySlot()) { + boolean skip = false; + ReferenceIterator refToIter = instr.getReferenceIteratorTo(); + while (refToIter.hasNext()) { + Reference ref = refToIter.next(); + RefType refType = ref.getReferenceType(); + if (refType.isJump() || refType.isCall()) { + skip = true; + break; + } + } + if (skip) { + continue; // skip since it will be picked-up by flow if appropriate + } } } else { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsDialog.java index 91ba90a4e2..2c335a7a52 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/comments/CommentsDialog.java @@ -240,10 +240,11 @@ public class CommentsDialog extends DialogComponentProvider implements KeyListen //////////////////////////////////////////////////////////////////// private AnnotationAdapterWrapper[] getAnnotationAdapterWrappers() { - AnnotatedStringHandler[] annotations = Annotation.getAnnotatedStringHandlers(); - AnnotationAdapterWrapper[] retVal = new AnnotationAdapterWrapper[annotations.length]; - for (int i = 0; i < annotations.length; i++) { - retVal[i] = new AnnotationAdapterWrapper(annotations[i]); + List annotations = Annotation.getAnnotatedStringHandlers(); + int count = annotations.size(); + AnnotationAdapterWrapper[] retVal = new AnnotationAdapterWrapper[count]; + for (int i = 0; i < count; i++) { + retVal[i] = new AnnotationAdapterWrapper(annotations.get(i)); } return retVal; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java index 2d8a5df43f..9d13288744 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypeManagerPlugin.java @@ -56,7 +56,6 @@ import ghidra.framework.options.SaveState; import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.program.database.DataTypeArchiveContentHandler; import ghidra.program.database.data.ProgramDataTypeManager; import ghidra.program.model.address.AddressSetView; import ghidra.program.model.data.*; @@ -245,8 +244,7 @@ public class DataTypeManagerPlugin extends ProgramPlugin Project project = tool.getProjectManager().getActiveProject(); if (project != null && project.getName().equals(projectName)) { DomainFile df = project.getProjectData().getFile(pathname); - if (df != null && DataTypeArchiveContentHandler.DATA_TYPE_ARCHIVE_CONTENT_TYPE - .equals(df.getContentType())) { + if (DataTypeArchive.class.isAssignableFrom(df.getDomainObjectClass())) { return df; } } @@ -588,12 +586,9 @@ public class DataTypeManagerPlugin extends ProgramPlugin openArchive(domainFile, version); } }; - DomainFileFilter filter = f -> { - Class c = f.getDomainObjectClass(); - return DataTypeArchive.class.isAssignableFrom(c); - }; openDialog = - new OpenVersionedFileDialog(tool, "Open Project Data Type Archive", filter); + new OpenVersionedFileDialog(tool, "Open Project Data Type Archive", + df -> DataTypeArchive.class.isAssignableFrom(df.getDomainObjectClass())); openDialog.setHelpLocation(new HelpLocation(HelpTopics.PROGRAM, "Open_File_Dialog")); openDialog.addOkActionListener(listener); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java index ff576efdc3..840271f4bb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java @@ -608,8 +608,15 @@ public class DataTypesProvider extends ComponentProviderAdapter { ArchiveNode archiveNode = dataTypeNode.getArchiveNode(); if (archiveNode instanceof ProjectArchiveNode && !archiveNode.isModifiable()) { - Msg.showInfo(getClass(), archiveGTree, "Archive Not Checked Out", - "You must checkout this archive before you may edit data types."); + ProjectArchiveNode projectArchive = (ProjectArchiveNode) archiveNode; + if (projectArchive.getDomainFile().isReadOnly()) { + Msg.showInfo(getClass(), archiveGTree, "Read-Only Archive", + "You may not edit data type within a read-only project archive."); + } + else { + Msg.showInfo(getClass(), archiveGTree, "Archive Not Checked Out", + "You must checkout this archive before you may edit data types."); + } return; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/associate/AssociateDataTypeAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/associate/AssociateDataTypeAction.java index 3e6a4f8b51..20e1a7c2b4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/associate/AssociateDataTypeAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/associate/AssociateDataTypeAction.java @@ -38,6 +38,7 @@ import ghidra.app.plugin.core.datamgr.tree.ArchiveNode; import ghidra.app.plugin.core.datamgr.tree.DataTypeNode; import ghidra.app.plugin.core.datamgr.util.DataTypeTreeCopyMoveTask; import ghidra.app.plugin.core.datamgr.util.DataTypeTreeCopyMoveTask.ActionType; +import ghidra.app.plugin.core.datamgr.util.DataTypeUtils; import ghidra.program.model.data.*; import ghidra.util.Msg; import ghidra.util.layout.PairLayout; @@ -85,26 +86,21 @@ public class AssociateDataTypeAction extends DockingAction { return !nodes.isEmpty(); } - private boolean hasSingleModifiableSourceArchive(List nodes) { + private Archive getSingleDTArchive(List nodes) { - Archive sourceArchive = null; + Archive dtArchive = null; for (GTreeNode node : nodes) { Archive archive = findArchive(node); - if (sourceArchive == null) { - sourceArchive = archive; + if (dtArchive == null) { + dtArchive = archive; continue; } - if (sourceArchive != archive) { - return false; + if (dtArchive != archive) { + return null; } } - - if (sourceArchive != null && sourceArchive.isModifiable()) { - return true; - } - - return false; + return dtArchive; } private static Archive findArchive(GTreeNode node) { @@ -134,13 +130,20 @@ public class AssociateDataTypeAction extends DockingAction { List nodes = ((DataTypesActionContext) context).getSelectedNodes(); - if (!hasSingleModifiableSourceArchive(nodes)) { - Msg.showInfo(this, getProviderComponent(), "Multiple Source Archives", + Archive dtArchive = getSingleDTArchive(nodes); + if (dtArchive == null) { + Msg.showInfo(this, getProviderComponent(), "Multiple Data Type Archives", "The currently selected nodes are from multiple archives.\n" + "Please select only nodes from a single archvie."); return; } + if (!dtArchive.isModifiable()) { + DataTypeUtils.showUnmodifiableArchiveErrorMessage(context.getSourceComponent(), + "Disassociate Failed", dtArchive.getDataTypeManager()); + return; + } + if (isAlreadyAssociated((DataTypesActionContext) context)) { Msg.showInfo(this, getProviderComponent(), "Already Associated", "One or more of the currently selected nodes are already associated\n" + diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java index 5a65c0eaf7..ba12a297cf 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.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,7 +76,7 @@ public class DataTypeManagerHandler { private DataTreeDialog dataTreeSaveDialog; private CreateDataTypeArchiveDataTreeDialog dataTreeCreateDialog; private boolean treeDialogCancelled = false; - private DomainFileFilter domainFileFilter; + private DomainFileFilter createArchiveFileFilter; private DataTypeIndexer dataTypeIndexer; private List archiveManagerlisteners = new ArrayList<>(); @@ -102,9 +102,17 @@ public class DataTypeManagerHandler { dataTypeIndexer.addDataTypeManager(builtInDataTypesManager); openArchives.add(new BuiltInArchive(this, builtInDataTypesManager)); - domainFileFilter = f -> { - Class c = f.getDomainObjectClass(); - return DataTypeArchive.class.isAssignableFrom(c); + createArchiveFileFilter = new DomainFileFilter() { + + @Override + public boolean accept(DomainFile df) { + return DataTypeArchive.class.isAssignableFrom(df.getDomainObjectClass()); + } + + @Override + public boolean followLinkedFolders() { + return false; + } }; folderListener = new MyFolderListener(); @@ -1425,7 +1433,7 @@ public class DataTypeManagerHandler { } }; dataTreeSaveDialog = - new DataTreeDialog(null, "Save As", DataTreeDialog.SAVE, domainFileFilter); + new DataTreeDialog(null, "Save As", DataTreeDialog.SAVE, createArchiveFileFilter); dataTreeSaveDialog.addOkActionListener(listener); dataTreeSaveDialog @@ -1465,7 +1473,7 @@ public class DataTypeManagerHandler { }; dataTreeCreateDialog = new CreateDataTypeArchiveDataTreeDialog(null, "Create", - DataTreeDialog.CREATE, domainFileFilter); + DataTreeDialog.CREATE, createArchiveFileFilter); dataTreeCreateDialog.addOkActionListener(listener); dataTreeCreateDialog.setHelpLocation( diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/util/DataTypeUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/util/DataTypeUtils.java index f637ed089e..0bf821c53c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/util/DataTypeUtils.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/util/DataTypeUtils.java @@ -24,6 +24,7 @@ import javax.swing.Icon; import generic.theme.GColor; import generic.theme.GIcon; import ghidra.app.services.DataTypeQueryService; +import ghidra.program.database.data.ProjectDataTypeManager; import ghidra.program.model.data.*; import ghidra.program.model.data.Enum; import ghidra.util.Msg; @@ -438,9 +439,18 @@ public class DataTypeUtils { msg = "The archive file is not modifiable!\nYou must open the archive for editing\n" + "before performing this operation.\n" + dtm.getName(); } + else if (dtm instanceof ProjectDataTypeManager) { + ProjectDataTypeManager projectDtm = (ProjectDataTypeManager) dtm; + if (!projectDtm.isUpdatable() && !projectDtm.getDomainFile().canCheckout()) { + msg = "The project archive is not modifiable!\n" + dtm.getName(); + } + else { + msg = "The project archive is not modifiable!\nYou must check out the archive\n" + + "before performing this operation.\n" + dtm.getName(); + } + } else { - msg = "The project archive is not modifiable!\nYou must check out the archive\n" + - "before performing this operation.\n" + dtm.getName(); + msg = "The Archive is not modifiable!\n"; } Msg.showInfo(DataTypeUtils.class, parent, title, msg); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterDialog.java index 79a56332fc..3c579e1f92 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterDialog.java @@ -295,11 +295,15 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa return comboBox; } + @SuppressWarnings("unchecked") private List getApplicableExporters() { List list = new ArrayList<>(ClassSearcher.getInstances(Exporter.class)); - Class domainObjectClass = domainFile.getDomainObjectClass(); - list.removeIf(exporter -> !exporter.canExportDomainObject(domainObjectClass)); - Collections.sort(list, (o1, o2) -> o1.toString().compareTo(o2.toString())); + Class domainObjectClass = domainFile.getDomainObjectClass(); + if (DomainObject.class.isAssignableFrom(domainObjectClass)) { + list.removeIf(exporter -> !exporter + .canExportDomainObject((Class) domainObjectClass)); + Collections.sort(list, (o1, o2) -> o1.toString().compareTo(o2.toString())); + } return list; } @@ -422,8 +426,16 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa private void doOpenFile(TaskMonitor monitor) { try { - domainObject = domainFile.getImmutableDomainObject(this, DomainFile.DEFAULT_VERSION, - TaskMonitor.DUMMY); + if (domainFile.isLinkFile()) { + // Linked files are always subject to upgrade if needed and do not support + // getImmutableDomainObject + domainObject = + domainFile.getReadOnlyDomainObject(this, DomainFile.DEFAULT_VERSION, monitor); + } + else { + domainObject = + domainFile.getImmutableDomainObject(this, DomainFile.DEFAULT_VERSION, monitor); + } } catch (VersionException | CancelledException | IOException e) { Msg.showError(this, getComponent(), "Error Opening File", diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/misc/MyProgramChangesDisplayPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/misc/MyProgramChangesDisplayPlugin.java index b375368cf5..4e3a9f303a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/misc/MyProgramChangesDisplayPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/misc/MyProgramChangesDisplayPlugin.java @@ -147,7 +147,8 @@ public class MyProgramChangesDisplayPlugin extends ProgramPlugin implements Doma @Override public boolean isEnabledForContext(ActionContext context) { - return currentProgram != null && currentProgram.getDomainFile().canCheckin(); + return currentProgram != null && + currentProgram.getDomainFile().modifiedSinceCheckout(); } }; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/LanguageProviderPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/LanguageProviderPlugin.java index 2dbf91aba7..c68b4c582d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/LanguageProviderPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/LanguageProviderPlugin.java @@ -26,8 +26,8 @@ import docking.action.MenuData; import docking.widgets.OptionDialog; import ghidra.app.CorePluginPackage; import ghidra.app.plugin.PluginCategoryNames; -import ghidra.framework.main.FrontEndTool; import ghidra.framework.main.ApplicationLevelPlugin; +import ghidra.framework.main.FrontEndTool; import ghidra.framework.main.datatable.ProjectDataContext; import ghidra.framework.model.*; import ghidra.framework.plugintool.*; @@ -316,18 +316,20 @@ public final class LanguageProviderPlugin extends Plugin implements ApplicationL try { SwingUtilities.invokeAndWait(() -> { ToolServices toolServices = tool.getToolServices(); - String defaultToolName = toolServices.getDefaultToolTemplate(file).getName(); - for (PluginTool t : toolServices.getRunningTools()) { - if (t.getName().equals(defaultToolName)) { - openTool = t; - break; + ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(file); + if (defaultToolTemplate != null) { + String defaultToolName = defaultToolTemplate.getName(); + for (PluginTool t : toolServices.getRunningTools()) { + if (t.getName().equals(defaultToolName)) { + openTool = t; + break; + } } } - if (openTool != null) { - openTool.acceptDomainFiles(new DomainFile[] { file }); - } - else { - openTool = tool.getToolServices().launchDefaultTool(file); + if (openTool == null || + !openTool.acceptDomainFiles(new DomainFile[] { file })) { + Msg.showError(this, tool.getToolFrame(), "Failed to Open Program", + "A suitable default tool could not found!"); } }); } @@ -336,7 +338,7 @@ public final class LanguageProviderPlugin extends Plugin implements ApplicationL } catch (InvocationTargetException e) { Throwable t = e.getCause(); - Msg.showError(this, tool.getToolFrame(), "Tool Launch Failed", + Msg.showError(this, tool.getToolFrame(), "Failed to Open Program", "An error occurred while attempting to launch your default tool!", t); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java index 1cd8236b27..cd5e593d04 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiProgramManager.java @@ -30,6 +30,7 @@ import ghidra.app.events.*; import ghidra.app.nav.Navigatable; import ghidra.app.services.*; import ghidra.app.util.task.OpenProgramTask; +import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest; import ghidra.framework.data.DomainObjectAdapterDB; import ghidra.framework.model.*; import ghidra.framework.plugintool.PluginTool; @@ -42,9 +43,6 @@ import ghidra.util.task.TaskLauncher; class MultiProgramManager implements DomainObjectListener, TransactionListener { - // arbitrary counter for given ProgramInfo objects and ID to use for sorting - private static final AtomicInteger nextAvailableId = new AtomicInteger(); - private ProgramManagerPlugin plugin; private PluginTool tool; private ProgramInfo currentInfo; @@ -82,25 +80,31 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { }; } - void addProgram(Program p, URL ghidraURL, int state) { + void addProgram(Program p, DomainFile domainFile, int state) { + addProgram(new ProgramInfo(p, domainFile, state != ProgramManager.OPEN_HIDDEN), state); + } + + void addProgram(Program p, URL ghidraUrl, int state) { + addProgram(new ProgramInfo(p, ghidraUrl, state != ProgramManager.OPEN_HIDDEN), state); + } + + private void addProgram(ProgramInfo programInfo, int state) { + Program p = programInfo.program; ProgramInfo oldInfo = getInfo(p); if (oldInfo == null) { + oldInfo = programInfo; p.addConsumer(tool); - ProgramInfo info = new ProgramInfo(p, state != ProgramManager.OPEN_HIDDEN); - info.ghidraURL = ghidraURL; - openPrograms.add(info); + openPrograms.add(oldInfo); openPrograms.sort(Comparator.naturalOrder()); - programMap.put(p, info); + programMap.put(p, oldInfo); fireOpenEvents(p); p.addListener(this); p.addTransactionListener(this); } - else { - if (!oldInfo.visible && state != ProgramManager.OPEN_HIDDEN) { - oldInfo.setVisible(true); - } + else if (!oldInfo.visible && state != ProgramManager.OPEN_HIDDEN) { + oldInfo.setVisible(true); } if (state == ProgramManager.OPEN_CURRENT) { saveLocation(); @@ -246,12 +250,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { TransientToolState toolState = null; if (currentInfo != null) { currentInfo.setVisible(true); - DomainFile df = currentInfo.program.getDomainFile(); - String title = df.toString(); - if (df.isReadOnly()) { - title = title + " [Read-Only]"; - } - tool.setSubTitle(title); + tool.setSubTitle(currentInfo.toString()); txMonitor.setProgram(currentInfo.program); if (currentInfo.lastState != null) { toolState = currentInfo.lastState; @@ -370,7 +369,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { return (info != null && info.owner != null); } - private ProgramInfo getInfo(Program p) { + ProgramInfo getInfo(Program p) { if (p == null) { return null; } @@ -378,9 +377,6 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { } Program getOpenProgram(URL ghidraURL) { - if (!GhidraURL.isServerRepositoryURL(ghidraURL)) { - return null; - } URL normalizedURL = GhidraURL.getNormalizedURL(ghidraURL); for (ProgramInfo info : programMap.values()) { URL url = info.ghidraURL; @@ -392,10 +388,10 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { } Program getOpenProgram(DomainFile domainFile, int version) { - for (Program program : programMap.keySet()) { - DomainFile programDomainFile = program.getDomainFile(); - if (filesMatch(domainFile, version, programDomainFile)) { - return program; + for (ProgramInfo info : programMap.values()) { + DomainFile df = info.domainFile; + if (df != null && filesMatch(domainFile, version, df)) { + return info.program; } } return null; @@ -413,7 +409,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { if (!SystemUtilities.isEqual(file1.getProjectLocator(), file2.getProjectLocator())) { return false; } - + // TODO: version check is questionable - unclear how proxy file would work int openVersion = file2.isReadOnly() ? file2.getVersion() : -1; return version == openVersion; } @@ -488,28 +484,54 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { OpenProgramTask openTask = new OpenProgramTask(file, -1, this); openTask.setSilent(); new TaskLauncher(openTask, tool.getToolFrame()); - Program openProgram = openTask.getOpenProgram(); - plugin.openProgram(openProgram, - dataState != null ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE); - openProgram.release(this); - removeProgram((Program) oldObject); - if (dataState != null) { - tool.restoreDataStateFromXml(dataState); + OpenProgramRequest openProgramReq = openTask.getOpenProgram(); + if (openProgramReq != null) { + plugin.openProgram(openProgramReq.getProgram(), + dataState != null ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE); + openProgramReq.release(); + removeProgram((Program) oldObject); + if (dataState != null) { + tool.restoreDataStateFromXml(dataState); + } } } } - private class ProgramInfo implements Comparable { + class ProgramInfo implements Comparable { + + // arbitrary counter for given ProgramInfo objects and ID to use for sorting + private static final AtomicInteger nextAvailableId = new AtomicInteger(); + + public final Program program; + + // NOTE: domainFile and ghidraURL use are mutually exclusive and reflect how program was + // opened. Supported cases include: + // 1. Opened via Program file + // 2. Opened via ProgramLink file + // 3. Opened via Program URL + + public final DomainFile domainFile; // may be link file + public final URL ghidraURL; - private Program program; - private URL ghidraURL; private TransientToolState lastState; private int instance; - private boolean visible; + private boolean visible = false; private Object owner; - ProgramInfo(Program p, boolean visible) { + private String str; // cached toString + + ProgramInfo(Program p, DomainFile domainFile, boolean visible) { this.program = p; + this.domainFile = domainFile; + this.ghidraURL = null; + this.visible = visible; + instance = nextAvailableId.incrementAndGet(); + } + + ProgramInfo(Program p, URL ghidraURL, boolean visible) { + this.program = p; + this.domainFile = null; + this.ghidraURL = ghidraURL; this.visible = visible; instance = nextAvailableId.incrementAndGet(); } @@ -523,5 +545,24 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener { public int compareTo(ProgramInfo info) { return instance - info.instance; } + + @Override + public String toString() { + if (str != null) { + return str; + } + StringBuilder buf = new StringBuilder(); + DomainFile df = program.getDomainFile(); + if (domainFile != null && domainFile.isLinkFile()) { + buf.append(domainFile.getName()); + buf.append("->"); + } + buf.append(df.toString()); + if (df.isReadOnly()) { + buf.append(" [Read-Only]"); + } + str = buf.toString(); + return str; + } } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java index 6fc4d4aac8..454ecd2fd9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramManagerPlugin.java @@ -18,7 +18,6 @@ package ghidra.app.plugin.core.progmgr; import java.awt.Component; import java.awt.event.ActionListener; import java.beans.PropertyEditor; -import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -32,26 +31,26 @@ import ghidra.app.CorePluginPackage; import ghidra.app.context.ProgramActionContext; import ghidra.app.events.*; import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.core.progmgr.MultiProgramManager.ProgramInfo; import ghidra.app.services.ProgramManager; import ghidra.app.util.HelpTopics; import ghidra.app.util.NamespaceUtils; import ghidra.app.util.task.OpenProgramTask; +import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest; import ghidra.framework.client.ClientUtil; -import ghidra.framework.data.ProjectFileManager; import ghidra.framework.main.OpenVersionedFileDialog; import ghidra.framework.model.*; import ghidra.framework.options.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.framework.protocol.ghidra.*; -import ghidra.program.database.ProgramContentHandler; +import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.program.model.address.*; import ghidra.program.model.listing.Program; import ghidra.program.model.symbol.Symbol; import ghidra.program.model.symbol.SymbolType; import ghidra.program.util.*; import ghidra.util.*; -import ghidra.util.exception.NotFoundException; +import ghidra.util.exception.AssertException; import ghidra.util.task.TaskLauncher; //@formatter:off @@ -121,16 +120,21 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { if (domainFile == null) { continue; } - if (!(Program.class.isAssignableFrom(domainFile.getDomainObjectClass()))) { - continue; + Class domainObjectClass = domainFile.getDomainObjectClass(); + if (Program.class.isAssignableFrom(domainObjectClass)) { + filesToOpen.add(domainFile); } - filesToOpen.add(domainFile); } openPrograms(filesToOpen); return !filesToOpen.isEmpty(); } + @Override + public boolean accept(URL url) { + return openProgram(url, OPEN_CURRENT) != null; + } + @Override public Class[] getSupportedDataTypes() { return new Class[] { Program.class }; @@ -144,89 +148,53 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { return null; } - return Swing.runNow(() -> doOpenProgram(ghidraURL, state)); - } - - private void messageBadProgramURL(URL ghidraURL) { - Msg.showError(this, null, "Invalid Ghidra URL", - "Ghidra URL does not reference a Ghidra Program: " + ghidraURL); - } - - protected Program doOpenProgram(URL ghidraURL, int openState) { - if (!GhidraURL.isServerRepositoryURL(ghidraURL)) { - Msg.showError(this, null, "Invalid Ghidra URL", - "Ghidra URL does not reference a Ghidra Program: " + ghidraURL); - return null; - } - Program openProgram = programMgr.getOpenProgram(ghidraURL); - if (openProgram != null) { - programMgr.addProgram(openProgram, GhidraURL.getNormalizedURL(ghidraURL), openState); - if (openState == ProgramManager.OPEN_CURRENT) { - gotoProgramRef(openProgram, ghidraURL.getRef()); + // Check for URL already open and re-use + URL url = GhidraURL.getNormalizedURL(ghidraURL); + Program p = programMgr.getOpenProgram(url); + if (p != null) { + showProgram(p, url, state); + if (state == ProgramManager.OPEN_CURRENT) { + gotoProgramRef(p, ghidraURL.getRef()); programMgr.saveLocation(); } - contextChanged(); - return openProgram; + return p; } - GhidraURLWrappedContent wrappedContent = null; - Object content = null; + Program program = Swing.runNow(() -> doOpenProgram(ghidraURL, state)); + + if (program != null) { + Msg.info(this, "Opened program in " + tool.getName() + " tool: " + ghidraURL); + } + return program; + } + + /** + * Open GhidraURL which corresponds to {@code ghidra://} remote URLs which correspond to a + * repository program file. + * @param ghidraURL Ghidra URL which specified Program to be opened which optional ref + * @param openState open state + * @return program instance of null if open failed + */ + private Program doOpenProgram(URL ghidraURL, int openState) { + Program p = null; try { - GhidraURLConnection c = (GhidraURLConnection) ghidraURL.openConnection(); - Object obj = c.getContent(); - if (c.getResponseCode() == GhidraURLConnection.GHIDRA_UNAUTHORIZED) { - return null; // assume user already notified + URL url = GhidraURL.getNormalizedURL(ghidraURL); + OpenProgramTask task = new OpenProgramTask(url, this); + new TaskLauncher(task, tool.getToolFrame()); + OpenProgramRequest openProgramReq = task.getOpenProgram(); + if (openProgramReq != null) { + p = openProgramReq.getProgram(); + showProgram(p, url, openState); + openProgramReq.release(); } - if (!(obj instanceof GhidraURLWrappedContent)) { - messageBadProgramURL(ghidraURL); - return null; - } - wrappedContent = (GhidraURLWrappedContent) obj; - content = wrappedContent.getContent(this); - if (!(content instanceof DomainFile)) { - messageBadProgramURL(ghidraURL); - return null; - } - DomainFile df = (DomainFile) content; - if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(df.getContentType())) { - messageBadProgramURL(ghidraURL); - return null; - } - - OpenProgramTask task = new OpenProgramTask(df, true, this); - TaskLauncher.launch(task); - - openProgram = task.getOpenProgram(); - if (openProgram == null) { - return null; - } - - programMgr.addProgram(openProgram, GhidraURL.getNormalizedURL(ghidraURL), openState); - contextChanged(); - openProgram.release(this); - if (openState == ProgramManager.OPEN_CURRENT) { - gotoProgramRef(openProgram, ghidraURL.getRef()); - programMgr.saveLocation(); - } - return openProgram; - } - catch (NotFoundException e) { - messageBadProgramURL(ghidraURL); - } - catch (MalformedURLException e) { - Msg.showError(this, null, "Invalid Ghidra URL", - "Improperly formed Ghidra URL: " + ghidraURL); - } - catch (IOException e) { - Msg.showError(this, null, "Program Open Failed", - "Failed to open Ghidra URL: " + e.getMessage()); } finally { - if (content != null) { - wrappedContent.release(content, this); + if (p != null && openState == ProgramManager.OPEN_CURRENT) { + gotoProgramRef(p, ghidraURL.getRef()); + programMgr.saveLocation(); } } - return null; + return p; } private boolean gotoProgramRef(Program program, String ref) { @@ -296,9 +264,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { } Program program = Swing.runNow(() -> { - Program p = doOpenProgram(domainFile, version, state); - contextChanged(); - return p; + return doOpenProgram(domainFile, version, state); }); if (program != null) { @@ -460,13 +426,39 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { @Override public void openProgram(final Program program, final int state) { + showProgram(program, program.getDomainFile(), state); + } + + private void showProgram(Program p, URL ghidraUrl, final int state) { + if (p == null || p.isClosed()) { + throw new AssertException("Opened program required"); + } if (locked) { throw new IllegalStateException( "Progam manager is locked and cannot accept a new program"); } Runnable r = () -> { - programMgr.addProgram(program, null, state); + programMgr.addProgram(p, ghidraUrl, state); + if (state == ProgramManager.OPEN_CURRENT) { + programMgr.saveLocation(); + } + contextChanged(); + }; + Swing.runNow(r); + } + + private void showProgram(Program p, DomainFile domainFile, final int state) { + if (p == null || p.isClosed()) { + throw new AssertException("Opened program required"); + } + if (locked) { + throw new IllegalStateException( + "Progam manager is locked and cannot accept a new program"); + } + + Runnable r = () -> { + programMgr.addProgram(p, domainFile, state); if (state == ProgramManager.OPEN_CURRENT) { programMgr.saveLocation(); } @@ -625,11 +617,9 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { doOpenProgram(domainFile, version, OPEN_CURRENT); } }; - DomainFileFilter filter = f -> { - Class c = f.getDomainObjectClass(); - return Program.class.isAssignableFrom(c); - }; - openDialog = new OpenVersionedFileDialog(tool, "Open Program", filter); + openDialog = new OpenVersionedFileDialog(tool, "Open Program", f -> { + return Program.class.isAssignableFrom(f.getDomainObjectClass()); + }); openDialog.setHelpLocation(new HelpLocation(HelpTopics.PROGRAM, "Open_File_Dialog")); openDialog.addOkActionListener(listener); } @@ -638,9 +628,12 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { } public void openPrograms(List filesToOpen) { + Program showIfNeeded = null; OpenProgramTask openTask = null; for (DomainFile domainFile : filesToOpen) { - if (programMgr.getOpenProgram(domainFile, -1) != null) { + Program p = programMgr.getOpenProgram(domainFile, -1); + if (p != null) { + showIfNeeded = p; continue; } if (openTask == null) { @@ -652,32 +645,37 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { } if (openTask != null) { new TaskLauncher(openTask, tool.getToolFrame()); - List openPrograms = openTask.getOpenPrograms(); - - for (Program program : openPrograms) { - openProgram(program, OPEN_VISIBLE); - program.release(this); - } - if (!openPrograms.isEmpty()) { - openProgram(openPrograms.get(0), OPEN_CURRENT); + List openProgramReqs = openTask.getOpenPrograms(); + boolean isFirst = true; + for (OpenProgramRequest programReq : openProgramReqs) { + showProgram(programReq.getProgram(), programReq.getDomainFile(), + isFirst ? OPEN_CURRENT : OPEN_VISIBLE); + programReq.release(); + isFirst = false; + showIfNeeded = null; } } + if (showIfNeeded != null) { + showProgram(showIfNeeded, showIfNeeded.getDomainFile(), OPEN_CURRENT); + } } protected Program doOpenProgram(DomainFile domainFile, int version, int openState) { - Program openProgram = programMgr.getOpenProgram(domainFile, version); - if (openProgram != null) { - openProgram(openProgram, openState); - return openProgram; + Program p = programMgr.getOpenProgram(domainFile, version); + if (p != null) { + openProgram(p, openState); } - OpenProgramTask task = new OpenProgramTask(domainFile, version, this); - new TaskLauncher(task, tool.getToolFrame()); - openProgram = task.getOpenProgram(); - if (openProgram != null) { - openProgram(openProgram, openState); - openProgram.release(this); + else { + OpenProgramTask task = new OpenProgramTask(domainFile, version, this); + new TaskLauncher(task, tool.getToolFrame()); + OpenProgramRequest programReq = task.getOpenProgram(); + if (programReq != null) { + p = programReq.getProgram(); + showProgram(p, programReq.getDomainFile(), openState); + programReq.release(); + } } - return openProgram; + return p; } @Override @@ -705,18 +703,20 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { */ @Override public void writeDataState(SaveState saveState) { - // Only remember programs from non-transient projects - ArrayList programs = new ArrayList<>(); + + ArrayList programInfos = new ArrayList<>(); for (Program p : programMgr.getAllPrograms()) { - ProjectLocator projectLocator = p.getDomainFile().getProjectLocator(); - if (projectLocator != null && !projectLocator.isTransient()) { - programs.add(p); + ProgramInfo info = programMgr.getInfo(p); + if (info != null) { + programInfos.add(info); } } - saveState.putInt("NUM_PROGRAMS", programs.size()); + + saveState.putInt("NUM_PROGRAMS", programInfos.size()); + int i = 0; - for (Program p : programs) { - writeProgramInfo(p, saveState, i++); + for (ProgramInfo programInfo : programInfos) { + writeProgramInfo(programInfo, saveState, i++); } Program p = programMgr.getCurrentProgram(); if (p != null) { @@ -768,13 +768,21 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { } } - private void writeProgramInfo(Program program, SaveState saveState, int index) { + private void writeProgramInfo(ProgramInfo programInfo, SaveState saveState, int index) { if (locked) { return; // do not save state when locked. } + + if (programInfo.ghidraURL != null) { + saveState.putString("URL_" + index, programInfo.ghidraURL.toString()); + return; + } + String projectLocation = null; String projectName = null; String path = null; + + Program program = programInfo.program; DomainFile df = program.getDomainFile(); ProjectLocator projectLocator = df.getProjectLocator(); if (projectLocator != null && !projectLocator.isTransient()) { @@ -797,28 +805,30 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { * Read in my data state. */ private void loadPrograms(SaveState saveState) { + int n = saveState.getInt("NUM_PROGRAMS", 0); if (n == 0) { return; } - OpenProgramTask openTask = null; + OpenProgramTask openTask = new OpenProgramTask(this); for (int index = 0; index < n; index++) { + + URL url = getGhidraURL(saveState, index); + if (url != null) { + openTask.addProgramToOpen(url); + continue; + } + DomainFile domainFile = getDomainFile(saveState, index); if (domainFile == null) { continue; } int version = getVersion(saveState, index); - - if (openTask == null) { - openTask = new OpenProgramTask(domainFile, version, this); - } - else { - openTask.addProgramToOpen(domainFile, version); - } + openTask.addProgramToOpen(domainFile, version); } - if (openTask == null) { + if (!openTask.hasOpenProgramRequests()) { return; } @@ -835,10 +845,29 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { "Can't open program", e); } - List openPrograms = openTask.getOpenPrograms(); - for (Program program : openPrograms) { - openProgram(program, OPEN_VISIBLE); - program.release(this); + List openProgramReqs = openTask.getOpenPrograms(); + for (OpenProgramRequest programReq : openProgramReqs) { + DomainFile df = programReq.getDomainFile(); + if (df != null) { + showProgram(programReq.getProgram(), df, OPEN_VISIBLE); + } + else { + showProgram(programReq.getProgram(), programReq.getGhidraURL(), OPEN_VISIBLE); + } + programReq.release(); + } + } + + private URL getGhidraURL(SaveState saveState, int index) { + String url = saveState.getString("URL_" + index, null); + if (url == null) { + return null; + } + try { + return new URL(url); + } + catch (MalformedURLException e) { + return null; } } @@ -853,20 +882,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager { ProjectData projectData = tool.getProject().getProjectData(projectLocator); if (projectData == null) { - // Viewed project not available - try { - projectData = new ProjectFileManager(projectLocator, false, false); - } - catch (NotOwnerException e) { - Msg.showError(this, tool.getToolFrame(), "Program Open Failed", - "Not project owner: " + projectLocator + "(" + pathname + ")"); - return null; - } - catch (IOException e) { - Msg.showError(this, tool.getToolFrame(), "Program Open Failed", - "Project error: " + e.getMessage()); - return null; - } + return null; } DomainFile df = projectData.getFile(pathname); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java index f1a6e1a98d..6301568dd2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java @@ -45,9 +45,17 @@ class ProgramSaveManager { ProgramSaveManager(PluginTool tool, ProgramManager programMgr) { this.tool = tool; this.programMgr = programMgr; - domainFileFilter = f -> { - Class c = f.getDomainObjectClass(); - return Program.class.isAssignableFrom(c); + domainFileFilter = new DomainFileFilter() { + + @Override + public boolean accept(DomainFile df) { + return Program.class.isAssignableFrom(df.getDomainObjectClass()); + } + + @Override + public boolean followLinkedFolders() { + return false; // can't save to linked-folder (read-only) + } }; } @@ -244,7 +252,8 @@ class ProgramSaveManager { return; } if (existingFile != null) { - String msg = "Program " + name + " already exists.\n" + "Do you want to overwrite it?"; + String msg = existingFile.getContentType() + " file " + name + " already exists.\n" + + "Do you want to overwrite it?"; if (OptionDialog.showOptionDialog(tool.getToolFrame(), "Duplicate Name", msg, "Overwrite", OptionDialog.QUESTION_MESSAGE) == OptionDialog.CANCEL_OPTION) { return; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java index df63992d9a..88a9d243b0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java @@ -312,7 +312,7 @@ public class HeadlessAnalyzer { Object obj = c.getContent(); if (!(obj instanceof GhidraURLWrappedContent)) { throw new IOException( - "Connect to repository folder failed. Response code: " + c.getResponseCode()); + "Connect to repository folder failed. Response code: " + c.getStatusCode()); } GhidraURLWrappedContent wrappedContent = (GhidraURLWrappedContent) obj; Object content = null; @@ -336,7 +336,7 @@ public class HeadlessAnalyzer { processWithImport(folder.getPathname(), filesToImport); } } - catch (NotFoundException e) { + catch (FileNotFoundException e) { throw new IOException("Connect to repository folder failed"); } finally { @@ -369,7 +369,8 @@ public class HeadlessAnalyzer { * @param rootFolderPath root folder for imports * @param filesToImport directories and files to be imported (null or empty is acceptable if * we are in -process mode) - * @throws IOException if there was an IO-related problem + * @throws IOException if there was an IO-related problem. If caused by a failure to obtain a + * write-lock on the project the exception cause will a {@code LockException}. */ public void processLocal(String projectLocation, String projectName, String rootFolderPath, List filesToImport) throws IOException { @@ -1107,6 +1108,8 @@ public class HeadlessAnalyzer { return; } + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) { return; // skip non-Program files } @@ -1275,6 +1278,8 @@ public class HeadlessAnalyzer { for (DomainFile domFile : parentFolder.getFiles()) { if (filenamePattern == null || filenamePattern.matcher(domFile.getName()).matches()) { + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. if (ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) { filesProcessed = true; processFileNoImport(domFile); @@ -1308,6 +1313,8 @@ public class HeadlessAnalyzer { boolean filesProcessed = false; DomainFile domFile = parentFolder.getFile(filename); + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. if (domFile != null && ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) { filesProcessed = true; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramTask.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramTask.java index 7f2f55a5b7..53ad559271 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramTask.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/OpenProgramTask.java @@ -16,8 +16,9 @@ package ghidra.app.util.task; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; import docking.widgets.OptionDialog; import ghidra.app.util.dialog.CheckoutDialog; @@ -25,8 +26,11 @@ import ghidra.framework.client.ClientUtil; import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.main.AppInfo; import ghidra.framework.model.DomainFile; +import ghidra.framework.protocol.ghidra.*; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; import ghidra.framework.remote.User; import ghidra.framework.store.ExclusiveCheckoutException; +import ghidra.program.database.ProgramLinkContentHandler; import ghidra.program.model.lang.LanguageNotFoundException; import ghidra.program.model.listing.Program; import ghidra.util.*; @@ -37,8 +41,8 @@ import ghidra.util.task.TaskMonitor; public class OpenProgramTask extends Task { - private final List domainFileInfoList = new ArrayList<>(); - private List programList = new ArrayList<>(); + private final List openProgramRequests = new ArrayList<>(); + private List openedProgramList = new ArrayList<>(); private final Object consumer; private boolean silent; // if true operation does not permit interaction @@ -46,11 +50,16 @@ public class OpenProgramTask extends Task { private String openPromptText = "Open"; + public OpenProgramTask(Object consumer) { + super("Open Program(s)", true, false, true); + this.consumer = consumer; + } + public OpenProgramTask(DomainFile domainFile, int version, boolean forceReadOnly, Object consumer) { super("Open Program(s)", true, false, true); this.consumer = consumer; - domainFileInfoList.add(new DomainFileInfo(domainFile, version, forceReadOnly)); + openProgramRequests.add(new OpenProgramRequest(domainFile, version, forceReadOnly)); } public OpenProgramTask(DomainFile domainFile, int version, Object consumer) { @@ -65,17 +74,10 @@ public class OpenProgramTask extends Task { this(domainFile, DomainFile.DEFAULT_VERSION, false, consumer); } - public OpenProgramTask(List domainFileList, boolean forceReadOnly, - Object consumer) { - super("Open Program(s)", true, domainFileList.size() > 1, true); + public OpenProgramTask(URL ghidraURL, Object consumer) { + super("Open Program(s)", true, false, true); this.consumer = consumer; - for (DomainFile domainFile : domainFileList) { - domainFileInfoList.add(new DomainFileInfo(domainFile, -1, forceReadOnly)); - } - } - - public OpenProgramTask(List domainFileList, Object consumer) { - this(domainFileList, false, consumer); + openProgramRequests.add(new OpenProgramRequest(ghidraURL)); } public void setOpenPromptText(String text) { @@ -88,7 +90,16 @@ public class OpenProgramTask extends Task { public void addProgramToOpen(DomainFile domainFile, int version, boolean forceReadOnly) { setHasProgress(true); - domainFileInfoList.add(new DomainFileInfo(domainFile, version, forceReadOnly)); + openProgramRequests.add(new OpenProgramRequest(domainFile, version, forceReadOnly)); + } + + public void addProgramToOpen(URL ghidraURL) { + setHasProgress(true); + openProgramRequests.add(new OpenProgramRequest(ghidraURL)); + } + + public boolean hasOpenProgramRequests() { + return !openProgramRequests.isEmpty(); } /** @@ -110,100 +121,97 @@ public class OpenProgramTask extends Task { this.noCheckout = true; } - public List getOpenPrograms() { - return programList; + /** + * Get all successful open program requests + * @return all successful open program requests + */ + public List getOpenPrograms() { + return Collections.unmodifiableList(openedProgramList); } - public Program getOpenProgram() { - if (programList.isEmpty()) { + /** + * Get the first successful open program request + * @return first successful open program request or null if none + */ + public OpenProgramRequest getOpenProgram() { + if (openedProgramList.isEmpty()) { return null; } - return programList.get(0); + return openedProgramList.get(0); } @Override public void run(TaskMonitor monitor) { - taskMonitor.initialize(domainFileInfoList.size()); + taskMonitor.initialize(openProgramRequests.size()); - for (DomainFileInfo domainFileInfo : domainFileInfoList) { + for (OpenProgramRequest domainFileInfo : openProgramRequests) { if (taskMonitor.isCancelled()) { return; } - openDomainFile(domainFileInfo); - + domainFileInfo.open(); taskMonitor.incrementProgress(1); } } - private void openDomainFile(DomainFileInfo domainFileInfo) { - int version = domainFileInfo.getVersion(); - DomainFile domainFile = domainFileInfo.getDomainFile(); - if (version != DomainFile.DEFAULT_VERSION) { - openVersionedFile(domainFile, version); - } - else if (domainFileInfo.isReadOnly()) { - openReadOnlyFile(domainFile, version); - } - else { - openUnversionedFile(domainFile); - } - } - - private void openReadOnlyFile(DomainFile domainFile, int version) { + private Object openReadOnlyFile(DomainFile domainFile, URL url, int version) { taskMonitor.setMessage("Opening " + domainFile.getName()); - openReadOnly(domainFile, version); + return openReadOnly(domainFile, url, version); } - private void openVersionedFile(DomainFile domainFile, int version) { + private Object openVersionedFile(DomainFile domainFile, URL url, int version) { taskMonitor.setMessage("Getting Version " + version + " for " + domainFile.getName()); - openReadOnly(domainFile, version); + return openReadOnly(domainFile, url, version); } - private void openReadOnly(DomainFile domainFile, int version) { - String contentType = null; + private Object openReadOnly(DomainFile domainFile, URL url, int version) { + String contentType = domainFile.getContentType(); + String path = url != null ? url.toString() : domainFile.getPathname(); + Object obj = null; try { - contentType = domainFile.getContentType(); - Program program = - (Program) domainFile.getReadOnlyDomainObject(consumer, version, taskMonitor); - if (program == null) { - String errorMessage = "Can't open program - \"" + domainFile.getPathname() + "\""; + obj = domainFile.getReadOnlyDomainObject(consumer, version, taskMonitor); + + if (obj == null) { + String errorMessage = "Can't open " + contentType + " - \"" + path + "\""; if (version != DomainFile.DEFAULT_VERSION) { errorMessage += " version " + version; } - Msg.showError(this, null, "DomainFile Not Found", errorMessage); - } - else { - programList.add(program); + Msg.showError(this, null, "File Not Found", errorMessage); } + } catch (CancelledException e) { // we don't care, the task has been cancelled } catch (IOException e) { - if (domainFile.isInWritableProject()) { + if (url == null && domainFile.isInWritableProject()) { ClientUtil.handleException(AppInfo.getActiveProject().getRepository(), e, - "Get Versioned Object", null); + "Get " + contentType, null); + } + else if (version != DomainFile.DEFAULT_VERSION) { + Msg.showError(this, null, "Error Getting Versioned Program", + "Could not get version " + version + " for " + path, e); } else { - Msg.showError(this, null, "Error Getting Versioned Object", - "Could not get version " + version + " for " + domainFile.getName(), e); + Msg.showError(this, null, "Error Getting Program", + "Open program failed for " + path, e); } } catch (VersionException e) { VersionExceptionHandler.showVersionError(null, domainFile.getName(), contentType, "Open", e); } + return obj; } - private void openUnversionedFile(DomainFile domainFile) { + private Program openUnversionedFile(DomainFile domainFile) { String filename = domainFile.getName(); taskMonitor.setMessage("Opening " + filename); performOptionalCheckout(domainFile); try { - openFileMaybeUgrade(domainFile); + return openFileMaybeUgrade(domainFile); } catch (VersionException e) { String contentType = domainFile.getContentType(); @@ -226,9 +234,10 @@ public class OpenProgramTask extends Task { "Getting domain object failed.\n" + e.getMessage(), e); } } + return null; } - private void openFileMaybeUgrade(DomainFile domainFile) + private Program openFileMaybeUgrade(DomainFile domainFile) throws IOException, CancelledException, VersionException { boolean recoverFile = false; @@ -236,24 +245,18 @@ public class OpenProgramTask extends Task { recoverFile = askRecoverFile(domainFile.getName()); } + Program program = null; try { - Program program = + program = (Program) domainFile.getDomainObject(consumer, false, recoverFile, taskMonitor); - - if (program != null) { - programList.add(program); - } - } catch (VersionException e) { if (VersionExceptionHandler.isUpgradeOK(null, domainFile, openPromptText, e)) { - Program program = + program = (Program) domainFile.getDomainObject(consumer, true, recoverFile, taskMonitor); - if (program != null) { - programList.add(program); - } } } + return program; } private boolean askRecoverFile(final String filename) { @@ -294,32 +297,158 @@ public class OpenProgramTask extends Task { } } - static class DomainFileInfo { - private final DomainFile domainFile; - private final int version; - private boolean forceReadOnly; + public class OpenProgramRequest { - public DomainFileInfo(DomainFile domainFile, int version, boolean forceReadOnly) { + // ghidraURL and domainFile use are mutually exclusive + private final URL ghidraURL; + private final DomainFile domainFile; + + private URL linkURL; // link URL read from domainFile + + private final int version; + private final boolean forceReadOnly; + private Program program; + + public OpenProgramRequest(URL ghidraURL) { + if (!GhidraURL.PROTOCOL.equals(ghidraURL.getProtocol())) { + throw new IllegalArgumentException( + "unsupported protocol: " + ghidraURL.getProtocol()); + } + this.ghidraURL = ghidraURL; + this.domainFile = null; + this.version = -1; + this.forceReadOnly = true; + } + + public OpenProgramRequest(DomainFile domainFile, int version, boolean forceReadOnly) { this.domainFile = domainFile; + this.ghidraURL = null; this.version = (domainFile.isReadOnly() && domainFile.isVersioned()) ? domainFile.getVersion() : version; this.forceReadOnly = forceReadOnly; } - public boolean isReadOnly() { - return forceReadOnly || domainFile.isReadOnly() || - version != DomainFile.DEFAULT_VERSION; - } - + /** + * Get the {@link DomainFile} which corresponds to program open request. This will be + * null for all URL-based open requests. + * @return {@link DomainFile} which corresponds to program open request or null. + */ public DomainFile getDomainFile() { return domainFile; } - public int getVersion() { - return version; + /** + * Get the {@link URL} which corresponds to program open request. This will be + * null for all non-URL-based open requests. URL will be a {@link GhidraURL}. + * @return {@link URL} which corresponds to program open request or null. + */ + public URL getGhidraURL() { + return ghidraURL; } + /** + * Get the {@link URL} which corresponds to the link domainFile used to open a program. + * @return {@link URL} which corresponds to the link domainFile used to open a program. + */ + public URL getLinkURL() { + return linkURL; + } + + /** + * Get the open Program instance which corresponds to this open request. + * @return program instance or null if never opened. + */ + public Program getProgram() { + return program; + } + + /** + * Release opened program. This must be done once, and only once, on a successful + * open request. If handing ownership off to another consumer, they should be added + * as a program consumer prior to invoking this method. Releasing the last consumer + * will close the program instance. + */ + public void release() { + if (program != null) { + program.release(consumer); + } + } + + private Program openProgram(DomainFile df, URL url) { + if (version != DomainFile.DEFAULT_VERSION) { + return (Program) openVersionedFile(df, url, version); + } + if (forceReadOnly) { + return (Program) openReadOnlyFile(df, url, version); + } + return openUnversionedFile(df); + } + + void open() { + DomainFile df = domainFile; + URL url = ghidraURL; + GhidraURLWrappedContent wrappedContent = null; + Object content = null; + try { + if (df == null && url != null) { + GhidraURLConnection c = (GhidraURLConnection) url.openConnection(); + Object obj = c.getContent(); // read-only access + if (c.getStatusCode() == StatusCode.UNAUTHORIZED) { + return; // assume user already notified + } + if (!(obj instanceof GhidraURLWrappedContent)) { + messageBadProgramURL(url); + return; + } + wrappedContent = (GhidraURLWrappedContent) obj; + content = wrappedContent.getContent(this); + if (!(content instanceof DomainFile)) { + messageBadProgramURL(url); + return; + } + df = (DomainFile) content; + + if (ProgramLinkContentHandler.PROGRAM_LINK_CONTENT_TYPE + .equals(df.getContentType())) { + Msg.showError(this, null, "Program Multi-Link Error", + "Multi-link Program access not supported: " + url); + return; + } + } + + if (!Program.class.isAssignableFrom(df.getDomainObjectClass())) { + Msg.showError(this, null, "Error Opening Program", + "File does not correspond to a Ghidra Program: " + df.getPathname()); + return; + } + + program = openProgram(df, url); + + } + catch (MalformedURLException e) { + Msg.showError(this, null, "Invalid Ghidra URL", + "Improperly formed Ghidra URL: " + url); + } + catch (IOException e) { + Msg.showError(this, null, "Program Open Failed", + "Failed to open Ghidra URL: " + e.getMessage()); + } + finally { + if (content != null) { + wrappedContent.release(content, this); + } + } + + if (program != null) { + openedProgramList.add(this); + } + } + + private void messageBadProgramURL(URL url) { + Msg.error("Invalid Ghidra URL", + "Ghidra URL does not reference a Ghidra Program: " + url); + } } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/Annotation.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/Annotation.java index cd0bcd5581..74ef0cadc6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/Annotation.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/Annotation.java @@ -33,6 +33,7 @@ public class Annotation { private static final Pattern QUOTATION_PATTERN = Pattern.compile("(? ANNOTATED_STRING_HANDLERS; private static Map ANNOTATED_STRING_MAP; private String annotationText; @@ -40,6 +41,13 @@ public class Annotation { private AnnotatedStringHandler annotatedStringHandler; private AttributedString displayString; + public static List getAnnotatedStringHandlers() { + if (ANNOTATED_STRING_HANDLERS == null) { + ANNOTATED_STRING_HANDLERS = getSupportedAnnotationHandlers(); + } + return ANNOTATED_STRING_HANDLERS; + } + private static Map getAnnotatedStringHandlerMap() { if (ANNOTATED_STRING_MAP == null) { // lazy init due to our use of ClassSearcher ANNOTATED_STRING_MAP = createAnnotatedStringHandlerMap(); @@ -47,24 +55,28 @@ public class Annotation { return ANNOTATED_STRING_MAP; } - // locates AnnotatedStringHandler implementations to handle annotations private static Map createAnnotatedStringHandlerMap() { Map map = new HashMap<>(); - - // find all instances of AnnotatedString - List instances = - ClassSearcher.getInstances(AnnotatedStringHandler.class); - - for (AnnotatedStringHandler instance : instances) { + for (AnnotatedStringHandler instance : getAnnotatedStringHandlers()) { String[] supportedAnnotations = instance.getSupportedAnnotations(); for (String supportedAnnotation : supportedAnnotations) { map.put(supportedAnnotation, instance); } } - return Collections.unmodifiableMap(map); } + // locates AnnotatedStringHandler implementations to handle annotations + private static List getSupportedAnnotationHandlers() { + List list = new ArrayList<>(); + for (AnnotatedStringHandler h : ClassSearcher.getInstances(AnnotatedStringHandler.class)) { + if (h.getSupportedAnnotations().length != 0) { + list.add(h); + } + } + return Collections.unmodifiableList(list); + } + /** * Constructor * Note: This constructor assumes that the string starts with "{
    @
    " and ends with '}' @@ -184,14 +196,6 @@ public class Annotation { return annotationText; } - public static AnnotatedStringHandler[] getAnnotatedStringHandlers() { - Set annotations = - new HashSet<>(getAnnotatedStringHandlerMap().values()); - AnnotatedStringHandler[] retVal = new AnnotatedStringHandler[annotations.size()]; - annotations.toArray(retVal); - return retVal; - } - /*package*/ static Set getAnnotationNames() { return Collections.unmodifiableSet(getAnnotatedStringHandlerMap().keySet()); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/GhidraLocalURLAnnotatedStringHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/GhidraLocalURLAnnotatedStringHandler.java new file mode 100644 index 0000000000..c04148eb0d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/GhidraLocalURLAnnotatedStringHandler.java @@ -0,0 +1,34 @@ +/* ### + * 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.app.util.viewer.field; + +/** + * This implementation expands {@link URLAnnotatedStringHandler} providing an example form + * of a local project Ghidra URL. + */ +public class GhidraLocalURLAnnotatedStringHandler extends URLAnnotatedStringHandler { + + @Override + public String getDisplayString() { + return "Ghidra-URL(local)"; + } + + @Override + public String getPrototypeString() { + return "{@url \"ghidra:/dirpath/myproject?/folder/program.exe#symbol\" \"display string\"}"; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/GhidraServerURLAnnotatedStringHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/GhidraServerURLAnnotatedStringHandler.java new file mode 100644 index 0000000000..650c0f0696 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/GhidraServerURLAnnotatedStringHandler.java @@ -0,0 +1,34 @@ +/* ### + * 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.app.util.viewer.field; + +/** + * This implementation expands {@link URLAnnotatedStringHandler} providing an example form + * of a Ghidra Server URL. + */ +public class GhidraServerURLAnnotatedStringHandler extends URLAnnotatedStringHandler { + + @Override + public String getDisplayString() { + return "Ghidra-URL(remote)"; + } + + @Override + public String getPrototypeString() { + return "{@url \"ghidra://myserver/myrepo/folder/program.exe#symbol\" \"display string\"}"; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java index 6895494fd3..f283829aaf 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/URLAnnotatedStringHandler.java @@ -36,8 +36,10 @@ import ghidra.util.Msg; * displayed. */ public class URLAnnotatedStringHandler implements AnnotatedStringHandler { + private static final String INVALID_SYMBOL_TEXT = - "@url annotation must have a URL string " + "optionally followed by a display string"; + "@url annotation must have a URL string optionally followed by a display string"; + private static final String[] SUPPORTED_ANNOTATIONS = { "url", "hyperlink", "href", "link" }; @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/base/project/GhidraProject.java b/Ghidra/Features/Base/src/main/java/ghidra/base/project/GhidraProject.java index c5190dfd39..0a6546b321 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/base/project/GhidraProject.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/base/project/GhidraProject.java @@ -233,6 +233,13 @@ public class GhidraProject { return project; } + /** + * Returns the underlying ProjectData instance. + */ + public ProjectData getProjectData() { + return projectData; + } + /** * Closes the ghidra project, closing (without saving!) any open programs in * that project. Also deletes the project if created as a temporary project. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java index 53dd6ae165..9fa143dd82 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java @@ -93,7 +93,10 @@ public class DataTreeDialog extends DialogComponentProvider private Integer treeSelectionMode; /** - * Construct a new DataTreeDialog. + * Construct a new DataTreeDialog. This chooser will show all project files. + * Following linked-folders will only be allowed if a type of {@link #CHOOSE_FOLDER} + * or {@link #OPEN} is specified. If different behavior is required a filter should + * be specified using the other constructor. * * @param parent dialog's parent * @param title title to use @@ -101,7 +104,7 @@ public class DataTreeDialog extends DialogComponentProvider * @throws IllegalArgumentException if invalid type is specified */ public DataTreeDialog(Component parent, String title, int type) { - this(parent, title, type, null); + this(parent, title, type, getDefaultFilter(type)); } /** @@ -119,6 +122,20 @@ public class DataTreeDialog extends DialogComponentProvider initDataTreeDialog(type, filter); } + private static DomainFileFilter getDefaultFilter(int type) { + if (type == CHOOSE_FOLDER || type == OPEN) { + // return filter which forces folder selection and allow navigation into linked-folders + return new DomainFileFilter() { + + @Override + public boolean accept(DomainFile df) { + return true; // show all files (legacy behavior) + } + }; + } + return null; + } + public void setTreeSelectionMode(int mode) { if (treePanel != null) { treePanel.getTreeSelectionModel().setSelectionMode(mode); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java b/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java index 869b939bd0..c37943595a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java @@ -18,6 +18,7 @@ package ghidra.test; import java.awt.Dialog; import java.awt.Window; import java.io.*; +import java.net.URL; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -47,6 +48,7 @@ import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.util.PluginException; import ghidra.framework.project.DefaultProjectManager; +import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.program.database.ProgramDB; import ghidra.program.model.data.FileDataTypeManager; import ghidra.program.model.lang.*; @@ -874,26 +876,15 @@ public class TestEnv { * @param domainFile The domain file used to launch the tool; may be null * @return the tool that is launched */ - public PluginTool launchTool(final String toolName, final DomainFile domainFile) { + public PluginTool launchTool(String toolName, DomainFile domainFile) { AtomicReference ref = new AtomicReference<>(); AbstractGenericTest.runSwing(() -> { - boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI(); - AbstractDockingTest.setErrorGUIEnabled(false); // disable the error GUI while launching the tool - FrontEndTool frontEndToolInstance = getFrontEndTool(); - - Project project = frontEndToolInstance.getProject(); - ToolServices toolServices = project.getToolServices(); - PluginTool newTool = toolServices.launchTool(toolName, null); - if (newTool == null) { - // couldn't find the tool in the workspace...check the test area - newTool = launchDefaultToolByName(toolName); - } - + PluginTool newTool = doLaunchTool(toolName); ref.set(newTool); - - AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); - newTool.acceptDomainFiles(new DomainFile[] { domainFile }); + if (newTool != null) { + newTool.acceptDomainFiles(new DomainFile[] { domainFile }); + } }); PluginTool launchedTool = ref.get(); @@ -906,6 +897,52 @@ public class TestEnv { return launchedTool; } + /** + * Launches a tool of the given name using the given Ghidra URL. + *

    + * Note: the tool returned will have auto save disabled by default. + * + * @param toolName the name of the tool to launch + * @param ghidraUrl The Ghidra URL to be opened in tool (see {@link GhidraURL}) + * @return the tool that is launched + */ + public PluginTool launchToolWithURL(String toolName, URL ghidraUrl) { + AtomicReference ref = new AtomicReference<>(); + + AbstractGenericTest.runSwing(() -> { + PluginTool newTool = doLaunchTool(toolName); + ref.set(newTool); + if (newTool != null) { + newTool.accept(ghidraUrl); + } + }); + + PluginTool launchedTool = ref.get(); + if (launchedTool == null) { + throw new NullPointerException("Unable to launch the tool: " + toolName); + } + + // this will make sure that our tool is closed during disposal + extraTools.add(launchedTool); + return launchedTool; + } + + private PluginTool doLaunchTool(String toolName) { + boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI(); + AbstractDockingTest.setErrorGUIEnabled(false); // disable the error GUI while launching the tool + FrontEndTool frontEndToolInstance = getFrontEndTool(); + + Project project = frontEndToolInstance.getProject(); + ToolServices toolServices = project.getToolServices(); + PluginTool newTool = toolServices.launchTool(toolName, null); + if (newTool == null) { + // couldn't find the tool in the workspace...check the test area + newTool = launchDefaultToolByName(toolName); + } + AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); + return newTool; + } + /** * Sets the auto-save feature for all tool instances running under the {@link FrontEndTool} * created by this TestEnv instance. Auto-save is off by default when testing. diff --git a/Ghidra/Features/Base/src/main/resources/images/link.png b/Ghidra/Features/Base/src/main/resources/images/link.png deleted file mode 100644 index 1c654a0d61..0000000000 Binary files a/Ghidra/Features/Base/src/main/resources/images/link.png and /dev/null differ diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java index 8bb7972354..b1d488d6bc 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java @@ -19,6 +19,8 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import java.awt.*; +import java.net.MalformedURLException; +import java.net.URL; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -37,6 +39,8 @@ import ghidra.framework.model.*; import ghidra.framework.plugintool.ServiceProvider; import ghidra.framework.plugintool.TestDummyServiceProvider; import ghidra.framework.project.ProjectDataService; +import ghidra.framework.protocol.ghidra.GhidraURLConnection; +import ghidra.framework.store.FileSystem; import ghidra.program.database.ProgramBuilder; import ghidra.program.database.ProgramDB; import ghidra.program.model.address.Address; @@ -620,6 +624,60 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { assertTrue(spyNavigatable.navigatedTo(otherProgramPath, address)); } + @Test + public void testGhidraLocalUrlAnnotation_Program_WithAddress() { + + SpyNavigatable spyNavigatable = new SpyNavigatable(); + SpyServiceProvider spyServiceProvider = new SpyServiceProvider(); + + String addresstring = "1001000"; + + String pathname = "/a/b/prog"; + String url = "ghidra:/folder/project?" + pathname + "#" + addresstring; + String annotationText = "{@url \"" + url + "\"}"; + String rawComment = "My comment - " + annotationText; + AttributedString prototype = prototype(); + FieldElement element = + CommentUtils.parseTextForAnnotations(rawComment, program, prototype, 0); + + String displayString = element.getText(); + assertEquals("My comment - " + url, displayString); + + AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element); + click(spyNavigatable, spyServiceProvider, annotatedElement); + + assertTrue(spyServiceProvider.programOpened(pathname)); + + // Navigation performed by ProgramManager not tested due to use of spyServiceProvider + } + + @Test + public void testGhidraServerUrlAnnotation_Program_WithAddress() { + + SpyNavigatable spyNavigatable = new SpyNavigatable(); + SpyServiceProvider spyServiceProvider = new SpyServiceProvider(); + + String addresstring = "1001000"; + + String pathname = "/a/b/prog"; + String url = "ghidra://server/repo" + pathname + "#" + addresstring; + String annotationText = "{@url \"" + url + "\"}"; + String rawComment = "My comment - " + annotationText; + AttributedString prototype = prototype(); + FieldElement element = + CommentUtils.parseTextForAnnotations(rawComment, program, prototype, 0); + + String displayString = element.getText(); + assertEquals("My comment - " + url, displayString); + + AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element); + click(spyNavigatable, spyServiceProvider, annotatedElement); + + assertTrue(spyServiceProvider.programOpened(pathname)); + + // Navigation performed by ProgramManager not tested due to use of spyServiceProvider + } + @Test public void testUnknownAnnotation() { String rawComment = "This is a symbol {@syyyybol bob} annotation"; @@ -903,13 +961,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { private Set openedPrograms = new HashSet<>(); private Set closedPrograms = new HashSet<>(); - @Override - public Program openProgram(DomainFile domainFile, int version, int state) { - String name = domainFile.getName(); - String pathname = domainFile.getPathname(); - - openedPrograms.add(name); - + private Program generateProgram(String pathname, String name) { try { ProgramBuilder builder = new ProgramBuilder(); builder.setName(pathname); @@ -923,6 +975,39 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { } } + @Override + public Program openProgram(URL ghidraURL, int state) { + try { + GhidraURLConnection c = new GhidraURLConnection(ghidraURL); + String folderpath = c.getFolderPath(); + String name = c.getFolderItemName(); + String pathname = folderpath; + if (!pathname.endsWith(FileSystem.SEPARATOR)) { + pathname += FileSystem.SEPARATOR; + } + pathname += name; + openedPrograms.add(name); + + Program p = generateProgram(pathname, name); + + // NOTE: URL ref navigation not performed + + return p; + } + catch (MalformedURLException e) { + failWithException("Bad URL", e); + } + return null; + } + + @Override + public Program openProgram(DomainFile domainFile, int version, int state) { + String name = domainFile.getName(); + String pathname = domainFile.getPathname(); + openedPrograms.add(name); + return generateProgram(pathname, name); + } + @Override public boolean closeProgram(Program p, boolean ignoreChanges) { String name = FilenameUtils.getName(p.getName()); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/ProjectFileManagerTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/ProjectFileManagerTest.java index 4a1a59efac..6512d66de2 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/ProjectFileManagerTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/data/ProjectFileManagerTest.java @@ -121,12 +121,7 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest // If there are queued actions, then we have to kick the handling thread and // let it finish running. - try { - assertTrue(eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS)); - } - catch (InterruptedException e) { - failWithException("Interrupted waiting for filesystem events", e); - } + assertTrue(eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS)); } private void deleteAll(File file) { diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java index d0a768d832..19201b68cc 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java @@ -421,15 +421,10 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest { assertNotNull(dialog); } - private DomainFileFilter createStartsWithFilter(String startsWith) { - return (df) -> df.getName().startsWith(startsWith); - } - - private void showFiltered(String startsWith) { + private void showFiltered(final String startsWith) { SwingUtilities.invokeLater(() -> { dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog", - DataTreeDialog.OPEN, createStartsWithFilter(startsWith)); - + DataTreeDialog.OPEN, f -> f.getName().startsWith(startsWith)); dialog.showComponent(); }); waitForSwing(); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/AddViewToProjectTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/AddViewToProjectTest.java index 35e42f2751..0b4cdd321a 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/AddViewToProjectTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/AddViewToProjectTest.java @@ -79,10 +79,10 @@ public class AddViewToProjectTest extends AbstractGhidraHeadlessIntegrationTest try { URL view = GhidraURL.makeURL(DIRECTORY_NAME, PROJECT_VIEW1); - project.addProjectView(view); + project.addProjectView(view, true); // add another view that will be removed to test the remove - project.addProjectView(GhidraURL.makeURL(DIRECTORY_NAME, PROJECT_VIEW2)); + project.addProjectView(GhidraURL.makeURL(DIRECTORY_NAME, PROJECT_VIEW2), true); // validate the view was added to project ProjectLocator[] projViews = project.getProjectViews(); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/CreateDomainObjectTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/CreateDomainObjectTest.java index 8cf67aa389..e13c9446f8 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/CreateDomainObjectTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/CreateDomainObjectTest.java @@ -117,7 +117,7 @@ public class CreateDomainObjectTest extends AbstractGhidraHeadedIntegrationTest project.close(); Project project2 = ProjectTestUtils.getProject(testDir, PROJECT_NAME2); try { - project2.addProjectView(GhidraURL.makeURL(testDir, PROJECT_NAME1)); + project2.addProjectView(GhidraURL.makeURL(testDir, PROJECT_NAME1), true); } catch (Exception e) { Assert.fail("View Not found"); diff --git a/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeRepository.java b/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeRepository.java index 594df2de14..15d421ea3e 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeRepository.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeRepository.java @@ -15,15 +15,18 @@ */ package ghidra.base.project; +import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; -import generic.test.TestUtils; +import generic.test.AbstractGTest; import ghidra.framework.model.DomainFile; import ghidra.framework.remote.User; +import ghidra.framework.store.local.IndexedV1LocalFileSystem; import ghidra.framework.store.local.LocalFileSystem; import ghidra.test.TestEnv; +import utilities.util.FileUtilities; /** * This class represents the idea of a shared Ghidra repository. This class is meant to be @@ -54,11 +57,34 @@ public class FakeRepository { private Map usersByName = new HashMap<>(); private Map projectsByUser = new HashMap<>(); + private File versionedFSDir; private LocalFileSystem versionedFileSystem; - public FakeRepository() { + public FakeRepository() throws IOException { // validation must be enabled if both environments are utilized by a test LocalFileSystem.setValidationRequired(); + + versionedFSDir = + new File(AbstractGTest.getTestDirectoryPath() + File.separator + "TestRepo.rep"); + if (versionedFSDir.exists()) { + FileUtilities.deleteDir(versionedFSDir); + } + if (versionedFSDir.exists() || !FileUtilities.createDir(versionedFSDir)) { + throw new IOException("Failed to create clean repo dir: " + versionedFSDir); + } + versionedFileSystem = new MyVersionedFileSystem(versionedFSDir.getPath()); + } + + private static class MyVersionedFileSystem extends IndexedV1LocalFileSystem { + MyVersionedFileSystem(String rootPath) throws IOException { + super(rootPath, true, false, true, true); + } + + @Override + public boolean isShared() { + // Enables use of asyncronous event dispatching thread + return true; + } } /** @@ -109,11 +135,6 @@ public class FakeRepository { FakeSharedProject project = new FakeSharedProject(this, user); projectsByUser.put(user, project); - - if (versionedFileSystem == null) { - versionedFileSystem = project.getVersionedFileSystem(); - TestUtils.setInstanceField("isShared", versionedFileSystem, Boolean.TRUE); - } return project; } @@ -139,6 +160,8 @@ public class FakeRepository { */ public void dispose() { projectsByUser.values().forEach(p -> disposeProject(p)); + versionedFileSystem.dispose(); + FileUtilities.deleteDir(versionedFSDir); } private void disposeProject(FakeSharedProject p) { diff --git a/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeSharedProject.java b/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeSharedProject.java index 2378fc9db0..075b87a862 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeSharedProject.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/base/project/FakeSharedProject.java @@ -25,7 +25,7 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; -import generic.test.AbstractGenericTest; +import generic.test.AbstractGTest; import generic.test.TestUtils; import ghidra.framework.data.*; import ghidra.framework.model.*; @@ -39,6 +39,7 @@ import ghidra.test.TestProgramManager; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; import junit.framework.AssertionFailedError; +import utilities.util.FileUtilities; /** * This class represents the idea of a shared Ghidra project. Each project is associated with @@ -61,21 +62,18 @@ public class FakeSharedProject { public FakeSharedProject(FakeRepository repo, User user) throws IOException { this.repo = repo; - String projectDirPath = AbstractGenericTest.getTestDirectoryPath(); + String projectDirPath = AbstractGTest.getTestDirectoryPath(); gProject = GhidraProject.createProject(projectDirPath, "TestProject_" + user.getName(), true); gProject.setDeleteOnClose(true); - LocalFileSystem fs = repo.getSharedFileSystem(); - if (fs != null) { - // first project will keeps its versioned file system - setVersionedFileSystem(fs); - } + // use local shared fake repo versioned file system + setVersionedFileSystem(repo.getSharedFileSystem()); } FakeSharedProject(User user) throws IOException { - String projectDirPath = AbstractGenericTest.getTestDirectoryPath(); + String projectDirPath = AbstractGTest.getTestDirectoryPath(); gProject = GhidraProject.createProject(projectDirPath, "TestProject_" + user.getName(), true); } @@ -101,7 +99,7 @@ public class FakeSharedProject { * @return the project file manager */ public ProjectFileManager getProjectFileManager() { - return (ProjectFileManager) gProject.getProject().getProjectData(); + return (ProjectFileManager) gProject.getProjectData(); } /** @@ -369,8 +367,11 @@ public class FakeSharedProject { * @see FakeRepository#dispose() */ public void dispose() { + ProjectLocator projectLocator = getProjectFileManager().getProjectLocator(); programManager.disposeOpenPrograms(); gProject.close(); + FileUtilities.deleteDir(projectLocator.getProjectDir()); + projectLocator.getMarkerFile().delete(); } @Override @@ -400,12 +401,7 @@ public class FakeSharedProject { (FileSystemEventManager) TestUtils.getInstanceField("eventManager", versionedFileSystem); - try { - eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS); - } - catch (InterruptedException e) { - failWithException("Interrupted waiting for filesystem events", e); - } + eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS); } private DomainFolder getFolder(String path) throws Exception { diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/CollectFailedRelocations.java b/Ghidra/Features/FunctionID/ghidra_scripts/CollectFailedRelocations.java index da0da488a8..c72b79c3fc 100644 --- a/Ghidra/Features/FunctionID/ghidra_scripts/CollectFailedRelocations.java +++ b/Ghidra/Features/FunctionID/ghidra_scripts/CollectFailedRelocations.java @@ -99,6 +99,10 @@ public class CollectFailedRelocations extends GhidraScript { if (monitor.isCancelled()) { return; } + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. If program links should be considered + // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())" + // should be used. if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) { programs.add(domainFile); } diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/CreateMultipleLibraries.java b/Ghidra/Features/FunctionID/ghidra_scripts/CreateMultipleLibraries.java index 40fd19d123..9c3b8f3bfa 100644 --- a/Ghidra/Features/FunctionID/ghidra_scripts/CreateMultipleLibraries.java +++ b/Ghidra/Features/FunctionID/ghidra_scripts/CreateMultipleLibraries.java @@ -258,6 +258,10 @@ public class CreateMultipleLibraries extends GhidraScript { DomainFile[] files = myFolder.getFiles(); for (DomainFile domainFile : files) { monitor.checkCanceled(); + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. If program links should be considered + // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())" + // should be used. if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) { programs.add(domainFile); } diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/FidStatistics.java b/Ghidra/Features/FunctionID/ghidra_scripts/FidStatistics.java index 95b13d34d6..e1c6e5b688 100644 --- a/Ghidra/Features/FunctionID/ghidra_scripts/FidStatistics.java +++ b/Ghidra/Features/FunctionID/ghidra_scripts/FidStatistics.java @@ -372,6 +372,10 @@ public class FidStatistics extends GhidraScript { DomainFile[] files = folder.getFiles(); for (DomainFile domainFile : files) { monitor.checkCanceled(); + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. If program links should be considered + // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())" + // should be used. if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) { programs.add(domainFile); } diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/FindErrors.java b/Ghidra/Features/FunctionID/ghidra_scripts/FindErrors.java index 10a739e013..0f5e597a11 100644 --- a/Ghidra/Features/FunctionID/ghidra_scripts/FindErrors.java +++ b/Ghidra/Features/FunctionID/ghidra_scripts/FindErrors.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +16,9 @@ //Opens all programs under a chosen domain folder, grabs their error count, //then sorts in increasing error order and prints them //@category FunctionID +import java.io.IOException; +import java.util.*; + import generic.stl.Pair; import ghidra.app.script.GhidraScript; import ghidra.framework.model.DomainFile; @@ -27,9 +29,6 @@ import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; -import java.io.IOException; -import java.util.*; - public class FindErrors extends GhidraScript { @Override @@ -82,6 +81,10 @@ public class FindErrors extends GhidraScript { if (monitor.isCancelled()) { return; } + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. If program links should be considered + // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())" + // should be used. if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) { programs.add(domainFile); } diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/FindFunctionByHash.java b/Ghidra/Features/FunctionID/ghidra_scripts/FindFunctionByHash.java index 890204b81a..f9e7f0eb4c 100644 --- a/Ghidra/Features/FunctionID/ghidra_scripts/FindFunctionByHash.java +++ b/Ghidra/Features/FunctionID/ghidra_scripts/FindFunctionByHash.java @@ -100,6 +100,10 @@ public class FindFunctionByHash extends GhidraScript { if (monitor.isCancelled()) { return; } + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. If program links should be considered + // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())" + // should be used. if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) { programs.add(domainFile); } diff --git a/Ghidra/Features/FunctionID/ghidra_scripts/FindNamedFunction.java b/Ghidra/Features/FunctionID/ghidra_scripts/FindNamedFunction.java index fb5ad98b2f..81acfa0be0 100644 --- a/Ghidra/Features/FunctionID/ghidra_scripts/FindNamedFunction.java +++ b/Ghidra/Features/FunctionID/ghidra_scripts/FindNamedFunction.java @@ -16,6 +16,9 @@ //Opens all programs under a chosen domain folder, scans them for functions //that match a user supplied name, and prints info about the match. //@category FunctionID +import java.io.IOException; +import java.util.ArrayList; + import ghidra.app.script.GhidraScript; import ghidra.feature.fid.service.FidService; import ghidra.framework.model.DomainFile; @@ -26,9 +29,6 @@ import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; -import java.io.IOException; -import java.util.ArrayList; - public class FindNamedFunction extends GhidraScript { FidService service; @@ -85,6 +85,10 @@ public class FindNamedFunction extends GhidraScript { if (monitor.isCancelled()) { return; } + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. If program links should be considered + // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())" + // should be used. if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) { programs.add(domainFile); } diff --git a/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java b/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java index 3b9bd1cd04..ae3e50ba69 100644 --- a/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java +++ b/Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/plugin/IngestTask.java @@ -153,6 +153,10 @@ public class IngestTask extends Task { for (DomainFile domainFile : files) { monitor.checkCanceled(); monitor.incrementProgress(1); + // Do not follow folder-links or consider program links. Using content type + // to filter is best way to control this. If program links should be considered + // "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())" + // should be used. if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) { programs.add(domainFile); } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java index 8c1278cb7b..3dc956ef27 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java @@ -21,6 +21,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import db.*; import ghidra.app.util.task.OpenProgramTask; +import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest; import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator; import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator; import ghidra.feature.vt.api.impl.*; @@ -309,7 +310,8 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession, VTC TaskLauncher.launch(openTask); - return openTask.getOpenProgram(); + OpenProgramRequest openProgram = openTask.getOpenProgram(); + return openProgram != null ? openProgram.getProgram() : null; } @Override diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java index ef12f97735..4c8abe14f0 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/VTSessionContentHandler.java @@ -24,7 +24,8 @@ import db.OpenMode; import db.buffers.BufferFile; import generic.theme.GIcon; import ghidra.feature.vt.api.db.VTSessionDB; -import ghidra.framework.data.*; +import ghidra.framework.data.DBContentHandler; +import ghidra.framework.data.DomainObjectMergeManager; import ghidra.framework.model.ChangeSet; import ghidra.framework.model.DomainObject; import ghidra.framework.store.*; @@ -34,7 +35,8 @@ import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; -public class VTSessionContentHandler extends DBContentHandler { +public class VTSessionContentHandler extends DBContentHandler { + private static Icon ICON = new GIcon("icon.version.tracking.session.content.type"); public final static String CONTENT_TYPE = "VersionTracking"; @@ -49,7 +51,6 @@ public class VTSessionContentHandler extends DBContentHandler { "Unsupported domain object: " + domainObject.getClass().getName()); } return createFile((VTSessionDB) domainObject, CONTENT_TYPE, fs, path, name, monitor); - } @Override @@ -74,7 +75,7 @@ public class VTSessionContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, + public VTSessionDB getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor) throws IOException, CancelledException, VersionException { @@ -119,7 +120,7 @@ public class VTSessionContentHandler extends DBContentHandler { } @Override - public Class getDomainObjectClass() { + public Class getDomainObjectClass() { return VTSessionDB.class; } @@ -129,7 +130,7 @@ public class VTSessionContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version, + public VTSessionDB getImmutableObject(FolderItem item, Object consumer, int version, int minChangeVersion, TaskMonitor monitor) throws IOException, CancelledException, VersionException { @@ -149,7 +150,7 @@ public class VTSessionContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, + public VTSessionDB getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, Object consumer, TaskMonitor monitor) throws IOException, VersionException, CancelledException { diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java index 4e176a6ee9..fe4ab3b24c 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java @@ -15,6 +15,10 @@ */ package ghidra.feature.vt.gui.actions; +import docking.ActionContext; +import docking.action.DockingAction; +import docking.action.MenuData; +import docking.tool.ToolConstants; import ghidra.feature.vt.api.main.VTSession; import ghidra.feature.vt.gui.plugin.VTController; import ghidra.feature.vt.gui.plugin.VTPlugin; @@ -23,10 +27,6 @@ import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFileFilter; import ghidra.framework.plugintool.PluginTool; import ghidra.util.HelpLocation; -import docking.ActionContext; -import docking.action.DockingAction; -import docking.action.MenuData; -import docking.tool.ToolConstants; public class OpenVersionTrackingSessionAction extends DockingAction { @@ -60,5 +60,10 @@ public class OpenVersionTrackingSessionAction extends DockingAction { Class c = f.getDomainObjectClass(); return VTSession.class.isAssignableFrom(c); } + + @Override + public boolean followLinkedFolders() { + return false; + } } } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java index a0b69e6f8c..f93f0f527a 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/NewSessionPanel.java @@ -29,6 +29,7 @@ import docking.widgets.label.GDLabel; import docking.wizard.*; import generic.theme.*; import ghidra.app.util.task.OpenProgramTask; +import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest; import ghidra.framework.main.DataTreeDialog; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFolder; @@ -383,8 +384,8 @@ public class NewSessionPanel extends AbstractMageJPanel { OpenProgramTask openProgramTask = new OpenProgramTask(programInfo.getFile(), tool); new TaskLauncher(openProgramTask, tool.getActiveWindow()); - Program program = openProgramTask.getOpenProgram(); - programInfo.setProgram(program); + OpenProgramRequest openProgram = openProgramTask.getOpenProgram(); + programInfo.setProgram(openProgram != null ? openProgram.getProgram() : null); } @Override diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTWizardUtils.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTWizardUtils.java index e338d08f75..d462548dd8 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTWizardUtils.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/VTWizardUtils.java @@ -20,12 +20,12 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import docking.widgets.OptionDialog; -import ghidra.feature.vt.api.impl.VTSessionContentHandler; +import ghidra.feature.vt.api.main.VTSession; import ghidra.feature.vt.gui.task.SaveTask; import ghidra.framework.main.DataTreeDialog; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFileFilter; -import ghidra.program.database.ProgramDB; +import ghidra.program.model.listing.Program; import ghidra.util.HTMLUtilities; import ghidra.util.task.TaskLauncher; @@ -36,23 +36,20 @@ public class VTWizardUtils { } public static final DomainFileFilter VT_SESSION_FILTER = new DomainFileFilter() { + @Override public boolean accept(DomainFile df) { - if (VTSessionContentHandler.CONTENT_TYPE.equals(df.getContentType())) { - return true; - } + return VTSession.class.isAssignableFrom(df.getDomainObjectClass()); + } + + @Override + public boolean followLinkedFolders() { return false; } }; - public static final DomainFileFilter PROGRAM_FILTER = new DomainFileFilter() { - @Override - public boolean accept(DomainFile df) { - if (ProgramDB.CONTENT_TYPE.equals(df.getContentType())) { - return true; - } - return false; - } + public static final DomainFileFilter PROGRAM_FILTER = f -> { + return Program.class.isAssignableFrom(f.getDomainObjectClass()); }; static DomainFile chooseDomainFile(Component parent, String domainIdentifier, diff --git a/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java b/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java index a544eb9025..a9f4633155 100644 --- a/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java +++ b/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java @@ -16,8 +16,7 @@ package db.buffers; import java.io.IOException; -import java.rmi.NoSuchObjectException; -import java.rmi.Remote; +import java.rmi.*; import java.util.NoSuchElementException; import ghidra.util.Msg; @@ -104,8 +103,8 @@ public class BufferFileAdapter implements BufferFile { bufferFileHandle.dispose(); } catch (IOException e) { - // handle may have already been disposed - if (!(e instanceof NoSuchObjectException)) { + // handle may have already been disposed or disconnected + if (!(e instanceof NoSuchObjectException) && !(e instanceof ConnectException)) { Msg.error(this, e); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java index 33e742c1ac..9e4b071559 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java @@ -16,6 +16,7 @@ package docking.widgets.tree; import java.util.*; +import java.util.function.Predicate; import java.util.stream.Stream; import javax.swing.Icon; @@ -197,6 +198,22 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable filter) { + for (GTreeNode node : children()) { + if (name.equals(node.getName()) && filter.test(node)) { + return node; + } + } + return null; + } + /** * Returns the child node at the given index. Returns null if the index is out of bounds. * @@ -488,6 +505,15 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable= MAX) { return; @@ -57,6 +67,9 @@ public class GTreeExpandAllTask extends GTreeTask { if (parent.isLeaf()) { return; } + if (!force && !parent.isAutoExpandPermitted()) { + return; + } monitor.checkCanceled(); List allChildren = parent.getChildren(); if (allChildren.size() == 0) { @@ -68,9 +81,9 @@ public class GTreeExpandAllTask extends GTreeTask { } for (GTreeNode child : allChildren) { monitor.checkCanceled(); - expandNode(child, monitor); + expandNode(child, false, monitor); } - monitor.incrementProgress(1); + monitor.incrementProgress(1); // TODO: total node count is unknown } private void expandPath(final TreePath treePath, final TaskMonitor monitor) { diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java index ef42c7f013..7e3d9fccfe 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java @@ -158,8 +158,10 @@ public class RepositoryAdapter implements RemoteAdapterListener { /** * Attempt to connect to the server. + * @throws RepositoryNotFoundException if named repository does not exist + * @throws IOException if IO error occurs */ - public void connect() throws IOException { + public void connect() throws RepositoryNotFoundException, IOException { synchronized (serverAdapter) { if (repository != null) { try { @@ -180,7 +182,7 @@ public class RepositoryAdapter implements RemoteAdapterListener { unexpectedDisconnect = false; if (repository == null) { noSuchRepository = true; - throw new IOException("Repository '" + name + "': not found"); + throw new RepositoryNotFoundException("Repository '" + name + "': not found"); } Msg.info(this, "Connected to repository '" + name + "'"); changeDispatcher.start(); diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryNotFoundException.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryNotFoundException.java new file mode 100644 index 0000000000..3b7903e8cc --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryNotFoundException.java @@ -0,0 +1,31 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.client; + +import java.io.IOException; + +/** + * {@code RepositoryNotFoundException} thrown when a failed connection occurs to a + * non-existing repository. A valid server connection is required to make this + * determination. + */ +public class RepositoryNotFoundException extends IOException { + + public RepositoryNotFoundException(String msg) { + super(msg); + } + +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java index a2532eceae..ff21415bab 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java @@ -52,6 +52,9 @@ public class RepositoryServerAdapter { // Keeps track of whether the connection attempt was cancelled by the user private boolean connectCancelled = false; + // Keep track of last connect error + private Throwable lastConnectError; + private WeakSet listenerList = WeakDataStructureFactory.createCopyOnWriteWeakSet(); @@ -105,6 +108,14 @@ public class RepositoryServerAdapter { return connectCancelled; } + /** + * Returns the last error associated with a failed connection attempt. + * @return last connect error or null + */ + public Throwable getLastConnectError() { + return lastConnectError; + } + /** * Notify listeners of a connection state change. */ @@ -135,7 +146,7 @@ public class RepositoryServerAdapter { } } - Throwable cause = null; + lastConnectError = null; try { try { serverHandle = ClientUtil.connect(server); @@ -162,22 +173,22 @@ public class RepositoryServerAdapter { catch (LoginException e) { Msg.showError(this, null, "Server Error", "Server access denied (" + serverInfoStr + ")."); - cause = e; + lastConnectError = e; } catch (GeneralSecurityException e) { Msg.showError(this, null, "Server Error", "Server access denied (" + serverInfoStr + "): " + e.getMessage()); - cause = e; + lastConnectError = e; } catch (SocketTimeoutException | java.net.ConnectException | java.rmi.ConnectException e) { Msg.showError(this, null, "Server Error", "Connection to server failed (" + server + ")."); - cause = e; + lastConnectError = e; } catch (java.net.UnknownHostException | java.rmi.UnknownHostException e) { Msg.showError(this, null, "Server Error", "Server Not Found (" + server.getServerName() + ")."); - cause = e; + lastConnectError = e; } catch (RemoteException e) { String msg = e.getMessage(); @@ -185,7 +196,7 @@ public class RepositoryServerAdapter { while ((t = t.getCause()) != null) { String err = t.getMessage(); msg = err != null ? err : t.toString(); - cause = t; + lastConnectError = t; } Msg.showError(this, null, "Server Error", "An error occurred on the server (" + serverInfoStr + ").\n" + msg, e); @@ -200,7 +211,7 @@ public class RepositoryServerAdapter { "An error occurred while connecting to the server (" + serverInfoStr + ").\n" + msg, e); } - throw new NotConnectedException("Not connected to repository server", cause); + throw new NotConnectedException("Not connected to repository server", lastConnectError); } private void checkPasswordExpiration() { diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java index c24dfbd022..f317923ae2 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java @@ -32,7 +32,7 @@ public interface RepositoryHandle { // TODO: NOTE! Debugging client or sever garbage collection delays could // cause handle to be disposed prematurely. - public final static int CLIENT_CHECK_PERIOD = SystemUtilities.isInTestingMode() ? 1000 : 30000; + public final static int CLIENT_CHECK_PERIOD = SystemUtilities.isInTestingMode() ? 2000 : 30000; /** * Returns the name of this repository. diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystemEventManager.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystemEventManager.java index 6df9e92f98..9026b62212 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystemEventManager.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystemEventManager.java @@ -22,33 +22,60 @@ import java.util.concurrent.*; * FileSystemListenerList maintains a list of FileSystemListener's. * This class, acting as a FileSystemListener, simply relays each callback to * all FileSystemListener's within its list. Employs either a synchronous - * and asynchronous notification mechanism. + * and asynchronous notification mechanism. Once disposed event dispatching will + * discontinue. */ public class FileSystemEventManager implements FileSystemListener { + private static enum ThreadState { + STOPPED, RUNNING, DISPOSED + } + private List listeners = new CopyOnWriteArrayList<>(); private BlockingQueue eventQueue = new LinkedBlockingQueue<>(); - private volatile boolean disposed = false; + private final boolean asyncDispatchEnabled; + + private volatile ThreadState state = ThreadState.STOPPED; private Thread thread; /** * Constructor * @param enableAsynchronousDispatching if true a separate dispatch thread will be used - * to notify listeners. If false, blocking notification will be performed. + * to notify listeners. If false, blocking notification will be performed. Events are + * immediately discarded in the absence of any listener(s). */ public FileSystemEventManager(boolean enableAsynchronousDispatching) { + asyncDispatchEnabled = enableAsynchronousDispatching; + } - if (enableAsynchronousDispatching) { - thread = new FileSystemEventProcessingThread(); - thread.start(); + /** + * Return true if asynchornous event processing is enabled. + * @return true if asynchornous event processing is enabled, else false + */ + public boolean isAsynchronous() { + return asyncDispatchEnabled; + } + + /** + * Discontinue event dispatching and terminate dispatch thread if it exists. + */ + public synchronized void dispose() { + state = ThreadState.DISPOSED; + if (asyncDispatchEnabled) { + if (thread != null && thread.isAlive()) { + thread.interrupt(); + } + eventQueue.clear(); } } - public void dispose() { - disposed = true; - if (thread != null) { - thread.interrupt(); + private synchronized void startDispatchThread() { + if (asyncDispatchEnabled && state == ThreadState.STOPPED) { + // only starts when first listener is added + state = ThreadState.RUNNING; + thread = new FileSystemEventProcessingThread(); + thread.start(); } } @@ -57,6 +84,7 @@ public class FileSystemEventManager implements FileSystemListener { * @param listener the listener */ public void add(FileSystemListener listener) { + startDispatchThread(); // if asyncDispatchEnabled listeners.add(listener); } @@ -116,30 +144,34 @@ public class FileSystemEventManager implements FileSystemListener { @Override public void syncronize() { // Note: synchronize calls will only work when using a threaded event queue - if (isAsynchronous()) { - add(new SynchronizeEvent()); + if (asyncDispatchEnabled) { + queueEvent(new SynchronizeEvent()); } } - private boolean isAsynchronous() { - return thread != null; - } - - private void add(FileSystemEvent ev) { - if (!listeners.isEmpty()) { - eventQueue.add(ev); + /** + * Queue specified event if listener thread is running + * @param ev filesystm event + * @return true if queued, else false if listener thread not running + */ + private boolean queueEvent(FileSystemEvent ev) { + if (state == ThreadState.RUNNING) { + return eventQueue.add(ev); } + return false; } private void handleEvent(FileSystemEvent e) { - if (disposed) { + if (state == ThreadState.DISPOSED) { return; } - if (isAsynchronous()) { - add(e); + if (asyncDispatchEnabled) { + // if there are no listeners event will be discarded (i.e., listener thread not running) + queueEvent(e); } else { + // process in a synchronous fashion in current thread e.process(listeners); } } @@ -154,17 +186,26 @@ public class FileSystemEventManager implements FileSystemListener { * * @param timeout the maximum time to wait * @param unit the time unit of the {@code time} argument - * @return true if the events were processed in the given timeout - * @throws InterruptedException if this waiting thread is interrupted + * @return true if the events were processed in the given timeout. A false value will be + * returned if either a timeout occured */ - public boolean flushEvents(long timeout, TimeUnit unit) throws InterruptedException { - if (!isAsynchronous()) { + public boolean flushEvents(long timeout, TimeUnit unit) { + if (!asyncDispatchEnabled) { return true; // each thread processes its own event } MarkerEvent event = new MarkerEvent(); - eventQueue.add(event); - return event.waitForEvent(timeout, unit); + if (!queueEvent(event)) { + // events are not queuing since there are no listeners or dispose has occured + return true; + } + try { + return event.waitForEvent(timeout, unit); + } + catch (InterruptedException e) { + // ignore - listener thread stopped or disposed + return true; + } } //================================================================================================== @@ -180,18 +221,14 @@ public class FileSystemEventManager implements FileSystemListener { @Override public void run() { - while (!disposed) { - + while (state == ThreadState.RUNNING) { FileSystemEvent event; try { event = eventQueue.take(); event.process(listeners); } catch (InterruptedException e) { - // interrupt has been cleared; if other threads rely on this interrupted state, - // then mark the thread as interrupted again by calling: - // Thread.currentThread().interrupt(); - // For now, this code relies on the 'alive' flag to know when to terminate + // ignore - interrupt has been cleared } } } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java index a1dd2ffaf7..0222a5011c 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java @@ -916,6 +916,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem { @Override public int getItemCount() throws IOException { + checkDisposed(); if (readOnly) { refreshReadOnlyIndex(); } @@ -930,11 +931,9 @@ public class IndexedLocalFileSystem extends LocalFileSystem { return count; } - /* - * @see ghidra.framework.store.FileSystem#getFolders(java.lang.String) - */ @Override public synchronized String[] getFolderNames(String folderPath) throws IOException { + checkDisposed(); if (readOnly) { refreshReadOnlyIndex(); } @@ -948,13 +947,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem { } } - /* - * @see ghidra.framework.store.FileSystem#createFolder(java.lang.String, java.lang.String) - */ @Override public synchronized void createFolder(String parentPath, String folderName) throws InvalidNameException, IOException { + checkDisposed(); + if (readOnly) { throw new ReadOnlyException(); } @@ -987,12 +985,11 @@ public class IndexedLocalFileSystem extends LocalFileSystem { eventManager.folderCreated(parentPath, getName(path)); } - /* - * @see ghidra.framework.store.FileSystem#deleteFolder(java.lang.String) - */ @Override public synchronized void deleteFolder(String folderPath) throws IOException { + checkDisposed(); + if (readOnly) { throw new ReadOnlyException(); } @@ -1059,13 +1056,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem { getListener().itemCreated(destFolderPath, itemName); } - /* - * @see ghidra.framework.store.FileSystem#moveItem(java.lang.String, java.lang.String, java.lang.String) - */ @Override public synchronized void moveItem(String folderPath, String name, String newFolderPath, String newName) throws IOException, InvalidNameException { + checkDisposed(); + if (readOnly) { throw new ReadOnlyException(); } @@ -1140,13 +1136,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem { deleteEmptyVersionedFolders(folderPath); } - /* - * @see ghidra.framework.store.FileSystem#moveFolder(java.lang.String, java.lang.String, java.lang.String) - */ @Override public synchronized void moveFolder(String parentPath, String folderName, String newParentPath) throws InvalidNameException, IOException { + checkDisposed(); + if (readOnly) { throw new ReadOnlyException(); } @@ -1206,13 +1201,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem { } } - /* - * @see ghidra.framework.store.FileSystem#renameFolder(java.lang.String, java.lang.String, java.lang.String) - */ @Override public synchronized void renameFolder(String parentPath, String folderName, String newFolderName) throws InvalidNameException, IOException { + checkDisposed(); + if (readOnly) { throw new ReadOnlyException(); } @@ -1247,16 +1241,14 @@ public class IndexedLocalFileSystem extends LocalFileSystem { eventManager.folderRenamed(parentPath, folderName, newFolderName); } - /* - * @see ghidra.framework.store.FileSystem#folderExists(java.lang.String) - */ @Override public synchronized boolean folderExists(String folderPath) { try { + checkDisposed(); getFolder(folderPath, GetFolderOption.READ_ONLY); return true; } - catch (NotFoundException e) { + catch (IOException | NotFoundException e) { return false; } } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java index 41bd6d3213..6c8cc9a542 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java @@ -46,7 +46,7 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem { * @throws FileNotFoundException if specified rootPath does not exist * @throws IOException if error occurs while reading/writing index files */ - IndexedV1LocalFileSystem(String rootPath, boolean isVersioned, boolean readOnly, + protected IndexedV1LocalFileSystem(String rootPath, boolean isVersioned, boolean readOnly, boolean enableAsyncronousDispatching, boolean create) throws IOException { super(rootPath, isVersioned, readOnly, enableAsyncronousDispatching, create); } @@ -134,6 +134,7 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem { @Override public FolderItem getItem(String fileID) throws IOException, UnsupportedOperationException { + checkDisposed(); if (fileIdMap == null) { return null; } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java index b569e2b940..c014e43ac8 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java @@ -71,6 +71,8 @@ public abstract class LocalFileSystem implements FileSystem { private static boolean refreshRequired = false; + private boolean disposed = false; + protected final File root; protected final boolean isVersioned; protected final boolean readOnly; @@ -78,9 +80,6 @@ public abstract class LocalFileSystem implements FileSystem { private RepositoryLogger repositoryLogger; - // Always false in production; can be manipulated by tests - private boolean isShared; - /** * Construct a local filesystem for existing data * @param rootPath @@ -285,25 +284,16 @@ public abstract class LocalFileSystem implements FileSystem { return refreshRequired; } - /* - * @see ghidra.framework.store.FileSystem#isVersioned() - */ @Override public boolean isVersioned() { return isVersioned; } - /* - * @see ghidra.framework.store.FileSystem#isOnline() - */ @Override public boolean isOnline() { - return true; + return !disposed; } - /* - * @see ghidra.framework.store.FileSystem#isReadOnly() - */ @Override public boolean isReadOnly() { return readOnly; @@ -388,9 +378,6 @@ public abstract class LocalFileSystem implements FileSystem { return getItemNames(folderPath, false); } - /* - * @see ghidra.framework.store.FileSystem#getItem(java.lang.String, java.lang.String) - */ @Override public synchronized LocalFolderItem getItem(String folderPath, String name) throws IOException { try { @@ -424,9 +411,6 @@ public abstract class LocalFileSystem implements FileSystem { throw new UnsupportedOperationException("getItem by File-ID"); } - /* - * @see ghidra.framework.store.FileSystem#createDatabase(java.lang.String, java.lang.String, java.lang.String, db.buffers.BufferFile, java.lang.String, java.lang.String, boolean, ghidra.util.task.TaskMonitor, java.lang.String) - */ @Override public synchronized LocalDatabaseItem createDatabase(String parentPath, String name, String fileID, BufferFile bufferFile, String comment, String contentType, @@ -483,9 +467,6 @@ public abstract class LocalFileSystem implements FileSystem { return item; } - /* - * @see ghidra.framework.store.FileSystem#createDatabase(java.lang.String, java.lang.String, java.lang.String, int, java.lang.String) - */ @Override public LocalManagedBufferFile createDatabase(String parentPath, String name, String fileID, String contentType, int bufferSize, String user, String projectPath) @@ -513,9 +494,6 @@ public abstract class LocalFileSystem implements FileSystem { return bufferFile; } - /* - * @see ghidra.framework.store.FileSystem#createDataFile(java.lang.String, java.lang.String, java.io.InputStream, java.lang.String, java.lang.String, ghidra.util.task.TaskMonitor) - */ @Override public synchronized LocalDataFile createDataFile(String parentPath, String name, InputStream istream, String comment, String contentType, TaskMonitor monitor) @@ -546,9 +524,6 @@ public abstract class LocalFileSystem implements FileSystem { return dataFile; } - /* - * @see ghidra.framework.store.FileSystem#createFile(java.lang.String, java.lang.String, java.io.File, ghidra.util.task.TaskMonitor, java.lang.String) - */ @Override public LocalDatabaseItem createFile(String parentPath, String name, File packedFile, TaskMonitor monitor, String user) @@ -591,9 +566,6 @@ public abstract class LocalFileSystem implements FileSystem { return item; } - /* - * @see ghidra.framework.store.FileSystem#moveItem(java.lang.String, java.lang.String, java.lang.String) - */ @Override public synchronized void moveItem(String folderPath, String name, String newFolderPath, String newName) throws IOException, InvalidNameException { @@ -652,9 +624,6 @@ public abstract class LocalFileSystem implements FileSystem { @Override public abstract boolean folderExists(String folderPath); - /* - * @see ghidra.framework.store.FileSystem#fileExists(java.lang.String, java.lang.String) - */ @Override public boolean fileExists(String folderPath, String name) { try { @@ -669,9 +638,6 @@ public abstract class LocalFileSystem implements FileSystem { } } - /* - * @see ghidra.framework.store.FileSystem#addFileSystemListener(ghidra.framework.store.FileSystemListener) - */ @Override public void addFileSystemListener(FileSystemListener listener) { if (eventManager != null) { @@ -679,9 +645,6 @@ public abstract class LocalFileSystem implements FileSystem { } } - /* - * @see ghidra.framework.store.FileSystem#removeFileSystemListener(ghidra.framework.store.FileSystemListener) - */ @Override public void removeFileSystemListener(FileSystemListener listener) { if (eventManager != null) { @@ -829,8 +792,7 @@ public abstract class LocalFileSystem implements FileSystem { @Override public boolean isShared() { - // Does not support direct sharing in production - return isShared; + return false; } // static void testValidPathLength(File file) throws IOException { @@ -846,6 +808,17 @@ public abstract class LocalFileSystem implements FileSystem { if (eventManager != null) { eventManager.dispose(); } + disposed = true; + } + + /** + * Check to see if file-system has been disposed. + * @throws IOException if file-system has been disposed + */ + protected void checkDisposed() throws IOException { + if (disposed) { + throw new IOException("File-system has been disposed"); + } } public boolean migrationInProgress() { diff --git a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java index f74cfa8538..b65503a6ce 100644 --- a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java +++ b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java @@ -929,12 +929,7 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest { FileSystemEventManager eventManager = (FileSystemEventManager) TestUtils.getInstanceField("eventManager", fs); - try { - eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS); - } - catch (InterruptedException e) { - failWithException("Interrupted waiting for filesystem events", e); - } + eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java b/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java index 5d052c3e20..d2d318f7e4 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/test/AbstractGenericTest.java @@ -507,6 +507,41 @@ public abstract class AbstractGenericTest extends AbstractGTest { return null; } + public static List findComponents(Container parent, + Class desiredClass) { + return findComponents(parent, desiredClass, false); + } + + public static List findComponents(Container parent, + Class desiredClass, boolean checkOwnedWindows) { + Component[] comps = parent.getComponents(); + List list = new ArrayList<>(); + for (Component element : comps) { + if (element == null) { + continue;// this started happening in 1.6, not sure why + } + if (desiredClass.isAssignableFrom(element.getClass())) { + list.add(desiredClass.cast(element)); + } + else if (element instanceof Container) { + T c = findComponent((Container) element, desiredClass, checkOwnedWindows); + if (c != null) { + list.add(desiredClass.cast(c)); + } + } + } + if (checkOwnedWindows && (parent instanceof Window)) { + Window[] windows = ((Window) parent).getOwnedWindows(); + for (int i = windows.length - 1; i >= 0; i--) { + Component c = findComponent(windows[i], desiredClass, checkOwnedWindows); + if (c != null) { + list.add(desiredClass.cast(c)); + } + } + } + return list; + } + /** * Get the first field object contained within object ownerInstance which * has the type classType. This method is only really useful if it is known diff --git a/Ghidra/Framework/Generic/src/main/java/generic/util/FileChannelLock.java b/Ghidra/Framework/Generic/src/main/java/generic/util/FileChannelLock.java index ccb2e52bdf..d80b7c2108 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/util/FileChannelLock.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/util/FileChannelLock.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +16,7 @@ package generic.util; import java.io.*; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; +import java.nio.channels.*; public class FileChannelLock { @@ -51,7 +49,7 @@ public class FileChannelLock { return isLocked; } - catch (IOException e) { + catch (IOException | OverlappingFileLockException e) { release(); } return false; diff --git a/Ghidra/Framework/Project/certification.manifest b/Ghidra/Framework/Project/certification.manifest index ec700e3b9d..2a05b72413 100644 --- a/Ghidra/Framework/Project/certification.manifest +++ b/Ghidra/Framework/Project/certification.manifest @@ -36,6 +36,7 @@ src/main/resources/images/disconnected.gif||GHIDRA||reviewed||END| src/main/resources/images/disk.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/face-glasses.png||Tango Icons - Public Domain|||tango icon set|END| src/main/resources/images/folder_add.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| +src/main/resources/images/link.png||Crystal Clear Icons - LGPL 2.1||||END| src/main/resources/images/lock.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| src/main/resources/images/monitor.png||FAMFAMFAM Icons - CC 2.5|||silk|END| src/main/resources/images/noneInTool.gif||GHIDRA||reviewed||END| diff --git a/Ghidra/Framework/Project/data/project.icons.theme.properties b/Ghidra/Framework/Project/data/project.icons.theme.properties index 5a8f6132d4..cf81d7ad06 100644 --- a/Ghidra/Framework/Project/data/project.icons.theme.properties +++ b/Ghidra/Framework/Project/data/project.icons.theme.properties @@ -6,10 +6,14 @@ icon.project.data.file.ghidra.unsupported = unknownFile.gif icon.project.data.file.ghidra.checked.out = icon.check icon.project.data.file.ghidra.checked.out.exclusive = checkex.png icon.project.data.file.ghidra.hijacked = small_hijack.gif -icon.project.data.file.ghidra.read.only = user-busy.png [size(10,10)] +icon.project.data.file.ghidra.read.only = user-busy.png [size(8,8)] icon.project.data.file.ghidra.not.latest = checkNotLatest.gif +icon.content.handler.link = link.png +icon.content.handler.link.overlay = EMPTY_ICON[size(16,16)]{icon.content.handler.link[move(0,8)]} // lower-left of 16x16 icon +icon.content.handler.linked.folder.open = icon.datatree.node.domain.folder.open{icon.content.handler.link.overlay} +icon.content.handler.linked.folder.closed = icon.datatree.node.domain.folder.closed{icon.content.handler.link.overlay} [Dark Defaults] diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java index 59754c8e4d..9c48a7d8c6 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java @@ -19,9 +19,7 @@ import java.io.IOException; import javax.swing.Icon; -import db.DBHandle; -import ghidra.framework.model.ChangeSet; -import ghidra.framework.model.DomainObject; +import ghidra.framework.model.*; import ghidra.framework.store.FileSystem; import ghidra.framework.store.FolderItem; import ghidra.util.InvalidNameException; @@ -31,15 +29,17 @@ import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; /** - * NOTE: ALL ContentHandler CLASSES MUST END IN "ContentHandler". If not, + * NOTE: ALL ContentHandler implementations MUST END IN "ContentHandler". If not, * the ClassSearcher will not find them. * * ContentHandler defines an application interface for converting * between a specific domain object implementation and folder item storage. * This interface also defines a method which provides an appropriate icon * corresponding to the content. + * + * @param {@link DomainObjectAdapter} implementation class */ -public interface ContentHandler extends ExtensionPoint { +public interface ContentHandler extends ExtensionPoint { public static final String UNKNOWN_CONTENT = "Unknown-File"; public static final String MISSING_CONTENT = "Missing-File"; @@ -56,7 +56,8 @@ public interface ContentHandler extends ExtensionPoint { * @param domainObject the domain object to store in the newly created folder item * @param monitor the monitor that allows the user to cancel * @return checkout ID for new item - * @throws IOException if an i/o error occurs + * @throws IOException if an IO error occurs or an unsupported {@code domainObject} + * implementation is specified. * @throws InvalidNameException if the specified name contains invalid characters * @throws CancelledException if the user cancels */ @@ -77,12 +78,12 @@ public interface ContentHandler extends ExtensionPoint { * set. * @param monitor the monitor that allows the user to cancel * @return immutable domain object - * @throws IOException if a folder item access error occurs + * @throws IOException if an IO or folder item access error occurs * @throws CancelledException if operation is cancelled by user * @throws VersionException if unable to handle file content due to version * difference which could not be handled. */ - DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version, + T getImmutableObject(FolderItem item, Object consumer, int version, int minChangeVersion, TaskMonitor monitor) throws IOException, CancelledException, VersionException; @@ -98,12 +99,12 @@ public interface ContentHandler extends ExtensionPoint { * @param consumer consumer of the returned object * @param monitor the monitor that allows the user to cancel * @return read-only domain object - * @throws IOException if a folder item access error occurs + * @throws IOException if an IO or folder item access error occurs * @throws CancelledException if operation is cancelled by user * @throws VersionException if unable to handle file content due to version * difference which could not be handled. */ - DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, + T getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, Object consumer, TaskMonitor monitor) throws IOException, VersionException, CancelledException; @@ -121,12 +122,12 @@ public interface ContentHandler extends ExtensionPoint { * @param consumer consumer of the returned object * @param monitor cancelable task monitor * @return updateable domain object - * @throws IOException if a folder item access error occurs + * @throws IOException if an IO or folder item access error occurs * @throws CancelledException if operation is cancelled by user * @throws VersionException if unable to handle file content due to version * difference which could not be handled. */ - DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, + T getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor) throws IOException, CancelledException, VersionException; @@ -138,7 +139,7 @@ public interface ContentHandler extends ExtensionPoint { * @param newerVersion the newer version number * @return the set of changes that were made * @throws VersionException if a database version change prevents reading of data. - * @throws IOException if a folder item access error occurs or change set was + * @throws IOException if an IO or folder item access error occurs or change set was * produced by newer version of software and can not be read */ ChangeSet getChangeSet(FolderItem versionedFolderItem, int olderVersion, int newerVersion) @@ -161,55 +162,46 @@ public interface ContentHandler extends ExtensionPoint { /** * Returns true if the content type is always private * (i.e., can not be added to the versioned filesystem). + * @return true if private content type, else false */ boolean isPrivateContentType(); /** - * Returns list of unique content-types supported. - * A minimum of one content-type will be returned. If more than one - * is returned, these are considered equivalent aliases. + * Returns a unique content-type identifier + * @return content type identifier for associated domain object(s). */ String getContentType(); /** * A string that is meant to be presented to the user. + * @return user friendly content type for associated domain object(s). */ String getContentTypeDisplayString(); /** * Returns the Icon associated with this handlers content type. + * @return base icon to be used for a {@link DomainFile} with the associated content type. */ Icon getIcon(); /** - * Returns the name of the default tool that should be used to open this content type + * Returns the name of the default tool that should be used to open this content type. + * @return associated default tool for this content type */ String getDefaultToolName(); /** * Returns domain object implementation class supported. + * @return implementation class for the associated {@link DomainObjectAdapter} implementation. */ - Class getDomainObjectClass(); + Class getDomainObjectClass(); /** - * Create user data file associated with existing content. - * This facilitates the lazy creation of the user data file. - * @param associatedDomainObj associated domain object corresponding to this content handler - * @param userDbh user data handle - * @param userfs private user data filesystem - * @param monitor task monitor - * @throws IOException if an access error occurs - * @throws CancelledException if operation is cancelled by user + * If linking is supported return an instanceof the appropriate {@link LinkHandler}. + * @return corresponding link handler or null if not supported. */ - void saveUserDataFile(DomainObject associatedDomainObj, DBHandle userDbh, FileSystem userfs, - TaskMonitor monitor) throws CancelledException, IOException; - - /** - * Remove user data file associated with an existing folder item. - * @param item folder item - * @param userFilesystem - * @throws IOException if an access error occurs - */ - void removeUserDataFile(FolderItem item, FileSystem userFilesystem) throws IOException; + default LinkHandler getLinkHandler() { + return null; + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBContentHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBContentHandler.java index b911cd998a..4b62da8926 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBContentHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBContentHandler.java @@ -18,13 +18,10 @@ package ghidra.framework.data; import java.io.IOException; import db.DBHandle; -import db.buffers.BufferFile; import db.buffers.ManagedBufferFile; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainObject; import ghidra.framework.store.*; -import ghidra.util.*; -import ghidra.util.exception.AssertException; +import ghidra.util.InvalidNameException; +import ghidra.util.SystemUtilities; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -32,8 +29,11 @@ import ghidra.util.task.TaskMonitor; * DBContentHandler provides an abstract ContentHandler for * domain object content which is stored within a database file. * This class provides helper methods for working with database files. + * + * @param {@link DomainObjectAdapterDB} implementation class */ -public abstract class DBContentHandler implements ContentHandler { +public abstract class DBContentHandler + implements ContentHandler { /** * Create a new database file from an open database handle. @@ -70,6 +70,7 @@ public abstract class DBContentHandler implements ContentHandler { bf.delete(); } catch (IOException e) { + // ignore } abortCreate(fs, path, name, checkoutId); } @@ -77,7 +78,7 @@ public abstract class DBContentHandler implements ContentHandler { return checkoutId; } - private void abortCreate(FileSystem fs, String path, String name, long checkoutId) { + protected void abortCreate(FileSystem fs, String path, String name, long checkoutId) { try { FolderItem item = fs.getItem(path, name); if (item != null) { @@ -92,98 +93,4 @@ public abstract class DBContentHandler implements ContentHandler { } } - /** - * Return user data content type corresponding to associatedContentType. - */ - private static String getUserDataContentType(String associatedContentType) { - return associatedContentType + "UserData"; - } - - /** - * @see ghidra.framework.data.ContentHandler#saveUserDataFile(ghidra.framework.model.DomainObject, db.DBHandle, ghidra.framework.store.FileSystem, ghidra.util.task.TaskMonitor) - */ - @Override - public final void saveUserDataFile(DomainObject domainObj, DBHandle userDbh, FileSystem userfs, - TaskMonitor monitor) throws CancelledException, IOException { - if (userfs.isVersioned()) { - throw new IllegalArgumentException("User data file-system may not be versioned"); - } - String associatedContentType = getContentType(); - DomainFile associatedDf = domainObj.getDomainFile(); - if (associatedDf == null) { - throw new IllegalStateException("associated " + associatedContentType + - " file must be saved before user data can be saved"); - } - String associatedFileID = associatedDf.getFileID(); - if (associatedFileID == null) { - Msg.error(this, associatedContentType + " '" + associatedDf.getName() + - "' has not been assigned a file ID, user settings can not be saved!"); - return; - } - String path = "/"; - String name = ProjectFileManager.getUserDataFilename(associatedFileID); - BufferFile bf = null; - boolean success = false; - try { - bf = - userfs.createDatabase(path, name, FileIDFactory.createFileID(), - getUserDataContentType(associatedContentType), userDbh.getBufferSize(), - SystemUtilities.getUserName(), null); - userDbh.saveAs(bf, true, monitor); - success = true; - } - catch (InvalidNameException e) { - throw new AssertException("Unexpected Error", e); - } - finally { - if (bf != null && !success) { - try { - bf.delete(); - } - catch (IOException e) { - } - abortCreate(userfs, path, name, FolderItem.DEFAULT_CHECKOUT_ID); - } - } - } - - /** - * @see ghidra.framework.data.ContentHandler#removeUserDataFile(ghidra.framework.store.FolderItem, ghidra.framework.store.FileSystem) - */ - @Override - public final void removeUserDataFile(FolderItem associatedItem, FileSystem userfs) - throws IOException { - String path = "/"; - String name = ProjectFileManager.getUserDataFilename(associatedItem.getFileID()); - FolderItem item = userfs.getItem(path, name); - if (item != null) { - item.delete(-1, null); - } - } - - /** - * Open user data file associatedDbh - * @param associatedFileID - * @param associatedContentType - * @param userfs - * @param monitor - * @return user data file database handle - * @throws IOException - * @throws CancelledException - */ - protected final DBHandle openAssociatedUserFile(String associatedFileID, - String associatedContentType, FileSystem userfs, TaskMonitor monitor) - throws IOException, CancelledException { - String path = "/"; - String name = ProjectFileManager.getUserDataFilename(associatedFileID); - FolderItem item = userfs.getItem(path, name); - if (item == null || !(item instanceof DatabaseItem) || - !getUserDataContentType(associatedContentType).equals(item.getContentType())) { - return null; - } - DatabaseItem dbItem = (DatabaseItem) item; - BufferFile bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID); - return new DBHandle(bf, false, monitor); - } - } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBWithUserDataContentHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBWithUserDataContentHandler.java new file mode 100644 index 0000000000..84738a3077 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DBWithUserDataContentHandler.java @@ -0,0 +1,144 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.data; + +import java.io.IOException; + +import db.DBHandle; +import db.buffers.BufferFile; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainObject; +import ghidra.framework.store.*; +import ghidra.util.*; +import ghidra.util.exception.AssertException; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * DBContentHandler provides an abstract ContentHandler for + * domain object content which is stored within a database file. + * This class provides helper methods for working with database files. + * + * @param {@link DomainObjectAdapterDB} implementation class + */ +public abstract class DBWithUserDataContentHandler + extends DBContentHandler { + + /** + * Return user data content type corresponding to associatedContentType. + */ + private static String getUserDataContentType(String associatedContentType) { + return associatedContentType + "UserData"; + } + + /** + * Create user data file associated with existing content. + * This facilitates the lazy creation of the user data file. + * @param associatedDomainObj associated domain object corresponding to this content handler + * @param userDbh user data handle + * @param userfs private user data filesystem + * @param monitor task monitor + * @throws IOException if an IO or access error occurs + * @throws CancelledException if operation is cancelled by user + */ + public final void saveUserDataFile(DomainObject associatedDomainObj, DBHandle userDbh, + FileSystem userfs, + TaskMonitor monitor) throws CancelledException, IOException { + if (userfs.isVersioned()) { + throw new IllegalArgumentException("User data file-system may not be versioned"); + } + String associatedContentType = getContentType(); + DomainFile associatedDf = associatedDomainObj.getDomainFile(); + if (associatedDf == null) { + throw new IllegalStateException("associated " + associatedContentType + + " file must be saved before user data can be saved"); + } + String associatedFileID = associatedDf.getFileID(); + if (associatedFileID == null) { + Msg.error(this, associatedContentType + " '" + associatedDf.getName() + + "' has not been assigned a file ID, user settings can not be saved!"); + return; + } + String path = "/"; + String name = ProjectFileManager.getUserDataFilename(associatedFileID); + BufferFile bf = null; + boolean success = false; + try { + bf = + userfs.createDatabase(path, name, FileIDFactory.createFileID(), + getUserDataContentType(associatedContentType), userDbh.getBufferSize(), + SystemUtilities.getUserName(), null); + userDbh.saveAs(bf, true, monitor); + success = true; + } + catch (InvalidNameException e) { + throw new AssertException("Unexpected Error", e); + } + finally { + if (bf != null && !success) { + try { + bf.delete(); + } + catch (IOException e) { + // ignore + } + abortCreate(userfs, path, name, FolderItem.DEFAULT_CHECKOUT_ID); + } + } + } + + /** + * Remove user data file associated with an existing folder item. + * @param associatedItem associated folder item + * @param userFilesystem user data file system from which corresponding data should be removed. + * @throws IOException if an access error occurs + */ + public final void removeUserDataFile(FolderItem associatedItem, FileSystem userFilesystem) + throws IOException { + String path = "/"; + String name = ProjectFileManager.getUserDataFilename(associatedItem.getFileID()); + FolderItem item = userFilesystem.getItem(path, name); + if (item != null) { + item.delete(-1, null); + } + } + + /** + * Open user data file associatedDbh + * @param associatedFileID + * @param associatedContentType + * @param userfs + * @param monitor + * @return user data file database handle + * @throws IOException + * @throws CancelledException + */ + protected final DBHandle openAssociatedUserFile(String associatedFileID, + String associatedContentType, FileSystem userfs, TaskMonitor monitor) + throws IOException, CancelledException { + String path = "/"; + String name = ProjectFileManager.getUserDataFilename(associatedFileID); + FolderItem item = userfs.getItem(path, name); + if (item == null || !(item instanceof DatabaseItem) || + !getUserDataContentType(associatedContentType).equals(item.getContentType())) { + return null; + } + DatabaseItem dbItem = (DatabaseItem) item; + BufferFile bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID); + return new DBHandle(bf, false, monitor); + } + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java index dece247192..7a13516e27 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java @@ -17,11 +17,14 @@ package ghidra.framework.data; import java.io.File; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.*; import javax.swing.Icon; import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.store.ItemCheckoutStatus; import ghidra.framework.store.Version; import ghidra.framework.store.db.PackedDatabase; @@ -111,6 +114,29 @@ public class DomainFileProxy implements DomainFile { return parentPath + DomainFolder.SEPARATOR + getName(); } + @Override + public URL getSharedProjectURL() { + if (projectLocation != null && version == DomainFile.DEFAULT_VERSION) { + URL projectURL = projectLocation.getURL(); + if (GhidraURL.isServerRepositoryURL(projectURL)) { + try { + // Direct URL construction done so that ghidra protocol + // extension may be supported + String urlStr = projectURL.toExternalForm(); + if (urlStr.endsWith("/")) { + urlStr = urlStr.substring(0, urlStr.length() - 1); + } + urlStr += getPathname(); + return new URL(urlStr); + } + catch (MalformedURLException e) { + // ignore + } + } + } + return null; + } + @Override public int compareTo(DomainFile df) { return getName().compareToIgnoreCase(df.getName()); @@ -143,7 +169,7 @@ public class DomainFileProxy implements DomainFile { DomainObjectAdapter dobj = getDomainObject(); if (dobj != null) { try { - ContentHandler ch = DomainObjectAdapter.getContentHandler(dobj); + ContentHandler ch = DomainObjectAdapter.getContentHandler(dobj); return ch.getContentType(); } catch (IOException e) { @@ -153,6 +179,27 @@ public class DomainFileProxy implements DomainFile { return "Unknown File"; } + @Override + public boolean isLinkFile() { + DomainObjectAdapter dobj = getDomainObject(); + if (dobj != null) { + ContentHandler ch; + try { + ch = DomainObjectAdapter.getContentHandler(dobj); + return LinkHandler.class.isAssignableFrom(ch.getClass()); + } + catch (IOException e) { + // ignore + } + } + return false; + } + + @Override + public DomainFolder followLink() { + throw new UnsupportedOperationException(); + } + @Override public Class getDomainObjectClass() { DomainObjectAdapter dobj = getDomainObject(); @@ -228,6 +275,7 @@ public class DomainFileProxy implements DomainFile { dobj.release(consumer); } catch (IllegalArgumentException e) { + // ignore unknown consumer error } } } @@ -256,11 +304,6 @@ public class DomainFileProxy implements DomainFile { throw new UnsupportedOperationException("Repository operations not supported"); } - @Override - public boolean isVersionControlSupported() { - return false; - } - @Override public boolean canAddToRepository() { return false; @@ -303,6 +346,16 @@ public class DomainFileProxy implements DomainFile { throw new UnsupportedOperationException("Repository operations not supported"); } + @Override + public boolean isLinkingSupported() { + return false; + } + + @Override + public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { + return null; // not supported by proxy file + } + @Override public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, CancelledException { @@ -318,11 +371,8 @@ public class DomainFileProxy implements DomainFile { } } - /** - * @see ghidra.framework.model.DomainFile#copyVersionTo(int, ghidra.framework.model.DomainFolder, ghidra.util.task.TaskMonitor) - */ @Override - public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor) + public DomainFile copyVersionTo(int ver, DomainFolder destFolder, TaskMonitor monitor) throws IOException, CancelledException { throw new UnsupportedOperationException("copyVersionTo unsupported for DomainFileProxy"); } @@ -420,7 +470,7 @@ public class DomainFileProxy implements DomainFile { throw new UnsupportedOperationException("packFile() only valid for Database files"); } DomainObjectAdapterDB dbObj = (DomainObjectAdapterDB) domainObj; - ContentHandler ch = DomainObjectAdapter.getContentHandler(domainObj); + ContentHandler ch = DomainObjectAdapter.getContentHandler(domainObj); PackedDatabase.packDatabase(dbObj.getDBHandle(), dbObj.getName(), ch.getContentType(), file, monitor); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java index adc0c0d95c..58c161c7f8 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java @@ -38,8 +38,8 @@ public abstract class DomainObjectAdapter implements DomainObject { protected final static String DEFAULT_NAME = "untitled"; private static Class defaultDomainObjClass; // Domain object implementation mapped to unknown content type - private static HashMap contentHandlerTypeMap; // maps content-type string to handler - private static HashMap, ContentHandler> contentHandlerClassMap; // maps domain object class to handler + private static HashMap> contentHandlerTypeMap; // maps content-type string to handler + private static HashMap, ContentHandler> contentHandlerClassMap; // maps domain object class to handler private static ChangeListener contentHandlerUpdateListener = new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { @@ -399,7 +399,7 @@ public abstract class DomainObjectAdapter implements DomainObject { contentHandlerTypeMap.remove(null); } else { - ContentHandler ch = contentHandlerClassMap.get(doClass); + ContentHandler ch = contentHandlerClassMap.get(doClass); if (ch != null) { contentHandlerTypeMap.put(null, ch); } @@ -414,9 +414,9 @@ public abstract class DomainObjectAdapter implements DomainObject { * @return content handler * @throws IOException if no content handler can be found */ - static synchronized ContentHandler getContentHandler(String contentType) throws IOException { + static synchronized ContentHandler getContentHandler(String contentType) throws IOException { checkContentHandlerMaps(); - ContentHandler ch = contentHandlerTypeMap.get(contentType); + ContentHandler ch = contentHandlerTypeMap.get(contentType); if (ch == null) { throw new IOException("Content handler not found for " + contentType); } @@ -430,10 +430,10 @@ public abstract class DomainObjectAdapter implements DomainObject { * @return content handler * @throws IOException if no content handler can be found */ - public static synchronized ContentHandler getContentHandler(DomainObject dobj) + public static synchronized ContentHandler getContentHandler(DomainObject dobj) throws IOException { checkContentHandlerMaps(); - ContentHandler ch = contentHandlerClassMap.get(dobj.getClass()); + ContentHandler ch = contentHandlerClassMap.get(dobj.getClass()); if (ch == null) { throw new IOException("Content handler not found for " + dobj.getClass().getName()); } @@ -450,17 +450,15 @@ public abstract class DomainObjectAdapter implements DomainObject { } private synchronized static void getContentHandlers() { - contentHandlerClassMap = new HashMap, ContentHandler>(); - contentHandlerTypeMap = new HashMap(); + contentHandlerClassMap = new HashMap, ContentHandler>(); + contentHandlerTypeMap = new HashMap>(); + @SuppressWarnings("rawtypes") List handlers = ClassSearcher.getInstances(ContentHandler.class); - for (ContentHandler ch : handlers) { - String type = ch.getContentType(); - Class DOClass = ch.getDomainObjectClass(); - if (type != null && DOClass != null) { - contentHandlerClassMap.put(DOClass, ch); - contentHandlerTypeMap.put(type, ch); - continue; + for (ContentHandler ch : handlers) { + contentHandlerTypeMap.put(ch.getContentType(), ch); + if (!(ch instanceof LinkHandler)) { + contentHandlerClassMap.put(ch.getDomainObjectClass(), ch); } } setDefaultContentClass(defaultDomainObjClass); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java new file mode 100644 index 0000000000..0dccc72cf9 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java @@ -0,0 +1,119 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.data; + +import java.io.IOException; +import java.net.URL; + +import javax.swing.Icon; + +import ghidra.framework.main.AppInfo; +import ghidra.framework.model.*; +import ghidra.framework.store.FileSystem; +import ghidra.util.InvalidNameException; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * {@code FolderLinkContentHandler} provide folder-link support. + * Implementation relies on {@link AppInfo#getActiveProject()} to provide life-cycle + * management for related transient-projects opened while following folder-links. + */ +public class FolderLinkContentHandler extends LinkHandler { + + public static FolderLinkContentHandler INSTANCE = new FolderLinkContentHandler(); + + public static final String FOLDER_LINK_CONTENT_TYPE = "FolderLink"; + + @Override + public long createFile(FileSystem fs, FileSystem userfs, String path, String name, + DomainObject obj, TaskMonitor monitor) + throws IOException, InvalidNameException, CancelledException { + if (!(obj instanceof URLLinkObject)) { + throw new IOException("Unsupported domain object: " + obj.getClass().getName()); + } + return createFile((URLLinkObject) obj, FOLDER_LINK_CONTENT_TYPE, fs, path, name, + monitor); + } + + @Override + public String getContentType() { + return FOLDER_LINK_CONTENT_TYPE; + } + + @Override + public String getContentTypeDisplayString() { + return FOLDER_LINK_CONTENT_TYPE; + } + + @Override + public Class getDomainObjectClass() { + return NullFolderDomainObject.class; // special case since link corresponds to a Domain Folder + } + + @Override + public Icon getIcon() { + return DomainFolder.CLOSED_FOLDER_ICON; + } + + @Override + public String getDefaultToolName() { + return null; + } + + /** + * Get linked domain folder + * @param folderLinkFile folder-link file. + * @return {@link LinkedGhidraFolder} referenced by specified folder-link file or null if + * folderLinkFile content type is not {@value #FOLDER_LINK_CONTENT_TYPE}. + * @throws IOException if an IO or folder item access error occurs + */ + public static LinkedGhidraFolder getReadOnlyLinkedFolder(DomainFile folderLinkFile) + throws IOException { + + if (!FOLDER_LINK_CONTENT_TYPE.equals(folderLinkFile.getContentType())) { + return null; + } + + URL url = getURL(folderLinkFile); + + Project activeProject = AppInfo.getActiveProject(); + GhidraFolder parent = ((GhidraFile) folderLinkFile).getParent(); + return new LinkedGhidraFolder(activeProject, parent, folderLinkFile.getName(), url); + } + +} + +/** + * Dummy domain object to satisfy {@link FolderLinkContentHandler#getDomainObjectClass()} + */ +final class NullFolderDomainObject extends DomainObjectAdapterDB { + private NullFolderDomainObject() { + // this object may not be instantiated + super(null, null, 0, NullFolderDomainObject.class); + throw new RuntimeException("Object may not be instantiated"); + } + + @Override + public boolean isChangeable() { + return false; + } + + @Override + public String getDescription() { + return "Dummy FolderLink Domain Object"; + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java index a2f6b78ea6..b784e944e2 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java @@ -16,6 +16,7 @@ package ghidra.framework.data; import java.io.*; +import java.net.URL; import java.util.*; import javax.swing.Icon; @@ -24,8 +25,7 @@ import ghidra.framework.model.*; import ghidra.framework.store.ItemCheckoutStatus; import ghidra.framework.store.Version; import ghidra.framework.store.local.LocalFileSystem; -import ghidra.util.InvalidNameException; -import ghidra.util.ReadOnlyException; +import ghidra.util.*; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; @@ -123,6 +123,17 @@ public class GhidraFile implements DomainFile { return fileManager.getProjectLocator(); } + @Override + public URL getSharedProjectURL() { + try { + return getFileData().getSharedProjectURL(); + } + catch (IOException e) { + // ignore + } + return null; + } + @Override public String getContentType() { try { @@ -134,6 +145,37 @@ public class GhidraFile implements DomainFile { return ContentHandler.UNKNOWN_CONTENT; } + @Override + public boolean isLinkFile() { + try { + return getFileData().isLinkFile(); + } + catch (IOException e) { + return false; + } + } + + @Override + public DomainFolder followLink() { + try { + return FolderLinkContentHandler.getReadOnlyLinkedFolder(this); + } + catch (IOException e) { + Msg.error(this, "Failed to following folder-link: " + getPathname()); + } + return null; + } + + @Override + public boolean isLinkingSupported() { + try { + return getFileData().isLinkingSupported(); + } + catch (IOException e) { + return false; + } + } + @Override public Class getDomainObjectClass() { try { @@ -146,7 +188,7 @@ public class GhidraFile implements DomainFile { } @Override - public DomainFolder getParent() { + public GhidraFolder getParent() { return parent; } @@ -257,7 +299,7 @@ public class GhidraFile implements DomainFile { catch (IOException e) { fileError(e); } - return GhidraFileData.UNSUPPORTED_FILE_ICON; + return UNSUPPORTED_FILE_ICON; } @Override @@ -353,17 +395,6 @@ public class GhidraFile implements DomainFile { return true; } - @Override - public boolean isVersionControlSupported() { - try { - return getFileData().isVersionControlSupported(); - } - catch (IOException e) { - fileError(e); - } - return false; - } - @Override public boolean isVersioned() { try { @@ -482,6 +513,9 @@ public class GhidraFile implements DomainFile { @Override public GhidraFile moveTo(DomainFolder newParent) throws IOException { + if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { + throw new UnsupportedOperationException("newParent does not support moveTo"); + } GhidraFolder newGhidraParent = (GhidraFolder) newParent; return getFileData().moveTo(newGhidraParent.getFolderData()); } @@ -489,15 +523,30 @@ public class GhidraFile implements DomainFile { @Override public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, CancelledException { - GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation + if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { + throw new UnsupportedOperationException("newParent does not support copyTo"); + } + GhidraFolder newGhidraParent = (GhidraFolder) newParent; return getFileData().copyTo(newGhidraParent.getFolderData(), monitor != null ? monitor : TaskMonitor.DUMMY); } + @Override + public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { + if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { + throw new UnsupportedOperationException("newParent does not support copyToAsLink"); + } + GhidraFolder newGhidraParent = (GhidraFolder) newParent; + return getFileData().copyToAsLink(newGhidraParent.getFolderData()); + } + @Override public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor) throws IOException, CancelledException { - GhidraFolder destGhidraFolder = (GhidraFolder) destFolder; // assumes single implementation + if (!GhidraFolder.class.isAssignableFrom(destFolder.getClass())) { + throw new UnsupportedOperationException("destFolder does not support copyVersionTo"); + } + GhidraFolder destGhidraFolder = (GhidraFolder) destFolder; return getFileData().copyVersionTo(version, destGhidraFolder.getFolderData(), monitor != null ? monitor : TaskMonitor.DUMMY); } @@ -507,7 +556,7 @@ public class GhidraFile implements DomainFile { * only when a non shared project is being converted to a shared project. * @param monitor task monitor * @throws IOException if an IO error occurs - * @throws CancelledException if task cancelled + * @throws CancelledException if task is cancelled */ void convertToPrivateFile(TaskMonitor monitor) throws IOException, CancelledException { getFileData().convertToPrivateFile( @@ -596,6 +645,10 @@ public class GhidraFile implements DomainFile { @Override public String toString() { + ProjectLocator projectLocator = parent.getProjectData().getProjectLocator(); + if (projectLocator.isTransient()) { + return fileManager.getProjectLocator().getName() + getPathname(); + } return fileManager.getProjectLocator().getName() + ":" + getPathname(); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java index a0b0c7413c..e0a09f6be6 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java @@ -17,6 +17,8 @@ package ghidra.framework.data; import java.awt.*; import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; import java.util.HashMap; import java.util.Map; @@ -27,9 +29,9 @@ import db.Field; import db.buffers.*; import generic.theme.GColor; import generic.theme.GIcon; -import ghidra.framework.client.ClientUtil; -import ghidra.framework.client.NotConnectedException; +import ghidra.framework.client.*; import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.store.*; import ghidra.framework.store.FileSystem; import ghidra.framework.store.local.LocalFileSystem; @@ -37,12 +39,14 @@ import ghidra.framework.store.local.LocalFolderItem; import ghidra.util.*; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; -import ghidra.util.task.TaskMonitorAdapter; import resources.MultiIcon; import resources.icons.TranslateIcon; public class GhidraFileData { + static final int ICON_WIDTH = 18; + static final int ICON_HEIGHT = 17; + private static final boolean ALWAYS_MERGE = System.getProperty("ForceMerge") != null; //@formatter:off @@ -50,6 +54,7 @@ public class GhidraFileData { public static final Icon CHECKED_OUT_ICON = new GIcon("icon.project.data.file.ghidra.checked.out"); public static final Icon CHECKED_OUT_EXCLUSIVE_ICON = new GIcon("icon.project.data.file.ghidra.checked.out.exclusive"); public static final Icon HIJACKED_ICON = new GIcon("icon.project.data.file.ghidra.hijacked"); + public static final Icon VERSION_ICON = new VersionIcon(); public static final Icon READ_ONLY_ICON = new GIcon("icon.project.data.file.ghidra.read.only"); public static final Icon NOT_LATEST_CHECKED_OUT_ICON = new GIcon("icon.project.data.file.ghidra.not.latest"); @@ -202,9 +207,32 @@ public class GhidraFileData { return new GhidraFile(parent.getDomainFolder(), name); } + /** + * Get a remote Ghidra URL for this domain file if available within a remote repository. + * @return remote Ghidra URL for this file or null + */ + URL getSharedProjectURL() { + synchronized (fileSystem) { + RepositoryAdapter repository = parent.getProjectFileManager().getRepository(); + if (versionedFolderItem != null && repository != null) { + URL folderURL = parent.getDomainFolder().getSharedProjectURL(); + try { + // Direct URL construction done so that ghidra protocol + // extension may be supported + return new URL(folderURL.toExternalForm() + name); + } + catch (MalformedURLException e) { + // ignore + } + } + return null; + } + } + /** * Reassign a new file-ID to resolve file-ID conflict. * Conflicts can occur as a result of a cancelled check-out. + * @throws IOException if an IO error occurs */ void resetFileID() throws IOException { synchronized (fileSystem) { @@ -262,6 +290,8 @@ public class GhidraFileData { String getContentType() { synchronized (fileSystem) { FolderItem item = folderItem != null ? folderItem : versionedFolderItem; + // this can happen when we are trying to load a version file from + // a server to which we are not connected if (item == null) { return ContentHandler.MISSING_CONTENT; } @@ -270,14 +300,27 @@ public class GhidraFileData { } } - Class getDomainObjectClass() { + /** + * Get content handler + * @return content handler + * @throws IOException if an IO error occurs, file not found, or unsupported content + */ + ContentHandler getContentHandler() throws IOException { synchronized (fileSystem) { FolderItem item = folderItem != null ? folderItem : versionedFolderItem; + // this can happen when we are trying to load a version file from + // a server to which we are not connected + if (item == null) { + throw new FileNotFoundException(name + " not found"); + } + return DomainObjectAdapter.getContentHandler(item.getContentType()); + } + } + + Class getDomainObjectClass() { + synchronized (fileSystem) { try { - ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType()); - if (ch != null) { - return ch.getDomainObjectClass(); - } + return getContentHandler().getDomainObjectClass(); } catch (IOException e) { // ignore missing content handler @@ -289,8 +332,7 @@ public class GhidraFileData { ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException { synchronized (fileSystem) { if (versionedFolderItem != null && folderItem != null && folderItem.isCheckedOut()) { - ContentHandler ch = - DomainObjectAdapter.getContentHandler(folderItem.getContentType()); + ContentHandler ch = getContentHandler(); return ch.getChangeSet(versionedFolderItem, folderItem.getCheckoutVersion(), versionedFolderItem.getCurrentVersion()); } @@ -305,10 +347,9 @@ public class GhidraFileData { DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover, TaskMonitor monitor) throws VersionException, IOException, CancelledException { FolderItem myFolderItem; - ContentHandler ch; DomainObjectAdapter domainObj = null; synchronized (fileSystem) { - if (fileSystem.isReadOnly()) { + if (fileSystem.isReadOnly() || isLinkFile()) { return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor); } domainObj = getOpenedDomainObject(); @@ -321,8 +362,8 @@ public class GhidraFileData { return domainObj; } } + ContentHandler ch = getContentHandler(); if (folderItem == null) { - ch = DomainObjectAdapter.getContentHandler(versionedFolderItem.getContentType()); DomainObjectAdapter doa = ch.getReadOnlyObject(versionedFolderItem, DomainFile.DEFAULT_VERSION, true, consumer, monitor); doa.setChanged(false); @@ -331,7 +372,6 @@ public class GhidraFileData { proxy.setLastModified(getLastModifiedTime()); return doa; } - ch = DomainObjectAdapter.getContentHandler(folderItem.getContentType()); myFolderItem = folderItem; domainObj = ch.getDomainObject(myFolderItem, parent.getUserFileSystem(), @@ -368,14 +408,7 @@ public class GhidraFileData { FolderItem item = (folderItem != null && version == DomainFile.DEFAULT_VERSION) ? folderItem : versionedFolderItem; - - // this can happen when we are trying to load a version file from - // a server to which we are not connected - if (item == null) { - return null; - } - - ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType()); + ContentHandler ch = getContentHandler(); DomainObjectAdapter doa = ch.getReadOnlyObject(item, version, true, consumer, monitor); doa.setChanged(false); @@ -390,15 +423,12 @@ public class GhidraFileData { throws VersionException, IOException, CancelledException { synchronized (fileSystem) { DomainObjectAdapter obj = null; + ContentHandler ch = getContentHandler(); if (versionedFolderItem == null || (version == DomainFile.DEFAULT_VERSION && folderItem != null) || isHijacked()) { - ContentHandler ch = - DomainObjectAdapter.getContentHandler(folderItem.getContentType()); obj = ch.getImmutableObject(folderItem, consumer, version, -1, monitor); } else { - ContentHandler ch = - DomainObjectAdapter.getContentHandler(versionedFolderItem.getContentType()); obj = ch.getImmutableObject(versionedFolderItem, consumer, version, -1, monitor); } DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), obj, @@ -419,8 +449,11 @@ public class GhidraFileData { } boolean takeRecoverySnapshot() throws IOException { + if (fileSystem.isReadOnly()) { + return true; + } DomainObjectAdapter dobj = fileManager.getOpenedDomainObject(getPathname()); - if (fileSystem.isReadOnly() || !(dobj instanceof DomainObjectAdapterDB) || + if (!(dobj instanceof DomainObjectAdapterDB) || !dobj.isChanged()) { return true; } @@ -481,13 +514,19 @@ public class GhidraFileData { private Icon generateIcon(boolean disabled) { if (parent == null) { // instance has been disposed - return UNSUPPORTED_FILE_ICON; + return DomainFile.UNSUPPORTED_FILE_ICON; } synchronized (fileSystem) { + + boolean isLink = isLinkFile(); + FolderItem item = folderItem != null ? folderItem : versionedFolderItem; + + Icon baseIcon = new TranslateIcon(getBaseIcon(item), 1, 1); + if (versionedFolderItem != null) { MultiIcon multiIcon = new MultiIcon(VERSION_ICON, disabled); - multiIcon.addIcon(getBaseIcon(item)); + multiIcon.addIcon(baseIcon); if (isHijacked()) { multiIcon.addIcon(HIJACKED_ICON); } @@ -504,12 +543,15 @@ public class GhidraFileData { } } } + if (isLink) { + multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1)); + } return multiIcon; } else if (folderItem != null) { - MultiIcon multiIcon = new MultiIcon(getBaseIcon(item), disabled); + MultiIcon multiIcon = new MultiIcon(baseIcon, disabled, ICON_WIDTH, ICON_HEIGHT); if (isReadOnly() && !fileSystem.isReadOnly()) { - multiIcon.addIcon(new TranslateIcon(READ_ONLY_ICON, 6, 6)); + multiIcon.addIcon(new TranslateIcon(READ_ONLY_ICON, 8, 9)); } if (isCheckedOut()) { if (isCheckedOutExclusive()) { @@ -519,23 +561,23 @@ public class GhidraFileData { multiIcon.addIcon(CHECKED_OUT_ICON); } } + if (isLink) { + multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1)); + } return multiIcon; } } - return UNSUPPORTED_FILE_ICON; + return DomainFile.UNSUPPORTED_FILE_ICON; } private Icon getBaseIcon(FolderItem item) { try { - ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType()); - if (ch != null) { - return ch.getIcon(); - } + return getContentHandler().getIcon(); } catch (IOException e) { // ignore missing content handler } - return UNSUPPORTED_FILE_ICON; + return DomainFile.UNSUPPORTED_FILE_ICON; } boolean isChanged() { @@ -593,9 +635,19 @@ public class GhidraFileData { boolean canAddToRepository() { synchronized (fileSystem) { try { - return (!fileSystem.isReadOnly() && !versionedFileSystem.isReadOnly() && - folderItem != null && versionedFolderItem == null && - !folderItem.isCheckedOut() && isVersionControlSupported()); + if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) { + return false; + } + if (folderItem == null || versionedFolderItem != null) { + return false; + } + if (folderItem.isCheckedOut()) { + return false; + } + if (isLinkFile()) { + return GhidraURL.isServerRepositoryURL(LinkHandler.getURL(folderItem)); + } + return !getContentHandler().isPrivateContentType(); } catch (IOException e) { return false; @@ -606,8 +658,11 @@ public class GhidraFileData { boolean canCheckout() { synchronized (fileSystem) { try { - return folderItem == null && !fileSystem.isReadOnly() && - !versionedFileSystem.isReadOnly(); + if (folderItem != null || fileSystem.isReadOnly() || + versionedFileSystem.isReadOnly()) { + return false; + } + return !isLinkFile(); } catch (IOException e) { return false; @@ -627,26 +682,6 @@ public class GhidraFileData { } } - boolean isVersionControlSupported() { - synchronized (fileSystem) { - if (versionedFolderItem != null) { - return true; - } - if (!(folderItem instanceof DatabaseItem)) { - return false; - } - try { - ContentHandler ch = - DomainObjectAdapter.getContentHandler(folderItem.getContentType()); - return !ch.isPrivateContentType(); - } - catch (IOException e) { - // ignore missing content handler - } - return false; - } - } - int getVersion() { synchronized (fileSystem) { try { @@ -714,18 +749,15 @@ public class GhidraFileData { throws IOException, CancelledException { DomainObjectAdapter oldDomainObj = null; synchronized (fileSystem) { - if (!isVersionControlSupported()) { - throw new AssertException("file type does supported version control"); + if (!canAddToRepository()) { + if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) { + throw new ReadOnlyException( + "addToVersionControl permitted within writeable project and repository only"); + } + throw new IOException("addToVersionControl not allowed for file"); } - if (versionedFolderItem != null) { - throw new AssertException("file already versioned"); - } - if (!versionedFileSystem.isOnline()) { - throw new NotConnectedException("Not connected to repository server"); - } - if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) { - throw new ReadOnlyException( - "addToVersionControl permitted within writeable project and repository only"); + if (isLinkFile()) { + keepCheckedOut = false; } String parentPath = parent.getPathname(); String user = ClientUtil.getUserName(); @@ -832,6 +864,9 @@ public class GhidraFileData { if (!versionedFileSystem.isOnline()) { throw new NotConnectedException("Not connected to repository server"); } + if (isLinkFile()) { + return false; + } String user = ClientUtil.getUserName(); ProjectLocator projectLocator = parent.getProjectLocator(); CheckoutType checkoutType; @@ -934,8 +969,7 @@ public class GhidraFileData { if (checkinHandler.createKeepFile()) { DomainObject sourceObj = null; try { - ContentHandler ch = - DomainObjectAdapter.getContentHandler(folderItem.getContentType()); + ContentHandler ch = getContentHandler(); sourceObj = ch.getImmutableObject(folderItem, this, DomainFile.DEFAULT_VERSION, -1, monitor); createKeepFile(sourceObj, monitor); @@ -968,18 +1002,18 @@ public class GhidraFileData { /** * Verify that current user is the checkout user for this file - * @param caseName name of user case (e.g., checkin) - * @return true if server/repository will permit current user to checkin, - * or update checkout version of current file. (i.e., server login matches + * @param operationName name of user case (e.g., checkin) + * @throws IOException if server/repository will not permit current user to checkin, + * or update checkout version of current file. (i.e., server login does not match * user name used at time of initial checkout) */ - private void verifyRepoUser(String caseName) throws IOException { + private void verifyRepoUser(String operationName) throws IOException { if (versionedFileSystem instanceof LocalFileSystem) { return; // rely on local project ownership } String repoUserName = versionedFileSystem.getUserName(); if (repoUserName == null) { - throw new IOException("File " + caseName + " not permitted (not connected)"); + throw new IOException("File " + operationName + " not permitted (not connected)"); } ItemCheckoutStatus checkoutStatus = getCheckoutStatus(); if (checkoutStatus == null) { @@ -987,7 +1021,7 @@ public class GhidraFileData { } String checkoutUserName = checkoutStatus.getUser(); if (!repoUserName.equals(checkoutUserName)) { - throw new IOException("File " + caseName + " not permitted - checkout user '" + + throw new IOException("File " + operationName + " not permitted - checkout user '" + checkoutUserName + "' differs from repository user '" + repoUserName + "'"); } } @@ -1016,7 +1050,7 @@ public class GhidraFileData { } verifyRepoUser("checkin"); if (monitor == null) { - monitor = TaskMonitorAdapter.DUMMY_MONITOR; + monitor = TaskMonitor.DUMMY; } synchronized (fileSystem) { if (busy) { @@ -1036,9 +1070,7 @@ public class GhidraFileData { Msg.info(this, "Checkin with merge for " + name); - ContentHandler ch = - DomainObjectAdapter.getContentHandler(folderItem.getContentType()); - + ContentHandler ch = getContentHandler(); DomainObjectAdapter checkinObj = ch.getDomainObject(versionedFolderItem, null, folderItem.getCheckoutId(), okToUpgrade, false, this, monitor); checkinObj.setDomainFile(new DomainFileProxy(name, getParent().getPathname(), @@ -1366,9 +1398,12 @@ public class GhidraFileData { private void removeAssociatedUserDataFile() { try { - FolderItem item = folderItem != null ? folderItem : versionedFolderItem; - ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType()); - ch.removeUserDataFile(item, parent.getUserFileSystem()); + ContentHandler ch = getContentHandler(); + if (ch instanceof DBWithUserDataContentHandler) { + FolderItem item = folderItem != null ? folderItem : versionedFolderItem; + ((DBWithUserDataContentHandler) ch).removeUserDataFile(item, + parent.getUserFileSystem()); + } } catch (Exception e) { // ignore missing content handler @@ -1403,7 +1438,7 @@ public class GhidraFileData { } verifyRepoUser("merge"); if (monitor == null) { - monitor = TaskMonitorAdapter.DUMMY_MONITOR; + monitor = TaskMonitor.DUMMY; } synchronized (fileSystem) { if (busy) { @@ -1425,8 +1460,7 @@ public class GhidraFileData { "Merge failed, file merge is not supported in headless mode"); } - ContentHandler ch = - DomainObjectAdapter.getContentHandler(folderItem.getContentType()); + ContentHandler ch = getContentHandler(); // Test versioned file for VersionException int mergeVer = versionedFolderItem.getCurrentVersion(); @@ -1556,7 +1590,7 @@ public class GhidraFileData { checkInUse(); GhidraFolderData oldParent = parent; String oldName = name; - String newName = getTargetName(name, newParent); + String newName = newParent.getTargetName(name); try { if (isHijacked()) { fileSystem.moveItem(parent.getPathname(), name, newParent.getPathname(), @@ -1595,15 +1629,49 @@ public class GhidraFileData { } } - private String getTargetName(String preferredName, GhidraFolderData newParent) - throws IOException { - String newName = preferredName; - int i = 1; - while (newParent.getFileData(newName, false) != null) { - newName = preferredName + "." + i; - i++; + boolean isLinkFile() { + synchronized (fileSystem) { + try { + return LinkHandler.class.isAssignableFrom(getContentHandler().getClass()); + } + catch (IOException e) { + return false; + } + } + } + + /** + * Get URL associated with a link-file + * @return link-file URL or null if not a link-file + * @throws IOException if an IO error occurs + */ + URL getLinkFileURL() throws IOException { + if (!isLinkFile()) { + return null; + } + FolderItem item = folderItem != null ? folderItem : versionedFolderItem; + return LinkHandler.getURL(item); + } + + public boolean isLinkingSupported() { + synchronized (fileSystem) { + try { + return getContentHandler().getLinkHandler() != null; + } + catch (IOException e) { + return false; // ignore error + } + } + } + + public DomainFile copyToAsLink(GhidraFolderData newParentData) throws IOException { + synchronized (fileSystem) { + LinkHandler lh = getContentHandler().getLinkHandler(); + if (lh == null) { + return null; + } + return newParentData.copyAsLink(fileManager, getPathname(), name, lh); } - return newName; } GhidraFile copyTo(GhidraFolderData newParentData, TaskMonitor monitor) @@ -1615,7 +1683,7 @@ public class GhidraFileData { FolderItem item = folderItem != null ? folderItem : versionedFolderItem; String pathname = newParentData.getPathname(); String contentType = item.getContentType(); - String targetName = getTargetName(name, newParentData); + String targetName = newParentData.getTargetName(name); String user = ClientUtil.getUserName(); try { if (item instanceof DatabaseItem) { @@ -1668,7 +1736,7 @@ public class GhidraFileData { } String pathname = destFolderData.getPathname(); String contentType = versionedFolderItem.getContentType(); - String targetName = getTargetName(name + "_v" + version, destFolderData); + String targetName = destFolderData.getTargetName(name + "_v" + version); String user = ClientUtil.getUserName(); try { BufferFile bufferFile = ((DatabaseItem) versionedFolderItem).open(version); @@ -1697,7 +1765,9 @@ public class GhidraFileData { /** * Copy this file to make a private file if it is versioned. This method should be called * only when a non shared project is being converted to a shared project. - * @throws IOException + * @param monitor task monitor + * @throws IOException if an IO error occurs + * @throws CancelledException if task is cancelled */ void convertToPrivateFile(TaskMonitor monitor) throws IOException, CancelledException { synchronized (fileSystem) { @@ -1749,7 +1819,10 @@ public class GhidraFileData { Map getMetadata() { FolderItem item = (folderItem != null) ? folderItem : versionedFolderItem; + return getMetadata(item); + } + static Map getMetadata(FolderItem item) { GenericDomainObjectDB genericDomainObj = null; try { if (item instanceof DatabaseItem) { @@ -1767,7 +1840,7 @@ public class GhidraFileData { // file created with newer version of Ghidra } catch (IOException e) { - Msg.error(this, "Read meta-data error", e); + Msg.error(GhidraFileData.class, "Read meta-data error", e); } finally { if (genericDomainObj != null) { @@ -1782,13 +1855,17 @@ public class GhidraFileData { if (fileManager == null) { return name + "(disposed)"; } + ProjectLocator projectLocator = fileManager.getProjectLocator(); + if (projectLocator.isTransient()) { + return fileManager.getProjectLocator().getName() + getPathname(); + } return fileManager.getProjectLocator().getName() + ":" + getPathname(); } - private class GenericDomainObjectDB extends DomainObjectAdapterDB { + private static class GenericDomainObjectDB extends DomainObjectAdapterDB { protected GenericDomainObjectDB(DBHandle dbh) throws IOException { - super(dbh, "Generic", 500, GhidraFileData.this); + super(dbh, "Generic", 500, dbh); loadMetadata(); } @@ -1803,7 +1880,7 @@ public class GhidraFileData { } public void release() { - release(GhidraFileData.this); + release(dbh); } } @@ -1816,8 +1893,8 @@ class VersionIcon implements Icon { private static Color VERSION_ICON_COLOR_LIGHT = new GColor("color.bg.ghidra.file.data.version.icon.light"); - private static final int WIDTH = 18; - private static final int HEIGHT = 17; + private static final int WIDTH = GhidraFileData.ICON_WIDTH; + private static final int HEIGHT = GhidraFileData.ICON_HEIGHT; @Override public int getIconHeight() { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java index 12986ee1e3..3a3de7ff1e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java @@ -16,16 +16,19 @@ package ghidra.framework.data; import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; import java.util.List; +import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.store.FileSystem; import ghidra.framework.store.local.LocalFileSystem; import ghidra.util.InvalidNameException; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -import ghidra.util.task.TaskMonitorAdapter; public class GhidraFolder implements DomainFolder { @@ -83,19 +86,6 @@ public class GhidraFolder implements DomainFolder { return fileData; } - GhidraFolderData getFolderPathData(String folderPath) throws FileNotFoundException { - GhidraFolderData parentData = (folderPath.startsWith(FileSystem.SEPARATOR)) - ? fileManager.getRootFolderData() - : getFolderData(); - GhidraFolderData folderData = parentData.getFolderPathData(folderPath, false); - if (folderData == null) { - String path = (folderPath.startsWith(FileSystem.SEPARATOR)) ? folderPath - : getPathname(folderPath); - throw new FileNotFoundException("folder " + path + " not found"); - } - return folderData; - } - GhidraFolderData getFolderData() throws FileNotFoundException { if (parent == null) { return fileManager.getRootFolderData(); @@ -140,10 +130,10 @@ public class GhidraFolder implements DomainFolder { /** * Refresh folder data - used for testing only - * @throws IOException + * @throws IOException if an IO error occurs */ void refreshFolderData() throws IOException { - getFolderData().refresh(false, true, TaskMonitorAdapter.DUMMY_MONITOR); + getFolderData().refresh(false, true, TaskMonitor.DUMMY); } @Override @@ -193,6 +183,40 @@ public class GhidraFolder implements DomainFolder { return path; } + @Override + public URL getSharedProjectURL() { + ProjectLocator projectLocator = getProjectLocator(); + URL projectURL = projectLocator.getURL(); + if (!GhidraURL.isServerRepositoryURL(projectURL)) { + RepositoryAdapter repository = fileManager.getRepository(); + if (repository == null) { + return null; + } + // NOTE: only supports ghidra protocol without extension protocol. + // Assumes any extension protocol use would be reflected in projectLocator URL. + ServerInfo serverInfo = repository.getServerInfo(); + projectURL = GhidraURL.makeURL(serverInfo.getServerName(), serverInfo.getPortNumber(), + repository.getName()); + } + try { + // Direct URL construction done so that ghidra protocol + // extension may be supported + String urlStr = projectURL.toExternalForm(); + if (urlStr.endsWith(FileSystem.SEPARATOR)) { + urlStr = urlStr.substring(0, urlStr.length() - 1); + } + String path = getPathname(); + if (!path.endsWith(FileSystem.SEPARATOR)) { + path += FileSystem.SEPARATOR; + } + urlStr += path; + return new URL(urlStr); + } + catch (MalformedURLException e) { + return null; + } + } + @Override public boolean isInWritableProject() { return !getProjectData().getLocalFileSystem().isReadOnly(); @@ -244,7 +268,9 @@ public class GhidraFolder implements DomainFolder { return folderData.isEmpty(); } catch (FileNotFoundException e) { - return false; // TODO: what should we return if folder not found + // TODO: what should we return if folder not found or error occurs? + // True is returned to allow this method to be used to avoid continued access. + return true; } } } @@ -295,14 +321,14 @@ public class GhidraFolder implements DomainFolder { public DomainFile createFile(String fileName, DomainObject obj, TaskMonitor monitor) throws InvalidNameException, IOException, CancelledException { return createFolderData().createFile(fileName, obj, - monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR); + monitor != null ? monitor : TaskMonitor.DUMMY); } @Override public DomainFile createFile(String fileName, File packFile, TaskMonitor monitor) throws InvalidNameException, IOException, CancelledException { return createFolderData().createFile(fileName, packFile, - monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR); + monitor != null ? monitor : TaskMonitor.DUMMY); } @Override @@ -325,8 +351,11 @@ public class GhidraFolder implements DomainFolder { if (parent == null) { throw new UnsupportedOperationException("root folder may not be moved"); } + if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { + throw new UnsupportedOperationException("newParent does not support moveTo"); + } GhidraFolderData folderData = getFolderData(); - GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation + GhidraFolder newGhidraParent = (GhidraFolder) newParent; return folderData.moveTo(newGhidraParent.getFolderData()); } @@ -334,9 +363,22 @@ public class GhidraFolder implements DomainFolder { public GhidraFolder copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, CancelledException { GhidraFolderData folderData = getFolderData(); - GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation + if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { + throw new UnsupportedOperationException("newParent does not support copyTo"); + } + GhidraFolder newGhidraParent = (GhidraFolder) newParent; return folderData.copyTo(newGhidraParent.getFolderData(), - monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR); + monitor != null ? monitor : TaskMonitor.DUMMY); + } + + @Override + public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { + GhidraFolderData folderData = getFolderData(); + if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { + throw new UnsupportedOperationException("newParent does not support copyToAsLink"); + } + GhidraFolder newGhidraParent = (GhidraFolder) newParent; + return folderData.copyToAsLink(newGhidraParent.getFolderData()); } /** diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java index 609b16dc1a..39e78a57fa 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java @@ -16,10 +16,13 @@ package ghidra.framework.data; import java.io.*; +import java.net.URL; import java.util.*; import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.GhidraURL; +import ghidra.framework.protocol.ghidra.TransientProjectData; import ghidra.framework.store.FileSystem; import ghidra.framework.store.local.LocalFileSystem; import ghidra.util.*; @@ -257,9 +260,10 @@ class GhidraFolderData { return folderList.isEmpty() && fileList.isEmpty(); } catch (IOException e) { - // ignore + // TODO: what should we return if folder not found or error occurs? + // True is returned to allow this method to be used to avoid continued access. + return true; } - return false; } List getFileNames() { @@ -919,7 +923,7 @@ class GhidraFolderData { DomainFile oldDf = doa.getDomainFile(); try { - ContentHandler ch = DomainObjectAdapter.getContentHandler(doa); + ContentHandler ch = DomainObjectAdapter.getContentHandler(doa); ch.createFile(fileSystem, null, getPathname(), fileName, obj, monitor); if (oldDf != null) { @@ -1140,6 +1144,88 @@ class GhidraFolderData { } } + DomainFile copyToAsLink(GhidraFolderData newParentData) throws IOException { + synchronized (fileSystem) { + String linkFilename = name; + if (linkFilename == null) { + if (fileManager instanceof TransientProjectData) { + linkFilename = fileManager.getRepository().getName(); + } + else { + linkFilename = fileManager.getProjectLocator().getName(); + } + } + return newParentData.copyAsLink(fileManager, getPathname(), linkFilename, + FolderLinkContentHandler.INSTANCE); + } + } + + DomainFile copyAsLink(ProjectData sourceProjectData, String pathname, String linkFilename, + LinkHandler lh) throws IOException { + synchronized (fileSystem) { + if (fileSystem.isReadOnly()) { + throw new ReadOnlyException("copyAsLink permitted to writeable project only"); + } + + if (sourceProjectData == fileManager) { + // internal linking not yet supported + Msg.error(this, "Internal file/folder links not yet supported"); + return null; + } + + URL ghidraUrl = null; + if (sourceProjectData instanceof TransientProjectData) { + RepositoryAdapter repository = sourceProjectData.getRepository(); + ServerInfo serverInfo = repository.getServerInfo(); + ghidraUrl = + GhidraURL.makeURL(serverInfo.getServerName(), serverInfo.getPortNumber(), + repository.getName(), pathname); + } + else { + ProjectLocator projectLocator = sourceProjectData.getProjectLocator(); + if (projectLocator.equals(fileManager.getProjectLocator())) { + return null; // local internal linking not supported + } + ghidraUrl = GhidraURL.makeURL(projectLocator, pathname, null); + } + + String newName = linkFilename; + int i = 1; + while (true) { + GhidraFileData fileData = getFileData(newName, false); + if (fileData != null) { + // return existing file if link URL matches + if (ghidraUrl.equals(fileData.getLinkFileURL())) { + return getDomainFile(newName); + } + newName = linkFilename + "." + i; + ++i; + } + break; + } + + try { + lh.createLink(ghidraUrl, fileSystem, getPathname(), newName); + } + catch (InvalidNameException e) { + throw new IOException(e); // unexpected + } + + fileChanged(newName); + return getDomainFile(newName); + } + } + + String getTargetName(String preferredName) throws IOException { + String newName = preferredName; + int i = 1; + while (getFileData(newName, false) != null) { + newName = preferredName + "." + i; + i++; + } + return newName; + } + /** * used for testing */ @@ -1156,6 +1242,10 @@ class GhidraFolderData { @Override public String toString() { + ProjectLocator projectLocator = fileManager.getProjectLocator(); + if (projectLocator.isTransient()) { + return fileManager.getProjectLocator().getName() + getPathname(); + } return fileManager.getProjectLocator().getName() + ":" + getPathname(); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java new file mode 100644 index 0000000000..f9e65aacd5 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java @@ -0,0 +1,210 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.data; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +import javax.help.UnsupportedOperationException; +import javax.swing.Icon; + +import generic.theme.GIcon; +import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.*; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; +import ghidra.framework.store.FileSystem; +import ghidra.framework.store.FolderItem; +import ghidra.framework.store.local.LocalFileSystem; +import ghidra.util.InvalidNameException; +import ghidra.util.exception.*; +import ghidra.util.task.TaskMonitor; + +/** + * NOTE: ALL ContentHandler implementations MUST END IN "ContentHandler". If not, + * the ClassSearcher will not find them. + * + * LinkHandler defines an application interface for handling domain files which are + * shortcut links to another supported content type. + * + * @param {@link URLLinkObject} implementation class + */ +public abstract class LinkHandler extends DBContentHandler { + + // TODO: Need to improve by making this meta data on file instead of database content. + // Metadata use would eliminate need for DB but we lack support for non-DB files. + + public static final String URL_METADATA_KEY = "link.url"; + + // 16x16 link icon where link is placed in lower-left corner + public static final Icon LINK_ICON = new GIcon("icon.content.handler.link.overlay"); + + /** + * Create a link file using the specified URL + * @param ghidraUrl link URL (must be a Ghidra URL - see {@link GhidraURL}). + * @param fs filesystem where link file should be created + * @param folderPath folder path which should contain link file + * @param linkFilename link filename + * @throws IOException if an IO error occurs + * @throws InvalidNameException if invalid folderPath or linkFilename specified + */ + protected final void createLink(URL ghidraUrl, LocalFileSystem fs, String folderPath, + String linkFilename) throws IOException, InvalidNameException { + URLLinkObject link = new URLLinkObject(linkFilename, ghidraUrl, this); + try { + createFile(fs, null, folderPath, linkFilename, link, TaskMonitor.DUMMY); + } + catch (CancelledException e) { + throw new AssertException(e); // won't happen + } + finally { + link.release(this); + } + } + + @SuppressWarnings("unchecked") + @Override + public final T getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, + Object consumer, TaskMonitor monitor) + throws IOException, VersionException, CancelledException { + + if (!okToUpgrade) { + throw new IllegalArgumentException("okToUpgrade must be true"); + } + + URL url = getURL(item); + + Class domainObjectClass = getDomainObjectClass(); + if (domainObjectClass == null) { + throw new UnsupportedOperationException(""); + } + + GhidraURLWrappedContent wrappedContent = null; + Object content = null; + try { + GhidraURLConnection c = (GhidraURLConnection) url.openConnection(); + Object obj = c.getContent(); // read-only access + if (c.getStatusCode() == StatusCode.UNAUTHORIZED) { + throw new IOException("Authorization failure"); + } + if (!(obj instanceof GhidraURLWrappedContent)) { + throw new IOException("Unsupported linked content"); + } + wrappedContent = (GhidraURLWrappedContent) obj; + content = wrappedContent.getContent(consumer); + if (!(content instanceof DomainFile)) { + throw new IOException("Unsupported linked content: " + content.getClass()); + } + DomainFile linkedFile = (DomainFile) content; + if (!getDomainObjectClass().isAssignableFrom(linkedFile.getDomainObjectClass())) { + throw new BadLinkException( + "Excepted " + getDomainObjectClass() + " but linked to " + + linkedFile.getDomainObjectClass()); + } + return (T) linkedFile.getReadOnlyDomainObject(consumer, version, monitor); + } + finally { + if (content != null) { + wrappedContent.release(content, consumer); + } + } + } + + @Override + public final T getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, + boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor) + throws IOException, CancelledException, VersionException { + // Always upgrade if needed for read-only object + return getReadOnlyObject(item, DomainFile.DEFAULT_VERSION, true, consumer, monitor); + } + + @Override + public T getImmutableObject(FolderItem item, Object consumer, int version, int minChangeVersion, + TaskMonitor monitor) throws IOException, CancelledException, VersionException { + throw new UnsupportedOperationException("link-file does not support getImmutableObject"); + } + + @Override + public final ChangeSet getChangeSet(FolderItem versionedFolderItem, int olderVersion, + int newerVersion) throws VersionException, IOException { + return null; + } + + @Override + public final DomainObjectMergeManager getMergeManager(DomainObject resultsObj, + DomainObject sourceObj, + DomainObject originalObj, DomainObject latestObj) { + return null; + } + + @Override + public final boolean isPrivateContentType() { + // NOTE: URL must be checked - only repository-based links may be versioned + return true; + } + + /** + * Get the link URL which corresponds to the specified link file. + * See {@link DomainFile#isLinkFile()}. + * @param linkFile link-file domain file + * @return link URL + * @throws MalformedURLException if link is bad or unsupported. + * @throws IOException if IO error or supported link file not specified + */ + public static URL getURL(DomainFile linkFile) throws IOException { + String contentType = linkFile.getContentType(); + ContentHandler ch = DomainObjectAdapter.getContentHandler(contentType); + if (ch instanceof LinkHandler) { + Map metadata = linkFile.getMetadata(); + String urlStr = metadata.get(URL_METADATA_KEY); + if (urlStr != null) { + return new URL(urlStr); + } + } + throw new IOException("Invalid link file: " + contentType); + } + + /** + * Get the link URL which corresponds to the specified link file. + * See {@link DomainFile#isLinkFile()}. + * @param linkFile link-file folder item + * @return link URL + * @throws MalformedURLException if link is bad or unsupported. + * @throws IOException if IO error or supported link file not specified + */ + static URL getURL(FolderItem linkFile) throws IOException { + + String contentType = linkFile.getContentType(); + ContentHandler ch = DomainObjectAdapter.getContentHandler(contentType); + if (ch instanceof LinkHandler) { + Map metadata = GhidraFileData.getMetadata(linkFile); + String urlStr = metadata.get(URL_METADATA_KEY); + if (urlStr != null) { + return new URL(urlStr); + } + } + throw new IOException("Invalid link file: " + contentType); + } + + /** + * Get the base icon for this link-file which does not include the + * link overlay icon. + */ + @Override + abstract public Icon getIcon(); + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java new file mode 100644 index 0000000000..848f89c494 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java @@ -0,0 +1,426 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.data; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Map; + +import javax.help.UnsupportedOperationException; +import javax.swing.Icon; + +import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.GhidraURL; +import ghidra.framework.store.*; +import ghidra.util.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.VersionException; +import ghidra.util.task.TaskMonitor; + +/** + * {@code LinkedGhidraFile} corresponds to a {@link DomainFile} contained within a + * {@link LinkedGhidraFolder}. + */ +class LinkedGhidraFile implements LinkedDomainFile { + + private final LinkedGhidraSubFolder parent; + private final String fileName; + + LinkedGhidraFile(LinkedGhidraSubFolder parent, String fileName) { + this.parent = parent; + this.fileName = fileName; + } + + @Override + public DomainFile getLinkedFile() throws IOException { + return parent.getLinkedFile(fileName); + } + + private DomainFile getLinkedFileNoError() { + return parent.getLinkedFileNoError(fileName); + } + + @Override + public DomainFolder getParent() { + return parent; + } + + @Override + public String getName() { + return fileName; + } + + @Override + public int compareTo(DomainFile df) { + return fileName.compareToIgnoreCase(df.getName()); + } + + @Override + public boolean exists() { + return getLinkedFileNoError() != null; + } + + @Override + public String getFileID() { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getFileID() : null; + } + + @Override + public DomainFile setName(String newName) throws InvalidNameException, IOException { + throw new ReadOnlyException("linked file is read only"); + } + + @Override + public String getPathname() { + // pathname within project containing folder-link + // getParent() may return a non-linked folder + String path = getParent().getPathname(); + if (path.length() != FileSystem.SEPARATOR.length()) { + path += FileSystem.SEPARATOR; + } + path += fileName; + return path; + } + + @Override + public URL getSharedProjectURL() { + URL folderURL = parent.getSharedProjectURL(); + if (GhidraURL.isServerRepositoryURL(folderURL)) { + // Direct URL construction done so that ghidra protocol + // extension may be supported + try { + return new URL(folderURL.toExternalForm() + fileName); + } + catch (MalformedURLException e) { + // ignore + } + } + return null; + } + + @Override + public ProjectLocator getProjectLocator() { + return parent.getProjectLocator(); + } + + @Override + public String getContentType() { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getContentType() : ContentHandler.UNKNOWN_CONTENT; + } + + @Override + public Class getDomainObjectClass() { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getDomainObjectClass() : DomainObject.class; + } + + @Override + public ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException { + return null; + } + + @Override + public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover, + TaskMonitor monitor) throws VersionException, IOException, CancelledException { + return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor); + } + + @Override + public DomainObject getOpenedDomainObject(Object consumer) { + return null; + } + + @Override + public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor) + throws VersionException, IOException, CancelledException { + return getLinkedFile().getReadOnlyDomainObject(consumer, version, monitor); + } + + @Override + public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor) + throws VersionException, IOException, CancelledException { + return getLinkedFile().getImmutableDomainObject(consumer, version, monitor); + } + + @Override + public void save(TaskMonitor monitor) throws IOException, CancelledException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean canSave() { + return false; + } + + @Override + public boolean canRecover() { + return false; + } + + @Override + public boolean takeRecoverySnapshot() throws IOException { + return true; + } + + @Override + public boolean isInWritableProject() { + return false; // While project may be writeable this folder/file is not + } + + @Override + public long getLastModifiedTime() { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getLastModifiedTime() : 0; + } + + @Override + public Icon getIcon(boolean disabled) { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getIcon(disabled) : UNSUPPORTED_FILE_ICON; + } + + @Override + public boolean isCheckedOut() { + return false; + } + + @Override + public boolean isCheckedOutExclusive() { + return false; + } + + @Override + public boolean modifiedSinceCheckout() { + return false; + } + + @Override + public boolean canCheckout() { + return false; + } + + @Override + public boolean canCheckin() { + return false; + } + + @Override + public boolean canMerge() { + return false; + } + + @Override + public boolean canAddToRepository() { + return false; + } + + @Override + public void setReadOnly(boolean state) throws IOException { + // ignore + } + + @Override + public boolean isReadOnly() { + return true; // not reflected by icon + } + + @Override + public boolean isVersioned() { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.isVersioned() : false; + } + + @Override + public boolean isHijacked() { + return false; + } + + @Override + public int getLatestVersion() { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getLatestVersion() : DomainFile.DEFAULT_VERSION; + } + + @Override + public boolean isLatestVersion() { + return true; + } + + @Override + public int getVersion() { + // TODO: Do we want to reveal linked-local-project checkout details? + return getLatestVersion(); + } + + @Override + public Version[] getVersionHistory() throws IOException { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getVersionHistory() : new Version[0]; + } + + @Override + public void addToVersionControl(String comment, boolean keepCheckedOut, TaskMonitor monitor) + throws IOException, CancelledException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean checkout(boolean exclusive, TaskMonitor monitor) + throws IOException, CancelledException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkin(CheckinHandler checkinHandler, boolean okToUpgrade, TaskMonitor monitor) + throws IOException, VersionException, CancelledException { + throw new UnsupportedOperationException(); + } + + @Override + public void merge(boolean okToUpgrade, TaskMonitor monitor) + throws IOException, VersionException, CancelledException { + throw new UnsupportedOperationException(); + } + + @Override + public void undoCheckout(boolean keep) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void undoCheckout(boolean keep, boolean force) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void terminateCheckout(long checkoutId) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public ItemCheckoutStatus[] getCheckouts() throws IOException { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getCheckouts() : new ItemCheckoutStatus[0]; + } + + @Override + public ItemCheckoutStatus getCheckoutStatus() throws IOException { + // TODO: Do we want to reveal linked-local-project checkout details? + return null; + } + + @Override + public void delete() throws IOException { + throw new ReadOnlyException("linked file is read only"); + } + + @Override + public void delete(int version) throws IOException { + throw new ReadOnlyException("linked file is read only"); + } + + @Override + public DomainFile moveTo(DomainFolder newParent) throws IOException { + throw new ReadOnlyException("linked file is read only"); + } + + @Override + public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) + throws IOException, CancelledException { + return getLinkedFile().copyTo(newParent, monitor); + } + + @Override + public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor) + throws IOException, CancelledException { + return getLinkedFile().copyVersionTo(version, destFolder, monitor); + } + + @Override + public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { + return getLinkedFile().copyToAsLink(newParent); + } + + @Override + public boolean isLinkingSupported() { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.isLinkingSupported() : false; + } + + @Override + public List getConsumers() { + return List.of(); + } + + @Override + public boolean isChanged() { + return false; + } + + @Override + public boolean isOpen() { + return false; // domain file proxy always used + } + + @Override + public boolean isBusy() { + return false; // domain file proxy always used + } + + @Override + public void packFile(File file, TaskMonitor monitor) throws IOException, CancelledException { + getLinkedFile().packFile(file, monitor); + } + + @Override + public Map getMetadata() { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getMetadata() : Map.of(); + } + + @Override + public long length() throws IOException { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.length() : 0; + } + + @Override + public boolean isLinkFile() { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.isLinkFile() : false; + } + + @Override + public DomainFolder followLink() { + try { + return FolderLinkContentHandler.getReadOnlyLinkedFolder(this); + } + catch (IOException e) { + Msg.error(this, "Failed to following folder-link: " + getPathname()); + } + return null; + } + + @Override + public String toString() { + return "LinkedGhidraFile: " + getPathname(); + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java new file mode 100644 index 0000000000..6193d4864b --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java @@ -0,0 +1,138 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.data; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; + +import javax.swing.Icon; + +import generic.theme.GIcon; +import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.GhidraURL; +import ghidra.framework.store.FileSystem; + +/** + * {@code LinkedGhidraFolder} provides the base {@link LinkedDomainFolder} implementation which + * corresponds to a project folder-link (see {@link FolderLinkContentHandler}). + */ +public class LinkedGhidraFolder extends LinkedGhidraSubFolder { + + public static Icon FOLDER_LINK_CLOSED_ICON = + new GIcon("icon.content.handler.linked.folder.closed"); + public static Icon FOLDER_LINK_OPEN_ICON = + new GIcon("icon.content.handler.linked.folder.open"); + + private final Project activeProject; + private final DomainFolder localParent; + private final URL folderUrl; + + private String linkedPathname; + + private URL projectUrl; + + /** + * Construct a linked-folder. + * @param activeProject active project responsible for linked project life-cycle management. + * @param localParent local domain folder which contains folder-link or corresponds directly to + * folder-link (name=null). + * @param linkFilename folder-link filename + * @param folderUrl linked folder URL + */ + LinkedGhidraFolder(Project activeProject, DomainFolder localParent, String linkFilename, + URL folderUrl) { + super(linkFilename); + + if (!GhidraURL.isServerRepositoryURL(folderUrl) && + !GhidraURL.isLocalProjectURL(folderUrl)) { + throw new IllegalArgumentException("Invalid Ghidra URL: " + folderUrl); + } + + this.activeProject = activeProject; + this.localParent = localParent; + this.folderUrl = folderUrl; + + linkedPathname = GhidraURL.getProjectPathname(folderUrl); + if (linkedPathname.length() > 0 && linkedPathname.endsWith(FileSystem.SEPARATOR)) { + linkedPathname = linkedPathname.substring(0, linkedPathname.length() - 1); + } + } + + /** + * Get the Ghidra URL associated with this linked folder's project or repository + * @return Ghidra URL associated with this linked folder's project or repository + */ + public URL getProjectURL() { + if (projectUrl == null) { + projectUrl = GhidraURL.getProjectURL(folderUrl); + } + return projectUrl; + } + + LinkedGhidraFolder getLinkedRootFolder() { + return this; + } + + DomainFolder getLinkedFolder(String linkedPath) throws IOException { + + ProjectData projectData = activeProject.addProjectView(getProjectURL(), false); + if (projectData == null) { + throw new FileNotFoundException(); + } + + DomainFolder folder = projectData.getFolder(linkedPath); + if (folder == null) { + throw new FileNotFoundException(folderUrl.toExternalForm()); + } + return folder; + } + + @Override + public String getLinkedPathname() { + return linkedPathname; + } + + @Override + public ProjectLocator getProjectLocator() { + return activeProject.getProjectLocator(); + } + + @Override + public ProjectData getProjectData() { + return activeProject.getProjectData(); + } + + @Override + public DomainFolder getParent() { + return localParent; + } + + @Override + public String toString() { + return "LinkedGhidraFolder: " + getPathname(); + } + + @Override + public Icon getIcon(boolean isOpen) { + return isOpen ? FOLDER_LINK_OPEN_ICON : FOLDER_LINK_CLOSED_ICON; + } + + @Override + public boolean isLinked() { + return true; + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java new file mode 100644 index 0000000000..e2577b2da8 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java @@ -0,0 +1,294 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.data; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; + +import javax.swing.Icon; + +import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.GhidraURL; +import ghidra.framework.store.FileSystem; +import ghidra.util.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +class LinkedGhidraSubFolder implements LinkedDomainFolder { + + private final LinkedGhidraFolder linkedRootFolder; + private final LinkedGhidraSubFolder parent; + private final String folderName; + + LinkedGhidraSubFolder(String folderName) { + this.linkedRootFolder = getLinkedRootFolder(); + this.parent = null; // must override getParent() + this.folderName = folderName; + } + + LinkedGhidraSubFolder(LinkedGhidraSubFolder parent, String folderName) { + this.linkedRootFolder = parent.getLinkedRootFolder(); + this.parent = parent; + this.folderName = folderName; + } + + /** + * Get the linked root folder which corresponds to a folder-link + * (see {@link FolderLinkContentHandler}). + * @return linked root folder + */ + LinkedGhidraFolder getLinkedRootFolder() { + return linkedRootFolder; + } + + @Override + public boolean isInWritableProject() { + return false; // While project may be writeable this folder is not + } + + @Override + public DomainFolder getParent() { + return parent; + } + + @Override + public String getName() { + return folderName; + } + + @Override + public DomainFolder getLinkedFolder() throws IOException { + return linkedRootFolder.getLinkedFolder(getLinkedPathname()); + } + + @Override + public int compareTo(DomainFolder df) { + return getName().compareToIgnoreCase(df.getName()); + } + + @Override + public DomainFolder setName(String newName) throws InvalidNameException, IOException { + throw new ReadOnlyException("linked folder is read only"); + } + + @Override + public URL getSharedProjectURL() { + URL projectURL = getLinkedRootFolder().getProjectURL(); + if (GhidraURL.isServerRepositoryURL(projectURL)) { + String urlStr = projectURL.toExternalForm(); + if (urlStr.endsWith(FileSystem.SEPARATOR)) { + urlStr = urlStr.substring(0, urlStr.length() - 1); + } + String path = getLinkedPathname(); + if (!path.endsWith(FileSystem.SEPARATOR)) { + path += FileSystem.SEPARATOR; + } + try { + return new URL(urlStr + path); + } + catch (MalformedURLException e) { + // ignore + } + } + return null; + } + + @Override + public ProjectLocator getProjectLocator() { + return parent.getProjectLocator(); + } + + @Override + public ProjectData getProjectData() { + return parent.getProjectData(); + } + + @Override + public String getPathname() { + // pathname within project containing folder-link + // getParent() may return a non-linked folder + String path = getParent().getPathname(); + if (path.length() != FileSystem.SEPARATOR.length()) { + path += FileSystem.SEPARATOR; + } + path += folderName; + return path; + } + + /** + * Get the pathname of this folder within the linked-project/repository + * @return absolute linked folder path within the linked-project/repository + */ + public String getLinkedPathname() { + String path = parent.getLinkedPathname(); + if (!path.endsWith(FileSystem.SEPARATOR)) { + path += FileSystem.SEPARATOR; + } + path += folderName; + return path; + } + + @Override + public LinkedGhidraSubFolder[] getFolders() { + try { + DomainFolder linkedFolder = getLinkedFolder(); + DomainFolder[] folders = linkedFolder.getFolders(); + LinkedGhidraSubFolder[] linkedSubFolders = new LinkedGhidraSubFolder[folders.length]; + for (int i = 0; i < folders.length; i++) { + linkedSubFolders[i] = new LinkedGhidraSubFolder(this, folders[i].getName()); + } + return linkedSubFolders; + } + catch (IOException e) { + Msg.error(this, "Linked folder failure: " + e.getMessage()); + return new LinkedGhidraSubFolder[0]; + } + } + + @Override + public LinkedGhidraSubFolder getFolder(String name) { + try { + DomainFolder linkedFolder = getLinkedFolder(); + DomainFolder f = linkedFolder.getFolder(name); + if (f != null) { + return new LinkedGhidraSubFolder(this, name); + } + } + catch (IOException e) { + Msg.error(this, "Linked folder failure: " + e.getMessage()); + } + return null; + } + + @Override + public DomainFile[] getFiles() { + try { + DomainFolder linkedFolder = getLinkedFolder(); + DomainFile[] files = linkedFolder.getFiles(); + LinkedGhidraFile[] linkedSubFolders = new LinkedGhidraFile[files.length]; + for (int i = 0; i < files.length; i++) { + linkedSubFolders[i] = new LinkedGhidraFile(this, files[i].getName()); + } + return linkedSubFolders; + } + catch (IOException e) { + Msg.error(this, "Linked folder failure: " + e.getMessage()); + return new LinkedGhidraFile[0]; + } + } + + /** + * Get the true file within this linked folder. + * @param name file name + * @return file or null if not found or error occurs + */ + public DomainFile getLinkedFileNoError(String name) { + try { + DomainFolder linkedFolder = getLinkedFolder(); + return linkedFolder.getFile(name); + } + catch (IOException e) { + Msg.error(this, "Linked folder failure: " + e.getMessage()); + } + return null; + } + + DomainFile getLinkedFile(String name) throws IOException { + DomainFolder linkedFolder = getLinkedFolder(); + DomainFile df = linkedFolder.getFile(name); + if (df == null) { + throw new FileNotFoundException("linked-file '" + name + "' not found"); + } + return df; + } + + @Override + public DomainFile getFile(String name) { + DomainFile f = getLinkedFileNoError(name); + return f != null ? new LinkedGhidraFile(this, name) : null; + } + + @Override + public boolean isEmpty() { + try { + DomainFolder linkedFolder = getLinkedFolder(); + return linkedFolder.isEmpty(); + } + catch (IOException e) { + Msg.error(this, "Linked folder failure: " + e.getMessage()); + // TODO: what should we return if folder not found or error occurs? + // True is returned to allow this method to be used to avoid continued access. + return true; + } + } + + @Override + public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor) + throws InvalidNameException, IOException, CancelledException { + throw new ReadOnlyException("linked folder is read only"); + } + + @Override + public DomainFile createFile(String name, File packFile, TaskMonitor monitor) + throws InvalidNameException, IOException, CancelledException { + throw new ReadOnlyException("linked folder is read only"); + } + + @Override + public DomainFolder createFolder(String name) throws InvalidNameException, IOException { + throw new ReadOnlyException("linked folder is read only"); + } + + @Override + public void delete() throws IOException { + throw new ReadOnlyException("linked folder is read only"); + } + + @Override + public DomainFolder moveTo(DomainFolder newParent) throws IOException { + throw new ReadOnlyException("linked folder is read only"); + } + + @Override + public DomainFolder copyTo(DomainFolder newParent, TaskMonitor monitor) + throws IOException, CancelledException { + DomainFolder linkedFolder = getLinkedFolder(); + return linkedFolder.copyTo(newParent, monitor); + } + + @Override + public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { + DomainFolder linkedFolder = getLinkedFolder(); + return linkedFolder.copyToAsLink(newParent); + } + + @Override + public void setActive() { + // do nothing + } + + @Override + public String toString() { + return "LinkedGhidraSubFolder: " + getPathname(); + } + + @Override + public Icon getIcon(boolean isOpen) { + return isOpen ? OPEN_FOLDER_ICON : CLOSED_FOLDER_ICON; + } + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectFileManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectFileManager.java index 9f1044f07a..11c0d51ab2 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectFileManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectFileManager.java @@ -16,13 +16,12 @@ package ghidra.framework.data; import java.io.*; -import java.net.URL; import java.util.*; +import docking.widgets.OptionDialog; import generic.timer.GhidraSwinglessTimer; import ghidra.framework.client.*; import ghidra.framework.model.*; -import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.remote.User; import ghidra.framework.store.*; import ghidra.framework.store.FileSystem; @@ -82,6 +81,7 @@ public class ProjectFileManager implements ProjectData { private TaskMonitorAdapter projectDisposalMonitor = new TaskMonitorAdapter(); + private ProjectLock projectLock; private String owner; /** @@ -91,34 +91,44 @@ public class ProjectFileManager implements ProjectData { * @param resetOwner true to reset the project owner * @throws IOException if an i/o error occurs * @throws NotOwnerException if inProject is true and user is not owner + * @throws LockException if {@code isInWritableProject} is true and unable to establish project + * write lock (i.e., project in-use) * @throws FileNotFoundException if project directory not found */ public ProjectFileManager(ProjectLocator localStorageLocator, boolean isInWritableProject, - boolean resetOwner) throws NotOwnerException, IOException { + boolean resetOwner) throws NotOwnerException, IOException, LockException { this.localStorageLocator = localStorageLocator; - init(false, isInWritableProject); - if (resetOwner) { - owner = SystemUtilities.getUserName(); - properties.putString(OWNER, owner); - properties.writeState(); - } - else if (isInWritableProject && !SystemUtilities.getUserName().equals(owner)) { - if (owner == null) { - throw new NotOwnerException("Older projects may only be opened as a View.\n" + - "You must first create a new project or open an existing current project, \n" + - "then use the \"Project->View\" menu action to open the older project as a view.\n" + - "You can then drag old files into your active project."); + boolean success = false; + try { + init(false, isInWritableProject); + if (resetOwner) { + owner = SystemUtilities.getUserName(); + properties.putString(OWNER, owner); + properties.writeState(); + } + else if (isInWritableProject && !SystemUtilities.getUserName().equals(owner)) { + if (owner == null) { + throw new NotOwnerException("Older projects may only be opened as a View.\n" + + "You must first create a new project or open an existing current project, \n" + + "then use the \"Project->View\" menu action to open the older project as a view.\n" + + "You can then drag old files into your active project."); + } + throw new NotOwnerException("Project is owned by " + owner); } - throw new NotOwnerException("Project is owned by " + owner); - } - synchronized (fileSystem) { - getVersionedFileSystem(isInWritableProject); - rootFolderData = new RootGhidraFolderData(this, listenerList); - versionedFSListener = new MyFileSystemListener(); - versionedFileSystem.addFileSystemListener(versionedFSListener); - scheduleUserDataReconcilation(); + synchronized (fileSystem) { + getVersionedFileSystem(isInWritableProject); + rootFolderData = new RootGhidraFolderData(this, listenerList); + initVersionedFSListener(); + scheduleUserDataReconcilation(); + } + success = true; + } + finally { + if (!success) { + dispose(); + } } } @@ -128,34 +138,75 @@ public class ProjectFileManager implements ProjectData { * @param repository a repository if this is a shared project or null if it is a private project * @param isInWritableProject true if project content is writable, false if project is read-only * @throws IOException if an i/o error occurs + * @throws LockException if {@code isInWritableProject} is true and unable to establish project + * lock (i.e., project in-use) */ public ProjectFileManager(ProjectLocator localStorageLocator, RepositoryAdapter repository, - boolean isInWritableProject) throws IOException { + boolean isInWritableProject) throws IOException, LockException { this.localStorageLocator = localStorageLocator; this.repository = repository; - init(true, isInWritableProject); - synchronized (fileSystem) { - createVersionedFileSystem(); - rootFolderData = new RootGhidraFolderData(this, listenerList); - versionedFSListener = new MyFileSystemListener(); - versionedFileSystem.addFileSystemListener(versionedFSListener); + boolean success = false; + try { + init(true, isInWritableProject); + synchronized (fileSystem) { + createVersionedFileSystem(); + rootFolderData = new RootGhidraFolderData(this, listenerList); + initVersionedFSListener(); + } + success = true; + } + finally { + if (!success) { + dispose(); + } } } - ProjectFileManager(LocalFileSystem fileSystem, FileSystem versionedFileSystem) { + /** + * Constructor for test use only. A non-existing {@link ProjectLocator} is used without + * project locking. + * @param fileSystem an existing non-versioned local file-system + * @param versionedFileSystem an existing versioned file-system + * @throws IOException if an IO error occurs + */ + ProjectFileManager(LocalFileSystem fileSystem, FileSystem versionedFileSystem) + throws IOException { this.localStorageLocator = new ProjectLocator(null, "Test"); owner = SystemUtilities.getUserName(); - synchronized (fileSystem) { - this.fileSystem = fileSystem; - this.versionedFileSystem = versionedFileSystem; - rootFolderData = new RootGhidraFolderData(this, listenerList); - versionedFSListener = new MyFileSystemListener(); - versionedFileSystem.addFileSystemListener(versionedFSListener); - scheduleUserDataReconcilation(); + boolean success = false; + try { + synchronized (fileSystem) { + this.fileSystem = fileSystem; + this.versionedFileSystem = versionedFileSystem; + rootFolderData = new RootGhidraFolderData(this, listenerList); + initVersionedFSListener(); + scheduleUserDataReconcilation(); + success = true; + } + } + finally { + if (!success) { + dispose(); + } } } - private void init(boolean create, boolean isInWritableProject) throws IOException { + private void initVersionedFSListener() throws IOException { + // Listener not installed for local read-only versioned file-system + if (versionedFileSystem.isShared() || !versionedFileSystem.isReadOnly()) { + if (versionedFSListener == null) { + versionedFSListener = new MyFileSystemListener(); + } + versionedFileSystem.addFileSystemListener(versionedFSListener); + } + else { + versionedFSListener = null; + } + } + + private void init(boolean create, boolean isInWritableProject) + throws IOException, LockException { + projectDir = localStorageLocator.getProjectDir(); properties = new PropertyFile(projectDir, PROPERTY_FILENAME, "/", PROPERTY_FILENAME); if (create) { @@ -163,11 +214,13 @@ public class ProjectFileManager implements ProjectData { throw new DuplicateFileException( "Project directory already exists: " + projectDir.getCanonicalPath()); } + File markerFile = localStorageLocator.getMarkerFile(); + if (markerFile.exists()) { + throw new DuplicateFileException( + "Project marker file already exists: " + markerFile.getCanonicalPath()); + } projectDir.mkdir(); localStorageLocator.getMarkerFile().createNewFile(); - owner = SystemUtilities.getUserName(); - properties.putString(OWNER, owner); - properties.writeState(); } else { if (!projectDir.isDirectory()) { @@ -178,22 +231,92 @@ public class ProjectFileManager implements ProjectData { throw new ReadOnlyException( "Project " + localStorageLocator.getName() + " is read-only"); } - properties.readState(); owner = properties.getString(OWNER, SystemUtilities.getUserName()); } - else if (isInWritableProject) { - owner = SystemUtilities.getUserName(); - properties.putString(OWNER, owner); - properties.writeState(); - } else { owner = ""; // Unknown owner } } + + if (isInWritableProject) { + initLock(create); + } + getPrivateFileSystem(create, isInWritableProject); getUserFileSystem(isInWritableProject); } + private void initLock(boolean creatingProject) throws LockException, IOException { + this.projectLock = getProjectLock(localStorageLocator, !creatingProject); + if (projectLock == null) { + throw new LockException("Unable to lock project! " + localStorageLocator); + } + + if (!properties.exists()) { + owner = SystemUtilities.getUserName(); + properties.putString(OWNER, owner); + properties.writeState(); + } + } + + /** + * Creates a ProjectLock and attempts to lock it. This handles the case + * where the project was previously locked. + * + * @param locator the project locator + * @param allowInteractiveForce if true, when a lock cannot be obtained, the + * user will be prompted + * @return A locked ProjectLock or null if lock fails + */ + private ProjectLock getProjectLock(ProjectLocator locator, boolean allowInteractiveForce) { + ProjectLock lock = new ProjectLock(locator); + if (lock.lock()) { + return lock; + } + + // in headless mode, just spit out an error + if (!allowInteractiveForce || SystemUtilities.isInHeadlessMode()) { + return null; + } + + String projectStr = "Project: " + HTMLUtilities.escapeHTML(locator.getLocation()) + + System.getProperty("file.separator") + HTMLUtilities.escapeHTML(locator.getName()); + String lockInformation = lock.getExistingLockFileInformation(); + if (!lock.canForceLock()) { + Msg.showInfo(getClass(), null, "Project Locked", + "Project is locked. You have another instance of Ghidra
    " + + "already running with this project open (locally or remotely).

    " + + projectStr + "

    " + "Lock information: " + lockInformation); + return null; + } + + int userChoice = OptionDialog.showOptionDialog(null, "Project Locked - Delete Lock?", + "Project is locked. You may have another instance of Ghidra
    " + + "already running with this project opened (locally or remotely).
    " + projectStr + + "

    " + "If this is not the case, you can delete the lock file:
    " + + locator.getProjectLockFile().getAbsolutePath() + ".

    " + + "Lock information: " + lockInformation, + "Delete Lock", OptionDialog.QUESTION_MESSAGE); + if (userChoice == OptionDialog.OPTION_ONE) { // Delete Lock + if (lock.forceLock()) { + return lock; + } + + Msg.showError(this, null, "Error", "Attempt to force lock failed! " + locator); + } + return null; + } + + /** + * Determine if the specified project location currently has a write lock. + * @param locator project storage locator + * @return true if project data current has write-lock else false + */ + public static boolean isLocked(ProjectLocator locator) { + ProjectLock lock = new ProjectLock(locator); + return lock.isLocked(); + } + @Override public int getMaxNameLength() { return fileSystem.getMaxNameLength(); @@ -314,8 +437,9 @@ public class ProjectFileManager implements ProjectData { /** * Change the versioned filesystem associated with this project file manager. - * This method is provided for testing. Care should be taken when using a - * LocalFileSystem in a shared capacity since locking is not supported. + * This method is provided for testing (see {@code FakeSharedProject}). + * Care should be taken when using a LocalFileSystem in a shared capacity since + * locking is not supported. * @param fs versioned filesystem * @throws IOException if an IO error occurs */ @@ -323,9 +447,11 @@ public class ProjectFileManager implements ProjectData { if (!fs.isVersioned()) { throw new IllegalArgumentException("versioned filesystem required"); } - versionedFileSystem.removeFileSystemListener(versionedFSListener); + if (versionedFSListener != null) { + versionedFileSystem.removeFileSystemListener(versionedFSListener); + } versionedFileSystem = fs; - versionedFileSystem.addFileSystemListener(versionedFSListener); + initVersionedFSListener(); rootFolderData.setVersionedFileSystem(versionedFileSystem); } @@ -385,18 +511,36 @@ public class ProjectFileManager implements ProjectData { } @Override - public GhidraFolder getFolder(String path) { + public DomainFolder getFolder(String path) { int len = path.length(); if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { throw new IllegalArgumentException( "Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'"); } - try { - return getRootFolder().getFolderPathData(path).getDomainFolder(); + + DomainFolder folder = getRootFolder(); + String[] split = path.split(FileSystem.SEPARATOR); + if (split.length == 0) { + return folder; } - catch (FileNotFoundException e) { - return null; + + for (int i = 1; i < split.length; i++) { + DomainFolder subFolder = folder.getFolder(split[i]); + if (subFolder == null) { + // Check for folder link-file if folder not found + // NOTE: if real folder name matches link-file name it will block + // use of folder link-file. + DomainFile file = folder.getFile(split[i]); + if (file != null && file.isLinkFile()) { + subFolder = file.followLink(); + } + if (subFolder == null) { + return null; + } + } + folder = subFolder; } + return folder; } @Override @@ -468,19 +612,6 @@ public class ProjectFileManager implements ProjectData { return fileIndex.getFileByID(fileID); } - @Override - public URL getSharedFileURL(String path) { - if (repository != null) { - DomainFile df = getFile(path); - if (df != null && df.isVersioned()) { - ServerInfo server = repository.getServerInfo(); - return GhidraURL.makeURL(server.getServerName(), server.getPortNumber(), - repository.getName(), path); - } - } - return null; - } - public void releaseDomainFiles(Object consumer) { for (DomainObjectAdapter domainObj : openDomainObjects.values()) { try { @@ -707,7 +838,11 @@ public class ProjectFileManager implements ProjectData { monitor.checkCanceled(); LocalFolderItem item = fileSystem.getItem(folderPath, name); if (item.getCheckoutId() != FolderItem.DEFAULT_CHECKOUT_ID) { - checkoutList.add(new GhidraFile(getFolder(folderPath), name)); + GhidraFolderData folderData = + getRootFolderData().getFolderPathData(folderPath, false); + if (folderData != null) { + checkoutList.add(new GhidraFile(folderData.getDomainFolder(), name)); + } } } @@ -1037,16 +1172,24 @@ public class ProjectFileManager implements ProjectData { listenerList.clearAll(); } - synchronized (fileSystem) { - rootFolderData.dispose(); - fileSystem.dispose(); - versionedFileSystem.dispose(); - versionedFileSystem.removeFileSystemListener(versionedFSListener); - if (repository != null) { - repository.disconnect(); - repository = null; + if (fileSystem != null) { + synchronized (fileSystem) { + if (versionedFSListener != null) { + versionedFileSystem.removeFileSystemListener(versionedFSListener); + } + if (repository != null) { + repository.disconnect(); + repository = null; + } + rootFolderData.dispose(); + versionedFileSystem.dispose(); + fileSystem.dispose(); } } + + if (projectLock != null) { + projectLock.release(); + } } GhidraFolderData getRootFolderData() { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/ProjectLock.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectLock.java similarity index 97% rename from Ghidra/Framework/Project/src/main/java/ghidra/framework/project/ProjectLock.java rename to Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectLock.java index 1509309fb0..cb1552bc45 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/ProjectLock.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ProjectLock.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.framework.project; +package ghidra.framework.data; import java.io.File; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java new file mode 100644 index 0000000000..c04cd40978 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java @@ -0,0 +1,101 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.data; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import javax.help.UnsupportedOperationException; + +import db.DBHandle; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainObject; +import ghidra.framework.protocol.ghidra.GhidraURL; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * {@code DomainObjectAdapterLink} object provides a Ghidra URL (see {@link GhidraURL}) wrapper + * where the URL is intended to refer to a {@link DomainFile} within another local or remote + * project/repository. Link files which correspond to this type of {@link DomainObject} are + * not intended to be modified and should be created or deleted. A checkout may be used when + * an offline copy is required but otherwise serves no purpose since a modification and checkin + * is not supported. + */ +public class URLLinkObject extends DomainObjectAdapterDB { + + // Use a reduced DB buffer size to reduce file size for minimal content. + // This will allow a 4-KByte DB buffer file to hold a URL upto ~470 bytes long. + // Longer URLs will rely on 1-KByte chained buffers which will increase file length. + private static final int DB_BUFFER_SIZE = 1024; + + private URL url; + + /** + * Constructs a new link file object + * @param name link name + * @param ghidraUrl link URL + * @param consumer the object that is using this program. + * @throws IOException if there is an error accessing the database or invalid URL specified. + */ + public URLLinkObject(String name, URL ghidraUrl, Object consumer) throws IOException { + super(new DBHandle(DB_BUFFER_SIZE), name, 500, consumer); + metadata.put(LinkHandler.URL_METADATA_KEY, ghidraUrl.toString()); + updateMetadata(); + } + + /** + * Constructs a link file object from a DBHandle (read-only) + * @param dbh a handle to an open program database. + * @param consumer the object that keeping the program open. + * @throws IOException if an error accessing the database occurs. + */ + public URLLinkObject(DBHandle dbh, Object consumer) throws IOException { + super(dbh, "Untitled", 500, consumer); + loadMetadata(); + String urlText = metadata.get(LinkHandler.URL_METADATA_KEY); + if (urlText == null) { + throw new IOException("Null link object"); + } + url = new URL(urlText); + } + + @Override + public String getDescription() { + return "Link-File"; + } + + /** + * Get link URL + * @return link URL + */ + public URL getLink() { + return url; + } + + @Override + public final boolean isChangeable() { + return false; + } + + @Override + public final void saveToPackedFile(File outputFile, TaskMonitor monitor) + throws IOException, CancelledException { + throw new UnsupportedOperationException(); + } + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java index 1d4617c933..2175211e14 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java @@ -40,6 +40,8 @@ import generic.theme.GIcon; import ghidra.app.plugin.PluginCategoryNames; import ghidra.framework.GenericRunInfo; import ghidra.framework.client.*; +import ghidra.framework.data.FolderLinkContentHandler; +import ghidra.framework.data.LinkedGhidraFolder; import ghidra.framework.main.datatable.ProjectDataTablePanel; import ghidra.framework.main.datatree.*; import ghidra.framework.main.projectdata.actions.*; @@ -69,7 +71,7 @@ import ghidra.util.filechooser.GhidraFileFilter; ) //@formatter:on public class FrontEndPlugin extends Plugin - implements FrontEndService, RemoteAdapterListener, ProgramaticUseOnly { + implements FrontEndService, RemoteAdapterListener, ProjectViewListener, ProgramaticUseOnly { private final static String TITLE_PREFIX = "Ghidra: "; private final static String EXPORT_TOOL_ACTION_NAME = "Export Tool"; @@ -128,6 +130,7 @@ public class FrontEndPlugin extends Plugin private ClearCutAction clearCutAction; private ProjectDataCopyAction copyAction; private ProjectDataPasteAction pasteAction; + private ProjectDataPasteLinkAction pasteLinkAction; private ProjectDataRenameAction renameAction; private ProjectDataOpenDefaultToolAction openAction; private ProjectDataExpandAction expandAction; @@ -217,6 +220,7 @@ public class FrontEndPlugin extends Plugin clearCutAction = new ClearCutAction(owner); copyAction = new ProjectDataCopyAction(owner, groupName); pasteAction = new ProjectDataPasteAction(owner, groupName); + pasteLinkAction = new ProjectDataPasteLinkAction(owner, groupName); groupName = "Delete/Rename"; renameAction = new ProjectDataRenameAction(owner, groupName); @@ -239,6 +243,7 @@ public class FrontEndPlugin extends Plugin tool.addAction(clearCutAction); tool.addAction(copyAction); tool.addAction(pasteAction); + tool.addAction(pasteLinkAction); tool.addAction(deleteAction); tool.addAction(openAction); tool.addAction(renameAction); @@ -393,6 +398,10 @@ public class FrontEndPlugin extends Plugin toolChest.addToolChestChangeListener(toolBar); toolChest.addToolChestChangeListener(toolChestChangeListener); createToolSpecificOpenActions(); + + // Add project view listener + activeProject.addProjectViewListener(this); + // Add the repository listener RepositoryAdapter repository = activeProject.getRepository(); if (repository != null) { @@ -403,6 +412,16 @@ public class FrontEndPlugin extends Plugin // gui.validate(); } + @Override + public void viewedProjectAdded(URL projectView) { + SwingUtilities.invokeLater(() -> rebuildRecentMenus()); + } + + @Override + public void viewedProjectRemoved(URL projectView) { + SwingUtilities.invokeLater(() -> rebuildRecentMenus()); + } + /** * sets the name of the project, using the default name if no project is active */ @@ -1074,24 +1093,56 @@ public class FrontEndPlugin extends Plugin } public void openDomainFile(DomainFile domainFile) { - Project project = tool.getProject(); - final ToolServices toolServices = project.getToolServices(); - ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(domainFile); - if (defaultToolTemplate == null) { - // assume no tools in the tool chest - Msg.showInfo(this, tool.getToolFrame(), "Cannot Find Tool", - "Cannot find tool to open file: " + - HTMLUtilities.escapeHTML(domainFile.getName()) + - ".

    Make sure you have an appropriate tool installed
    from the " + - "Tools->Import Default Tools... menu. Alternatively, you can " + - "use Tool->Set Tool Associations menu to change how Ghidra " + - "opens this type of file"); + if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(domainFile.getContentType())) { + showLinkedFolder(domainFile); return; } - ToolButton button = toolBar.getToolButtonForToolConfig(defaultToolTemplate); - button.launchTool(domainFile); + Project project = tool.getProject(); + final ToolServices toolServices = project.getToolServices(); + ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(domainFile); + if (defaultToolTemplate != null) { + ToolButton button = toolBar.getToolButtonForToolConfig(defaultToolTemplate); + if (button != null) { + button.launchTool(domainFile); + return; + } + } + + Msg.showInfo(this, tool.getToolFrame(), "Cannot Find Tool", + "Cannot find tool to open file: " + + HTMLUtilities.escapeHTML(domainFile.getName()) + + ".

    Make sure you have an appropriate tool installed
    from the " + + "Tools->Import Default Tools... menu. Alternatively, you can " + + "use Tool->Set Tool Associations menu to change how Ghidra " + + "opens this type of file"); + } + + private void showLinkedFolder(DomainFile domainFile) { + + try { + LinkedGhidraFolder linkedFolder = + FolderLinkContentHandler.getReadOnlyLinkedFolder(domainFile); + if (linkedFolder == null) { + return; // unsupported use + } + + ProjectDataTreePanel dtp = projectDataPanel.openView(linkedFolder.getProjectURL()); + if (dtp == null) { + return; + } + + DomainFolder domainFolder = linkedFolder.getLinkedFolder(); + if (domainFolder != null) { + // delayed to ensure tree is displayd + Swing.runLater(() -> dtp.selectDomainFolder(domainFolder)); + } + } + catch (IOException e) { + Msg.showError(this, projectDataPanel, "Linked-folder failure: " + domainFile.getName(), + e); + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java index 4ee788cd3d..6ece295885 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectActionManager.java @@ -554,10 +554,15 @@ class ProjectActionManager { return; } - ProjectDataPanel pdp = plugin.getProjectDataPanel(); - pdp.openView(view); - // also update the recent views menu - plugin.rebuildRecentMenus(); + try { + activeProject.addProjectView(view, true); // listener will trigger data panel panel display + } + catch (IOException e) { + ProjectManager projectManager = tool.getProjectManager(); + projectManager.forgetViewedProject(view); + Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View", + "Failed to view project/repository: " + e.getMessage(), e); + } } private void editProjectAccess() { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java index 7606c410e1..3e9cb85d9e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java @@ -27,6 +27,7 @@ import javax.swing.*; import docking.ActionContext; import docking.ComponentProvider; import docking.widgets.tabbedpane.DockingTabRenderer; +import ghidra.framework.client.NotConnectedException; import ghidra.framework.main.datatable.ProjectDataTablePanel; import ghidra.framework.main.datatree.ProjectDataTreePanel; import ghidra.framework.model.*; @@ -40,7 +41,7 @@ import help.HelpService; * Manages the data tree for the active project, and the trees for the * project views. */ -class ProjectDataPanel extends JSplitPane { +class ProjectDataPanel extends JSplitPane implements ProjectViewListener { private final static String BORDER_PREFIX = "Active Project: "; private final static String READ_ONLY_BORDER = "READ-ONLY Project Data"; private final static int TYPICAL_NUM_VIEWS = 2; @@ -155,6 +156,21 @@ class ProjectDataPanel extends JSplitPane { setViewsVisible(views.length > 0); } + @Override + public void viewedProjectAdded(URL projectView) { + SwingUtilities.invokeLater(() -> openView(projectView)); + } + + @Override + public void viewedProjectRemoved(URL projectView) { + SwingUtilities.invokeLater(() -> { + ProjectDataTreePanel dtp = getViewPanel(projectView); + if (dtp != null) { + viewRemoved(dtp, projectView, false); + } + }); + } + private void clearReadOnlyViews() { readOnlyTab.removeAll(); readOnlyViews.clear(); @@ -167,7 +183,13 @@ class ProjectDataPanel extends JSplitPane { this.setDividerLocation(visible ? DIVIDER_LOCATION : 1.0); } - void openView(URL projectView) { + /** + * Open specified project URL in tabbed READ-Only project views + * @param projectView project URL to be opened/added to view + * @return corresponding tree panel or null on failure + */ + ProjectDataTreePanel openView(URL projectView) { + ProjectManager projectManager = tool.getProjectManager(); Project activeProject = tool.getProject(); @@ -176,19 +198,23 @@ class ProjectDataPanel extends JSplitPane { if (dtp != null) { readOnlyTab.setSelectedComponent(dtp); try { - activeProject.addProjectView(projectView); + activeProject.addProjectView(projectView, true); projectManager.rememberViewedProject(projectView); + return dtp; } catch (Exception e) { projectManager.forgetViewedProject(projectView); Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View", e.toString()); } - return; + return null; } try { // TODO: addProjectView should be done in a model task - ProjectData projectData = activeProject.addProjectView(projectView); + ProjectData projectData = activeProject.addProjectView(projectView, true); + if (projectData == null) { + return null; // repository connection may have been cancelled + } projectManager.rememberViewedProject(projectView); String viewName = projectData.getProjectLocator().getName(); final ProjectDataTreePanel newPanel = @@ -204,13 +230,20 @@ class ProjectDataPanel extends JSplitPane { readOnlyTab.setSelectedIndex(0); readOnlyViews.put(projectData.getProjectLocator(), newPanel); setViewsVisible(true); + return newPanel; + } + catch (NotConnectedException e) { + // already handled (e..g, cancelled login) - ignore } catch (Exception e) { projectManager.forgetViewedProject(projectView); Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View", - "Failed to view project/repository: " + e.getMessage()); + "Failed to view project/repository: " + e.getMessage(), e); } - validate(); + finally { + validate(); + } + return null; } ProjectLocator[] getProjectViews() { @@ -315,6 +348,7 @@ class ProjectDataPanel extends JSplitPane { treePanel.setProjectData(project.getName(), project.getProjectData()); tablePanel.setProjectData(project.getName(), project.getProjectData()); populateReadOnlyViews(project); + project.addProjectViewListener(this); } else { tablePanel.setProjectData("No Active Project", null); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java index 559ea7631f..28d95d2eed 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java @@ -182,9 +182,12 @@ public class ProjectDataTablePanel extends JPanel { this.projectData.removeDomainFolderChangeListener(changeListener); model.setProjectData(null); SystemUtilities.runSwingLater(() -> { - GGlassPane glassPane = (GGlassPane) gTable.getRootPane().getGlassPane(); - glassPane.removePainter(painter); - glassPane.addPainter(painter); + JRootPane rootPane = gTable.getRootPane(); + if (rootPane != null) { + GGlassPane glassPane = (GGlassPane) rootPane.getGlassPane(); + glassPane.removePainter(painter); + glassPane.addPainter(painter); + } }); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeAction.java index 241af898ef..a62998cea7 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeAction.java @@ -70,7 +70,7 @@ public abstract class ProjectTreeAction extends DockingAction { @Override public boolean isAddToPopup(ActionContext context) { - if (!isEnabledForContext(context)) { + if (!(context instanceof FrontEndProjectTreeContext)) { return false; } return isAddToPopup((FrontEndProjectTreeContext) context); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java index e8c60ab6b7..99da701708 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java @@ -176,40 +176,15 @@ class ChangeManager implements DomainFolderChangeListener { if (lazy && !folderNode.isLoaded()) { return null; // not visited } - // must look at all children since a folder and file may have the same name - boolean found = false; - for (GTreeNode node : folderNode.getChildren()) { - if (!(node instanceof DomainFolderNode)) { - continue; - } - if (name.equals(node.getName())) { - folderNode = (DomainFolderNode) node; - found = true; - break; - } - } - if (!found) { + folderNode = + (DomainFolderNode) folderNode.getChild(name, n -> (n instanceof DomainFolderNode)); + if (folderNode == null) { return null; } } return folderNode; } -// private DomainFileNode findDomainFileNode(DomainFolder parent, String name, boolean lazy) { -// DomainFolderNode folderNode = findDomainFolderNode(parent, lazy); -// if (folderNode == null) { -// return null; -// } -// if (lazy && !folderNode.isChildrenLoadedOrInProgress()) { -// return null; // not visited -// } -// GTreeNode child = folderNode.getChild(name); -// if (child instanceof DomainFileNode) { -// return (DomainFileNode) child; -// } -// return null; -// } - private DomainFileNode findDomainFileNode(DomainFile domainFile, boolean lazy) { DomainFolderNode folderNode = findDomainFolderNode(domainFile.getParent(), lazy); if (folderNode == null) { @@ -219,11 +194,8 @@ class ChangeManager implements DomainFolderChangeListener { return null; // not visited } - GTreeNode child = folderNode.getChild(domainFile.getName()); - if (child instanceof DomainFileNode) { - return (DomainFileNode) child; - } - return null; + return (DomainFileNode) folderNode.getChild(domainFile.getName(), + n -> (n instanceof DomainFileNode)); } private void updateFolderNode(DomainFolder parent) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java index 1f283a3a60..e68dfa1c0e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java @@ -58,7 +58,7 @@ public class CheckInTask extends VersionControlTask implements CheckinHandler { throw new CancelledException(); } if (actionID != VersionControlDialog.APPLY_TO_ALL) { - showDialog(false, df.getName()); // false==> checking in vs. + showDialog(false, df.getName(), df.isLinkFile()); // false==> checking in vs. // adding to version control if (actionID == VersionControlDialog.CANCEL) { monitor.cancel(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java index 400be85269..785a8c93d2 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java @@ -16,6 +16,7 @@ package ghidra.framework.main.datatree; import java.io.IOException; +import java.net.URL; import java.text.SimpleDateFormat; import java.util.Date; @@ -24,6 +25,8 @@ import javax.swing.SwingWorker; import docking.widgets.tree.GTreeNode; import generic.theme.GIcon; +import ghidra.framework.data.FolderLinkContentHandler; +import ghidra.framework.data.LinkHandler; import ghidra.framework.model.DomainFile; import ghidra.framework.store.ItemCheckoutStatus; import ghidra.util.*; @@ -174,7 +177,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable { if (domainFile.isHijacked()) { newDisplayName += " (hijacked)"; } - else if (domainFile.isVersioned()) { + else if (domainFile.isVersioned() && !domainFile.isLinkFile()) { int versionNumber = domainFile.getVersion(); String versionStr = "" + versionNumber; @@ -208,20 +211,34 @@ public class DomainFileNode extends GTreeNode implements Cuttable { } private void setToolTipText() { - String newToolTipText = toolTipText; + String newToolTipText = null; if (domainFile.isInWritableProject() && domainFile.isHijacked()) { newToolTipText = "Hijacked file should be deleted or renamed"; } else { - long lastModified = domainFile.getLastModifiedTime(); - newToolTipText = "Last Modified " + formatter.format(new Date(lastModified)); + StringBuilder buf = new StringBuilder(); + try { + if (domainFile.isLinkFile()) { + URL url = LinkHandler.getURL(domainFile); + buf.append("URL: "); + buf.append(StringUtilities.trimMiddle(url.toString(), 120)); + newToolTipText = buf.toString(); + } + } + catch (IOException e1) { + // ignore + } + if (newToolTipText == null) { + long lastModified = domainFile.getLastModifiedTime(); + newToolTipText = "Last Modified " + formatter.format(new Date(lastModified)); + } if (domainFile.isCheckedOut()) { try { ItemCheckoutStatus status = domainFile.getCheckoutStatus(); if (status != null) { - newToolTipText = HTMLUtilities.toHTML( - "Checked out " + formatter.format(new Date(status.getCheckoutTime())) + - ";\n" + newToolTipText); + newToolTipText = "Checked out " + + formatter.format(new Date(status.getCheckoutTime())) + + "\n" + newToolTipText; } } catch (IOException e) { @@ -232,6 +249,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable { if (domainFile.isReadOnly()) { newToolTipText += " (read only)"; } + newToolTipText = HTMLUtilities.toLiteralHTML(newToolTipText, 0); } toolTipText = newToolTipText; } @@ -243,12 +261,38 @@ public class DomainFileNode extends GTreeNode implements Cuttable { @Override public int compareTo(GTreeNode node) { + // Goal is to sort folder link-files similar to a folder if (node instanceof DomainFolderNode) { + if (isFolderLink()) { + int c = super.compareTo(node); + if (c != 0) { + // A link-file name is permitted to match another folder node but + // should not be considered equal + return c; + } + } return 1; } + if (node instanceof DomainFileNode) { + DomainFileNode otherFileNode = (DomainFileNode) node; + if (isFolderLink()) { + if (otherFileNode.isFolderLink()) { + return super.compareTo(node); + } + return -1; + } + else if (otherFileNode.isFolderLink()) { + return 1; + } + } return super.compareTo(node); } + boolean isFolderLink() { + return FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE + .equals(domainFile.getContentType()); + } + @Override public void valueChanged(Object newValue) { if (newValue.equals(getName())) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java index 1b027ed0b3..6c70770541 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java @@ -22,20 +22,18 @@ import javax.swing.Icon; import docking.widgets.tree.GTreeLazyNode; import docking.widgets.tree.GTreeNode; -import generic.theme.GIcon; import ghidra.framework.model.*; -import ghidra.util.InvalidNameException; -import ghidra.util.Msg; +import ghidra.util.*; import resources.ResourceManager; /** * Class to represent a node in the Data tree. */ public class DomainFolderNode extends GTreeLazyNode implements Cuttable { - private static final Icon ENABLED_OPEN_FOLDER = - new GIcon("icon.datatree.node.domain.folder.open"); - private static final Icon ENABLED_CLOSED_FOLDER = - new GIcon("icon.datatree.node.domain.folder.closed"); + + private static final Icon ENABLED_OPEN_FOLDER = DomainFolder.OPEN_FOLDER_ICON; + private static final Icon ENABLED_CLOSED_FOLDER = DomainFolder.CLOSED_FOLDER_ICON; + private static final Icon DISABLED_OPEN_FOLDER = ResourceManager.getDisabledIcon(ENABLED_OPEN_FOLDER); private static final Icon DISABLED_CLOSED_FOLDER = @@ -55,11 +53,18 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable { // TODO: how can the folder be null?...doesn't really make sense...I don't think it ever is if (domainFolder != null) { - toolTipText = domainFolder.getPathname(); + toolTipText = StringUtilities.trimMiddle(domainFolder.getPathname(), 120); + toolTipText = HTMLUtilities.toLiteralHTML(toolTipText, 0); isEditable = domainFolder.isInWritableProject(); } } + @Override + public boolean isAutoExpandPermitted() { + // Prevent auto-expansion through linked-folders + return !domainFolder.isLinked(); + } + /** * Get the domain folder; returns null if this node represents a domain file. * @@ -96,6 +101,10 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable { @Override public Icon getIcon(boolean expanded) { + if (domainFolder instanceof LinkedDomainFolder) { + // NOTE: cut operation not supported + return ((LinkedDomainFolder) domainFolder).getIcon(expanded); + } if (expanded) { return isCut ? DISABLED_OPEN_FOLDER : ENABLED_OPEN_FOLDER; } @@ -119,8 +128,12 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable { @Override protected List generateChildren() { + List children = new ArrayList<>(); - if (domainFolder != null) { + if (domainFolder != null && !domainFolder.isEmpty()) { + + // NOTE: isEmpty() is used to avoid multiple failed connection attempts on this folder + DomainFolder[] folders = domainFolder.getFolders(); for (DomainFolder folder : folders) { children.add(new DomainFolderNode(folder, filter)); @@ -128,6 +141,13 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable { DomainFile[] files = domainFolder.getFiles(); for (DomainFile domainFile : files) { + if (domainFile.isLinkFile() && filter != null && filter.followLinkedFolders()) { + DomainFolder folder = domainFile.followLink(); + if (folder != null) { + children.add(new DomainFolderNode(folder, filter)); + continue; + } + } if (filter == null || filter.accept(domainFile)) { children.add(new DomainFileNode(domainFile)); } @@ -173,7 +193,8 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable { @Override public int compareTo(GTreeNode node) { if (node instanceof DomainFileNode) { - return -1; + // defer to DomainFileNode for comparison + return -((DomainFileNode) node).compareTo(this); } return super.compareTo(node); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java index 77aebb4327..5ef74d2d16 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java @@ -423,6 +423,9 @@ public class ProjectDataTreePanel extends JPanel { root = createRootNode(projectName); tree = new DataTree(tool, root); + if (!isActiveProject) { + tree.setName(tree.getName() + ": " + projectName); + } if (plugin != null) { tree.addGTreeSelectionListener(e -> { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlDialog.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlDialog.java index 4a10a953fb..f05062b9b6 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlDialog.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlDialog.java @@ -117,12 +117,6 @@ public class VersionControlDialog extends DialogComponentProvider { return keepFileCB.isSelected(); } - void setCreateKeepFile(boolean selected) { - if (!addToVersionControl) { - keepFileCB.setSelected(selected); - } - } - /** * Return the comments for the add to version control. * @return may be the empty string @@ -133,10 +127,14 @@ public class VersionControlDialog extends DialogComponentProvider { /* * Disable the check box for "keep checked out" because some files are still in use. + * @param enabled true if checkbox control should be enabled, false if disabled + * @param selected true if default state should be selected, else not-selected + * @param disabledMsg tooltip message if enabled is false, otherwise ignored. */ - public void setKeepCheckboxEnabled(boolean enabled) { + public void setKeepCheckboxEnabled(boolean enabled, boolean selected, String disabledMsg) { keepCB.setEnabled(enabled); - keepCB.setToolTipText(enabled ? "" : "Must keep Checked Out because the file is in use"); + keepCB.setSelected(selected); + keepCB.setToolTipText(enabled ? "" : disabledMsg); } private JPanel buildMainPanel() { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java index 1a424a16c3..9e21bbd4f3 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java @@ -58,13 +58,21 @@ public abstract class VersionControlTask extends Task { * @param addToVersionControl true if the dialog is for * adding files to version control, false for checking in files. * @param filename the name of the file currently to be added, whose comment we need. + * @param isLinkFile true if file is a link file, else false. Link-files may not be checked-out + * so keep-checked-out control disabled if this is true. */ - protected void showDialog(boolean addToVersionControl, String filename) { + protected void showDialog(boolean addToVersionControl, String filename, boolean isLinkFile) { Runnable r = () -> { VersionControlDialog vcDialog = new VersionControlDialog(addToVersionControl); vcDialog.setCurrentFileName(filename); vcDialog.setMultiFiles(list.size() > 1); - vcDialog.setKeepCheckboxEnabled(!filesInUse); + if (isLinkFile) { + vcDialog.setKeepCheckboxEnabled(false, false, "Link files may not be Checked Out"); + } + else if (filesInUse) { + vcDialog.setKeepCheckboxEnabled(false, true, + "Must keep Checked Out because the file is in use"); + } actionID = vcDialog.showDialog(tool, parent); keepCheckedOut = vcDialog.keepCheckedOut(); createKeep = vcDialog.shouldCreateKeepFile(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java index 486cfa50d0..665558b33a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java @@ -50,10 +50,6 @@ public class ProjectDataCopyAction extends ProjectDataCopyCutBaseAction { return false; } - if (!context.isInActiveProject()) { - return false; - } - return !context.containsRootFolder(); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java index 0b79f2e174..12cd31839a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java @@ -52,6 +52,11 @@ public class ProjectDataNewFolderAction return (context.getFolderCount() + context.getFileCount()) == 1; } + @Override + protected boolean isEnabledForContext(T context) { + return getFolder(context).isInWritableProject(); + } + private void createNewFolder(T context) { DomainFolder parentFolder = getFolder(context); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteLinkAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteLinkAction.java new file mode 100644 index 0000000000..abaee7b9ff --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteLinkAction.java @@ -0,0 +1,147 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.main.projectdata.actions; + +import java.io.IOException; +import java.util.List; + +import javax.swing.Icon; + +import docking.action.MenuData; +import docking.widgets.tree.GTreeNode; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.main.datatable.ProjectTreeAction; +import ghidra.framework.main.datatree.*; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.util.Msg; +import resources.MultiIcon; +import resources.ResourceManager; + +public class ProjectDataPasteLinkAction extends ProjectTreeAction { + private static Icon baseIcon = ResourceManager.loadImage("images/page_paste.png"); + + public ProjectDataPasteLinkAction(String owner, String group) { + super("Paste Link", owner); + setPopupMenuData(new MenuData(new String[] { "Paste as Link" }, getIcon(), group)); + } + + private static Icon getIcon() { + MultiIcon multiIcon = new MultiIcon(baseIcon); + multiIcon.addIcon(LinkHandler.LINK_ICON); + return multiIcon; + } + + @Override + protected void actionPerformed(FrontEndProjectTreeContext context) { + GTreeNode node = (GTreeNode) context.getContextObject(); + DomainFolderNode destNode = getFolderForNode(node); + if (!isEnabledForContext(context)) { + Msg.showWarn(getClass(), context.getTree(), "Unsupported Operation", + "Unsupported paste link condition"); + } + + GTreeNode copyNode = getFolderOrFileCopyNode(); + if (copyNode instanceof DomainFileNode) { + try { + DomainFile domainFile = ((DomainFileNode) copyNode).getDomainFile(); + domainFile.copyToAsLink(destNode.getDomainFolder()); + } + catch (IOException e) { + Msg.showError(getClass(), context.getTree(), "Cannot Create Link", + "Error occured while creating link file", e); + } + } + else { + try { + DomainFolder domainFolder = ((DomainFolderNode) copyNode).getDomainFolder(); + domainFolder.copyToAsLink(destNode.getDomainFolder()); + } + catch (IOException e) { + Msg.showError(getClass(), context.getTree(), "Cannot Create Link", + "Error occured while creating link file", e); + } + } + + } + + @Override + protected boolean isEnabledForContext(FrontEndProjectTreeContext context) { + if (!context.hasExactlyOneFileOrFolder()) { + return false; + } + if (!context.isInActiveProject()) { + return false; + } + GTreeNode node = (GTreeNode) context.getContextObject(); + DomainFolderNode destNode = getFolderForNode(node); + + GTreeNode copyNode = getFolderOrFileCopyNode(); + if (copyNode == null || copyNode.getParent() == null) { + return false; + } + + // local internal linking not supported + if (destNode.getRoot() == copyNode.getRoot()) { + return false; + } + + if (copyNode instanceof DomainFileNode) { + DomainFile df = ((DomainFileNode) copyNode).getDomainFile(); + return df.isLinkingSupported(); + } + return true; + } + + @Override + protected boolean isAddToPopup(FrontEndProjectTreeContext context) { + if (!context.hasOneOrMoreFilesAndFolders()) { + return false; + } + if (!context.isInActiveProject()) { + return false; + } + GTreeNode copyNode = getFolderOrFileCopyNode(); + return copyNode != null && copyNode.getParent() != null; + } + + private DomainFolderNode getFolderForNode(GTreeNode node) { + if (node instanceof DomainFolderNode) { + return (DomainFolderNode) node; + } + return (DomainFolderNode) node.getParent(); + } + + private GTreeNode getFolderOrFileCopyNode() { + List list = DataTreeClipboardUtils.getDataTreeNodesFromClipboard(); + if (list.size() != 1) { + return null; + } + GTreeNode copyNode = list.get(0); + if (copyNode instanceof DomainFileNode) { + if (!((DomainFileNode) copyNode).isCut()) { + return copyNode; + } + } + if (copyNode instanceof DomainFolderNode) { + if (!((DomainFolderNode) copyNode).isCut()) { + return copyNode; + } + } + return null; + } + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlAddAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlAddAction.java index 3352d4480d..d89d1637c6 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlAddAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlAddAction.java @@ -79,7 +79,7 @@ public class VersionControlAddAction extends VersionControlAction { } List unversioned = new ArrayList<>(); for (DomainFile domainFile : domainFiles) { - if (domainFile.isVersionControlSupported() && !domainFile.isVersioned()) { + if (domainFile.canAddToRepository()) { unversioned.add(domainFile); } } @@ -143,7 +143,7 @@ public class VersionControlAddAction extends VersionControlAction { monitor.setMessage("Adding " + name + " to Version Control"); if (actionID != VersionControlDialog.APPLY_TO_ALL) { - showDialog(true, name); + showDialog(true, name, df.isLinkFile()); } if (actionID == VersionControlDialog.CANCEL) { return; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlCheckInAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlCheckInAction.java index d4d64548c0..af05b4b60d 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlCheckInAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlCheckInAction.java @@ -69,7 +69,7 @@ public class VersionControlCheckInAction extends VersionControlAction { List domainFiles = context.getSelectedFiles(); for (DomainFile domainFile : domainFiles) { - if (domainFile.isCheckedOut() && domainFile.modifiedSinceCheckout()) { + if (domainFile.modifiedSinceCheckout()) { return true; // At least one checked out file selected. } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java index a012198a29..b5a01829ef 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java @@ -16,17 +16,19 @@ package ghidra.framework.model; import java.io.*; +import java.net.URL; import java.util.List; import java.util.Map; import javax.swing.Icon; import ghidra.framework.client.NotConnectedException; -import ghidra.framework.data.CheckinHandler; +import ghidra.framework.data.*; import ghidra.framework.store.*; import ghidra.util.InvalidNameException; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; +import resources.ResourceManager; /** * DomainFile provides a storage interface for project files. A @@ -36,6 +38,9 @@ import ghidra.util.task.TaskMonitor; */ public interface DomainFile extends Comparable { + public static final Icon UNSUPPORTED_FILE_ICON = + ResourceManager.loadImage("images/unknownFile.gif"); + /** * Use with getDomainObject to request the default version. The default version is * the private file or check-out file if one exists, or the latest version from the @@ -74,7 +79,7 @@ public interface DomainFile extends Comparable { * @throws DuplicateFileException if a file named newName * already exists in this files domain folder. * @throws FileInUseException if this file is in-use / checked-out. - * @throws IOException thrown if an IO or access error occurs. + * @throws IOException if an IO or access error occurs. */ public DomainFile setName(String newName) throws InvalidNameException, IOException; @@ -84,6 +89,14 @@ public interface DomainFile extends Comparable { */ public String getPathname(); + /** + * Get a remote Ghidra URL for this domain file if available within the associated shared + * project repository. A null value will be returned if shared file does not exist and + * may be returned if shared repository is not connected or a connection error occurs. + * @return remote Ghidra URL for this file or null + */ + public URL getSharedProjectURL(); + /** * Returns the local storage location for the project that this DomainFile belongs to. * @return the location @@ -92,13 +105,14 @@ public interface DomainFile extends Comparable { /** * Returns content-type string - * @return the content type + * @return the file content type or a reserved content type {@link ContentHandler#MISSING_CONTENT} + * or {@link ContentHandler#UNKNOWN_CONTENT}. */ public String getContentType(); /** * Returns the underlying Class for the domain object in this domain file. - * @return the class + * @return the class or null if does not correspond to a domain object. */ public Class getDomainObjectClass(); @@ -138,7 +152,7 @@ public interface DomainFile extends Comparable { * that the domain object cannot be upgraded to the current format. If okToUpgrade is false, * then the VersionException only means the object is not in the current format - it * may or may not be possible to upgrade. - * @throws IOException thrown if an IO or access error occurs. + * @throws IOException if an IO or access error occurs. * @throws CancelledException if monitor cancelled operation */ public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover, @@ -166,7 +180,7 @@ public interface DomainFile extends Comparable { * @throws VersionException if the domain object could not be read due * to a version format change. * @throws FileNotFoundException if the stored file/version was not found. - * @throws IOException thrown if an IO or access error occurs. + * @throws IOException if an IO or access error occurs. * @throws CancelledException if monitor cancelled operation */ public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor) @@ -184,7 +198,7 @@ public interface DomainFile extends Comparable { * @throws VersionException if the domain object could not be read due * to a version format change. * @throws FileNotFoundException if the stored file/version was not found. - * @throws IOException thrown if an IO or access error occurs. + * @throws IOException if an IO or access error occurs. * @throws CancelledException if monitor cancelled operation */ public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor) @@ -195,7 +209,7 @@ public interface DomainFile extends Comparable { * @param monitor monitor for the task that is doing the save on the file * @throws FileInUseException if the file is open for update by someone else, or * a transient-read is in progress. - * @throws IOException thrown if an IO error occurs. + * @throws IOException if an IO error occurs. * @throws CancelledException if monitor cancelled operation */ public void save(TaskMonitor monitor) throws IOException, CancelledException; @@ -291,7 +305,7 @@ public interface DomainFile extends Comparable { * for private files (i.e., not versioned). * @param state if true file will be read-only and may not be updated, if false the * file may be updated. - * @throws IOException thrown if an IO error occurs. + * @throws IOException if an IO error occurs. */ public void setReadOnly(boolean state) throws IOException; @@ -302,12 +316,6 @@ public interface DomainFile extends Comparable { */ public boolean isReadOnly(); - /** - * Returns true if the versioned filesystem can be used to store this files content type. - * @return true if supports version control - */ - public boolean isVersionControlSupported(); - /** * Return true if this is a versioned database, else false * @return true if versioned @@ -352,7 +360,7 @@ public interface DomainFile extends Comparable { * @param keepCheckedOut if true, the file will be initially checked-out * @param monitor progress monitor * @throws FileInUseException if this file is in-use. - * @throws IOException thrown if an IO or access error occurs. Also thrown if file is not + * @throws IOException if an IO or access error occurs. Also if file is not * private. * @throws CancelledException if the monitor cancelled the operation */ @@ -367,7 +375,7 @@ public interface DomainFile extends Comparable { * @return true if checkout successful, false if an exclusive checkout was not possible * due to other users having checkouts of this file. A request for a non-exclusive checkout * will never return false. - * @throws IOException thrown if an IO or access error occurs. + * @throws IOException if an IO or access error occurs. * @throws CancelledException if task monitor cancelled operation. */ public boolean checkout(boolean exclusive, TaskMonitor monitor) @@ -406,7 +414,7 @@ public interface DomainFile extends Comparable { * extension. * @throws NotConnectedException if shared project and not connected to repository * @throws FileInUseException if this file is in-use / checked-out. - * @throws IOException thrown if file is not checked-out or an IO / access error occurs. + * @throws IOException if file is not checked-out or an IO / access error occurs. */ public void undoCheckout(boolean keep) throws IOException; @@ -451,7 +459,7 @@ public interface DomainFile extends Comparable { * Delete the entire database for this file, including any version files. * @throws FileInUseException if this file is in-use / checked-out. * @throws UserAccessException if the user does not have permission to delete the file. - * @throws IOException thrown if an IO or access error occurs. + * @throws IOException if an IO or access error occurs. */ public void delete() throws IOException; @@ -476,7 +484,7 @@ public interface DomainFile extends Comparable { * @throws DuplicateFileException if a file with the same name * already exists in newParent folder. * @throws FileInUseException if this file is in-use / checked-out. - * @throws IOException thrown if an IO or access error occurs. + * @throws IOException if an IO or access error occurs. */ public DomainFile moveTo(DomainFolder newParent) throws IOException; @@ -486,7 +494,7 @@ public interface DomainFile extends Comparable { * @param monitor task monitor * @return newly created domain file * @throws FileInUseException if this file is in-use / checked-out. - * @throws IOException thrown if an IO or access error occurs. + * @throws IOException if an IO or access error occurs. * @throws CancelledException if task monitor cancelled operation. */ public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) @@ -498,12 +506,34 @@ public interface DomainFile extends Comparable { * @param destFolder destination parent folder * @param monitor task monitor * @return the copied file - * @throws IOException thrown if an IO or access error occurs. + * @throws IOException if an IO or access error occurs. * @throws CancelledException if task monitor cancelled operation. */ public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor) throws IOException, CancelledException; + /** + * Copy this file into the newParent folder as a link file. Restrictions: + *

      + *
    • Specified newParent must reside within a different project since internal linking is + * not currently supported.
    • + *
    • Content type must support linking (see {@link #isLinkingSupported()}).
    • + *
    + * If this file is associated with a temporary transient project (i.e., not a locally + * managed project) the generated link will refer to the remote file with a remote + * Ghidra URL, otherwise a local project storage path will be used. + * @param newParent new parent folder + * @return newly created domain file or null if content type does not support link use. + * @throws IOException if an IO or access error occurs. + */ + public DomainFile copyToAsLink(DomainFolder newParent) throws IOException; + + /** + * Determine if this file's content type supports linking. + * @return true if linking is supported, else false. + */ + public boolean isLinkingSupported(); + /** * Get the list of consumers (Objects) for this domain file. * @return empty array list if there are no consumers @@ -552,8 +582,30 @@ public interface DomainFile extends Comparable { * used for storing this file, but does not account for additional storage space * used to tracks changes, etc. * @return file length - * @throws IOException thrown if IO or access error occurs + * @throws IOException if IO or access error occurs */ public long length() throws IOException; + /** + * Determine if this file is a link file which corresponds to either a file or folder link. + * The {@link DomainObject} referenced by a link-file may be opened using + * {@link #getReadOnlyDomainObject(Object, int, TaskMonitor)}. The + * {@link #getDomainObject(Object, boolean, boolean, TaskMonitor)} method may also be used + * to obtain a read-only instance. {@link #getImmutableDomainObject(Object, int, TaskMonitor)} + * use is not supported. + * If the link-file content type equals {@value FolderLinkContentHandler#FOLDER_LINK_CONTENT_TYPE} + * the method {@link #followLink()} can be used to get the linked domain folder. + * The associated link URL may be obtained with {@link LinkHandler#getURL(DomainFile)}. + * The content type (see {@link #getContentType()} of a link file will differ from that of the + * linked object (e.g., "LinkedProgram" vs "Program"). + * @return true if link file else false for a normal domain file + */ + public boolean isLinkFile(); + + /** + * If this is a folder-link file get the corresponding linked folder. + * @return a linked domain folder or null if not a folder-link. + */ + public DomainFolder followLink(); + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java index f57d3b4569..9ceb871e8f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,4 +30,13 @@ public interface DomainFileFilter { * */ public boolean accept(DomainFile df); + + /** + * Determine if linked folders represented by a link-file should be followed. + * If this method is not implemented the default will return {@code true}. + * @return true if linked-folders should be followed or false to ignore. + */ + public default boolean followLinkedFolders() { + return true; + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java index 61f43881f8..18ced6cd51 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java @@ -17,7 +17,11 @@ package ghidra.framework.model; import java.io.File; import java.io.IOException; +import java.net.URL; +import javax.swing.Icon; + +import generic.theme.GIcon; import ghidra.framework.store.FolderNotEmptyException; import ghidra.util.InvalidNameException; import ghidra.util.exception.*; @@ -30,6 +34,13 @@ import ghidra.util.task.TaskMonitor; * referenced project folder. */ public interface DomainFolder extends Comparable { + + public static final Icon OPEN_FOLDER_ICON = + new GIcon("icon.datatree.node.domain.folder.open"); + + public static final Icon CLOSED_FOLDER_ICON = + new GIcon("icon.datatree.node.domain.folder.closed"); + /** * Character used to separate folder and item names within a path string. */ @@ -78,6 +89,14 @@ public interface DomainFolder extends Comparable { */ public String getPathname(); + /** + * Get a remote Ghidra URL for this domain folder within the associated shared + * project repository. URL path will end with "/". A null value will be returned if not + * associated with a shared project. + * @return remote Ghidra URL for this folder or null + */ + public URL getSharedProjectURL(); + /** * Returns true if this file is in a writable project. * @return true if writable @@ -92,7 +111,7 @@ public interface DomainFolder extends Comparable { /** * Get DomainFolders in this folder. - * This returns cached information and does not force a full refresh. + * This may return cached information and does not force a full refresh. * @return list of sub-folders */ public DomainFolder[] getFolders(); @@ -119,7 +138,7 @@ public interface DomainFolder extends Comparable { /** * Get all domain files in this folder. - * This returns cached information and does not force a full refresh. + * This may return cached information and does not force a full refresh. * @return list of domain files */ public DomainFile[] getFiles(); @@ -203,8 +222,31 @@ public interface DomainFolder extends Comparable { public DomainFolder copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, CancelledException; + /** + * Copy this folder into the newParent folder as a link file. Restrictions: + *
      + *
    • Specified newParent must reside within a different project since internal linking is + * not currently supported.
    • + *
    + * If this folder is associated with a temporary transient project (i.e., not a locally + * managed project) the generated link will refer to the remote file with a remote + * Ghidra URL, otherwise a local project storage path will be used. + * @param newParent new parent folder + * @return newly created domain file or null if link use not supported. + * @throws IOException if an IO or access error occurs. + */ + public DomainFile copyToAsLink(DomainFolder newParent) throws IOException; + /** * Allows the framework to react to a request to make this folder the "active" one. */ public void setActive(); + + /** + * Determine if this folder corresponds to a linked-folder. + * @return true if folder corresponds to a linked-folder, else false. + */ + public default boolean isLinked() { + return false; + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java new file mode 100644 index 0000000000..dab005fc34 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java @@ -0,0 +1,33 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.model; + +import java.io.IOException; + +/** + * {@code LinkedDomainFile} corresponds to a {@link DomainFile} contained within a + * {@link LinkedDomainFolder}. + */ +public interface LinkedDomainFile extends DomainFile { + + /** + * Get the real domain file which corresponds to this file contained within a linked-folder. + * @return domain file + * @throws IOException if IO error occurs or file not found + */ + public DomainFile getLinkedFile() throws IOException; + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java new file mode 100644 index 0000000000..5ea918735e --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java @@ -0,0 +1,44 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.model; + +import java.io.IOException; + +import javax.swing.Icon; + +import ghidra.framework.data.FolderLinkContentHandler; + +/** + * {@code LinkedDomainFolder} extends {@link DomainFolder} for all folders which are + * accessable via a folder-link (see {@link FolderLinkContentHandler}). + */ +public interface LinkedDomainFolder extends DomainFolder { + + /** + * Get the real domain folder which corresponds to this linked-folder. + * @return domain folder + * @throws IOException if an IO error occurs + */ + public DomainFolder getLinkedFolder() throws IOException; + + /** + * Get the appropriate icon for this folder + * @param isOpen true if open icon, false for closed + * @return folder icon + */ + public Icon getIcon(boolean isOpen); + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java index 5f7c8c207a..c5b4bbc62b 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java @@ -16,7 +16,6 @@ package ghidra.framework.model; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; import java.util.List; @@ -84,12 +83,14 @@ public interface Project { * Add the given project URL to this project's list project views. * The project view allows users to look at data files from another * project. - * @param projectURL identifier for the project view + * @param projectURL identifier for the project view (ghidra protocol only). + * @param visible true if project may be made visible or false if hidden. Hidden viewed + * projects are used when only life-cycle management is required (e.g., close view project + * when this project is closed). * @return project data for this view * @throws IOException if I/O error occurs or if project/repository not found - * @throws MalformedURLException if projectURL is invalid */ - public ProjectData addProjectView(URL projectURL) throws IOException, MalformedURLException; + public ProjectData addProjectView(URL projectURL, boolean visible) throws IOException; /** * Remove the project view from this project. @@ -98,7 +99,7 @@ public interface Project { public void removeProjectView(URL projectURL); /** - * Return the list of project views in this project. + * Return the list of visible project views in this project. */ public ProjectLocator[] getProjectViews(); @@ -174,9 +175,9 @@ public interface Project { public ProjectData getProjectData(URL url); /** - * Get the project data for other projects that are - * currently being viewed. - * @return zero length array if there are no viewed projects open + * Get the project data for visible viewed projects that are + * managed by this project. + * @return zero length array if there are no visible viewed projects open */ public ProjectData[] getViewedProjectData(); @@ -186,4 +187,16 @@ public interface Project { */ public void releaseFiles(Object consumer); + /** + * Add a listener to be notified when a visible project view is added or removed. + * @param listener project view listener + */ + public void addProjectViewListener(ProjectViewListener listener); + + /** + * Remove a project view listener previously added. + * @param listener project view listener + */ + public void removeProjectViewListener(ProjectViewListener listener); + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java index a6c0f41aed..fb635f8739 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java @@ -39,6 +39,7 @@ public interface ProjectData { /** * Returns the root folder of the project. + * @return root {@link DomainFolder} within project. */ public DomainFolder getRootFolder(); @@ -110,15 +111,6 @@ public interface ProjectData { */ public DomainFile getFileByID(String fileID); - /** - * Get a URL for a shared domain file which is available - * within a remote repository. - * @param path the absolute path of domain file relative to the root folder. - * @return URL object for accessing shared file from outside of a project, or - * null if file does not exist or is not shared. - */ - public URL getSharedFileURL(String path); - /** * Transform the specified name into an acceptable folder or file item name. Only an individual folder * or file name should be specified, since any separators will be stripped-out. @@ -130,6 +122,7 @@ public interface ProjectData { /** * Returns the projectLocator for the this ProjectData. + * @return project locator object */ public ProjectLocator getProjectLocator(); @@ -150,12 +143,14 @@ public interface ProjectData { * Sync the Domain folder/file structure with the underlying file structure. * @param force if true all folders will be be visited and refreshed, if false * only those folders previously visited will be refreshed. + * @throws IOException if an IO error occurs */ public void refresh(boolean force) throws IOException; /** * Returns User object associated with remote repository or null if a remote repository * is not used. + * @return current remote user identity or null */ public User getUser(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java index e3313d1702..5845a0900f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,39 +15,65 @@ */ package ghidra.framework.model; -import ghidra.framework.protocol.ghidra.GhidraURL; - import java.io.File; import java.net.URL; +import org.apache.commons.lang3.StringUtils; + +import ghidra.framework.protocol.ghidra.GhidraURL; + /** * Lightweight descriptor of a local Project storage location. */ public class ProjectLocator { - private static final String PROJECT_FILE_SUFFIX = ".gpr"; - private static final String PROJECT_DIR_SUFFIX = ".rep"; + public static final String PROJECT_FILE_SUFFIX = ".gpr"; + public static final String PROJECT_DIR_SUFFIX = ".rep"; + private static final String LOCK_FILE_SUFFIX = ".lock"; - private String name; - private String location; + private final String name; + private final String location; + private URL url; /** - * Construct a project URL. - * @param path path to parent directory + * Construct a project locator object. + * @param path path to parent directory (may or may not exist). The user's temp directory + * will be used if this value is null or blank. + * WARNING: Use of a relative paths should be avoided (e.g., on a windows platform + * an absolute path should start with a drive letter specification such as C:\path, + * while this same path on a Linux platform would be treated as relative). * @param name name of the project */ public ProjectLocator(String path, String name) { - this.name = name; if (name.endsWith(PROJECT_FILE_SUFFIX)) { - this.name = name.substring(0, name.length() - PROJECT_FILE_SUFFIX.length()); + name = name.substring(0, name.length() - PROJECT_FILE_SUFFIX.length()); } - this.location = path; - if (path == null) { - this.location = System.getProperty("java.io.tmpdir"); + this.name = name; + if (StringUtils.isBlank(path)) { + path = System.getProperty("java.io.tmpdir"); } - this.url = GhidraURL.makeURL(location, name); + this.location = checkAbsolutePath(path); + url = GhidraURL.makeURL(location, name); + } + + /** + * Ensure that absolute path is specified. + * @param path path to be checked and possibly modified. + * @return path to be used + */ + private static String checkAbsolutePath(String path) { + if (path.startsWith("/") && path.length() >= 4 && path.indexOf(":/") == 2 && + Character.isLetter(path.charAt(1))) { + // strip leading "/" on Windows paths (e.g., /C:/mydir) and transform separators to '\' + path = path.substring(1); + path = path.replace('/', '\\'); + } + if (path.endsWith("/") || path.endsWith("\\")) { + path = path.substring(0, path.length() - 1); + } + return path; } /** @@ -60,8 +85,8 @@ public class ProjectLocator { } /** - * Returns the URL associated with this local project. - * If this is a transient project, a remote repository URL will be returned. + * Returns the URL associated with this local project. If using a temporary transient + * project location this URL should not be used. */ public URL getURL() { return url; @@ -75,7 +100,10 @@ public class ProjectLocator { } /** - * Get the location of the project. + * Get the location of the project which will contain marker file + * ({@link #getMarkerFile()}) and project directory ({@link #getProjectDir()}). + * Note: directory may or may not exist. + * @return project location directory */ public String getLocation() { return location; @@ -110,9 +138,6 @@ public class ProjectLocator { return PROJECT_DIR_SUFFIX; } - /* (non-Javadoc) - * @see java.lang.Object#equals(java.lang.Object) - */ @Override public boolean equals(Object obj) { if (obj == null) { @@ -125,23 +150,14 @@ public class ProjectLocator { return false; } ProjectLocator projectLocator = (ProjectLocator) obj; - if (hashCode() != projectLocator.hashCode()) { - return false; - } return name.equals(projectLocator.name) && location.equals(projectLocator.location); } - /* (non-Javadoc) - * @see java.lang.Object#hashCode() - */ @Override public int hashCode() { return name.hashCode() + location.hashCode(); } - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ @Override public String toString() { return GhidraURL.getDisplayString(url); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java new file mode 100644 index 0000000000..9a65ec9ef8 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java @@ -0,0 +1,42 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.model; + +import java.net.URL; + +/** + * {@code ProjectViewListener} provides a listener interface for tracking project views added + * and removed from the associated project. + *
    + * NOTE: notification callbacks are not guarenteed to occur within the swing thread. + */ +public interface ProjectViewListener { + + /** + * Provides notification that a read-only viewed project has been added which is intended to + * be visible. Notification for hidden viewed projects will not be provided. + * @param projectView project view URL + */ + void viewedProjectAdded(URL projectView); + + /** + * Provides notification that a viewed project is being removed from the project. + * Notification for hidden viewed project removal will not be provided. + * @param projectView project view URL + */ + void viewedProjectRemoved(URL projectView); + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolServices.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolServices.java index c4b07d6fe5..11b2dd12e6 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolServices.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolServices.java @@ -16,10 +16,12 @@ package ghidra.framework.model; import java.io.*; +import java.net.URL; import java.util.Set; import ghidra.framework.plugintool.PluginEvent; import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.protocol.ghidra.GhidraURL; /** * Services that the Tool uses. @@ -73,15 +75,23 @@ public interface ToolServices { public void displaySimilarTool(PluginTool tool, DomainFile domainFile, PluginEvent event); /** - * Returns the default tool template used to open the tool. Here default means the - * tool that should be used to open the given file, whether defined by the user or the - * system default. + * Returns the default/preferred tool template which should be used to open the specified + * domain file, whether defined by the user or the system default. * - * @param domainFile The file for which to find the preferred tool. - * @return The preferred tool that should be used to open the given file. + * @param domainFile The file whose preferred tool should be found. + * @return The preferred tool that should be used to open the given file or null if none found. */ public ToolTemplate getDefaultToolTemplate(DomainFile domainFile); + /** + * Returns the default/preferred tool template which should be used to open the specified + * domain file content type, whether defined by the user or the system default. + * + * @param contentType The content type whose preferred tool should be found. + * @return The preferred tool that should be used to open the given file or null if none found. + */ + public ToolTemplate getDefaultToolTemplate(String contentType); + /** * Returns a set of tools that can open the given domain file class. * @param domainClass The domain file class type for which to get tools @@ -108,21 +118,43 @@ public interface ToolServices { public void setContentTypeToolAssociations(Set infos); /** - * Launch the default tool; if domainFile is not null, this file will - * be opened in the tool. - * @param domainFile the file to open; may be null - * @return the tool + * Launch the default tool and open the specified domainFile. + * @param domainFile the file to open + * @return the launched tool. Null returned if a suitable default tool + * for the file content type was not found. */ public PluginTool launchDefaultTool(DomainFile domainFile); /** - * Launch the tool with the given name + * Launch the tool with the given name. A domainFile may be specified and will be opened + * if its content type is supported by the tool. * @param toolName name of the tool to launch * @param domainFile the file to open; may be null - * @return the tool + * @return the requested tool or null if the specified tool not found. */ public PluginTool launchTool(String toolName, DomainFile domainFile); + /** + * Launch the default tool and open the specified Ghidra URL resource. + * The tool choosen well be based upon the content type of the specified resource. + * @param ghidraUrl resource to be opened (see {@link GhidraURL}) + * @return the launched tool. Null returned if a failure occurs while accessing the specified + * resource or a suitable default tool for the file content type was not found. + * @throws IllegalArgumentException if URL protocol is not supported. Currently, only + * the {@code ghidra} protocol is supported. + */ + public PluginTool launchDefaultToolWithURL(URL ghidraUrl); + + /** + * Launch the tool with the given name and attempt to open the specified Ghidra URL resource. + * @param toolName name of the tool to launch + * @param ghidraUrl resource to be opened (see {@link GhidraURL}) + * @return the requested tool or null if the specified tool not found. + * @throws IllegalArgumentException if URL protocol is not supported. Currently, only + * the {@code ghidra} protocol is supported. + */ + public PluginTool launchToolWithURL(String toolName, URL ghidraUrl); + /** * Add a listener that will be notified when the default tool specification changes * @param listener the listener diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/Plugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/Plugin.java index 3b67a286b4..3efd06adc5 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/Plugin.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/Plugin.java @@ -15,6 +15,7 @@ */ package ghidra.framework.plugintool; +import java.net.URL; import java.util.*; import docking.ComponentProvider; @@ -330,6 +331,17 @@ public abstract class Plugin implements ExtensionPoint, PluginEventListener, Ser return false; } + /** + * Request plugin to process URL if supported. Actual processing may be delayed and + * interaction with user may occur (e.g., authentication, approval, etc.). + *

    + * @param url data URL + * @return boolean true if this plugin can process URL. + */ + public boolean accept(URL url) { + return false; + } + /** * Get the domain files that this plugin has open. *

    diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginManager.java index 7308f92f80..6cfae8f1d4 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginManager.java @@ -15,6 +15,7 @@ */ package ghidra.framework.plugintool; +import java.net.URL; import java.util.*; import java.util.Map.Entry; @@ -52,6 +53,23 @@ class PluginManager { return false; } + /** + * Identify plugin which will accept specified URL. If no plugin accepts URL it will be + * rejected and false returned. If a plugin can accept the specified URL it will attempt to + * process and return true if successful. + * The user may be prompted if connecting to the URL requires user authentication. + * @param url read-only resource URL + * @return true if URL accepted and processed else false + */ + boolean accept(URL url) { + for (Plugin p : pluginList) { + if (p.accept(url)) { + return true; + } + } + return false; + } + public void dispose() { for (Iterator it = pluginList.iterator(); it.hasNext();) { Plugin plugin = it.next(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java index 07fe6bc906..590148b7d5 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java @@ -21,6 +21,7 @@ import java.awt.*; import java.awt.event.KeyEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; +import java.net.URL; import java.util.*; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -440,6 +441,18 @@ public abstract class PluginTool extends AbstractDockingTool { return pluginMgr.acceptData(data); } + /** + * Request tool to accept specified URL. Acceptance of URL depends greatly on the plugins + * confiugred into tool. If no plugin accepts URL it will be rejected and false returned. + * If a plugin can accept the specified URL it will attempt to process and return true if + * successful. The user may be prompted if connecting to the URL requires user authentication. + * @param url read-only resource URL + * @return true if URL accepted and processed else false + */ + public boolean accept(URL url) { + return pluginMgr.accept(url); + } + public void addPropertyChangeListener(PropertyChangeListener l) { propertyChangeMgr.addPropertyChangeListener(l); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/ToolServicesAdapter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/ToolServicesAdapter.java index b865f615e0..91a3f4cf78 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/ToolServicesAdapter.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/ToolServicesAdapter.java @@ -16,6 +16,7 @@ package ghidra.framework.plugintool; import java.io.*; +import java.net.URL; import java.util.Set; import ghidra.framework.model.*; @@ -61,6 +62,11 @@ public class ToolServicesAdapter implements ToolServices { return null; } + @Override + public ToolTemplate getDefaultToolTemplate(String contentType) { + return null; + } + @Override public PluginTool[] getRunningTools() { return null; @@ -81,6 +87,16 @@ public class ToolServicesAdapter implements ToolServices { return null; } + @Override + public PluginTool launchDefaultToolWithURL(URL url) { + return null; + } + + @Override + public PluginTool launchToolWithURL(String toolName, URL url) { + return null; + } + @Override public void removeDefaultToolChangeListener(DefaultToolChangeListener listener) { // override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java index 6e53c4e81f..3ea00eebac 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java @@ -16,7 +16,6 @@ package ghidra.framework.project; import java.io.*; -import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.Map.Entry; @@ -25,7 +24,6 @@ import org.jdom.*; import org.jdom.input.SAXBuilder; import org.jdom.output.XMLOutputter; -import docking.widgets.OptionDialog; import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.data.ProjectFileManager; import ghidra.framework.data.TransientDataManager; @@ -35,8 +33,11 @@ import ghidra.framework.project.tool.GhidraToolTemplate; import ghidra.framework.project.tool.ToolManagerImpl; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.protocol.ghidra.GhidraURLConnection; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; import ghidra.framework.store.LockException; import ghidra.util.*; +import ghidra.util.datastruct.WeakDataStructureFactory; +import ghidra.util.datastruct.WeakSet; import ghidra.util.exception.DuplicateNameException; import ghidra.util.xml.GenericXMLOutputter; import ghidra.util.xml.XmlUtilities; @@ -53,8 +54,6 @@ public class DefaultProject implements Project { private static final String PROJECT_STATE = "projectState"; - private ProjectLock projectLock; - // this may be null private DefaultProjectManager projectManager; @@ -63,11 +62,14 @@ public class DefaultProject implements Project { private ToolManagerImpl toolManager; private boolean changed; // flag for whether the project configuration has changed - private boolean isClosed; + private volatile boolean isClosed; private Map dataMap = new HashMap<>(); - private HashMap projectConfigMap = new HashMap<>(); - private HashMap otherViews = new HashMap<>(); + private Map projectConfigMap = new HashMap<>(); + private Map otherViews = new HashMap<>(); + private Set visibleViews = new HashSet<>(); + private WeakSet viewListeners = + WeakDataStructureFactory.createCopyOnWriteWeakSet(); /** * Constructor for creating a New project @@ -83,10 +85,6 @@ public class DefaultProject implements Project { RepositoryAdapter repository) throws IOException, LockException { this.projectManager = projectManager; this.projectLocator = projectLocator; - this.projectLock = getProjectLock(projectLocator, false); - if (projectLock == null) { - throw new LockException("Unable to lock project! " + projectLocator); - } boolean success = false; try { @@ -102,7 +100,6 @@ public class DefaultProject implements Project { if (fileMgr != null) { fileMgr.dispose(); } - projectLock.release(); } } initializeNewProject(); @@ -124,10 +121,6 @@ public class DefaultProject implements Project { this.projectManager = projectManager; this.projectLocator = projectLocator; - this.projectLock = getProjectLock(projectLocator, true); - if (projectLock == null) { - throw new LockException("Unable to lock project! " + projectLocator); - } boolean success = false; try { @@ -143,7 +136,6 @@ public class DefaultProject implements Project { if (fileMgr != null) { fileMgr.dispose(); } - projectLock.release(); } } } @@ -188,54 +180,64 @@ public class DefaultProject implements Project { return projectManager; } - /** - * Creates a ProjectLock and attempts to lock it. This handles the case - * where the project was previously locked. - * - * @param locator the project locator - * @param allowInteractiveForce if true, when a lock cannot be obtained, the - * user will be prompted - * @return A locked ProjectLock - * @throws ProjectLockException if lock failed - */ - private ProjectLock getProjectLock(ProjectLocator locator, boolean allowInteractiveForce) { - ProjectLock lock = new ProjectLock(locator); - if (lock.lock()) { - return lock; - } +// /** +// * Determine if the specified project location currently has a write lock. +// * @param locator project storage locator +// * @return true if project data current has write-lock else false +// */ +// public static boolean isLocked(ProjectLocator locator) { +// ProjectLock lock = new ProjectLock(locator); +// return lock.isLocked(); +// } - // in headless mode, just spit out an error - if (!allowInteractiveForce || SystemUtilities.isInHeadlessMode()) { - return null; - } - - String projectStr = "Project: " + HTMLUtilities.escapeHTML(locator.getLocation()) + - System.getProperty("file.separator") + HTMLUtilities.escapeHTML(locator.getName()); - String lockInformation = lock.getExistingLockFileInformation(); - if (!lock.canForceLock()) { - Msg.showInfo(getClass(), null, "Project Locked", - "Project is locked. You have another instance of Ghidra
    " + - "already running with this project open (locally or remotely).

    " + - projectStr + "

    " + "Lock information: " + lockInformation); - return null; - } - - int userChoice = OptionDialog.showOptionDialog(null, "Project Locked - Delete Lock?", - "Project is locked. You may have another instance of Ghidra
    " + - "already running with this project opened (locally or remotely).
    " + projectStr + - "

    " + "If this is not the case, you can delete the lock file:
    " + - locator.getProjectLockFile().getAbsolutePath() + ".

    " + - "Lock information: " + lockInformation, - "Delete Lock", OptionDialog.QUESTION_MESSAGE); - if (userChoice == OptionDialog.OPTION_ONE) { // Delete Lock - if (lock.forceLock()) { - return lock; - } - - Msg.showError(this, null, "Error", "Attempt to force lock failed! " + locator); - } - return null; - } +// /** +// * Creates a ProjectLock and attempts to lock it. This handles the case +// * where the project was previously locked. +// * +// * @param locator the project locator +// * @param allowInteractiveForce if true, when a lock cannot be obtained, the +// * user will be prompted +// * @return A locked ProjectLock +// * @throws ProjectLockException if lock failed +// */ +// private ProjectLock getProjectLock(ProjectLocator locator, boolean allowInteractiveForce) { +// ProjectLock lock = new ProjectLock(locator); +// if (lock.lock()) { +// return lock; +// } +// +// // in headless mode, just spit out an error +// if (!allowInteractiveForce || SystemUtilities.isInHeadlessMode()) { +// return null; +// } +// +// String projectStr = "Project: " + HTMLUtilities.escapeHTML(locator.getLocation()) + +// System.getProperty("file.separator") + HTMLUtilities.escapeHTML(locator.getName()); +// String lockInformation = lock.getExistingLockFileInformation(); +// if (!lock.canForceLock()) { +// Msg.showInfo(getClass(), null, "Project Locked", +// "Project is locked. You have another instance of Ghidra
    " + +// "already running with this project open (locally or remotely).

    " + +// projectStr + "

    " + "Lock information: " + lockInformation); +// return null; +// } +// +// int userChoice = OptionDialog.showOptionDialog(null, "Project Locked - Delete Lock?", +// "Project is locked. You may have another instance of Ghidra
    " + +// "already running with this project opened (locally or remotely).
    " + projectStr + +// "

    " + "If this is not the case, you can delete the lock file:
    " + +// locator.getProjectLockFile().getAbsolutePath() + ".

    " + +// "Lock information: " + lockInformation, +// "Delete Lock", OptionDialog.QUESTION_MESSAGE); +// if (userChoice == OptionDialog.OPTION_ONE) { // Delete Lock +// if (lock.forceLock()) { +// return lock; +// } +// +// Msg.showError(this, null, "Error", "Attempt to force lock failed! " + locator); +// } +// return null; +// } private void initializeNewProject() { if (toolManager == null) { @@ -261,54 +263,94 @@ public class DefaultProject implements Project { } @Override - public ProjectData addProjectView(URL url) throws IOException, MalformedURLException { + public void addProjectViewListener(ProjectViewListener listener) { + viewListeners.add(listener); + } - ProjectData pd = otherViews.get(url); - if (pd != null) { - return pd; - } + @Override + public void removeProjectViewListener(ProjectViewListener listener) { + viewListeners.remove(listener); + } - if (!GhidraURL.PROTOCOL.equals(url.getProtocol())) { - throw new IOException("Invalid Ghidra URL specified: " + url); + private void notifyVisibleViewAdded(URL projectView) { + for (ProjectViewListener listener : viewListeners) { + listener.viewedProjectAdded(projectView); } + } + + private void notifyVisibleViewRemoved(URL projectView) { + for (ProjectViewListener listener : viewListeners) { + listener.viewedProjectRemoved(projectView); + } + } + + private ProjectData openProjectView(URL url) throws IOException { GhidraURLConnection c = (GhidraURLConnection) url.openConnection(); c.setAllowUserInteraction(true); c.setReadOnly(true); - int responseCode = c.getResponseCode(); - if (responseCode == GhidraURLConnection.GHIDRA_NOT_FOUND) { + StatusCode responseCode = c.getStatusCode(); + if (responseCode == StatusCode.NOT_FOUND) { throw new IOException( "Project/repository not found: " + GhidraURL.getDisplayString(url)); } - if (responseCode == GhidraURLConnection.GHIDRA_UNAUTHORIZED) { - throw new IOException( - "Authentication Failed for project/repository: " + GhidraURL.getDisplayString(url)); + if (responseCode == StatusCode.UNAUTHORIZED) { + // assume already informed + return null; } ProjectFileManager projectData = (ProjectFileManager) c.getProjectData(); if (projectData == null) { throw new IOException( - "Failed to view specified project/repository: " + GhidraURL.getDisplayString(url)); + "Failed to view specified project/repository: " + + GhidraURL.getDisplayString(url)); } url = projectData.getProjectLocator().getURL(); // transform to repository root URL otherViews.put(url, projectData); - changed = true; - Msg.info(this, "Opened project view: " + GhidraURL.getDisplayString(url)); return projectData; } + @Override + public ProjectData addProjectView(URL url, boolean visible) throws IOException { + synchronized (otherViews) { + if (isClosed) { + throw new IOException("project is closed"); + } + + if (!GhidraURL.PROTOCOL.equals(url.getProtocol())) { + throw new IOException("Invalid Ghidra URL specified: " + url); + } + + ProjectData projectData = otherViews.get(url); + if (projectData == null) { + projectData = openProjectView(url); + } + + if (projectData != null && visible && visibleViews.add(url)) { + notifyVisibleViewAdded(url); + } + + return projectData; + } + } + /** * Remove the view from this project. */ @Override public void removeProjectView(URL url) { - ProjectFileManager dataMgr = otherViews.remove(url); - if (dataMgr != null) { - dataMgr.dispose(); - Msg.info(this, "Closed project view: " + GhidraURL.getDisplayString(url)); - changed = true; + synchronized (otherViews) { + ProjectFileManager dataMgr = otherViews.remove(url); + if (dataMgr != null) { + if (visibleViews.remove(url)) { + notifyVisibleViewRemoved(url); + } + dataMgr.dispose(); + Msg.info(this, "Closed project view: " + GhidraURL.getDisplayString(url)); + changed = true; + } } } @@ -348,6 +390,7 @@ public class DefaultProject implements Project { @Override public ProjectLocator[] getProjectViews() { + // Only includes visible viewed projects ProjectData[] pd = getViewedProjectData(); ProjectLocator[] views = new ProjectLocator[pd.length]; @@ -364,17 +407,21 @@ public class DefaultProject implements Project { @Override public void close() { - Iterator iter = otherViews.values().iterator(); - while (iter.hasNext()) { - ProjectFileManager dataMgr = iter.next(); - if (dataMgr != null) { - dataMgr.dispose(); + synchronized (otherViews) { + isClosed = true; + + Iterator iter = otherViews.values().iterator(); + while (iter.hasNext()) { + ProjectFileManager dataMgr = iter.next(); + if (dataMgr != null) { + dataMgr.dispose(); + } } + otherViews.clear(); } - otherViews.clear(); try { - isClosed = true; + if (toolManager != null) { toolManager.close(); toolManager.dispose(); @@ -382,12 +429,9 @@ public class DefaultProject implements Project { if (projectManager != null) { projectManager.projectClosed(this); } - fileMgr.dispose(); } finally { - if (projectLock != null) { - projectLock.release(); - } + fileMgr.dispose(); } } @@ -450,7 +494,7 @@ public class DefaultProject implements Project { String location = elem.getAttributeValue("LOCATION"); URL url = GhidraURL.makeURL(location, name); try { - addProjectView(url); + addProjectView(url, true); } catch (IOException e) { Msg.error(this, e.getMessage()); @@ -462,7 +506,7 @@ public class DefaultProject implements Project { String urlStr = elem.getAttributeValue("URL"); URL url = new URL(urlStr); try { - addProjectView(url); + addProjectView(url, true); } catch (IOException e) { Msg.error(this, e.getMessage()); @@ -602,7 +646,7 @@ public class DefaultProject implements Project { } @Override - public ProjectData getProjectData() { + public ProjectFileManager getProjectData() { return fileMgr; } @@ -622,11 +666,14 @@ public class DefaultProject implements Project { return fileMgr; } - for (ProjectData data : otherViews.values()) { - if (locator.equals(data.getProjectLocator())) { - return data; + synchronized (otherViews) { + for (ProjectData data : otherViews.values()) { + if (locator.equals(data.getProjectLocator())) { + return data; + } } } + return null; } @@ -640,18 +687,31 @@ public class DefaultProject implements Project { @Override public ProjectData[] getViewedProjectData() { - ProjectData[] projectData = new ProjectData[otherViews.size()]; - otherViews.values().toArray(projectData); - return projectData; + synchronized (otherViews) { + + // only return visible viewed project + List list = new ArrayList<>(); + for (URL url : otherViews.keySet()) { + if (visibleViews.contains(url)) { + list.add(otherViews.get(url)); + } + } + + ProjectData[] projectData = new ProjectData[list.size()]; + list.toArray(projectData); + return projectData; + } } @Override public void releaseFiles(Object consumer) { fileMgr.releaseDomainFiles(consumer); - Iterator it = otherViews.values().iterator(); - while (it.hasNext()) { - ProjectFileManager mgr = it.next(); - mgr.releaseDomainFiles(consumer); + synchronized (otherViews) { + Iterator it = otherViews.values().iterator(); + while (it.hasNext()) { + ProjectFileManager mgr = it.next(); + mgr.releaseDomainFiles(consumer); + } } TransientDataManager.releaseFiles(consumer); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java index 397680a173..0e63ccb3e1 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/GhidraToolTemplate.java @@ -152,7 +152,7 @@ public class GhidraToolTemplate implements ToolTemplate { dtList.add(Class.forName(className)); } catch (ClassNotFoundException e) { - Msg.error(this, "Class not found: " + className, e); + Msg.warn(this, "Tool supported content class not found: " + className); } catch (Exception exc) {//TODO Msg.error(this, "Unexpected Exception: " + exc.getMessage(), exc); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java index a78fa72238..cfebe63aee 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java @@ -16,6 +16,7 @@ package ghidra.framework.project.tool; import java.io.*; +import java.net.URL; import java.util.*; import org.jdom.Document; @@ -24,16 +25,18 @@ import org.jdom.output.XMLOutputter; import docking.widgets.OptionDialog; import docking.widgets.filechooser.GhidraFileChooser; import ghidra.framework.ToolUtils; -import ghidra.framework.data.ContentHandler; -import ghidra.framework.data.DomainObjectAdapter; +import ghidra.framework.data.*; import ghidra.framework.model.*; import ghidra.framework.plugintool.PluginEvent; import ghidra.framework.plugintool.PluginTool; import ghidra.framework.preferences.Preferences; +import ghidra.framework.protocol.ghidra.GetUrlContentTypeTask; +import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.util.Msg; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.filechooser.GhidraFileChooserModel; import ghidra.util.filechooser.GhidraFileFilter; +import ghidra.util.task.TaskLauncher; import ghidra.util.xml.GenericXMLOutputter; /** @@ -48,7 +51,7 @@ class ToolServicesImpl implements ToolServices { private ToolManagerImpl toolManager; private List listeners = new ArrayList<>(); private ToolChestChangeListener toolChestChangeListener; - private Set contentHandlers; + private Set> contentHandlers; ToolServicesImpl(ToolChest toolChest, ToolManagerImpl toolManager) { this.toolChest = toolChest; @@ -187,38 +190,88 @@ class ToolServicesImpl implements ToolServices { @Override public PluginTool launchDefaultTool(DomainFile domainFile) { ToolTemplate template = getDefaultToolTemplate(domainFile); - if (template != null) { - Workspace workspace = toolManager.getActiveWorkspace(); - PluginTool tool = workspace.runTool(template); - tool.setVisible(true); - if (domainFile != null) { - tool.acceptDomainFiles(new DomainFile[] { domainFile }); - } - return tool; + if (template == null) { + return null; } - return null; + Workspace workspace = toolManager.getActiveWorkspace(); + PluginTool tool = workspace.runTool(template); + if (tool == null) { + return null; + } + tool.setVisible(true); + tool.acceptDomainFiles(new DomainFile[] { domainFile }); + return tool; } @Override public PluginTool launchTool(String toolName, DomainFile domainFile) { ToolTemplate template = findToolChestToolTemplate(toolName); - if (template != null) { - Workspace workspace = toolManager.getActiveWorkspace(); - PluginTool tool = workspace.runTool(template); - tool.setVisible(true); - if (domainFile != null) { - tool.acceptDomainFiles(new DomainFile[] { domainFile }); - } - return tool; + if (template == null) { + return null; } - return null; + Workspace workspace = toolManager.getActiveWorkspace(); + PluginTool tool = workspace.runTool(template); + if (tool == null) { + return null; + } + tool.setVisible(true); + if (domainFile != null) { + tool.acceptDomainFiles(new DomainFile[] { domainFile }); + } + return tool; + } + + @Override + public PluginTool launchDefaultToolWithURL(URL ghidraUrl) throws IllegalArgumentException { + String contentType = getContentType(ghidraUrl); + if (contentType == null) { + return null; + } + ToolTemplate template = getDefaultToolTemplate(contentType); + if (template == null) { + return null; + } + Workspace workspace = toolManager.getActiveWorkspace(); + PluginTool tool = workspace.runTool(template); + if (tool == null) { + return null; + } + tool.setVisible(true); + tool.accept(ghidraUrl); + return tool; + } + + @Override + public PluginTool launchToolWithURL(String toolName, URL ghidraUrl) + throws IllegalArgumentException { + if (!GhidraURL.isLocalProjectURL(ghidraUrl) && + !GhidraURL.isServerRepositoryURL(ghidraUrl)) { + throw new IllegalArgumentException("unsupported URL"); + } + ToolTemplate template = findToolChestToolTemplate(toolName); + if (template == null) { + return null; + } + Workspace workspace = toolManager.getActiveWorkspace(); + PluginTool tool = workspace.runTool(template); + if (tool != null) { + tool.setVisible(true); + tool.accept(ghidraUrl); + } + return tool; + } + + private String getContentType(URL url) throws IllegalArgumentException { + GetUrlContentTypeTask task = new GetUrlContentTypeTask(url); + TaskLauncher.launch(task); // blocking task + return task.getContentType(); } @Override public void setContentTypeToolAssociations(Set infos) { for (ToolAssociationInfo info : infos) { - ContentHandler handler = info.getContentHandler(); + ContentHandler handler = info.getContentHandler(); String contentType = handler.getContentType(); String preferenceKey = getToolAssociationPreferenceKey(contentType); if (!info.isDefault()) { @@ -254,15 +307,15 @@ class ToolServicesImpl implements ToolServices { Set set = new HashSet<>(); // get all known content types - Set handlers = getContentHandlers(); - for (ContentHandler contentHandler : handlers) { + Set> handlers = getContentHandlers(); + for (ContentHandler contentHandler : handlers) { set.add(createToolAssociationInfo(contentHandler)); } return set; } - private ToolAssociationInfo createToolAssociationInfo(ContentHandler contentHandler) { + private ToolAssociationInfo createToolAssociationInfo(ContentHandler contentHandler) { String contentType = contentHandler.getContentType(); String defaultToolName = contentHandler.getDefaultToolName(); String userPreferredToolName = @@ -280,8 +333,11 @@ class ToolServicesImpl implements ToolServices { @Override public ToolTemplate getDefaultToolTemplate(DomainFile domainFile) { - String contentType = domainFile.getContentType(); + return getDefaultToolTemplate(domainFile.getContentType()); + } + @Override + public ToolTemplate getDefaultToolTemplate(String contentType) { String toolName = Preferences.getProperty(getToolAssociationPreferenceKey(contentType), null, true); if (toolName == null) { @@ -312,8 +368,8 @@ class ToolServicesImpl implements ToolServices { // // Next, look through for all compatible content handlers find tools for them // - Set compatibleHandlers = getCompatibleContentHandlers(domainClass); - for (ContentHandler handler : compatibleHandlers) { + Set> compatibleHandlers = getCompatibleContentHandlers(domainClass); + for (ContentHandler handler : compatibleHandlers) { String defaultToolName = handler.getDefaultToolName(); if (nameToTemplateMap.get(defaultToolName) != null) { continue; // already have tool in the map by this name; prefer that tool @@ -355,11 +411,11 @@ class ToolServicesImpl implements ToolServices { return new HashSet<>(nameToTemplateMap.values()); } - private Set getCompatibleContentHandlers( + private Set> getCompatibleContentHandlers( Class domainClass) { - Set set = new HashSet<>(); - Set handlers = getContentHandlers(); - for (ContentHandler contentHandler : handlers) { + Set> set = new HashSet<>(); + Set> handlers = getContentHandlers(); + for (ContentHandler contentHandler : handlers) { Class handlerDomainClass = contentHandler.getDomainObjectClass(); if (handlerDomainClass == domainClass) { @@ -374,8 +430,8 @@ class ToolServicesImpl implements ToolServices { } private String getDefaultToolAssociation(String contentType) { - Set handlers = getContentHandlers(); - for (ContentHandler contentHandler : handlers) { + Set> handlers = getContentHandlers(); + for (ContentHandler contentHandler : handlers) { String type = contentHandler.getContentType(); if (type.equals(contentType)) { return contentHandler.getDefaultToolName(); @@ -384,25 +440,31 @@ class ToolServicesImpl implements ToolServices { return null; } - private Set getContentHandlers() { + private Set> getContentHandlers() { if (contentHandlers != null) { return contentHandlers; } contentHandlers = new HashSet<>(); + @SuppressWarnings("rawtypes") List instances = ClassSearcher.getInstances(ContentHandler.class); - for (ContentHandler contentHandler : instances) { + for (ContentHandler contentHandler : instances) { + + if (contentHandler instanceof FolderLinkContentHandler) { + continue; // ignore folder link handler + } + // a bit of validation String contentType = contentHandler.getContentType(); if (contentType == null) { - Msg.error(DomainObjectAdapter.class, "ContentHandler " + + Msg.error(DomainObjectAdapter.class, "ContentHandler " + contentHandler.getClass().getName() + " does not specify a content type"); continue; } - + String toolName = contentHandler.getDefaultToolName(); if (toolName == null) { - Msg.error(DomainObjectAdapter.class, "ContentHandler " + + Msg.error(DomainObjectAdapter.class, "ContentHandler " + contentHandler.getClass().getName() + " does not specify a default tool"); continue; } @@ -479,7 +541,7 @@ class ToolServicesImpl implements ToolServices { private PluginTool findToolUsingFile(PluginTool[] tools, DomainFile domainFile) { PluginTool matchingTool = null; for (int toolNum = 0; (toolNum < tools.length) && (matchingTool == null); toolNum++) { - PluginTool pTool = (PluginTool) tools[toolNum]; + PluginTool pTool = tools[toolNum]; // Is this tool the same as the type we are in. DomainFile[] df = pTool.getDomainFiles(); for (DomainFile element : df) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java index 8cc721cfab..456329843e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnector.java @@ -19,8 +19,10 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import ghidra.framework.client.ClientUtil; -import ghidra.framework.client.NotConnectedException; +import javax.security.auth.login.LoginException; + +import ghidra.framework.client.*; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; import ghidra.util.Msg; /** @@ -44,36 +46,51 @@ public class DefaultGhidraProtocolConnector extends GhidraProtocolConnector { @Override public boolean isReadOnly() throws NotConnectedException { - if (responseCode == -1) { + if (statusCode == null) { throw new NotConnectedException("not connected"); } return readOnly; } @Override - public int connect(boolean readOnlyAccess) throws IOException { + public StatusCode connect(boolean readOnlyAccess) throws IOException { - if (responseCode != -1) { + if (statusCode != null) { throw new IllegalStateException("already connected"); } this.readOnly = readOnlyAccess; - responseCode = GhidraURLConnection.GHIDRA_NOT_FOUND; // just in case + statusCode = StatusCode.UNAVAILABLE; // if uncaught exception occurs repositoryServerAdapter = ClientUtil.getRepositoryServer(url.getHost(), url.getPort(), true); if (repositoryName == null) { - responseCode = GhidraURLConnection.GHIDRA_OK; - return responseCode; + statusCode = StatusCode.OK; + return statusCode; } repositoryAdapter = repositoryServerAdapter.getRepository(repositoryName); - repositoryAdapter.connect(); + if (repositoryServerAdapter.isConnected()) { + try { + repositoryAdapter.connect(); + } + catch (RepositoryNotFoundException e) { + statusCode = StatusCode.NOT_FOUND; + } + } + else if (!repositoryServerAdapter.isCancelled()) { + Throwable t = repositoryServerAdapter.getLastConnectError(); + if (t instanceof LoginException) { + statusCode = StatusCode.UNAUTHORIZED; + } + //throw new NotConnectedException("Not connected to repository server", t); + return statusCode; + } if (repositoryAdapter.isConnected()) { - responseCode = GhidraURLConnection.GHIDRA_OK; + statusCode = StatusCode.OK; if (!repositoryAdapter.getUser().hasWritePermission()) { if (!readOnly) { this.readOnly = true; // write access not permitted @@ -84,10 +101,10 @@ public class DefaultGhidraProtocolConnector extends GhidraProtocolConnector { resolveItemPath(); } else { - responseCode = GhidraURLConnection.GHIDRA_UNAUTHORIZED; + statusCode = StatusCode.UNAUTHORIZED; } - return responseCode; + return statusCode; } @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolHandler.java index 069bc5e0b8..ff456a69a4 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolHandler.java @@ -18,11 +18,14 @@ package ghidra.framework.protocol.ghidra; import java.net.MalformedURLException; import java.net.URL; +import org.apache.commons.lang3.StringUtils; + /** * DefaultGhidraProtocolHandler provides the default protocol * handler which corresponds to the original RMI-based Ghidra Server * and local file-based Ghidra projects. - * {@literal ghidra://host/repo/... or ghidra:/path/projectName} + * {@literal ghidra://host/repo/... or ghidra:/path/projectName/...} + * See {@link DefaultGhidraProtocolConnector} and {@link DefaultLocalGhidraProtocolConnector} */ public class DefaultGhidraProtocolHandler extends GhidraProtocolHandler { @@ -35,7 +38,7 @@ public class DefaultGhidraProtocolHandler extends GhidraProtocolHandler { public GhidraProtocolConnector getConnector(URL ghidraUrl) throws MalformedURLException { String protocol = ghidraUrl.getProtocol(); if (protocol != null) { - if (ghidraUrl.getAuthority() == null) { + if (StringUtils.isBlank(ghidraUrl.getAuthority())) { return new DefaultLocalGhidraProtocolConnector(ghidraUrl); } return new DefaultGhidraProtocolConnector(ghidraUrl); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java index a9ef6e0e63..770a6117aa 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnector.java @@ -21,8 +21,12 @@ import java.net.URL; import ghidra.framework.client.NotConnectedException; import ghidra.framework.client.RepositoryAdapter; +import ghidra.framework.data.ProjectFileManager; import ghidra.framework.model.ProjectLocator; -import ghidra.framework.store.FileSystem; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; +import ghidra.framework.store.LockException; +import ghidra.util.NotOwnerException; +import ghidra.util.ReadOnlyException; /** * DefaultLocalGhidraProtocolConnector provides support for the @@ -67,10 +71,12 @@ public class DefaultLocalGhidraProtocolConnector extends GhidraProtocolConnector @Override protected String parseItemPath() throws MalformedURLException { - // root folder access only - TODO: add support for specifying local item/folder path - folderPath = FileSystem.SEPARATOR; - folderItemName = null; - return folderPath; + + String path = url.getQuery(); + + initFolderItemPath(path); + + return path != null ? path : folderPath; } @Override @@ -94,22 +100,47 @@ public class DefaultLocalGhidraProtocolConnector extends GhidraProtocolConnector @Override public boolean isReadOnly() throws NotConnectedException { - if (responseCode == -1) { + if (statusCode == null) { throw new NotConnectedException("not connected"); } return readOnly; } @Override - public int connect(boolean readOnlyAccess) throws IOException { + public StatusCode connect(boolean readOnlyAccess) throws IOException { this.readOnly = readOnlyAccess; if (!localStorageLocator.exists()) { - responseCode = GhidraURLConnection.GHIDRA_NOT_FOUND; + statusCode = StatusCode.NOT_FOUND; } else { - responseCode = GhidraURLConnection.GHIDRA_OK; + statusCode = StatusCode.OK; } - return responseCode; + return statusCode; + } + + /** + * Connect and establish loca project project data instance. Opening a project for + * write access is subject to in-use lock restriction. + * See {@link #getStatusCode()} if null is returned. + * @param readOnlyAccess true if project data should be read-only + * @return project data instance or null if project not found + * @throws IOException if IO error occurs + */ + ProjectFileManager getLocalProjectData(boolean readOnlyAccess) throws IOException { + if (connect(readOnlyAccess) != StatusCode.OK) { + return null; + } + + try { + return new ProjectFileManager(localStorageLocator, !readOnlyAccess, false); + } + catch (NotOwnerException | ReadOnlyException e) { + statusCode = StatusCode.UNAUTHORIZED; + } + catch (LockException e) { + statusCode = StatusCode.LOCKED; + } + return null; } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GetUrlContentTypeTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GetUrlContentTypeTask.java new file mode 100644 index 0000000000..0e2e519d75 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GetUrlContentTypeTask.java @@ -0,0 +1,113 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.protocol.ghidra; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; + +import ghidra.framework.model.DomainFile; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; +import ghidra.util.Msg; +import ghidra.util.task.Task; +import ghidra.util.task.TaskMonitor; + +/** + * A blocking/modal Ghidra URL content type discovery task + */ +public class GetUrlContentTypeTask extends Task { + + private final URL ghidraUrl; + + private String contentType; + private boolean done = false; + + /** + * Construct a Ghidra URL content type discovery task + * @param ghidraUrl Ghidra URL (local or remote) + * @throws IllegalArgumentException if specified URL is not a Ghidra URL + * (see {@link GhidraURL}). + */ + public GetUrlContentTypeTask(URL ghidraUrl) { + super("Checking URL Content Type", true, false, true); + if (!GhidraURL.isLocalProjectURL(ghidraUrl) && + !GhidraURL.isServerRepositoryURL(ghidraUrl)) { + throw new IllegalArgumentException("unsupported URL"); + } + this.ghidraUrl = ghidraUrl; + } + + /** + * Get the discovered content type (e.g., "Program") + * @return content type or null if error occured or unsupported URL content + * @throws IllegalStateException if task has not completed execution + */ + public String getContentType() { + if (!done) { + throw new IllegalStateException("task has not completed"); + } + return contentType; + } + + @Override + public void run(TaskMonitor monitor) { + final Thread t = Thread.currentThread(); + monitor.addCancelledListener(() -> { + t.interrupt(); + }); + GhidraURLWrappedContent wrappedContent = null; + Object content = null; + try { + GhidraURLConnection c = (GhidraURLConnection) ghidraUrl.openConnection(); + Object obj = c.getContent(); // read-only access + if (c.getStatusCode() == StatusCode.UNAUTHORIZED) { + return; // assume user already notified + } + if (obj instanceof GhidraURLWrappedContent) { + wrappedContent = (GhidraURLWrappedContent) obj; + content = wrappedContent.getContent(this); + } + if (!(content instanceof DomainFile)) { + Msg.showError(this, null, "Unsupported Content", + "Invalid project file URL: " + ghidraUrl); + return; + } + contentType = ((DomainFile) content).getContentType(); + } + catch (FileNotFoundException e) { + Msg.showError(this, null, "Content Not Found", e.getMessage()); + } + catch (MalformedURLException e) { + Msg.showError(this, null, "Invalid Ghidra URL", + "Improperly formed Ghidra URL: " + ghidraUrl); + } + catch (InterruptedIOException e) { + // ignore - assume cancelled + } + catch (IOException e) { + Msg.showError(this, null, "URL Access Failure", + "Failed to open Ghidra URL: " + e.getMessage()); + } + finally { + if (content != null) { + wrappedContent.release(content, this); + } + done = true; + } + } + + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java index 8251bebe3f..f41706b2ca 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraProtocolConnector.java @@ -22,6 +22,7 @@ import java.net.URL; import org.apache.commons.lang3.StringUtils; import ghidra.framework.client.*; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; import ghidra.framework.store.FileSystem; /** @@ -40,7 +41,7 @@ public abstract class GhidraProtocolConnector { protected String folderPath; protected String folderItemName = null; - protected int responseCode = -1; + protected StatusCode statusCode = null; protected RepositoryAdapter repositoryAdapter; protected RepositoryServerAdapter repositoryServerAdapter; @@ -109,7 +110,7 @@ public abstract class GhidraProtocolConnector { String path = url.getPath(); // Divide path into pieces - if (path == null || path.length() < 2 || path.charAt(0) != '/') { + if (StringUtils.isBlank(path) || path.length() < 2 || path.charAt(0) != '/') { return null; // content corresponds to RepositoryServerAdapter } @@ -128,6 +129,51 @@ public abstract class GhidraProtocolConnector { return path; } + /** + * Initialize {@code folderPath} and {@code folderItemName} from specified {@code contentPath}. + * @param contentPath absolute content path (null not permitted) + * @throws MalformedURLException if non-null invalid {@code contentPath} specified + * @return full content path + */ + protected final String initFolderItemPath(String contentPath) throws MalformedURLException { + + if (StringUtils.isBlank(contentPath)) { + folderPath = FileSystem.SEPARATOR; + return folderPath; + } + + if (!contentPath.startsWith(FileSystem.SEPARATOR)) { + throw new MalformedURLException("invalid content path specification"); + } + + boolean isFolder = contentPath.endsWith(FileSystem.SEPARATOR); + folderPath = ""; + String pathToSplit = + isFolder ? contentPath.substring(0, contentPath.length() - 1) : contentPath; + String[] pieces = + StringUtils.splitByWholeSeparatorPreserveAllTokens(pathToSplit, FileSystem.SEPARATOR); + if (pieces.length == 0) { + folderPath = FileSystem.SEPARATOR; + return folderPath; + } + for (int i = 1; i < pieces.length; i++) { + String p = pieces[i]; + if (p.length() == 0) { + throw new MalformedURLException("invalid content path specification"); + } + if (!isFolder && i == (pieces.length - 1)) { + folderItemName = p; + } + else { + folderPath = folderPath + FileSystem.SEPARATOR + p; + } + } + if (folderPath.length() == 0) { + folderPath = FileSystem.SEPARATOR; + } + return contentPath; + } + /** * Parse item path name from URL and establish initial values for folderPath and * folderItemName. @@ -144,48 +190,19 @@ public abstract class GhidraProtocolConnector { // strip off repository name from path path = path.substring(repositoryName.length() + 1); - if (path.length() <= 1) { - // root path specified - folderPath = FileSystem.SEPARATOR; - return folderPath; // repository URL, root folder - } - // Handles server repository URL case ghidra://:/[/]/[] - - boolean isFolder = path.endsWith(FileSystem.SEPARATOR); - folderPath = ""; - String pathToSplit = isFolder ? path.substring(0, path.length() - 1) : path; - String[] pieces = - StringUtils.splitByWholeSeparatorPreserveAllTokens(pathToSplit, FileSystem.SEPARATOR); - if (pieces.length == 0) { - throw new MalformedURLException("invalid repository path specification"); - } - for (int i = 1; i < pieces.length; i++) { - String p = pieces[i]; - if (p.length() == 0) { - throw new MalformedURLException("invalid repository path specification"); - } - if (!isFolder && i == (pieces.length - 1)) { - folderItemName = p; - } - else { - folderPath = folderPath + FileSystem.SEPARATOR + p; - } - } - if (folderPath.length() == 0) { - folderPath = FileSystem.SEPARATOR; - } + path = initFolderItemPath(path); return path; } /** - * Gets the status code from a Ghidra URL connect response. - * @return the Ghidra Status-Code, or -1 if not yet connected + * Gets the status code from a Ghidra URL connect attempt. + * @return the Ghidra status code or null if not yet connected * @see #connect(boolean) */ - public int getResponseCode() { - return responseCode; + public StatusCode getStatusCode() { + return statusCode; } /** @@ -247,12 +264,12 @@ public abstract class GhidraProtocolConnector { /** * Fully resolve folder/item reference once connected to the associated * repository due to possible ambiguity - * @throws IOException + * @throws IOException if an IO error occurs */ protected void resolveItemPath() throws IOException { // NOTE: Assume path may correspond to non-existent folder if not found - // - this is why GHIDRA_NOT_FOUND response code setting has been disabled + // - this is why NOT_FOUND status code setting has been disabled if (folderItemName != null) { if (itemPath.endsWith("/")) { @@ -262,7 +279,7 @@ public abstract class GhidraProtocolConnector { // if (readOnly && !repository.folderExists(itemPath)) { // // TODO: URL location not found -// responseCode = GHIDRA_NOT_FOUND; +// statusCode = NOT_FOUND; // return; // } } @@ -278,7 +295,7 @@ public abstract class GhidraProtocolConnector { // if (folderItemName != null) { // // TODO: URL location not found -// responseCode = GHIDRA_NOT_FOUND; +// statusCode = NOT_FOUND; // return; // } } @@ -289,13 +306,13 @@ public abstract class GhidraProtocolConnector { * Utilized a cached connection via the specified repository adapter. * This method may only be invoked if not yet connected and the associated * URL corresponds to a repository (getRepositoryName() != null). The connection - * response code should be established based upon the availability of the + * status code should be established based upon the availability of the * URL referenced repository resource (i.e., folder or file). * @param repository existing connected repository adapter - * @throws IOException + * @throws IOException if an IO error occurs */ protected void connect(RepositoryAdapter repository) throws IOException { - if (responseCode != -1) { + if (statusCode != null) { throw new IllegalStateException("already connected"); } if (repositoryName == null || !repositoryName.equals(repository.getName())) { @@ -304,7 +321,7 @@ public abstract class GhidraProtocolConnector { if (!repository.isConnected()) { throw new IllegalStateException("expected connected repository"); } - responseCode = GhidraURLConnection.GHIDRA_OK; + statusCode = StatusCode.OK; this.repositoryAdapter = repository; this.repositoryServerAdapter = repository.getServer(); resolveItemPath(); @@ -314,10 +331,10 @@ public abstract class GhidraProtocolConnector { * Connect to the resource specified by the associated URL. This method should only be invoked * once, a second attempt may result in an IOException. * @param readOnly if resource should be requested for write access. - * @return connection response code @see {@link GhidraURLConnection} + * @return connection status code * @throws IOException if a connection error occurs */ - public abstract int connect(boolean readOnly) throws IOException; + public abstract StatusCode connect(boolean readOnly) throws IOException; /** * Determines the read-only nature of a connected resource diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java index cb7ab2d37a..b6ac57f582 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java @@ -15,20 +15,30 @@ */ package ghidra.framework.protocol.ghidra; -import java.io.File; import java.net.*; +import java.util.Objects; import java.util.regex.Pattern; -import ghidra.framework.OperatingSystem; -import ghidra.framework.Platform; +import org.apache.commons.lang3.StringUtils; + import ghidra.framework.model.ProjectLocator; import ghidra.framework.remote.GhidraServerHandle; +/** + * Supported URL forms include: + *

      + *
    • {@literal ghidra://:/[/]/[[#ref]]}
    • + *
    • {@literal ghidra:/[X:/]/[?[/]/[[#ref]]]}
    • + *
    + */ public class GhidraURL { public static final String PROTOCOL = "ghidra"; - private static Pattern IS_LOCAL_URL_PATTERN = Pattern.compile("^" + PROTOCOL + ":/[^/].*"); // e.g., ghidra:/path + private static final String PROTOCOL_URL_START = PROTOCOL + ":/"; + + private static Pattern IS_LOCAL_URL_PATTERN = + Pattern.compile("^" + PROTOCOL_URL_START + "[^/].*"); // e.g., ghidra:/path public static final String MARKER_FILE_EXTENSION = ".gpr"; public static final String PROJECT_DIRECTORY_EXTENSION = ".rep"; @@ -39,29 +49,29 @@ public class GhidraURL { /** * Determine if the specified URL refers to a local project and * it exists. - * @param url + * @param url ghidra URL * @return true if specified URL refers to a local project and * it exists. */ public static boolean localProjectExists(URL url) { - if (!isLocalProjectURL(url)) { - return false; - } - String path = url.getPath(); // assume path always starts with '/' - if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) { - if (path.indexOf(":/") == 2) { - path = path.substring(1); - } - } - File markerFile = new File(path + MARKER_FILE_EXTENSION); - File projectDir = new File(path + PROJECT_DIRECTORY_EXTENSION); - return (markerFile.isFile() && projectDir.isDirectory()); + ProjectLocator loc = getProjectStorageLocator(url); + return loc != null && loc.exists(); + } + + /** + * Determine if the specified string appears to be a possible ghidra URL + * (starts with "ghidra:/"). + * @param str string to be checked + * @return true if string is possible ghidra URL + */ + public static boolean isGhidraURL(String str) { + return str != null && str.startsWith(PROTOCOL_URL_START); } /** * Determine if the specified URL is a local project URL. * No checking is performed as to the existence of the project. - * @param url + * @param url ghidra URL * @return true if specified URL refers to a local * project (ghidra:/path/projectName...) */ @@ -70,83 +80,106 @@ public class GhidraURL { } /** - * Get the project name which corresponds to the specified - * local project URL. + * Get the project locator which corresponds to the specified local project URL. + * Confirm local project URL with {@link #isLocalProjectURL(URL)} prior to method use. * @param localProjectURL local Ghidra project URL - * @return project name - * @throws IllegalArgumentException URL is not a valid local project URL - */ - public static String getProjectName(URL localProjectURL) { - if (!isLocalProjectURL(localProjectURL)) { - throw new IllegalArgumentException("Invalid local Ghidra project URL"); - } - String path = localProjectURL.getPath(); - int index = path.lastIndexOf('/'); - return path.substring(index + 1); - } - - /** - * Get the project location path which corresponds to the specified - * local project URL. - * @param localProjectURL local Ghidra project URL - * @return project location path - * @throws IllegalArgumentException URL is not a valid local project URL - */ - public static String getProjectLocation(URL localProjectURL) { - if (!isLocalProjectURL(localProjectURL)) { - throw new IllegalArgumentException("Invalid local Ghidra project URL"); - } - String path = localProjectURL.getPath(); - int index = path.lastIndexOf('/'); - path = path.substring(0, index); - if (path.indexOf(":/") == 2) { - path = path.substring(1); - path = path.replace('/', File.separatorChar); - } - return path; - } - - /** - * Get the project locator which corresponds to the specified - * local project URL. - * @param localProjectURL local Ghidra project URL - * @return project locator + * @return project locator or null if invalid path specified * @throws IllegalArgumentException URL is not a valid local project URL */ public static ProjectLocator getProjectStorageLocator(URL localProjectURL) { if (!isLocalProjectURL(localProjectURL)) { throw new IllegalArgumentException("Invalid local Ghidra project URL"); } - String path = localProjectURL.getPath(); + + String path = localProjectURL.getPath(); // assume path always starts with '/' + +// if (path.indexOf(":/") == 2 && Character.isLetter(path.charAt(1))) { // check for drive letter after leading '/' +// if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) { +// path = path.substring(1); // Strip-off leading '/' +// } +// else { +// // assume drive letter separator ':' should be removed for non-windows +// path = path.substring(0, 2) + path.substring(3); +// } +// } + int index = path.lastIndexOf('/'); - String dirPath = path.substring(0, index); - if (dirPath.endsWith(":")) { - dirPath += "/"; - } - if (dirPath.indexOf(":/") == 2) { - dirPath = dirPath.substring(1); - dirPath = dirPath.replace('/', File.separatorChar); - } + String dirPath = index != 0 ? path.substring(0, index) : "/"; + String name = path.substring(index + 1); + if (name.length() == 0) { + return null; + } + return new ProjectLocator(dirPath, name); } + /** + * Get the shared repository name associated with a repository URL or null + * if not applicable. For ghidra URL extensions it is assumed that the first path element + * corresponds to the repository name. + * @param url ghidra URL for shared project resource + * @return repository name or null if not applicable to URL + */ + public static String getRepositoryName(URL url) { + if (!isServerRepositoryURL(url)) { + return null; + } + String path = url.getPath(); + if (!path.startsWith("/")) { + // handle possible ghidra protocol extension use which is assumed to encode + // repository and file path the same as standard ghidra URL. + try { + URL extensionURL = new URL(path); + path = extensionURL.getPath(); + } + catch (MalformedURLException e) { + path = ""; + } + } + path = path.substring(1); + int ix = path.indexOf("/"); + if (ix > 0) { + path = path.substring(0, ix); + } + return path; + } + /** * Determine if the specified URL is any type of server "repository" URL. * No checking is performed as to the existence of the server or repository. - * @param url + * NOTE: ghidra protocol extensions are not currently supported (e.g., ghidra:http://...). + * @param url ghidra URL * @return true if specified URL refers to a Ghidra server * repository (ghidra://host/repositoryNAME/path...) */ public static boolean isServerRepositoryURL(URL url) { + if (!isServerURL(url)) { + return false; + } String path = url.getPath(); - return isServerURL(url) && path != null && path.length() > 0; + if (StringUtils.isBlank(path)) { + return false; + } + if (!path.startsWith("/")) { + try { + URL extensionURL = new URL(path); + path = extensionURL.getPath(); + if (StringUtils.isBlank(path)) { + return false; + } + } + catch (MalformedURLException e) { + return false; + } + } + return path.charAt(0) == '/' && path.length() > 1 && path.charAt(1) != '/'; } /** * Determine if the specified URL is any type of server URL. * No checking is performed as to the existence of the server or repository. - * @param url + * @param url ghidra URL * @return true if specified URL refers to a Ghidra server * repository (ghidra://host/repositoryNAME/path...) */ @@ -157,14 +190,21 @@ public class GhidraURL { return Handler.isSupportedURL(url); } + /** + * Ensure that absolute path is specified. Any use of Windows + * separator (back-slash) will be converted to a forward-slash. + * @param path path to be checked and possibly modified. + * @return path to be used + */ private static String checkAbsolutePath(String path) { path = path.replace('\\', '/'); if (!path.startsWith("/")) { - if (path.length() >= 3 && path.substring(1).startsWith(":/")) { + if (path.length() >= 3 && path.indexOf(":/") == 1 && + Character.isLetter(path.charAt(0))) { // prepend a "/" on Windows paths (e.g., C:/mydir) path = "/" + path; } - else { + else { // absence of drive letter is tolerated even if not absolute on windows throw new IllegalArgumentException("Absolute directory path required"); } } @@ -174,24 +214,28 @@ public class GhidraURL { /** * Create a Ghidra URL from a string form of Ghidra URL or local project path. * This method can consume strings produced by the getDisplayString method. - * @param projectPathOrURL {@literal project path (/)} + * @param projectPathOrURL {@literal project path (/)} or + * string form of Ghidra URL. * @return local Ghidra project URL * @see #getDisplayString(URL) * @throws IllegalArgumentException invalid path or URL specified */ public static URL toURL(String projectPathOrURL) { - String path = projectPathOrURL; - if (!path.startsWith(PROTOCOL + ":")) { - path = checkAbsolutePath(projectPathOrURL); - int index = path.lastIndexOf('/'); - if (index <= 0 || index == (path.length() - 1)) { - // Ensure that path includes projectName - throw new IllegalArgumentException("Invalid project path or URL"); + if (!projectPathOrURL.startsWith(PROTOCOL + ":")) { + if (projectPathOrURL.endsWith(ProjectLocator.PROJECT_DIR_SUFFIX) || + projectPathOrURL.endsWith(ProjectLocator.PROJECT_FILE_SUFFIX)) { + String ext = projectPathOrURL.substring(projectPathOrURL.lastIndexOf('.')); + throw new IllegalArgumentException("Project path must omit extension: " + ext); } - path = PROTOCOL + ":" + path; + if (projectPathOrURL.contains("?") || projectPathOrURL.contains("#")) { + throw new IllegalArgumentException("Unsupported query/ref used with project path"); + } + projectPathOrURL = checkAbsolutePath(projectPathOrURL); + String[] splitName = splitOffName(projectPathOrURL); + return makeURL(splitName[0], splitName[1]); } try { - return new URL(path); + return new URL(projectPathOrURL); } catch (MalformedURLException e) { throw new IllegalArgumentException(e); @@ -199,26 +243,151 @@ public class GhidraURL { } /** - * Get a normalized URL which eliminates use of host names and additional URL refs + * Get normalized URL which corresponds to the local-project or repository + * @param ghidraUrl ghidra file/folder URL (server-only URL not permitted) + * @return local-project or repository URL + */ + public static URL getProjectURL(URL ghidraUrl) { + if (!PROTOCOL.equals(ghidraUrl.getProtocol())) { + throw new IllegalArgumentException("ghidra protocol required"); + } + + if (isLocalProjectURL(ghidraUrl)) { + String urlStr = ghidraUrl.toExternalForm(); + int queryIx = urlStr.indexOf('?'); + if (queryIx < 0) { + return ghidraUrl; + } + urlStr = urlStr.substring(0, queryIx); + try { + return new URL(urlStr); + } + catch (MalformedURLException e) { + throw new RuntimeException(e); // unexpected + } + } + + if (isServerRepositoryURL(ghidraUrl)) { + + String path = ghidraUrl.getPath(); + // handle possible ghidra protocol extension use which is assumed to encode + // repository and file path the same as standard ghidra URL. + if (!path.startsWith("/")) { + try { + URL extensionURL = new URL(path); + path = extensionURL.getPath(); + } + catch (MalformedURLException e) { + path = "/"; + } + } + + // Truncate ghidra URL + String urlStr = ghidraUrl.toExternalForm(); + + String tail = null; + int ix = path.indexOf('/', 1); + if (ix > 0) { + // identify path tail to be removed + tail = path.substring(ix); + } + + int refIx = urlStr.indexOf('#'); + if (refIx > 0) { + urlStr = urlStr.substring(0, refIx); + } + int queryIx = urlStr.indexOf('?'); + if (queryIx > 0) { + urlStr = urlStr.substring(0, queryIx); + } + + if (tail != null) { + urlStr = urlStr.substring(0, urlStr.lastIndexOf(tail)); + } + try { + return new URL(urlStr); + } + catch (MalformedURLException e) { + // ignore + } + } + + throw new IllegalArgumentException("Invalid project/repository URL: " + ghidraUrl); + } + + /** + * Get the project pathname referenced by the specified Ghidra file/folder URL. + * If path is missing root folder is returned. + * @param ghidraUrl ghidra file/folder URL (server-only URL not permitted) + * @return pathname of file or folder + */ + public static String getProjectPathname(URL ghidraUrl) { + + if (isLocalProjectURL(ghidraUrl)) { + String query = ghidraUrl.getQuery(); + return StringUtils.isBlank(query) ? "/" : query; + } + + if (isServerRepositoryURL(ghidraUrl)) { + String path = ghidraUrl.getPath(); + // handle possible ghidra protocol extension use + if (!path.startsWith("/")) { + try { + URL extensionURL = new URL(path); + path = extensionURL.getPath(); + } + catch (MalformedURLException e) { + path = "/"; + } + } + // skip repo name (first path element) + int ix = path.indexOf('/', 1); + if (ix > 1) { + return path.substring(ix); + } + return "/"; + } + + throw new IllegalArgumentException("not project/repository URL"); + } + + /** + * Get hostname as an IP address if possible + * @param host hostname + * @return host IP address or original host name + */ + private static String getHostAsIpAddress(String host) { + if (!StringUtils.isBlank(host)) { + try { + InetAddress addr = InetAddress.getByName(host); + host = addr.getHostAddress(); + } + catch (UnknownHostException e) { + // just use hostname if unable to resolve + } + } + return host; + } + + /** + * Get a normalized URL which eliminates use of host names and optional URL ref * which may prevent direct comparison. - * @param url + * @param url ghidra URL * @return normalized url */ public static URL getNormalizedURL(URL url) { - if (!isServerRepositoryURL(url)) { - // TODO: May need to add support for other ghidra URL forms - return url; - } String host = url.getHost(); - try { - InetAddress addr = InetAddress.getByName(host); - host = addr.getHostAddress(); + String revisedHost = getHostAsIpAddress(host); + if (Objects.equals(host, revisedHost) && url.getRef() == null) { + return url; // no change } - catch (UnknownHostException e) { - // just use hostname if unable to resolve + String file = url.getPath(); + String query = url.getQuery(); + if (!StringUtils.isBlank(query)) { + file += "?" + query; } try { - return new URL(PROTOCOL, host, url.getPort(), url.getPath()); + return new URL(PROTOCOL, revisedHost, url.getPort(), file); } catch (MalformedURLException e) { throw new RuntimeException(e); @@ -228,14 +397,15 @@ public class GhidraURL { /** * Generate preferred display string for Ghidra URLs. * Form can be parsed by the toURL method. - * @param url - * @return + * @param url ghidra URL + * @return formatted URL display string * @see #toURL(String) */ public static String getDisplayString(URL url) { - if (isLocalProjectURL(url)) { + if (isLocalProjectURL(url) && StringUtils.isBlank(url.getQuery()) && + StringUtils.isBlank(url.getRef())) { String path = url.getPath(); - if (path.indexOf(":/") == 2) { + if (path.indexOf(":/") == 2 && Character.isLetter(path.charAt(1))) { // assume windows path path = path.substring(1); path = path.replace('/', '\\'); @@ -245,27 +415,6 @@ public class GhidraURL { return url.toString(); } - /** - * Create a local project URL for a specified project marker file. - * @param projectMarkerFile project marker file - * @return local project URL - */ - public static URL makeURL(File projectMarkerFile) { - String name = projectMarkerFile.getName(); - if (Platform.CURRENT_PLATFORM.getOperatingSystem() == OperatingSystem.WINDOWS) { - if (!name.endsWith(MARKER_FILE_EXTENSION) && - !name.toLowerCase().endsWith(MARKER_FILE_EXTENSION)) { - throw new IllegalArgumentException("Invalid project marker file"); - } - } - else if (!name.endsWith(MARKER_FILE_EXTENSION)) { - throw new IllegalArgumentException("Invalid project marker file"); - } - name = name.substring(0, name.length() - MARKER_FILE_EXTENSION.length()); - String location = projectMarkerFile.getParentFile().getAbsolutePath(); - return makeURL(location, name); - } - /** * Create a URL which refers to a local Ghidra project * @param dirPath absolute path of project location directory @@ -273,12 +422,54 @@ public class GhidraURL { * @return local Ghidra project URL */ public static URL makeURL(String dirPath, String projectName) { - String path = checkAbsolutePath(dirPath); + return makeURL(dirPath, projectName, null, null); + } + + /** + * Create a URL which refers to a local Ghidra project + * @param projectLocator absolute project location + * @return local Ghidra project URL + * @throws IllegalArgumentException if {@code projectLocator} does not have an absolute location + */ + public static URL makeURL(ProjectLocator projectLocator) { + return makeURL(projectLocator, null, null); + } + + /** + * Create a URL which refers to a local Ghidra project with optional project file and ref + * @param projectLocation absolute path of project location directory + * @param projectName name of project + * @param projectFilePath file path (e.g., /a/b/c, may be null) + * @param ref location reference (may be null) + * @return local Ghidra project URL + */ + public static URL makeURL(String projectLocation, String projectName, String projectFilePath, + String ref) { + if (StringUtils.isBlank(projectLocation) || StringUtils.isBlank(projectName)) { + throw new IllegalArgumentException("Inavlid project location and/or name"); + } + String path = checkAbsolutePath(projectLocation); if (!path.endsWith("/")) { path += "/"; } + StringBuilder buf = new StringBuilder(PROTOCOL); + buf.append(":"); + buf.append(path); + buf.append(projectName); + + if (!StringUtils.isBlank(projectFilePath)) { + if (!projectFilePath.startsWith("/") || projectFilePath.contains("\\")) { + throw new IllegalArgumentException("Invalid project file path"); + } + buf.append("?"); + buf.append(projectFilePath); + } + if (!StringUtils.isBlank(ref)) { + buf.append("#"); + buf.append(ref); + } try { - return new URL(PROTOCOL + ":" + path + projectName); + return new URL(buf.toString()); } catch (MalformedURLException e) { throw new IllegalArgumentException(e); @@ -286,17 +477,30 @@ public class GhidraURL { } /** - * Create a URL which refers to Ghidra Server repository content. Path may correspond - * to either a file or folder. - * @param host server host name/address - * @param port optional server port (a value <= 0 refers to the default port) - * @param repositoryName repository name - * @param repositoryPath absolute folder or file path within repository. - * Folder paths should end with a '/' character. - * @return Ghidra Server repository content URL + * Create a URL which refers to a local Ghidra project with optional project file and ref + * @param projectLocator local project locator + * @param projectFilePath file path (e.g., /a/b/c, may be null) + * @param ref location reference (may be null) + * @return local Ghidra project URL + * @throws IllegalArgumentException if invalid {@code projectFilePath} specified or if URL + * instantion fails. */ - public static URL makeURL(String host, int port, String repositoryName, String repositoryPath) { - return makeURL(host, port, repositoryName, repositoryPath, null, null); + public static URL makeURL(ProjectLocator projectLocator, String projectFilePath, String ref) { + return makeURL(projectLocator.getLocation(), projectLocator.getName(), projectFilePath, + ref); + } + + private static String[] splitOffName(String path) { + String name = ""; + if (!StringUtils.isBlank(path) && !path.endsWith("/")) { + int index = path.lastIndexOf('/'); + if (index >= 0) { + // last name may or may not be a folder name + name = path.substring(index + 1); + path = path.substring(0, index); + } + } + return new String[] { path, name }; } /** @@ -305,40 +509,76 @@ public class GhidraURL { * @param host server host name/address * @param port optional server port (a value <= 0 refers to the default port) * @param repositoryName repository name - * @param repositoryPath absolute folder path within repository. - * @param fileName name of a file contained within the specified repository/path - * @param ref optional URL ref or null + * @param repositoryPath absolute folder or file path within repository. + * Folder paths should end with a '/' character. + * @return Ghidra Server repository content URL + */ + public static URL makeURL(String host, int port, String repositoryName, String repositoryPath) { + String[] splitName = splitOffName(repositoryPath); + return makeURL(host, port, repositoryName, splitName[0], splitName[1], null); + } + + /** + * Create a URL which refers to Ghidra Server repository content. Path may correspond + * to either a file or folder. + * @param host server host name/address + * @param port optional server port (a value <= 0 refers to the default port) + * @param repositoryName repository name + * @param repositoryPath absolute folder or file path within repository. + * @param ref ref or null * Folder paths should end with a '/' character. * @return Ghidra Server repository content URL */ public static URL makeURL(String host, int port, String repositoryName, String repositoryPath, - String fileName, String ref) { - if (host == null) { + String ref) { + String[] splitName = splitOffName(repositoryPath); + return makeURL(host, port, repositoryName, splitName[0], splitName[1], ref); + } + + /** + * Create a URL which refers to Ghidra Server repository content. Path may correspond + * to either a file or folder. + * @param host server host name/address + * @param port optional server port (a value <= 0 refers to the default port) + * @param repositoryName repository name + * @param repositoryFolderPath absolute folder path within repository. + * @param fileName name of a file or folder contained within the specified {@code repositoryFolderPath} + * @param ref optional URL ref or null + * Folder paths should end with a '/' character. + * @return Ghidra Server repository content URL + * @throws IllegalArgumentException if invalid arguments are specified + */ + public static URL makeURL(String host, int port, String repositoryName, + String repositoryFolderPath, String fileName, String ref) { + if (StringUtils.isBlank(host)) { throw new IllegalArgumentException("host required"); } - if (repositoryName == null) { + if (StringUtils.isBlank(repositoryName)) { throw new IllegalArgumentException("repository name required"); } if (port == 0 || port == GhidraServerHandle.DEFAULT_PORT) { port = -1; } String path = "/" + repositoryName; - if (repositoryPath != null) { - if (!repositoryPath.startsWith("/") || repositoryPath.indexOf('\\') >= 0) { + if (!StringUtils.isBlank(repositoryFolderPath)) { + if (!repositoryFolderPath.startsWith("/") || repositoryFolderPath.indexOf('\\') >= 0) { throw new IllegalArgumentException("Invalid repository path"); } - path += repositoryPath; + path += repositoryFolderPath; + if (!path.endsWith("/")) { + path += "/"; + } } - else { - path += "/"; - } - if (fileName != null) { + if (!StringUtils.isBlank(fileName)) { + if (fileName.contains("/")) { + throw new IllegalArgumentException("Invalid folder/file name: " + fileName); + } if (!path.endsWith("/")) { path += "/"; } path += fileName; } - if (ref != null) { + if (!StringUtils.isBlank(ref)) { path += "#" + ref; } try { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java index e210fa05cb..e3e75b906c 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLConnection.java @@ -21,30 +21,85 @@ import java.net.*; import ghidra.framework.client.*; import ghidra.framework.data.ProjectFileManager; import ghidra.framework.model.ProjectData; -import ghidra.framework.model.ProjectLocator; -import ghidra.util.NotOwnerException; import ghidra.util.exception.AssertException; public class GhidraURLConnection extends URLConnection { + /** + * Connection status codes + */ + public enum StatusCode { + OK(20, "OK"), + /** + * Ghidra Status-Code 401: Unauthorized. + * This status code occurs when repository access is denied. + */ + UNAUTHORIZED(401, "Unauthorized"), + /** + * Ghidra Status-Code 404: Not Found. + * This status code occurs when repository or project does not exist. + */ + NOT_FOUND(404, "Not Found"), + /** + * Ghidra Status-Code 423: Locked. + * This status code occurs when project is locked (i.e., in use). + */ + LOCKED(423, "Locked Project"), + /** + * Ghidra Status-Code 503: Unavailable. + * This status code includes a variety of connection errors + * which are reported/logged by the Ghidra Server support code. + */ + UNAVAILABLE(503, "Unavailable"); + + private int code; + private String description; + + private StatusCode(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + } + // TODO: consider implementing request and response headers - /** - * Ghidra Status-Code 200: OK. - */ - public static final int GHIDRA_OK = 200; - - /** - * Ghidra Status-Code 401: Unauthorized. - * This response code includes a variety of connection errors - * which are reported/logged by the Ghidra Server support code. - */ - public static final int GHIDRA_UNAUTHORIZED = 401; - - /** - * Ghidra Status-Code 404: Not Found. - */ - public static final int GHIDRA_NOT_FOUND = 404; +// /** +// * Ghidra Status-Code 200: OK. +// */ +// public static final int GHIDRA_OK = 200; +// +// /** +// * Ghidra Status-Code 401: Unauthorized. +// * This response code includes a variety of connection errors +// * which are reported/logged by the Ghidra Server support code. +// */ +// public static final int GHIDRA_UNAUTHORIZED = 401; +// +// /** +// * Ghidra Status-Code 404: Not Found. +// */ +// public static final int GHIDRA_NOT_FOUND = 404; +// +// /** +// * Ghidra Status-Code 423: Locked +// * Caused by attempt to open local project data with write-access when project is +// * already opened and locked. +// */ +// public static final int GHIDRA_LOCKED = 423; +// +// /** +// * Ghidra Status-Code 503: Unavailable +// * Caused by other connection failure +// */ +// public static final int GHIDRA_UNAVAILABLE = 503; /** * Ghidra content type - domain folder/file wrapped within GhidraURLWrappedContent object. @@ -58,7 +113,7 @@ public class GhidraURLConnection extends URLConnection { */ public static final String REPOSITORY_SERVER_CONTENT = "RepositoryServer"; - private int responseCode = -1; + private StatusCode statusCode = null; private GhidraProtocolConnector protocolConnector; @@ -110,16 +165,23 @@ public class GhidraURLConnection extends URLConnection { } /** - * Set the read-only state of the content. - * Extreme care must be taken when setting the state to false for local projects - * without the use of a ProjectLock. This setting is currently ignored - * for server repositories which are always read-only in Headed mode and - * read-write in Headless mode. + * Set the read-only state for this connection prior to connecting or getting content. + * The default access is read-only. Extreme care must be taken when setting the state to false + * for local projects without the use of a ProjectLock. + *

    + * NOTE: Local project URL connections only support read-only access. * @param state read-only if true, otherwise read-write + * @throws UnsupportedOperationException if an attempt is made to enable write access for + * a local project URL. + * @throws IllegalStateException if already connected */ public void setReadOnly(boolean state) { if (connected) throw new IllegalStateException("Already connected"); + if (GhidraURL.isLocalProjectURL(url) && !state) { + // local project write-access not supported due to inadequate cleanup/disposal strategy + throw new UnsupportedOperationException("write access to local projects not supported"); + } readOnly = state; } @@ -159,19 +221,19 @@ public class GhidraURLConnection extends URLConnection { } /** - * Gets the status code from a Ghidra URL response. + * Gets the status code from a Ghidra URL connect attempt. * @throws IOException if an error occurred connecting to the server. - * @return the Ghidra Status-Code, or -1 + * @return the Ghidra connection status code or null */ - public int getResponseCode() throws IOException { + public StatusCode getStatusCode() throws IOException { - if (responseCode != -1) { - return responseCode; + if (statusCode != null) { + return statusCode; } getContent(); // Ensure that we have connected to the server. - return responseCode; + return statusCode; } @Override @@ -195,7 +257,7 @@ public class GhidraURLConnection extends URLConnection { * @return URL content generally in the form of GhidraURLWrappedContent, although other * special cases may result in different content (Example: a server-only URL could result in * content class of RepositoryServerAdapter). - * @throws IOException + * @throws IOException if an IO error occurs */ @Override public Object getContent() throws IOException { @@ -214,7 +276,7 @@ public class GhidraURLConnection extends URLConnection { * failure to do so may prevent release of repository handle to server. * Only a single call to this method is permitted. * @return transient project data or null if unavailable - * @throws IOException + * @throws IOException if an IO error occurs */ public ProjectData getProjectData() throws IOException { @@ -228,24 +290,6 @@ public class GhidraURLConnection extends URLConnection { return projectData; } - private void localConnect(ProjectLocator localProjectLocator) throws IOException { - - responseCode = protocolConnector.connect(readOnly); - if (responseCode != GHIDRA_OK) { - return; - } - - try { - projectData = new ProjectFileManager(localProjectLocator, !readOnly, false); - - refObject = new GhidraURLWrappedContent(this); - responseCode = GHIDRA_OK; - } - catch (NotOwnerException e) { - responseCode = GHIDRA_UNAUTHORIZED; - } - } - @Override public void connect() throws IOException { @@ -258,9 +302,13 @@ public class GhidraURLConnection extends URLConnection { if (protocolConnector instanceof DefaultLocalGhidraProtocolConnector) { // local project connection DefaultLocalGhidraProtocolConnector localConnector = - ((DefaultLocalGhidraProtocolConnector) protocolConnector); - localConnect(localConnector.getLocalProjectLocator()); + (DefaultLocalGhidraProtocolConnector) protocolConnector; + projectData = localConnector.getLocalProjectData(readOnly); + statusCode = localConnector.getStatusCode(); connected = true; + if (statusCode == StatusCode.OK) { + refObject = new GhidraURLWrappedContent(this); + } return; } @@ -268,9 +316,9 @@ public class GhidraURLConnection extends URLConnection { if (repoName == null) { // assume only server adapter connection without repository name specified // any RepositoryServerAdapter caching is responsibility of connector - responseCode = protocolConnector.connect(readOnly); + statusCode = protocolConnector.connect(readOnly); connected = true; - if (responseCode == GHIDRA_OK) { + if (statusCode == StatusCode.OK) { refObject = protocolConnector.getRepositoryServerAdapter(); if (refObject == null) { throw new AssertException("expected RepositoryServerAdapter content"); @@ -292,9 +340,9 @@ public class GhidraURLConnection extends URLConnection { transientProjectManager.getTransientProject(protocolConnector, readOnly); connected = true; - responseCode = protocolConnector.getResponseCode(); + statusCode = protocolConnector.getStatusCode(); - if (responseCode != GHIDRA_OK) { + if (statusCode != StatusCode.OK) { return; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLWrappedContent.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLWrappedContent.java index 9946bc4404..8e919c29a5 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLWrappedContent.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLWrappedContent.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +15,14 @@ */ package ghidra.framework.protocol.ghidra; -import ghidra.framework.model.*; -import ghidra.util.InvalidNameException; -import ghidra.util.exception.NotFoundException; - +import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import ghidra.framework.model.*; +import ghidra.util.InvalidNameException; + /** * GhidraURLWrappedContent provides controlled access to a Ghidra folder/file * associated with a Ghidra URL. It is important to note the issuance of this object does @@ -81,9 +80,11 @@ public class GhidraURLWrappedContent { throw new RuntimeException("consumer not found"); } + /** + * Close associated {@link ProjectData} when all consumers have released wrapped object. + */ private void closeProjectData() { - // Local project data can only be closed once and is not handled here - if (projectData instanceof TransientProjectData) { + if (projectData != null) { projectData.close(); } projectData = null; @@ -103,7 +104,7 @@ public class GhidraURLWrappedContent { return folder; } - private void resolve() throws IOException, NotFoundException { + private void resolve() throws IOException, FileNotFoundException { if (projectData != null) { return; @@ -125,13 +126,13 @@ public class GhidraURLWrappedContent { catch (InvalidNameException e) { // TODO: URL folder path is invalid closeProjectData(); - throw new NotFoundException("URL specifies invalid path: " + folderPath); + throw new IOException("URL specifies invalid path: " + folderPath); } } if (folder == null) { // TODO: URL location not found closeProjectData(); - throw new NotFoundException("URL specifies unknown path: " + folderPath); + throw new FileNotFoundException("URL specifies unknown path: " + folderPath); } if (folderItemName == null) { @@ -145,22 +146,29 @@ public class GhidraURLWrappedContent { return; } + DomainFolder subfolder = folder.getFolder(folderItemName); + if (subfolder != null) { + refObject = subfolder; + return; + } + closeProjectData(); - throw new NotFoundException("URL specifies unknown path: " + folderPath); + throw new FileNotFoundException("URL specifies unknown path: " + folderPath); } /** * Get the domain folder or file associated with the Ghidra URL. * The consumer is responsible for releasing the content object via the release method - * when it is no longer in use. + * when it is no longer in use (see {@link #release(Object, Object)}}). * @param consumer object which is responsible for releasing the content * @return domain file or folder - * @throws IOException - * @throws NotFoundException if the Ghidra URL does no correspond to a folder or file + * @throws IOException if an IO error occurs + * @throws FileNotFoundException if the Ghidra URL does no correspond to a folder or file * within the Ghidra repository/project. * @see #release(Object, Object) */ - public synchronized Object getContent(Object consumer) throws IOException, NotFoundException { + public synchronized Object getContent(Object consumer) + throws IOException, FileNotFoundException { addConsumer(consumer); boolean success = false; try { @@ -180,8 +188,8 @@ public class GhidraURLWrappedContent { * no longer in-use and the underlying connection may be closed. A read-only * or immutable domain object may remain open and in-use after its associated * domain folder/file has been released. - * @param content - * @param consumer + * @param content object obtained via {@link #getContent(Object)} + * @param consumer object consumer which was specified to {@link #getContent(Object)} */ public synchronized void release(Object content, Object consumer) { if (content == null || content != refObject) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/RepositoryInfo.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/RepositoryInfo.java index 5021420aa6..045ae46e55 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/RepositoryInfo.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/RepositoryInfo.java @@ -29,6 +29,14 @@ public class RepositoryInfo { this.readOnly = readOnly; } + /** + * Get the Ghidra URL which corresponds to the repository + * @return repository URL + */ + public URL getURL() { + return repositoryURL; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof RepositoryInfo)) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectData.java index 389b7a4f15..411bf33734 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectData.java @@ -22,6 +22,7 @@ import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.data.ProjectFileManager; import ghidra.framework.model.ProjectLocator; import ghidra.framework.remote.RepositoryHandle; +import ghidra.framework.store.LockException; import ghidra.util.Msg; import ghidra.util.SystemUtilities; import utilities.util.FileUtilities; @@ -38,7 +39,8 @@ public class TransientProjectData extends ProjectFileManager { private GhidraSwinglessTimer cleanupTimer; TransientProjectData(TransientProjectManager dataMgr, ProjectLocator tmpProjectLocation, - RepositoryInfo repositoryInfo, RepositoryAdapter repository) throws IOException { + RepositoryInfo repositoryInfo, RepositoryAdapter repository) + throws IOException, LockException { // Resulting data is read-only in GUI mode, read-write in Headless mode // Allowing more control will cause issues for caching of transient project data - // although we could use two caches one for each mode diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java index f53ca14da5..fbd2a35c3c 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java @@ -25,6 +25,8 @@ import java.util.Set; import ghidra.framework.client.NotConnectedException; import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.model.ProjectLocator; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; +import ghidra.framework.store.LockException; import ghidra.util.Msg; import ghidra.util.SystemUtilities; import ghidra.util.datastruct.WeakValueHashMap; @@ -94,17 +96,11 @@ public class TransientProjectManager { * @param protocolConnector Ghidra protocol connector * @param readOnly true if project data should be treated as read-only * @return transient project data - * @throws IOException + * @throws IOException if an IO error occurs */ synchronized TransientProjectData getTransientProject(GhidraProtocolConnector protocolConnector, boolean readOnly) throws IOException { - TransientProjectData projectData; - - // try to avoid excessive accumulation of unreferenced transient project data. - // It is assumed that calls to this method are generally infrequent and may be slow - System.gc(); - String repoName = protocolConnector.getRepositoryName(); if (repoName == null) { throw new IllegalArgumentException( @@ -114,17 +110,18 @@ public class TransientProjectManager { RepositoryInfo repositoryInfo = new RepositoryInfo(protocolConnector.getRepositoryRootGhidraURL(), repoName, readOnly); - projectData = repositoryMap.get(repositoryInfo); + TransientProjectData projectData = repositoryMap.get(repositoryInfo); if (projectData == null || !projectData.stopCleanupTimer()) { // cleanup suspended - if (protocolConnector.connect(readOnly) != GhidraURLConnection.GHIDRA_OK) { - return null; + StatusCode statusCode = protocolConnector.connect(readOnly); + if (statusCode != StatusCode.OK) { + throw new NotConnectedException(statusCode.getDescription()); } RepositoryAdapter repositoryAdapter = protocolConnector.getRepositoryAdapter(); if (repositoryAdapter == null || !repositoryAdapter.isConnected()) { - throw new NotConnectedException("protocol connector not connected to repository"); + throw new NotConnectedException("Not connected to repository"); } projectData = createTransientProject(repositoryAdapter, repositoryInfo); @@ -182,7 +179,12 @@ public class TransientProjectManager { ProjectLocator tmpProjectLocation = new TransientProjectStorageLocator( tmp.getParentFile().getAbsolutePath(), tmp.getName(), repositoryInfo); - return new TransientProjectData(this, tmpProjectLocation, repositoryInfo, repository); + try { + return new TransientProjectData(this, tmpProjectLocation, repositoryInfo, repository); + } + catch (LockException e) { + throw new IOException(e); // unexpected for transient project storage + } } private static class TransientProjectStorageLocator extends ProjectLocator { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java b/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java new file mode 100644 index 0000000000..6722c55c15 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java @@ -0,0 +1,30 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.exception; + +import java.io.IOException; + +/** + * BadLinkException occurs when a link-file expected linked content type does not + * match the actual content type of the linked file. + */ +public class BadLinkException extends IOException { + + public BadLinkException(String msg) { + super(msg); + } + +} diff --git a/Ghidra/Framework/Project/src/main/resources/images/link.png b/Ghidra/Framework/Project/src/main/resources/images/link.png new file mode 100644 index 0000000000..024b295283 Binary files /dev/null and b/Ghidra/Framework/Project/src/main/resources/images/link.png differ diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java index 0ec40eb501..5e4ec72188 100644 --- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java @@ -17,6 +17,7 @@ package ghidra.framework.model; import java.io.File; import java.io.IOException; +import java.net.URL; import java.util.ArrayList; import java.util.Map; @@ -100,11 +101,26 @@ public class TestDummyDomainFile implements DomainFile { throw new UnsupportedOperationException(); } + @Override + public URL getSharedProjectURL() { + throw new UnsupportedOperationException(); + } + @Override public String getContentType() { throw new UnsupportedOperationException(); } + @Override + public boolean isLinkFile() { + throw new UnsupportedOperationException(); + } + + @Override + public DomainFolder followLink() { + throw new UnsupportedOperationException(); + } + @Override public Class getDomainObjectClass() { throw new UnsupportedOperationException(); @@ -223,11 +239,6 @@ public class TestDummyDomainFile implements DomainFile { return isReadOnly; } - @Override - public boolean isVersionControlSupported() { - throw new UnsupportedOperationException(); - } - @Override public synchronized boolean isVersioned() { return isVersioned; @@ -325,6 +336,16 @@ public class TestDummyDomainFile implements DomainFile { throw new UnsupportedOperationException(); } + @Override + public boolean isLinkingSupported() { + throw new UnsupportedOperationException(); + } + + @Override + public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, CancelledException { diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java index 6f8fad1d82..df115ddac6 100644 --- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java @@ -17,6 +17,7 @@ package ghidra.framework.model; import java.io.File; import java.io.IOException; +import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -82,6 +83,11 @@ public class TestDummyDomainFolder implements DomainFolder { return "/"; } + @Override + public URL getSharedProjectURL() { + throw new UnsupportedOperationException(); + } + @Override public boolean isInWritableProject() { throw new UnsupportedOperationException(); @@ -162,6 +168,11 @@ public class TestDummyDomainFolder implements DomainFolder { throw new UnsupportedOperationException(); } + @Override + public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public void setActive() { throw new UnsupportedOperationException(); diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java index 3a25bb8ccb..ca01d34d90 100644 --- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java @@ -16,7 +16,6 @@ package ghidra.framework.model; import java.io.IOException; -import java.net.URL; import java.util.List; import ghidra.framework.client.RepositoryAdapter; @@ -84,12 +83,6 @@ public class TestDummyProjectData implements ProjectData { return null; } - @Override - public URL getSharedFileURL(String path) { - // stub - return null; - } - @Override public String makeValidName(String name) { // stub diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveContentHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveContentHandler.java index 623a2e421e..f5d328b6c8 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveContentHandler.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveContentHandler.java @@ -24,7 +24,8 @@ import db.DBHandle; import db.buffers.BufferFile; import db.buffers.ManagedBufferFile; import generic.theme.GIcon; -import ghidra.framework.data.*; +import ghidra.framework.data.DBContentHandler; +import ghidra.framework.data.DomainObjectMergeManager; import ghidra.framework.model.ChangeSet; import ghidra.framework.model.DomainObject; import ghidra.framework.store.*; @@ -39,13 +40,19 @@ import ghidra.util.task.TaskMonitor; * and FolderItem storage. This class also produces the appropriate Icon for * DataTypeArchive files. */ -public class DataTypeArchiveContentHandler extends DBContentHandler { +public class DataTypeArchiveContentHandler extends DBContentHandler { - private static Icon DATA_TYPE_ARCHIVE_ICON; + static Icon DATA_TYPE_ARCHIVE_ICON = new GIcon("icon.content.handler.archive.dt"); - private final static String PROGRAM_ICON_ID = "icon.content.handler.archive.dt"; public final static String DATA_TYPE_ARCHIVE_CONTENT_TYPE = "Archive"; + final static Class DATA_TYPE_ARCHIVE_DOMAIN_OBJECT_CLASS = + DataTypeArchiveDB.class; + final static String DATA_TYPE_ARCHIVE_CONTENT_DEFAULT_TOOL = "CodeBrowser"; + + private static final DataTypeArchiveLinkContentHandler linkHandler = + new DataTypeArchiveLinkContentHandler(); + @Override public long createFile(FileSystem fs, FileSystem userfs, String path, String name, DomainObject obj, TaskMonitor monitor) @@ -59,7 +66,7 @@ public class DataTypeArchiveContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version, + public DataTypeArchiveDB getImmutableObject(FolderItem item, Object consumer, int version, int minChangeVersion, TaskMonitor monitor) throws IOException, VersionException, CancelledException { String contentType = item.getContentType(); @@ -113,7 +120,7 @@ public class DataTypeArchiveContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, + public DataTypeArchiveDB getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, Object consumer, TaskMonitor monitor) throws IOException, VersionException, CancelledException { @@ -168,7 +175,7 @@ public class DataTypeArchiveContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, + public DataTypeArchiveDB getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, boolean okToUpgrade, boolean recover, Object consumer, TaskMonitor monitor) throws IOException, VersionException, CancelledException { @@ -323,8 +330,8 @@ public class DataTypeArchiveContentHandler extends DBContentHandler { } @Override - public Class getDomainObjectClass() { - return DataTypeArchiveDB.class; + public Class getDomainObjectClass() { + return DATA_TYPE_ARCHIVE_DOMAIN_OBJECT_CLASS; } @Override @@ -339,16 +346,11 @@ public class DataTypeArchiveContentHandler extends DBContentHandler { @Override public String getDefaultToolName() { - return "CodeBrowser"; + return DATA_TYPE_ARCHIVE_CONTENT_DEFAULT_TOOL; } @Override public Icon getIcon() { - synchronized (DataTypeArchiveContentHandler.class) { - if (DATA_TYPE_ARCHIVE_ICON == null) { - DATA_TYPE_ARCHIVE_ICON = new GIcon(PROGRAM_ICON_ID); - } - } return DATA_TYPE_ARCHIVE_ICON; } @@ -364,4 +366,9 @@ public class DataTypeArchiveContentHandler extends DBContentHandler { originalObj, latestObj); } + @Override + public DataTypeArchiveLinkContentHandler getLinkHandler() { + return linkHandler; + } + } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java new file mode 100644 index 0000000000..7c64fdf1ac --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java @@ -0,0 +1,71 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database; + +import java.io.IOException; + +import javax.swing.Icon; + +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.URLLinkObject; +import ghidra.framework.model.DomainObject; +import ghidra.framework.store.FileSystem; +import ghidra.util.InvalidNameException; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class DataTypeArchiveLinkContentHandler extends LinkHandler { + + public static final String ARCHIVE_LINK_CONTENT_TYPE = "ArchiveLink"; + + @Override + public long createFile(FileSystem fs, FileSystem userfs, String path, String name, + DomainObject obj, TaskMonitor monitor) + throws IOException, InvalidNameException, CancelledException { + if (!(obj instanceof URLLinkObject)) { + throw new IOException("Unsupported domain object: " + obj.getClass().getName()); + } + return createFile((URLLinkObject) obj, ARCHIVE_LINK_CONTENT_TYPE, fs, path, name, + monitor); + } + + @Override + public String getContentType() { + return ARCHIVE_LINK_CONTENT_TYPE; + } + + @Override + public String getContentTypeDisplayString() { + return "Data Type Archive Link"; + } + + @Override + public Class getDomainObjectClass() { + // return linked content class + return DataTypeArchiveContentHandler.DATA_TYPE_ARCHIVE_DOMAIN_OBJECT_CLASS; + } + + @Override + public Icon getIcon() { + return DataTypeArchiveContentHandler.DATA_TYPE_ARCHIVE_ICON; + } + + @Override + public String getDefaultToolName() { + return DataTypeArchiveContentHandler.DATA_TYPE_ARCHIVE_CONTENT_DEFAULT_TOOL; + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramContentHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramContentHandler.java index 7f124b27d9..a7d6c10c0b 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramContentHandler.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramContentHandler.java @@ -23,7 +23,8 @@ import db.*; import db.buffers.BufferFile; import db.buffers.ManagedBufferFile; import generic.theme.GIcon; -import ghidra.framework.data.*; +import ghidra.framework.data.DBWithUserDataContentHandler; +import ghidra.framework.data.DomainObjectMergeManager; import ghidra.framework.model.ChangeSet; import ghidra.framework.model.DomainObject; import ghidra.framework.store.*; @@ -38,11 +39,16 @@ import ghidra.util.task.TaskMonitor; * and FolderItem storage. This class also produces the appropriate Icon for * Program files. */ -public class ProgramContentHandler extends DBContentHandler { +public class ProgramContentHandler extends DBWithUserDataContentHandler { + + public static final String PROGRAM_CONTENT_TYPE = "Program"; public static Icon PROGRAM_ICON = new GIcon("icon.content.handler.program"); - public static final String PROGRAM_CONTENT_TYPE = "Program"; + static final Class PROGRAM_DOMAIN_OBJECT_CLASS = ProgramDB.class; + static final String PROGRAM_CONTENT_DEFAULT_TOOL = "CodeBrowser"; + + private static final ProgramLinkContentHandler linkHandler = new ProgramLinkContentHandler(); @Override public long createFile(FileSystem fs, FileSystem userfs, String path, String name, @@ -52,11 +58,12 @@ public class ProgramContentHandler extends DBContentHandler { if (!(obj instanceof ProgramDB)) { throw new IOException("Unsupported domain object: " + obj.getClass().getName()); } - return createFile((ProgramDB) obj, PROGRAM_CONTENT_TYPE, fs, path, name, monitor); + return createFile((ProgramDB) obj, PROGRAM_CONTENT_TYPE, fs, path, name, + monitor); } @Override - public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version, + public ProgramDB getImmutableObject(FolderItem item, Object consumer, int version, int minChangeVersion, TaskMonitor monitor) throws IOException, VersionException, CancelledException { String contentType = item.getContentType(); @@ -107,7 +114,7 @@ public class ProgramContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, + public ProgramDB getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, Object consumer, TaskMonitor monitor) throws IOException, VersionException, CancelledException { @@ -161,7 +168,7 @@ public class ProgramContentHandler extends DBContentHandler { } @Override - public DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, + public ProgramDB getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, boolean okToUpgrade, boolean recover, Object consumer, TaskMonitor monitor) throws IOException, VersionException, CancelledException { @@ -316,8 +323,8 @@ public class ProgramContentHandler extends DBContentHandler { } @Override - public Class getDomainObjectClass() { - return ProgramDB.class; + public Class getDomainObjectClass() { + return PROGRAM_DOMAIN_OBJECT_CLASS; } @Override @@ -327,12 +334,12 @@ public class ProgramContentHandler extends DBContentHandler { @Override public String getContentTypeDisplayString() { - return "Program"; + return PROGRAM_CONTENT_TYPE; } @Override public String getDefaultToolName() { - return "CodeBrowser"; + return PROGRAM_CONTENT_DEFAULT_TOOL; } @Override @@ -352,4 +359,9 @@ public class ProgramContentHandler extends DBContentHandler { originalObj, latestObj); } + @Override + public ProgramLinkContentHandler getLinkHandler() { + return linkHandler; + } + } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java new file mode 100644 index 0000000000..09d02a94c1 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java @@ -0,0 +1,71 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.program.database; + +import java.io.IOException; + +import javax.swing.Icon; + +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.URLLinkObject; +import ghidra.framework.model.DomainObject; +import ghidra.framework.store.FileSystem; +import ghidra.util.InvalidNameException; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class ProgramLinkContentHandler extends LinkHandler { + + public static final String PROGRAM_LINK_CONTENT_TYPE = "ProgramLink"; + + @Override + public long createFile(FileSystem fs, FileSystem userfs, String path, String name, + DomainObject obj, TaskMonitor monitor) + throws IOException, InvalidNameException, CancelledException { + if (!(obj instanceof URLLinkObject)) { + throw new IOException("Unsupported domain object: " + obj.getClass().getName()); + } + return createFile((URLLinkObject) obj, PROGRAM_LINK_CONTENT_TYPE, fs, path, name, + monitor); + } + + @Override + public String getContentType() { + return PROGRAM_LINK_CONTENT_TYPE; + } + + @Override + public String getContentTypeDisplayString() { + return PROGRAM_LINK_CONTENT_TYPE; + } + + @Override + public Class getDomainObjectClass() { + // return linked content class + return ProgramContentHandler.PROGRAM_DOMAIN_OBJECT_CLASS; + } + + @Override + public Icon getIcon() { + return ProgramContentHandler.PROGRAM_ICON; + } + + @Override + public String getDefaultToolName() { + return ProgramContentHandler.PROGRAM_CONTENT_DEFAULT_TOOL; + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java index d866d23408..af1083569b 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.util.*; import db.*; -import ghidra.framework.data.ContentHandler; import ghidra.framework.data.DomainObjectAdapterDB; import ghidra.framework.store.FileSystem; import ghidra.program.database.map.AddressMapDB; @@ -44,6 +43,8 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData // TODO: WARNING! This implementation does not properly handle undo/redo in terms of cache invalidation + private static ProgramContentHandler programContentHandler = new ProgramContentHandler(); + /** * DB_VERSION should be incremented any time a change is made to the overall * database schema associated with any of the managers. @@ -669,10 +670,7 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData else { FileSystem userfs = program.getAssociatedUserFilesystem(); if (userfs != null) { - ContentHandler contentHandler = getContentHandler(program); - if (contentHandler != null) { - contentHandler.saveUserDataFile(program, dbh, userfs, monitor); - } + programContentHandler.saveUserDataFile(program, dbh, userfs, monitor); setChanged(false); } } diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java index d26eb3a9b2..ed7dd59c1c 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java @@ -36,6 +36,7 @@ import docking.wizard.WizardPanel; import generic.theme.GThemeDefaults.Colors; import ghidra.app.plugin.core.archive.RestoreDialog; import ghidra.framework.data.GhidraFileData; +import ghidra.framework.data.ProjectFileManager; import ghidra.framework.main.*; import ghidra.framework.model.*; import ghidra.framework.plugintool.dialog.*; @@ -351,8 +352,13 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator { @Test public void testProjectExists() { - OkDialog.show("Project Exists", - "Cannot restore project because project named TestPrj already exists."); + runSwing(() -> { + OkDialog.show("Project Exists", + "Cannot restore project because project named TestPrj already exists."); + }, false); + + waitForSwing(); + captureDialog(); } @@ -651,25 +657,60 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator { @Test public void testViewOtherProjects() throws IOException, LockException, InvalidNameException, CancelledException { - ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT); + Project project = env.getProject(); program = env.getProgram("WinHelloCPP.exe"); ProjectData projectData = project.getProjectData(); projectData.getRootFolder().createFile("HelloCpp.exe", program, TaskMonitor.DUMMY); - project.close(); - + // Create other project to be viewed + ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT); Project otherProject = ProjectTestUtils.getProject(TEMP_DIR, OTHER_PROJECT); - Language language = getZ80_LANGUAGE(); ProjectTestUtils.createProgramFile(otherProject, "Program1", language, language.getDefaultCompilerSpec(), null); ProjectTestUtils.createProgramFile(otherProject, "Program2", language, language.getDefaultCompilerSpec(), null); + otherProject.close(); + + waitForSwing(); + + performAction("View Project", "FrontEndPlugin", false); + final GhidraFileChooser fileChooser = (GhidraFileChooser) getDialog(); + runSwing(() -> fileChooser.setSelectedFile(new File(TEMP_DIR, OTHER_PROJECT))); + pressButtonOnDialog("Select Project"); + setToolSize(500, 600); + captureToolWindow(700, 600); + + ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT); + ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT); + + } + + @Test + public void testLinkOtherProject() + throws IOException, LockException, InvalidNameException, CancelledException { + + Project project = env.getProject(); + program = env.getProgram("WinHelloCPP.exe"); + ProjectFileManager projectData = (ProjectFileManager) project.getProjectData(); + projectData.getRootFolder().createFile("HelloCpp.exe", program, TaskMonitor.DUMMY); + + // Create other project to be viewed + ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT); + Project otherProject = ProjectTestUtils.getProject(TEMP_DIR, OTHER_PROJECT); + Language language = getZ80_LANGUAGE(); + DomainFile otherFile = + ProjectTestUtils.createProgramFile(otherProject, "Program1", language, + language.getDefaultCompilerSpec(), null); + ProjectTestUtils.createProgramFile(otherProject, "Program2", language, + language.getDefaultCompilerSpec(), null); + + otherFile.copyToAsLink(projectData.getRootFolder()); otherProject.close(); - waitForSwing(); - project = ProjectTestUtils.getProject(TEMP_DIR, PROJECT_NAME); + + waitForBusyTool(tool); performAction("View Project", "FrontEndPlugin", false); final GhidraFileChooser fileChooser = (GhidraFileChooser) getDialog(); diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/VersionControlScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/VersionControlScreenShots.java index 2ea3e10283..9588524b5e 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/VersionControlScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/VersionControlScreenShots.java @@ -55,7 +55,6 @@ public class VersionControlScreenShots extends GhidraScreenShotGenerator { VersionControlDialog dialog = new VersionControlDialog(false); dialog.setCurrentFileName(FrontEndTestEnv.PROGRAM_A); - dialog.setKeepCheckboxEnabled(true); runSwing(() -> tool.showDialog(dialog), false); VersionControlDialog d = waitForDialogComponent(VersionControlDialog.class); diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java new file mode 100644 index 0000000000..9bd51ec1fa --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/LaunchUrlInToolTest.java @@ -0,0 +1,343 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.main; + +import static org.junit.Assert.*; + +import java.io.File; +import java.net.URL; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.*; + +import docking.DialogComponentProvider; +import docking.test.AbstractDockingTest; +import ghidra.app.services.CodeViewerService; +import ghidra.app.services.ProgramManager; +import ghidra.framework.data.DomainFileProxy; +import ghidra.framework.model.*; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.protocol.ghidra.GhidraURL; +import ghidra.framework.protocol.ghidra.Handler; +import ghidra.program.database.*; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.*; +import ghidra.program.util.ProgramLocation; +import ghidra.server.remote.ServerTestUtil; +import ghidra.test.*; +import ghidra.util.exception.AssertException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +public class LaunchUrlInToolTest extends AbstractGhidraHeadedIntegrationTest { + + private TestEnv env; + private ProgramDB program; + + private File serverRoot; + + private static final String FILENAME = "Test"; + private static final String FOLDER = "/"; + private static final String FILEPATH = FOLDER + FILENAME; + private static final String NAMESPACE_NAME = "foo"; + private static final String SYMBOL_NAME = "xyz"; + private static final String REF = NAMESPACE_NAME + Namespace.DELIMITER + SYMBOL_NAME; + private static final String SYMBOL_ADDR = "0x1001030"; + private static final String REPO_NAME = "Test"; + + private URL remoteFileUrl; + + @Before + public void setUp() throws Exception { + + env = new TestEnv(); + + // NOTE: Use of tool templates requires active front-end tool + env.getFrontEndTool(); + + program = (ProgramDB) buildProgram(); + Project project = env.getProject(); + DomainFolder rootFolder = project.getProjectData().getRootFolder(); + rootFolder.createFile("Test", program, TaskMonitor.DUMMY); + } + + @After + public void tearDown() { + killServer(); + env.dispose(); + } + + private Program buildProgram() throws Exception { + ToyProgramBuilder builder = new ToyProgramBuilder(FILENAME, true, ProgramBuilder._TOY); + builder.createMemory("test1", "0x1001000", 0xb000); + builder.addBytesFallthrough("0x1001010"); + builder.addBytesFallthrough("0x1001020"); + builder.addBytesFallthrough("0x1001030"); + builder.addBytesFallthrough("0x1001040"); + builder.disassemble("0x1001010", 1); + builder.disassemble("0x1001020", 1); + builder.disassemble("0x1001030", 1); + builder.disassemble("0x1001040", 1); + Program p = builder.getProgram(); + + int txId = p.startTransaction("Add Label"); + try { + AddressSpace space = p.getAddressFactory().getDefaultAddressSpace(); + Address addr = space.getAddress(SYMBOL_ADDR); + SymbolTable symbolTable = p.getSymbolTable(); + Namespace ns = + symbolTable.createNameSpace(null, NAMESPACE_NAME, SourceType.USER_DEFINED); + symbolTable.createLabel(addr, SYMBOL_NAME, ns, SourceType.USER_DEFINED); + } + catch (Exception e) { + throw new AssertException(e); + } + finally { + p.endTransaction(txId, true); + } + + return p; + } + + @Test + public void testLocalLaunchDefaultTool() throws Exception { + + Project project = env.getProject(); + setupDefaultTestTool(project); + + ProjectLocator projectLocator = env.getProject().getProjectLocator(); + + URL url = GhidraURL.makeURL(projectLocator, FILEPATH, REF); + + AtomicReference ref = new AtomicReference<>(); + runSwing(() -> { + boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI(); + ToolServices toolServices = project.getToolServices(); + ref.set(toolServices.launchDefaultToolWithURL(url)); + AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); + }); + + verifyLaunch(ref.get()); + } + + @Test + public void testLocalLaunchNamedTool() throws Exception { + + Project project = env.getProject(); + ProjectLocator projectLocator = project.getProjectLocator(); + + URL url = GhidraURL.makeURL(projectLocator, FILEPATH, REF); + + AtomicReference ref = new AtomicReference<>(); + runSwing(() -> { + boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI(); + ToolServices toolServices = project.getToolServices(); + ref.set(toolServices.launchToolWithURL(DEFAULT_TEST_TOOL_NAME, url)); + AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); + }); + + verifyLaunch(ref.get()); + } + + @Test + public void testBad1LocalLaunchDefaultTool() throws Exception { + + Project project = env.getProject(); + setupDefaultTestTool(project); + + ProjectLocator projectLocator = env.getProject().getProjectLocator(); + + URL url = GhidraURL.makeURL(projectLocator, FOLDER, null); + + ToolServices toolServices = project.getToolServices(); + PluginTool tool = toolServices.launchDefaultToolWithURL(url); + assertNull(tool); + + DialogComponentProvider dlg = waitForDialogComponent("Unsupported Content"); + assertNotNull("Error dialog expected", dlg); + runSwing(() -> dlg.close()); + } + + @Test + public void testBad2LocalLaunchDefaultTool() throws Exception { + + Project project = env.getProject(); + setupDefaultTestTool(project); + + ProjectLocator projectLocator = env.getProject().getProjectLocator(); + + URL url = GhidraURL.makeURL(projectLocator, "/x/y", null); + + ToolServices toolServices = project.getToolServices(); + PluginTool tool = toolServices.launchDefaultToolWithURL(url); + assertNull(tool); + + DialogComponentProvider dlg = waitForDialogComponent("Content Not Found"); + assertNotNull("Error dialog expected", dlg); + runSwing(() -> dlg.close()); + } + + @Test + public void testRemoteLaunchDefaultTool() throws Exception { + startServer(); // also changes user's identity + + Project project = env.getProject(); + setupDefaultTestTool(project); + + AtomicReference ref = new AtomicReference<>(); + runSwing(() -> { + boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI(); + ToolServices toolServices = project.getToolServices(); + ref.set(toolServices.launchDefaultToolWithURL(remoteFileUrl)); + AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); + }); + + verifyLaunch(ref.get()); + } + + @Test + public void testRemoteLaunchNamedTool() throws Exception { + startServer(); // also changes user's identity + + Project project = env.getProject(); + setupDefaultTestTool(project); + + AtomicReference ref = new AtomicReference<>(); + runSwing(() -> { + boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI(); + ToolServices toolServices = project.getToolServices(); + ref.set(toolServices.launchToolWithURL(DEFAULT_TEST_TOOL_NAME, remoteFileUrl)); + AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); + }); + + verifyLaunch(ref.get()); + } + + @Test + public void testRemoteBad1LaunchDefaultTool() throws Exception { + startServer(); // also changes user's identity + + Project project = env.getProject(); + setupDefaultTestTool(project); + + URL badUrl = GhidraURL.makeURL(ServerTestUtil.LOCALHOST, + ServerTestUtil.GHIDRA_TEST_SERVER_PORT, REPO_NAME, FOLDER, null, null); + + ToolServices toolServices = project.getToolServices(); + PluginTool tool = toolServices.launchDefaultToolWithURL(badUrl); + assertNull(tool); + + DialogComponentProvider dlg = waitForDialogComponent("Unsupported Content"); + assertNotNull("Error dialog expected", dlg); + runSwing(() -> dlg.close()); + } + + @Test + public void testRemoteBad2LaunchDefaultTool() throws Exception { + startServer(); // also changes user's identity + + Project project = env.getProject(); + setupDefaultTestTool(project); + + URL badUrl = GhidraURL.makeURL(ServerTestUtil.LOCALHOST, + ServerTestUtil.GHIDRA_TEST_SERVER_PORT, REPO_NAME, FOLDER, "x", REF); + + ToolServices toolServices = project.getToolServices(); + PluginTool tool = toolServices.launchDefaultToolWithURL(badUrl); + assertNull(tool); + + DialogComponentProvider dlg = waitForDialogComponent("Content Not Found"); + assertNotNull("Error dialog expected", dlg); + runSwing(() -> dlg.close()); + } + + private void verifyLaunch(PluginTool tool) throws Exception { + assertNotNull("tool failed to launch", tool); + + ProgramManager pm = tool.getService(ProgramManager.class); + assertNotNull("ProgramManager not found", pm); + + CodeViewerService codeViewer = tool.getService(CodeViewerService.class); + assertNotNull("CodeViewerService not found", codeViewer); + + ProgramLocation currentLocation = codeViewer.getCurrentLocation(); + assertNotNull("Failed to determine current location", currentLocation); + + Program p = currentLocation.getProgram(); + + // Verify that it was not directly opened via active Project + assertTrue(p.getDomainFile() instanceof DomainFileProxy); + + AddressSpace space = p.getAddressFactory().getDefaultAddressSpace(); + Address addr = space.getAddress("0x1001030"); + assertEquals(addr, currentLocation.getAddress()); + } + + private void setupDefaultTestTool(Project project) { + ToolServices toolServices = project.getToolServices(); + ProgramContentHandler handler = new ProgramContentHandler(); + ToolTemplate toolTemplate = + project.getLocalToolChest().getToolTemplate(DEFAULT_TEST_TOOL_NAME); + assertNotNull(toolTemplate); + ToolAssociationInfo info = + new ToolAssociationInfo(handler, DEFAULT_TEST_TOOL_NAME, toolTemplate, toolTemplate); + toolServices.setContentTypeToolAssociations(Set.of(info)); + } + + private void killServer() { + + if (serverRoot == null) { + return; + } + + ServerTestUtil.disposeServer(); + + FileUtilities.deleteDir(serverRoot); + } + + private void startServer() throws Exception { + + // register ghidra protocol and define remote URL to access Test file + Handler.registerHandler(); + remoteFileUrl = GhidraURL.makeURL(ServerTestUtil.LOCALHOST, + ServerTestUtil.GHIDRA_TEST_SERVER_PORT, REPO_NAME, FOLDER, FILENAME, REF); + + // Create server instance + serverRoot = new File(getTestDirectoryPath(), "TestServer"); + FileUtilities.deleteDir(serverRoot); + + // Authorized admin user "test" is predefined by ServerTestUtil.createPopulatedTestServer + ServerTestUtil.setLocalUser(ServerTestUtil.ADMIN_USER); + + ServerTestUtil.createPopulatedTestServer(serverRoot.getAbsolutePath(), + REPO_NAME, fs -> { + try { + ServerTestUtil.createRepositoryItem(fs, FILENAME, FOLDER, program); + } + catch (Exception e) { + failWithException("Failed added server content", e); + } + }); + + ServerTestUtil.startServer(serverRoot.getAbsolutePath(), + ServerTestUtil.GHIDRA_TEST_SERVER_PORT, -1, false, false, false); + + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java index 9b2dcdd337..d006558cbc 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java @@ -21,6 +21,7 @@ import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.security.KeyStore.PrivateKeyEntry; import java.util.ArrayList; +import java.util.function.Consumer; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -114,7 +115,6 @@ public class ServerTestUtil { private static class ShutdownHook extends Thread { @Override public void run() { - Msg.debug(ServerTestUtil.class, "\n\n\n\n\tSHUTDOWN HOOK RUNNING"); disposeServer(); } } @@ -626,7 +626,6 @@ public class ServerTestUtil { System.setProperty(ApplicationTrustManagerFactory.GHIDRA_CACERTS_PATH_PROPERTY, ""); - Msg.debug(ServerTestUtil.class, "disposeServer() - process exist? " + serverProcess); if (serverProcess != null) { cmdOut.dispose(); @@ -752,7 +751,7 @@ public class ServerTestUtil { * dispose method on the returned object. * @throws IOException */ - public static LocalFileSystem createRepository(String dirPath, String repoName, + private static LocalFileSystem createRepository(String dirPath, String repoName, String... userAccessLines) throws IOException { File repoDir = new File(dirPath, NamingUtilities.mangle(repoName)); @@ -767,7 +766,7 @@ public class ServerTestUtil { return repoFileSystem; } - public static void createRepositoryItem(LocalFileSystem repoFilesystem, String name, + private static void createRepositoryItem(LocalFileSystem repoFilesystem, String name, String folderPath, int numFunctions) throws Exception { ToyProgramBuilder builder = new ToyProgramBuilder(name, true); @@ -787,14 +786,7 @@ public class ServerTestUtil { setProgramHashes(program); - ContentHandler contentHandler = DomainObjectAdapter.getContentHandler(program); - long checkoutId = contentHandler.createFile(repoFilesystem, null, folderPath, name, - program, TaskMonitor.DUMMY); - LocalFolderItem item = repoFilesystem.getItem(folderPath, name); - if (item == null) { - throw new IOException("Item not found: " + FileSystem.SEPARATOR + name); - } - item.terminateCheckout(checkoutId, false); + createRepositoryItem(repoFilesystem, name, folderPath, program); } catch (CancelledException e) { throw new RuntimeException(e); // unexpected @@ -807,6 +799,19 @@ public class ServerTestUtil { } } + public static void createRepositoryItem(LocalFileSystem repoFilesystem, String name, + String folderPath, Program program) throws Exception { + + ContentHandler contentHandler = DomainObjectAdapter.getContentHandler(program); + long checkoutId = contentHandler.createFile(repoFilesystem, null, folderPath, name, + program, TaskMonitor.DUMMY); + LocalFolderItem item = repoFilesystem.getItem(folderPath, name); + if (item == null) { + throw new IOException("Item not found: " + FileSystem.SEPARATOR + name); + } + item.terminateCheckout(checkoutId, false); + } + /** * Create and populate server test repositories "Test" and "Test1" with the specified * users added. The ADMIN_USER "test" is added by default. @@ -854,6 +859,43 @@ public class ServerTestUtil { } } + /** + * Create and populate server test repositories "Test" and "Test1" with the specified + * users added. The ADMIN_USER "test" is added by default. + * @param dirPath server root + * @param repoName repository name + * @param contentProvider repository content provider callback + * (use {@link #createRepositoryItem(LocalFileSystem, String, String, Program)} to add content. + * @param users optional inclusion of USER_A and/or USER_B to be added with no authentication required + * @throws Exception + */ + public static void createPopulatedTestServer(String dirPath, String repoName, + Consumer contentProvider, String... users) throws Exception { + + Msg.info(ServerTestUtil.class, "Constructing Ghidra Server for testing: " + dirPath); + + File rootDir = new File(dirPath); + FileUtilities.deleteDir(rootDir); + FileUtilities.mkdirs(rootDir); + + String[] userArray = new String[users.length + 1]; + userArray[0] = ADMIN_USER; + System.arraycopy(users, 0, userArray, 1, users.length); + createUsers(dirPath, userArray); + + String keys[] = SSHKeyUtil.generateSSHRSAKeys(); + addSSHKeys(dirPath, keys[0], "test.key", keys[1], "test.pub"); + + LocalFileSystem repoFilesystem = createRepository(dirPath, repoName, ADMIN_USER + "=ADMIN", + USER_A + "=READ_ONLY", USER_B + "=WRITE"); + try { + contentProvider.accept(repoFilesystem); + } + finally { + repoFilesystem.dispose(); + } + } + /** * Sets dummy hash values for the given program. * diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java new file mode 100644 index 0000000000..bcd6d81151 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java @@ -0,0 +1,152 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.test; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.tree.TreePath; + +import docking.ActionContext; +import docking.test.AbstractDockingTest; +import docking.widgets.tree.GTree; +import docking.widgets.tree.GTreeNode; +import generic.test.AbstractGTest; +import generic.test.AbstractGenericTest; +import ghidra.framework.main.datatable.ProjectDataContext; +import ghidra.framework.main.datatree.*; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.framework.store.FileSystem; + +/** + * This class provides some convenience methods for interacting with a {@link DataTree}. + */ +public class DataTreeHelper { + + private boolean isFrontEndTree; + private DataTree tree; + private DomainFolderRootNode rootNode; + + public DataTreeHelper(DataTree tree, boolean isFrontEndTree) { + this.tree = tree; + this.isFrontEndTree = isFrontEndTree; + rootNode = (DomainFolderRootNode) tree.getViewRoot(); + } + + public void waitForTree() { + AbstractDockingTest.waitForTree(tree); + } + + public DomainFolder getRootFolder() { + return rootNode.getDomainFolder(); + } + + public GTree getTree() { + return tree; + } + + public GTreeNode getRootNode() { + return tree.getModelRoot(); + } + + private GTreeNode getDataTreeNodeByPath(String path) { + int len = path.length(); + if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { + throw new IllegalArgumentException( + "Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'"); + } + + GTreeNode node = rootNode; + String[] split = path.split(FileSystem.SEPARATOR); + if (split.length == 0) { + return node; + } + + for (int i = 1; i < split.length; i++) { + GTreeNode child = getChild(node, split[i]); + if (child == null) { + return null; + } + node = child; + } + return node; + } + + private GTreeNode getChild(GTreeNode parent, String name) { + return AbstractGTest.waitForValue(() -> parent.getChild(name)); + } + + public GTreeNode waitForTreeNode(String name) { + return name.startsWith(FileSystem.SEPARATOR) ? getDataTreeNodeByPath(name) + : getChild(rootNode, name); + } + + public DomainFileNode waitForFileNode(String name) { + return (DomainFileNode) waitForTreeNode(name); + } + + public DomainFolderNode waitForFolderNode(String name) { + return (DomainFolderNode) waitForTreeNode(name); + } + + public void clearTreeSelection() { + AbstractGenericTest.runSwing(() -> tree.clearSelection()); + } + + public void setTreeSelection(final TreePath[] paths) throws Exception { + tree.setSelectionPaths(paths); + waitForTree(); + } + + public void selectNodes(GTreeNode... nodes) { + tree.setSelectedNodes(nodes); + waitForTree(); + } + + public void expandNode(GTreeNode node) { + tree.expandPath(node); + waitForTree(); + } + + public ActionContext getDomainFileActionContext(GTreeNode... nodes) { + + List fileList = new ArrayList<>(); + List folderList = new ArrayList<>(); + TreePath[] treePaths = new TreePath[nodes.length]; + for (int i = 0; i < nodes.length; i++) { + GTreeNode node = nodes[i]; + treePaths[i] = node.getTreePath(); + if (node instanceof DomainFileNode) { + fileList.add(((DomainFileNode) node).getDomainFile()); + } + else if (node instanceof DomainFolderNode) { + folderList.add(((DomainFolderNode) node).getDomainFolder()); + } + } + + if (isFrontEndTree) { + boolean isActiveProject = tree.getName().equals("Data Tree"); + return new FrontEndProjectTreeContext(null, rootNode.getDomainFolder().getProjectData(), + treePaths, folderList, fileList, tree, isActiveProject); + } + + return new ProjectDataContext(null, rootNode.getDomainFolder().getProjectData(), nodes[0], + folderList, fileList, tree, true); + + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndTestEnv.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndTestEnv.java index f7141a5476..0ffc529e39 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndTestEnv.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/FrontEndTestEnv.java @@ -33,7 +33,6 @@ import generic.test.AbstractGTest; import generic.test.AbstractGenericTest; import ghidra.framework.main.FrontEndTool; import ghidra.framework.main.SharedProjectUtil; -import ghidra.framework.main.datatable.ProjectDataContext; import ghidra.framework.main.datatree.*; import ghidra.framework.model.*; import ghidra.framework.plugintool.PluginTool; @@ -61,9 +60,8 @@ public class FrontEndTestEnv { // TODO make private protected TestEnv env; protected FrontEndTool frontEndTool; - protected DataTree tree; - protected DomainFolder rootFolder; - protected GTreeNode rootNode; + protected DomainFolder rootFolder; // active project root + protected DataTreeHelper projectTreeHelper; public FrontEndTestEnv() throws Exception { this(false); @@ -79,7 +77,12 @@ public class FrontEndTestEnv { startServer(); } - tree = AbstractGenericTest.findComponent(frontEndTool.getToolFrame(), DataTree.class); + // assume read-only project views are not active and only active + // project tree is present + DataTree tree = + AbstractGenericTest.findComponent(frontEndTool.getToolFrame(), DataTree.class); + projectTreeHelper = new DataTreeHelper(tree, true); + Project project = frontEndTool.getProject(); rootFolder = project.getProjectData().getRootFolder(); @@ -87,7 +90,6 @@ public class FrontEndTestEnv { rootFolder.createFile(PROGRAM_A, p, TaskMonitor.DUMMY); p.release(this); - rootNode = tree.getViewRoot(); waitForTree(); } @@ -122,7 +124,7 @@ public class FrontEndTestEnv { } public void waitForTree() { - AbstractDockingTest.waitForTree(tree); + projectTreeHelper.waitForTree(); } public DomainFolder getRootFolder() { @@ -130,11 +132,11 @@ public class FrontEndTestEnv { } public GTree getTree() { - return tree; + return projectTreeHelper.getTree(); } public GTreeNode getRootNode() { - return tree.getModelRoot(); + return projectTreeHelper.getRootNode(); } /** @@ -142,27 +144,27 @@ public class FrontEndTestEnv { * @return the default program node named {@link #PROGRAM_A} */ public DomainFileNode getProgramNode() { - return getTreeNode(PROGRAM_A); + return waitForFileNode(PROGRAM_A); } public DomainFileNode getTreeNode(String name) { - return waitForTreeNode(name); + return projectTreeHelper.waitForFileNode(name); } - public DomainFileNode waitForTreeNode(String name) { - return (DomainFileNode) AbstractGTest.waitForValue(() -> rootNode.getChild(name)); + public GTreeNode waitForTreeNode(String name) { + return projectTreeHelper.waitForTreeNode(name); } public DomainFileNode waitForFileNode(String name) { - return (DomainFileNode) AbstractGTest.waitForValue(() -> rootNode.getChild(name)); + return projectTreeHelper.waitForFileNode(name); } public DomainFolderNode waitForFolderNode(String name) { - return (DomainFolderNode) AbstractGTest.waitForValue(() -> rootNode.getChild(name)); + return projectTreeHelper.waitForFolderNode(name); } public void clearTreeSelection() { - runSwing(() -> tree.clearSelection()); + projectTreeHelper.clearTreeSelection(); } public void waitForSwing() { @@ -182,18 +184,15 @@ public class FrontEndTestEnv { } public void setTreeSelection(final TreePath[] paths) throws Exception { - tree.setSelectionPaths(paths); - waitForTree(); + projectTreeHelper.setTreeSelection(paths); } public void selectNodes(GTreeNode... nodes) { - tree.setSelectedNodes(nodes); - waitForTree(); + projectTreeHelper.selectNodes(nodes); } public void expandNode(GTreeNode node) { - tree.expandPath(node); - waitForTree(); + projectTreeHelper.expandNode(node); } public void dispose() { @@ -312,20 +311,7 @@ public class FrontEndTestEnv { } public ActionContext getDomainFileActionContext(GTreeNode... nodes) { - List fileList = new ArrayList<>(); - List folderList = new ArrayList<>(); - for (GTreeNode node : nodes) { - if (node instanceof DomainFileNode) { - fileList.add(((DomainFileNode) node).getDomainFile()); - } - else if (node instanceof DomainFolderNode) { - folderList.add(((DomainFolderNode) node).getDomainFolder()); - } - } - - return new ProjectDataContext(null, rootFolder.getProjectData(), nodes[0], folderList, - fileList, tree, true); - + return projectTreeHelper.getDomainFileActionContext(nodes); } public FrontEndTool getFrontEndTool() { @@ -447,4 +433,30 @@ public class FrontEndTestEnv { public interface ModifyProgramCallback { public void call(Program p) throws Exception; } + + /** + * Get the named READ-ONLY project view tree helper + * @param tabName project view name (should match tab name) + * @return named READ-ONLY project view tree helper or null if view tree not found + */ + public DataTreeHelper getReadOnlyProjectTreeHelper(String tabName) { + String dataTreeName = "Data Tree: " + tabName; + DataTree tree = (DataTree) AbstractGenericTest + .findComponentByName(frontEndTool.getToolFrame(), dataTreeName); + return tree != null ? new DataTreeHelper(tree, true) : null; + } + + /** + * Get the READ-ONLY project view tree helper for first view found + * @return READ-ONLY project view tree helper for first view found or null if none displayed + */ + public DataTreeHelper getFirstReadOnlyProjectTreeHelper() { + for (DataTree tree : AbstractGenericTest.findComponents(frontEndTool.getToolFrame(), + DataTree.class)) { + if (tree.getName().startsWith("Data Tree:")) { + return new DataTreeHelper(tree, true); + } + } + return null; + } } diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java new file mode 100644 index 0000000000..2c052ed32d --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultGhidraProtocolConnectorParseTest.java @@ -0,0 +1,128 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.protocol.ghidra; + +import static org.junit.Assert.*; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.Test; + +import generic.test.AbstractGenericTest; + +public class DefaultGhidraProtocolConnectorParseTest extends AbstractGenericTest { + + static { + Handler.registerHandler(); + } + + @Test + public void testParseURL() throws Exception { + + DefaultGhidraProtocolConnector pp = + new DefaultGhidraProtocolConnector(new URL("ghidra://myhost")); + + try { + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost//")); + fail(); + } + catch (MalformedURLException e) { + // expected + } + + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/")); + assertNull(pp.getRepositoryName()); + assertNull(pp.getFolderPath()); + assertNull(pp.getFolderItemName()); + assertNull(getInstanceField("itemPath", pp)); + + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo")); + assertEquals("repo", pp.getRepositoryName()); + assertEquals("/", pp.getFolderPath()); + assertNull(pp.getFolderItemName()); + assertEquals("/", getInstanceField("itemPath", pp)); + + try { + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo//")); + fail(); + } + catch (MalformedURLException e) { + // expected + } + + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/")); + assertEquals("repo", pp.getRepositoryName()); + assertEquals("/", pp.getFolderPath()); + assertNull(pp.getFolderItemName()); + assertEquals("/", getInstanceField("itemPath", pp)); + + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a")); + assertEquals("repo", pp.getRepositoryName()); + assertEquals("/", pp.getFolderPath()); + assertEquals("a", pp.getFolderItemName()); + assertEquals("/a", getInstanceField("itemPath", pp)); + + try { + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a//")); + fail(); + } + catch (MalformedURLException e) { + // expected + } + + try { + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a///")); + fail(); + } + catch (MalformedURLException e) { + // expected + } + + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/")); + assertEquals("repo", pp.getRepositoryName()); + assertEquals("/a", pp.getFolderPath()); + assertNull(pp.getFolderItemName()); + assertEquals("/a/", getInstanceField("itemPath", pp)); + + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b")); + assertEquals("repo", pp.getRepositoryName()); + assertEquals("/a", pp.getFolderPath()); + assertEquals("b", pp.getFolderItemName()); + assertEquals("/a/b", getInstanceField("itemPath", pp)); + + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b/")); + assertEquals("repo", pp.getRepositoryName()); + assertEquals("/a/b", pp.getFolderPath()); + assertNull(pp.getFolderItemName()); + assertEquals("/a/b/", getInstanceField("itemPath", pp)); + + try { + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b//")); + fail(); + } + catch (MalformedURLException e) { + // expected + } + + pp = new DefaultGhidraProtocolConnector(new URL("ghidra://myhost/repo/a/b#junk")); + assertEquals("repo", pp.getRepositoryName()); + assertEquals("/a", pp.getFolderPath()); + assertEquals("b", pp.getFolderItemName()); + assertEquals("/a/b", getInstanceField("itemPath", pp)); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java new file mode 100644 index 0000000000..f9442e70a2 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/DefaultLocalGhidraProtocolConnectorParseTest.java @@ -0,0 +1,77 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.protocol.ghidra; + +import static org.junit.Assert.*; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.Test; + +import generic.test.AbstractGenericTest; + +public class DefaultLocalGhidraProtocolConnectorParseTest extends AbstractGenericTest { + + static { + Handler.registerHandler(); + } + + @Test + public void testParseURL() throws Exception { + + DefaultLocalGhidraProtocolConnector pp = + new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/C:/x/y/proj")); + assertNull(pp.getRepositoryName()); + assertEquals("/", pp.getFolderPath()); + assertNull(pp.getFolderItemName()); + assertEquals("/", getInstanceField("itemPath", pp)); + + pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj")); + assertNull(pp.getRepositoryName()); + assertEquals("/", pp.getFolderPath()); + assertNull(pp.getFolderItemName()); + assertEquals("/", getInstanceField("itemPath", pp)); + + pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?/")); + assertNull(pp.getRepositoryName()); + assertEquals("/", pp.getFolderPath()); + assertNull(pp.getFolderItemName()); + assertEquals("/", getInstanceField("itemPath", pp)); + + pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?/a")); + assertNull(pp.getRepositoryName()); + assertEquals("/", pp.getFolderPath()); + assertEquals("a", pp.getFolderItemName()); + assertEquals("/a", getInstanceField("itemPath", pp)); + + pp = new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?/a/b#ref")); + assertNull(pp.getRepositoryName()); + assertEquals("/a", pp.getFolderPath()); + assertEquals("b", pp.getFolderItemName()); + assertEquals("/a/b", getInstanceField("itemPath", pp)); + + try { + pp = + new DefaultLocalGhidraProtocolConnector(new URL("ghidra:/x/y/proj?//")); + fail(); + } + catch (MalformedURLException e) { + // expected + } + + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java new file mode 100644 index 0000000000..72686a2b8b --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test/java/ghidra/framework/protocol/ghidra/GhidraURLTest.java @@ -0,0 +1,499 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.protocol.ghidra; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.framework.client.*; +import ghidra.framework.model.ProjectLocator; +import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; + +public class GhidraURLTest extends AbstractGenericTest { + + @Before + public void setUp() throws Exception { + Handler.registerHandler(); + } + + // makeURL(ProjectLocator) + @Test + public void testMakeLocalProjectURL() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); + URL ghidraUrl = GhidraURL.makeURL(loc); + URL url = new URL("ghidra:/C:/junk/Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("C:\\junk\\", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk/", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + url = new URL("ghidra:/a/b/Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b/", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + try { + loc = new ProjectLocator("a/b", "Test"); + fail("relative path shold not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + // makeURL(String, String) + @Test + public void testMakeLocalProjectURL2() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + URL url = new URL("ghidra:/C:/junk/Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("C:\\junk\\", "Test"); + ghidraUrl = GhidraURL.makeURL("C:\\junk\\", "Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk", "Test"); + ghidraUrl = GhidraURL.makeURL("/C:/junk", "Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk/", "Test"); + ghidraUrl = GhidraURL.makeURL("/C:/junk/", "Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b", "Test"); + ghidraUrl = GhidraURL.makeURL("/a/b", "Test"); + url = new URL("ghidra:/a/b/Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b/", "Test"); + ghidraUrl = GhidraURL.makeURL("/a/b/", "Test"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + try { + ghidraUrl = GhidraURL.makeURL("a/b/", "Test"); + fail("relative path shold not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + // makeURL(ProjectLocator, String, String) + @Test + public void testMakeLocalProjectFileURL() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); + + URL ghidraUrl = GhidraURL.makeURL(loc, "/a", "ref"); + URL url = new URL("ghidra:/C:/junk/Test?/a#ref"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL(loc, "/a/", "ref"); + url = new URL("ghidra:/C:/junk/Test?/a/#ref"); + assertEquals(url, ghidraUrl); + + try { + ghidraUrl = GhidraURL.makeURL(loc, "a/b", "ref"); + fail("relative path shold not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + // makeURL(String, String, String, String) + @Test + public void testMakeLocalProjectFileURL2() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); + + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + URL url = new URL("ghidra:/C:/junk/Test?/a#ref"); + assertEquals(url, ghidraUrl); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); + url = new URL("ghidra:/C:/junk/Test?/a/#ref"); + assertEquals(url, ghidraUrl); + + try { + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "a/b", "ref"); + fail("relative path shold not be permitted"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + // makeURL(String, int, String) + @Test + public void testMakeServerRepoURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + URL url = new URL("ghidra", "localhost", 123, "/Test"); + assertEquals(url, ghidraUrl); + } + + // makeURL(String, int, String, String) + @Test + public void testMakeServerRepoFileURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/"); + URL url = new URL("ghidra", "localhost", 123, "/Test/foo/"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo"); + url = new URL("ghidra", "localhost", 123, "/Test/foo"); + assertEquals(url, ghidraUrl); + + } + + // makeURL(String, int, String, String, String, String) + @Test + public void testMakeServerRepoFileURL2() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", null, null); + URL url = new URL("ghidra", "localhost", 123, "/Test/foo/"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", null, null); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); + url = new URL("ghidra", "localhost", 123, "/Test/foo/bar#ref"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertEquals(url, ghidraUrl); + } + +// makeURL(String, int, String, String) + @Test + public void testMakeServerRepoFileURL3() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo"); + URL url = new URL("ghidra", "localhost", 123, "/Test/foo"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/"); + url = new URL("ghidra", "localhost", 123, "/Test/foo/"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/bar", "ref"); + url = new URL("ghidra", "localhost", 123, "/Test/foo/bar#ref"); + assertEquals(url, ghidraUrl); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/bar"); + url = new URL("ghidra", "localhost", 123, "/Test/foo/bar"); + assertEquals(url, ghidraUrl); + } + + // getProjectStorageLocator(URL) + @Test + public void testGetProjectStorageLocator() throws Exception { + ProjectLocator loc = new ProjectLocator("C:\\junk", "Test"); + URL ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("C:\\junk\\", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/C:/junk/", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + + loc = new ProjectLocator("/a/b/", "Test"); + ghidraUrl = GhidraURL.makeURL(loc); + assertEquals(loc, GhidraURL.getProjectStorageLocator(ghidraUrl)); + } + + // isLocalProjectURL(URL) + @Test + public void testIsLocalProjectURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertTrue(GhidraURL.isLocalProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertTrue(GhidraURL.isLocalProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); + assertTrue(GhidraURL.isLocalProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + assertFalse(GhidraURL.isLocalProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertFalse(GhidraURL.isLocalProjectURL(ghidraUrl)); + } + + // isServerRepositoryURL(URL) + @Test + public void testIsServerRepositoryURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertNull(GhidraURL.getRepositoryName(ghidraUrl)); + assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertNull(GhidraURL.getRepositoryName(ghidraUrl)); + assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); + assertNull(GhidraURL.getRepositoryName(ghidraUrl)); + assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + assertEquals("Test", GhidraURL.getRepositoryName(ghidraUrl)); + assertTrue(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertEquals("Test", GhidraURL.getRepositoryName(ghidraUrl)); + assertTrue(GhidraURL.isServerRepositoryURL(ghidraUrl)); + + ghidraUrl = new URL("ghidra", "localhost", 123, ""); + assertNull(GhidraURL.getRepositoryName(ghidraUrl)); + assertFalse(GhidraURL.isServerRepositoryURL(ghidraUrl)); + } + + // isServerURL(URL) + @Test + public void testIsServerURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); + assertFalse(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + assertTrue(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertTrue(GhidraURL.isServerURL(ghidraUrl)); + + ghidraUrl = new URL("ghidra", "localhost", 123, ""); + assertTrue(GhidraURL.isServerURL(ghidraUrl)); + } + + // toURL(String) + @Test + public void testToURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertEquals(ghidraUrl, GhidraURL.toURL("C:\\junk\\Test")); + assertEquals(ghidraUrl, GhidraURL.toURL("/C:/junk/Test")); + assertEquals(ghidraUrl, GhidraURL.toURL("C:/junk/Test")); + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test?/a#ref")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/C:/junk/Test?/a/#ref")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref"); + assertEquals(ghidraUrl, GhidraURL.toURL("ghidra:/x/y/Test?/a/#ref")); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); + assertEquals(ghidraUrl, GhidraURL.toURL(ghidraUrl.toString())); + assertEquals(ghidraUrl, GhidraURL.toURL(GhidraURL.getDisplayString(ghidraUrl))); + + } + + @Test + public void testGetProjectURL() throws Exception { + + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertEquals(new URL("ghidra:/C:/junk/Test"), GhidraURL.getProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertEquals(new URL("ghidra:/C:/junk/Test"), GhidraURL.getProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a", "ref"); + assertEquals(new URL("ghidra:/x/y/Test"), GhidraURL.getProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + assertEquals(new URL("ghidra://localhost:123/Test"), GhidraURL.getProjectURL(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo/", "bar", "ref"); + assertEquals(new URL("ghidra://localhost:123/Test"), GhidraURL.getProjectURL(ghidraUrl)); + + ghidraUrl = new URL("ghidra", "localhost", 123, ""); + try { + GhidraURL.getProjectURL(ghidraUrl); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + + } + + // getDisplayString(URL) + @Test + public void testGetDisplayString() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertEquals("C:\\junk\\Test", GhidraURL.getDisplayString(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertEquals("ghidra:/C:/junk/Test?/a#ref", GhidraURL.getDisplayString(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); + assertEquals("ghidra:/C:/junk/Test?/a/#ref", GhidraURL.getDisplayString(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref"); + assertEquals("ghidra:/x/y/Test?/a/#ref", GhidraURL.getDisplayString(ghidraUrl)); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); + assertEquals(ghidraUrl.toString(), GhidraURL.getDisplayString(ghidraUrl)); + + } + + // getNormalizedURL(URL) + @Test + public void testNormalizedURL() throws Exception { + URL ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test"); + assertEquals("ghidra:/C:/junk/Test", GhidraURL.getNormalizedURL(ghidraUrl).toString()); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a", "ref"); + assertEquals("ghidra:/C:/junk/Test?/a", GhidraURL.getNormalizedURL(ghidraUrl).toString()); + + ghidraUrl = GhidraURL.makeURL("C:\\junk", "Test", "/a/", "ref"); + assertEquals("ghidra:/C:/junk/Test?/a/", GhidraURL.getNormalizedURL(ghidraUrl).toString()); + + ghidraUrl = GhidraURL.makeURL("/x/y", "Test", "/a/", "ref"); + assertEquals("ghidra:/x/y/Test?/a/", GhidraURL.getNormalizedURL(ghidraUrl).toString()); + + ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test", "/foo", "bar", "ref"); + assertEquals("ghidra://127.0.0.1:123/Test/foo/bar", + GhidraURL.getNormalizedURL(ghidraUrl).toString()); + } + + @Test + public void testTransientProjectURL() throws Exception { + // Dummy class implementations (see below) are used to stub objects required to establish + // transient project for URL verification testing only + URL ghidraUrl = GhidraURL.makeURL("localhost", 123, "Test"); + DummyGhidraProtocolConnector dummyRepoConnector = + new DummyGhidraProtocolConnector(ghidraUrl); + TransientProjectManager transientProjectManager = + TransientProjectManager.getTransientProjectManager(); + try { + TransientProjectData transientProject = + transientProjectManager.getTransientProject(dummyRepoConnector, true); + ProjectLocator projectLocator = transientProject.getProjectLocator(); + assertTrue(GhidraURL.isServerRepositoryURL(projectLocator.getURL())); + } + finally { + transientProjectManager.dispose(); + } + } + + private static class DummyGhidraProtocolConnector extends GhidraProtocolConnector { + + private URL repositoryURL; + private DummyRepositoryAdapter repoAdapter; + + DummyGhidraProtocolConnector(URL repositoryURL) throws MalformedURLException { + super(repositoryURL); + this.repositoryURL = repositoryURL; + repoAdapter = new DummyRepositoryAdapter(); + } + + @Override + protected URL getRepositoryRootGhidraURL() { + return repositoryURL; + } + + @Override + public StatusCode connect(boolean readOnly) throws IOException { + return StatusCode.OK; + } + + @Override + public boolean isReadOnly() throws NotConnectedException { + return true; + } + + @Override + public RepositoryAdapter getRepositoryAdapter() { + return repoAdapter; + } + + } + + private static class DummyRepositoryAdapter extends RepositoryAdapter { + DummyRepositoryAdapter() { + super(new DummyRepositoryServerAdapter(), "test"); + } + + @Override + public boolean isConnected() { + return true; + } + } + + private static class DummyRepositoryServerAdapter extends RepositoryServerAdapter { + DummyRepositoryServerAdapter() { + super(null, null); + } + } +}