Merge branch 'GP-2644_ghidra_LinkedFolders'

This commit is contained in:
ghidra1 2022-11-22 12:53:18 -05:00
commit f0a8af3e8b
153 changed files with 7083 additions and 1732 deletions

View file

@ -27,6 +27,7 @@ import ghidra.framework.data.ProjectFileManager;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.options.SaveState; import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.store.LockException;
import ghidra.trace.database.DBTraceContentHandler; import ghidra.trace.database.DBTraceContentHandler;
import ghidra.trace.model.Lifespan; import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace; import ghidra.trace.model.Trace;
@ -643,6 +644,7 @@ public class DebuggerCoordinates {
ProjectData projData = tool.getProject().getProjectData(projLoc); ProjectData projData = tool.getProject().getProjectData(projLoc);
if (projData == null) { if (projData == null) {
try { try {
// FIXME! orphaned instance - transient in nature
projData = new ProjectFileManager(projLoc, false, false); projData = new ProjectFileManager(projLoc, false, false);
} }
catch (NotOwnerException e) { catch (NotOwnerException e) {
@ -650,7 +652,7 @@ public class DebuggerCoordinates {
"Not project owner: " + projLoc + "(" + pathname + ")"); "Not project owner: " + projLoc + "(" + pathname + ")");
return null; return null;
} }
catch (IOException e) { catch (IOException | LockException e) {
Msg.showError(DebuggerCoordinates.class, tool.getToolFrame(), "Trace Open Failed", Msg.showError(DebuggerCoordinates.class, tool.getToolFrame(), "Trace Open Failed",
"Project error: " + e.getMessage()); "Project error: " + e.getMessage());
return null; return null;

View file

@ -368,7 +368,18 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
if (traceChooserDialog != null) { if (traceChooserDialog != null) {
return traceChooserDialog; 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 // TODO regarding the hack note below, I believe this issue ahs been fixed, but not sure how to test
return traceChooserDialog = return traceChooserDialog =

View file

@ -176,7 +176,7 @@ public class ProjectExperimentsTest extends AbstractGhidraHeadedIntegrationTest
assertNotNull(proj2 = pm.openProject(loc2, false, false)); assertNotNull(proj2 = pm.openProject(loc2, false, false));
ProjectData data1 = proj2.addProjectView(loc1.getURL()); ProjectData data1 = proj2.addProjectView(loc1.getURL(), true);
assertNotNull(data1); assertNotNull(data1);
// It's a cryin' shame. I don't get *any* callbacks. _ANY!_ // It's a cryin' shame. I don't get *any* callbacks. _ANY!_

View file

@ -18,11 +18,13 @@ package ghidra.trace.database;
import java.io.IOException; import java.io.IOException;
import javax.swing.Icon; import javax.swing.Icon;
import javax.swing.ImageIcon;
import db.DBHandle; import db.DBHandle;
import db.buffers.BufferFile; import db.buffers.BufferFile;
import db.buffers.ManagedBufferFile; 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.ChangeSet;
import ghidra.framework.model.DomainObject; import ghidra.framework.model.DomainObject;
import ghidra.framework.store.*; import ghidra.framework.store.*;
@ -34,9 +36,16 @@ import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException; import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
public class DBTraceContentHandler extends DBContentHandler { public class DBTraceContentHandler extends DBWithUserDataContentHandler<DBTrace> {
public static final String TRACE_CONTENT_TYPE = "Trace"; public static final String TRACE_CONTENT_TYPE = "Trace";
public static ImageIcon TRACE_ICON = Trace.TRACE_ICON;
static final Class<DBTrace> TRACE_DOMAIN_OBJECT_CLASS = DBTrace.class;
static final String TRACE_CONTENT_DEFAULT_TOOL = "Debugger";
private static final DBTraceLinkContentHandler linkHandler = new DBTraceLinkContentHandler();
@Override @Override
public long createFile(FileSystem fs, FileSystem userfs, String path, String name, public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
DomainObject obj, TaskMonitor monitor) DomainObject obj, TaskMonitor monitor)
@ -48,7 +57,7 @@ public class DBTraceContentHandler extends DBContentHandler {
} }
@Override @Override
public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version, public DBTrace getImmutableObject(FolderItem item, Object consumer, int version,
int minChangeVersion, TaskMonitor monitor) int minChangeVersion, TaskMonitor monitor)
throws IOException, CancelledException, VersionException { throws IOException, CancelledException, VersionException {
String contentType = item.getContentType(); String contentType = item.getContentType();
@ -96,7 +105,7 @@ public class DBTraceContentHandler extends DBContentHandler {
} }
@Override @Override
public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, public DBTrace getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
Object consumer, TaskMonitor monitor) Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException { throws IOException, VersionException, CancelledException {
String contentType = item.getContentType(); String contentType = item.getContentType();
@ -146,7 +155,7 @@ public class DBTraceContentHandler extends DBContentHandler {
} }
@Override @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) boolean okToUpgrade, boolean recover, Object consumer, TaskMonitor monitor)
throws IOException, CancelledException, VersionException { throws IOException, CancelledException, VersionException {
String contentType = item.getContentType(); String contentType = item.getContentType();
@ -296,8 +305,8 @@ public class DBTraceContentHandler extends DBContentHandler {
} }
@Override @Override
public Class<? extends DomainObject> getDomainObjectClass() { public Class<DBTrace> getDomainObjectClass() {
return DBTrace.class; return TRACE_DOMAIN_OBJECT_CLASS;
} }
@Override @Override
@ -312,12 +321,12 @@ public class DBTraceContentHandler extends DBContentHandler {
@Override @Override
public String getDefaultToolName() { public String getDefaultToolName() {
return "Debugger"; return TRACE_CONTENT_DEFAULT_TOOL;
} }
@Override @Override
public Icon getIcon() { public Icon getIcon() {
return Trace.TRACE_ICON; return TRACE_ICON;
} }
@Override @Override
@ -331,4 +340,9 @@ public class DBTraceContentHandler extends DBContentHandler {
// TODO: // TODO:
return null; return null;
} }
@Override
public DBTraceLinkContentHandler getLinkHandler() {
return linkHandler;
}
} }

View file

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

View file

@ -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/EditPluginPath.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/EditProjectAccessList.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/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/MemoryUsage.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/NonSharedProjectInfo.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| 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/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/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/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/lock.gif||GHIDRA||||END|
src/main/resources/images/magnifier.png||FAMFAMFAM Icons - CC 2.5||||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| src/main/resources/images/media-flash.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|

View file

@ -17,6 +17,8 @@
// NOTE: Script will only process unversioned and checked-out files. // NOTE: Script will only process unversioned and checked-out files.
//@category Examples //@category Examples
import java.io.IOException;
import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraScript;
import ghidra.app.script.GhidraState; import ghidra.app.script.GhidraState;
import ghidra.framework.model.*; import ghidra.framework.model.*;
@ -25,8 +27,6 @@ import ghidra.program.model.listing.Program;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException; import ghidra.util.exception.VersionException;
import java.io.IOException;
public class CallAnotherScriptForAllPrograms extends GhidraScript { public class CallAnotherScriptForAllPrograms extends GhidraScript {
// The script referenced in the following line should be replaced with the script to be called // 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 { 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())) { if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domainFile.getContentType())) {
return; // skip non-Program files return; // skip non-Program files
} }

View file

@ -143,6 +143,8 @@ public class RepositoryFileUpgradeScript extends GhidraScript {
} }
private boolean performProgramUpgrade(DomainFile df) throws IOException, CancelledException { 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())) { if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(df.getContentType())) {
return false; return false;
} }

View file

@ -48,7 +48,10 @@ public class VersionControl_AddAll extends GhidraScript {
if (monitor.isCancelled()) { if (monitor.isCancelled()) {
break; 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()) || if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(file.getContentType()) ||
file.isVersioned()) { file.isVersioned()) {
continue;// skip continue;// skip

View file

@ -22,13 +22,15 @@
hyperlink.</P> hyperlink.</P>
<!-- Annotation Example --> <!-- Annotation Example -->
<P>The following text shows the syntax of the URL annotation:</P> <P>The following text shows the syntax of a sample URL annotation:</P>
<pre><font size="4"><br> <pre><font size="4"><br>
<b>{@<i>url</i></b> "http://www.google.com"<b>}</b><br> <b>{@<i>url</i></b> "<i>http://www.google.com</i>"</b> "Search Web"<b>}</b><br>
</font><br></pre> </font><br></pre>
<P>The bold text is required for all annotations. The italicized text is required but is <P>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).</P> 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.</P>
<H2>Examples</H2> <H2>Examples</H2>
@ -47,10 +49,21 @@
<I>Rendered URL Annotation Example</I></P> <I>Rendered URL Annotation Example</I></P>
<P>When the URL text (e.g., "http://www.google.com") in the above image is clicked from within <P>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 Ghidra, a web browser is launched and attempts to load the corresponding web page. </P>
is of the form <i>ghidra://&lt;host&gt;[:&lt;port&gt;]/&lt;repository-name&gt;/&lt;program-path&gt;[#&lt;address-or-symbol-ref&gt;]</i>
an attempt will be made to open the corresponding program from the referenced Ghidra Server (e.g., <P>If the URL text corresponds to a Ghidra URL and attempt will be made to open the referenced
<i>ghidra://myserver/Repo/notepad.exe#entry</i>).</P> 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:</P>
<P><U>Remote Ghidra Server file</U><BR>
<i>ghidra://&lt;host&gt;[:&lt;port&gt;]/&lt;repository-name&gt;/&lt;program-path&gt;[#&lt;address-or-symbol-ref&gt;]</i><BR>
Example: <i>ghidra://hostname/Repo/notepad.exe#entry</i>
</P>
<P><U>Local Ghidra project file</U><BR>
<i>ghidra:/[&lt;project-path&gt/]&lt;project-name&gt;?/&lt;program-path&gt;[#&lt;address-or-symbol-ref&gt;]</i><BR>
Example: <i>ghidra:/share/MyProject?/notepad.exe#entry</i>
</P>
</BLOCKQUOTE> </BLOCKQUOTE>
<H2>Valid Annotations</H2> <H2>Valid Annotations</H2>
@ -174,9 +187,10 @@
<TD valign="top" width="15%">Displays the given URL has a hyperlink. This annotation <TD valign="top" width="15%">Displays the given URL has a hyperlink. This annotation
optionally takes display text so that the hyperlink may be displayed with text other optionally takes display text so that the hyperlink may be displayed with text other
than that of the URL.<BR><BR> than that of the URL.<BR><BR>
References to <i>ghidra://</i>, which refer to a program within a Ghidra Server repository, References to a program file on a Ghidra Server (<i>ghidra://&lt;host...</i>) or
will be opened within the Listing display, while all other URL protocols (e.g., <i>http://, https://, local project (<i>ghidra:/&lt;project-...</i>) will be opened within the Listing display,
file://,</i> etc.) will be launched via an external web browser (see while all other URL forms (e.g., <i>http://, https://, file://,</i> etc.) will be
launched via an external web browser (see
<a href="help/topics/ShowInstructionInfoPlugin/ShowInstructionInfo.htm#Show_Processor_Manual">command configuration <a href="help/topics/ShowInstructionInfoPlugin/ShowInstructionInfo.htm#Show_Processor_Manual">command configuration
for Processor Manuals</a>). for Processor Manuals</a>).
</TD> </TD>
@ -216,6 +230,12 @@
<LI>{@url "ghidra://myserver/Repo/notepad.exe#entry"}</LI> <LI>{@url "ghidra://myserver/Repo/notepad.exe#entry"}</LI>
<LI>{@url "ghidra://myserver/Repo/notepad.exe" "see notepad.exe"}</LI> <LI>{@url "ghidra://myserver/Repo/notepad.exe" "see notepad.exe"}</LI>
<LI>{@url "ghidra:/share/MyProject?/notepad.exe#entry"}</LI>
<LI>{@url "ghidra:/share/MyProject?/notepad.exe" "see notepad.exe"}</LI>
</UL> </UL>
</TD> </TD>
</TR> </TR>
@ -224,8 +244,9 @@
<TD valign="top" width="5%">Program<BR> <TD valign="top" width="5%">Program<BR>
</TD> </TD>
<TD valign="top" width="15%">Displays a hyperlink to the given Ghidra program name that <TD valign="top" width="15%">Displays a hyperlink to the given Ghidra program pathname
will open that program in a new Listing tab when clicked.<BR> with the current project. Referenced program
will open in a new Listing tab when clicked.<BR>
You may optionally provide an address or symbol to be displayed when the program is 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 opened by appending to the program name an '@' character, followed by an address or
symbol name.</TD> symbol name.</TD>

View file

@ -688,6 +688,54 @@
<P>The tabbed pane for read-only Project data is removed from the Project Window.</P> <P>The tabbed pane for read-only Project data is removed from the Project Window.</P>
</BLOCKQUOTE> </BLOCKQUOTE>
<H3><A name="Create_File_Links"></A>Create Linked Folder or File</H3>
<BLOCKQUOTE>
<P>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.</P>
<ol>
<li>Select a single folder or file from a viewed READ-ONLY Project Data tree.</li>
<li>Right mouse click on the selected tree node and choose the <I>Copy</I> option.</li>
<li>Select a destination folder in the active project tree.</li>
<li>Right mouse click on the folder and choose the <I>Paste as Link</I> option.
<P><IMG src="../../shared/note.png" border="0">Currently, linked-file types are
currently limited to <I>Program</I> and <I>Data Type Archive</I> files
only. The <I>Past as Link</I> menu item will be disabled for
unsupported file content types or for other unsupported situations such as internal
linking within the same project.</P>
</li>
</ol>
<P>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 <B>File->Open...</B> 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
<B>READ-ONLY Project Data</B> tree. The user may be prompted for a shared repository
connection password when accessing a linked folder or file.</P>
<P>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.</P>
<P><IMG src="../../shared/note.png" border="0"><B>Add to Version Control...</B> is supported
for repository folder and file links only and will be disabled for links to a
local project.</P>
<P><IMG src="../../shared/note.png" border="0">Currently, linked-files only provide access
to the latest file version and do not facilitate access to older file versions.</P>
<P>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.</P>
<CENTER>
<IMG src= "images/LinkOtherProject.png" border="0">
</CENTER>
</BLOCKQUOTE>
</BLOCKQUOTE> </BLOCKQUOTE>
<H2><A name="Workspace"></A>Workspaces</H2> <H2><A name="Workspace"></A>Workspaces</H2>

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before After
Before After

View file

@ -1266,19 +1266,24 @@ public class CallTreeProvider extends ComponentProviderAdapter implements Domain
} }
@Override @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(); TreePath treePath = node.getTreePath();
Object[] path = treePath.getPath(); Object[] path = treePath.getPath();
if (path.length > maxDepth) { if (path.length > maxDepth) {
return; return;
} }
if (!force && !node.isAutoExpandPermitted()) {
return;
}
CallNode callNode = (CallNode) node; CallNode callNode = (CallNode) node;
if (callNode.functionIsInPath()) { if (callNode.functionIsInPath()) {
return; // this path hit a function that is already in the path return; // this path hit a function that is already in the path
} }
super.expandNode(node, monitor); super.expandNode(node, false, monitor);
} }
} }

View file

@ -106,9 +106,33 @@ public class ClearFlowAndRepairCmd extends BackgroundCommand {
CodeUnit cu = cuIter.next(); CodeUnit cu = cuIter.next();
if (cu instanceof Instruction) { if (cu instanceof Instruction) {
Instruction instr = (Instruction) cu; 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(); Address ffAddr = instr.getFallFrom();
if (ffAddr != null && startAddrs.contains(ffAddr)) { 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 { else {

View file

@ -240,10 +240,11 @@ public class CommentsDialog extends DialogComponentProvider implements KeyListen
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
private AnnotationAdapterWrapper[] getAnnotationAdapterWrappers() { private AnnotationAdapterWrapper[] getAnnotationAdapterWrappers() {
AnnotatedStringHandler[] annotations = Annotation.getAnnotatedStringHandlers(); List<AnnotatedStringHandler> annotations = Annotation.getAnnotatedStringHandlers();
AnnotationAdapterWrapper[] retVal = new AnnotationAdapterWrapper[annotations.length]; int count = annotations.size();
for (int i = 0; i < annotations.length; i++) { AnnotationAdapterWrapper[] retVal = new AnnotationAdapterWrapper[count];
retVal[i] = new AnnotationAdapterWrapper(annotations[i]); for (int i = 0; i < count; i++) {
retVal[i] = new AnnotationAdapterWrapper(annotations.get(i));
} }
return retVal; return retVal;
} }

View file

@ -56,7 +56,6 @@ import ghidra.framework.options.SaveState;
import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.PluginInfo;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginStatus; import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.database.DataTypeArchiveContentHandler;
import ghidra.program.database.data.ProgramDataTypeManager; import ghidra.program.database.data.ProgramDataTypeManager;
import ghidra.program.model.address.AddressSetView; import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.data.*; import ghidra.program.model.data.*;
@ -245,8 +244,7 @@ public class DataTypeManagerPlugin extends ProgramPlugin
Project project = tool.getProjectManager().getActiveProject(); Project project = tool.getProjectManager().getActiveProject();
if (project != null && project.getName().equals(projectName)) { if (project != null && project.getName().equals(projectName)) {
DomainFile df = project.getProjectData().getFile(pathname); DomainFile df = project.getProjectData().getFile(pathname);
if (df != null && DataTypeArchiveContentHandler.DATA_TYPE_ARCHIVE_CONTENT_TYPE if (DataTypeArchive.class.isAssignableFrom(df.getDomainObjectClass())) {
.equals(df.getContentType())) {
return df; return df;
} }
} }
@ -588,12 +586,9 @@ public class DataTypeManagerPlugin extends ProgramPlugin
openArchive(domainFile, version); openArchive(domainFile, version);
} }
}; };
DomainFileFilter filter = f -> {
Class<?> c = f.getDomainObjectClass();
return DataTypeArchive.class.isAssignableFrom(c);
};
openDialog = 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.setHelpLocation(new HelpLocation(HelpTopics.PROGRAM, "Open_File_Dialog"));
openDialog.addOkActionListener(listener); openDialog.addOkActionListener(listener);
} }

View file

@ -608,8 +608,15 @@ public class DataTypesProvider extends ComponentProviderAdapter {
ArchiveNode archiveNode = dataTypeNode.getArchiveNode(); ArchiveNode archiveNode = dataTypeNode.getArchiveNode();
if (archiveNode instanceof ProjectArchiveNode && !archiveNode.isModifiable()) { if (archiveNode instanceof ProjectArchiveNode && !archiveNode.isModifiable()) {
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", Msg.showInfo(getClass(), archiveGTree, "Archive Not Checked Out",
"You must checkout this archive before you may edit data types."); "You must checkout this archive before you may edit data types.");
}
return; return;
} }

View file

@ -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.tree.DataTypeNode;
import ghidra.app.plugin.core.datamgr.util.DataTypeTreeCopyMoveTask; import ghidra.app.plugin.core.datamgr.util.DataTypeTreeCopyMoveTask;
import ghidra.app.plugin.core.datamgr.util.DataTypeTreeCopyMoveTask.ActionType; import ghidra.app.plugin.core.datamgr.util.DataTypeTreeCopyMoveTask.ActionType;
import ghidra.app.plugin.core.datamgr.util.DataTypeUtils;
import ghidra.program.model.data.*; import ghidra.program.model.data.*;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.layout.PairLayout; import ghidra.util.layout.PairLayout;
@ -85,26 +86,21 @@ public class AssociateDataTypeAction extends DockingAction {
return !nodes.isEmpty(); return !nodes.isEmpty();
} }
private boolean hasSingleModifiableSourceArchive(List<GTreeNode> nodes) { private Archive getSingleDTArchive(List<GTreeNode> nodes) {
Archive sourceArchive = null; Archive dtArchive = null;
for (GTreeNode node : nodes) { for (GTreeNode node : nodes) {
Archive archive = findArchive(node); Archive archive = findArchive(node);
if (sourceArchive == null) { if (dtArchive == null) {
sourceArchive = archive; dtArchive = archive;
continue; continue;
} }
if (sourceArchive != archive) { if (dtArchive != archive) {
return false; return null;
} }
} }
return dtArchive;
if (sourceArchive != null && sourceArchive.isModifiable()) {
return true;
}
return false;
} }
private static Archive findArchive(GTreeNode node) { private static Archive findArchive(GTreeNode node) {
@ -134,13 +130,20 @@ public class AssociateDataTypeAction extends DockingAction {
List<GTreeNode> nodes = ((DataTypesActionContext) context).getSelectedNodes(); List<GTreeNode> nodes = ((DataTypesActionContext) context).getSelectedNodes();
if (!hasSingleModifiableSourceArchive(nodes)) { Archive dtArchive = getSingleDTArchive(nodes);
Msg.showInfo(this, getProviderComponent(), "Multiple Source Archives", if (dtArchive == null) {
Msg.showInfo(this, getProviderComponent(), "Multiple Data Type Archives",
"The currently selected nodes are from multiple archives.\n" + "The currently selected nodes are from multiple archives.\n" +
"Please select only nodes from a single archvie."); "Please select only nodes from a single archvie.");
return; return;
} }
if (!dtArchive.isModifiable()) {
DataTypeUtils.showUnmodifiableArchiveErrorMessage(context.getSourceComponent(),
"Disassociate Failed", dtArchive.getDataTypeManager());
return;
}
if (isAlreadyAssociated((DataTypesActionContext) context)) { if (isAlreadyAssociated((DataTypesActionContext) context)) {
Msg.showInfo(this, getProviderComponent(), "Already Associated", Msg.showInfo(this, getProviderComponent(), "Already Associated",
"One or more of the currently selected nodes are already associated\n" + "One or more of the currently selected nodes are already associated\n" +

View file

@ -76,7 +76,7 @@ public class DataTypeManagerHandler {
private DataTreeDialog dataTreeSaveDialog; private DataTreeDialog dataTreeSaveDialog;
private CreateDataTypeArchiveDataTreeDialog dataTreeCreateDialog; private CreateDataTypeArchiveDataTreeDialog dataTreeCreateDialog;
private boolean treeDialogCancelled = false; private boolean treeDialogCancelled = false;
private DomainFileFilter domainFileFilter; private DomainFileFilter createArchiveFileFilter;
private DataTypeIndexer dataTypeIndexer; private DataTypeIndexer dataTypeIndexer;
private List<ArchiveManagerListener> archiveManagerlisteners = new ArrayList<>(); private List<ArchiveManagerListener> archiveManagerlisteners = new ArrayList<>();
@ -102,9 +102,17 @@ public class DataTypeManagerHandler {
dataTypeIndexer.addDataTypeManager(builtInDataTypesManager); dataTypeIndexer.addDataTypeManager(builtInDataTypesManager);
openArchives.add(new BuiltInArchive(this, builtInDataTypesManager)); openArchives.add(new BuiltInArchive(this, builtInDataTypesManager));
domainFileFilter = f -> { createArchiveFileFilter = new DomainFileFilter() {
Class<?> c = f.getDomainObjectClass();
return DataTypeArchive.class.isAssignableFrom(c); @Override
public boolean accept(DomainFile df) {
return DataTypeArchive.class.isAssignableFrom(df.getDomainObjectClass());
}
@Override
public boolean followLinkedFolders() {
return false;
}
}; };
folderListener = new MyFolderListener(); folderListener = new MyFolderListener();
@ -1425,7 +1433,7 @@ public class DataTypeManagerHandler {
} }
}; };
dataTreeSaveDialog = dataTreeSaveDialog =
new DataTreeDialog(null, "Save As", DataTreeDialog.SAVE, domainFileFilter); new DataTreeDialog(null, "Save As", DataTreeDialog.SAVE, createArchiveFileFilter);
dataTreeSaveDialog.addOkActionListener(listener); dataTreeSaveDialog.addOkActionListener(listener);
dataTreeSaveDialog dataTreeSaveDialog
@ -1465,7 +1473,7 @@ public class DataTypeManagerHandler {
}; };
dataTreeCreateDialog = new CreateDataTypeArchiveDataTreeDialog(null, "Create", dataTreeCreateDialog = new CreateDataTypeArchiveDataTreeDialog(null, "Create",
DataTreeDialog.CREATE, domainFileFilter); DataTreeDialog.CREATE, createArchiveFileFilter);
dataTreeCreateDialog.addOkActionListener(listener); dataTreeCreateDialog.addOkActionListener(listener);
dataTreeCreateDialog.setHelpLocation( dataTreeCreateDialog.setHelpLocation(

View file

@ -24,6 +24,7 @@ import javax.swing.Icon;
import generic.theme.GColor; import generic.theme.GColor;
import generic.theme.GIcon; import generic.theme.GIcon;
import ghidra.app.services.DataTypeQueryService; import ghidra.app.services.DataTypeQueryService;
import ghidra.program.database.data.ProjectDataTypeManager;
import ghidra.program.model.data.*; import ghidra.program.model.data.*;
import ghidra.program.model.data.Enum; import ghidra.program.model.data.Enum;
import ghidra.util.Msg; import ghidra.util.Msg;
@ -438,10 +439,19 @@ public class DataTypeUtils {
msg = "The archive file is not modifiable!\nYou must open the archive for editing\n" + msg = "The archive file is not modifiable!\nYou must open the archive for editing\n" +
"before performing this operation.\n" + dtm.getName(); "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 { else {
msg = "The project archive is not modifiable!\nYou must check out the archive\n" + msg = "The project archive is not modifiable!\nYou must check out the archive\n" +
"before performing this operation.\n" + dtm.getName(); "before performing this operation.\n" + dtm.getName();
} }
}
else {
msg = "The Archive is not modifiable!\n";
}
Msg.showInfo(DataTypeUtils.class, parent, title, msg); Msg.showInfo(DataTypeUtils.class, parent, title, msg);
} }
} }

View file

@ -295,11 +295,15 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
return comboBox; return comboBox;
} }
@SuppressWarnings("unchecked")
private List<Exporter> getApplicableExporters() { private List<Exporter> getApplicableExporters() {
List<Exporter> list = new ArrayList<>(ClassSearcher.getInstances(Exporter.class)); List<Exporter> list = new ArrayList<>(ClassSearcher.getInstances(Exporter.class));
Class<? extends DomainObject> domainObjectClass = domainFile.getDomainObjectClass(); Class<?> domainObjectClass = domainFile.getDomainObjectClass();
list.removeIf(exporter -> !exporter.canExportDomainObject(domainObjectClass)); if (DomainObject.class.isAssignableFrom(domainObjectClass)) {
list.removeIf(exporter -> !exporter
.canExportDomainObject((Class<? extends DomainObject>) domainObjectClass));
Collections.sort(list, (o1, o2) -> o1.toString().compareTo(o2.toString())); Collections.sort(list, (o1, o2) -> o1.toString().compareTo(o2.toString()));
}
return list; return list;
} }
@ -422,8 +426,16 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
private void doOpenFile(TaskMonitor monitor) { private void doOpenFile(TaskMonitor monitor) {
try { try {
domainObject = domainFile.getImmutableDomainObject(this, DomainFile.DEFAULT_VERSION, if (domainFile.isLinkFile()) {
TaskMonitor.DUMMY); // 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) { catch (VersionException | CancelledException | IOException e) {
Msg.showError(this, getComponent(), "Error Opening File", Msg.showError(this, getComponent(), "Error Opening File",

View file

@ -147,7 +147,8 @@ public class MyProgramChangesDisplayPlugin extends ProgramPlugin implements Doma
@Override @Override
public boolean isEnabledForContext(ActionContext context) { public boolean isEnabledForContext(ActionContext context) {
return currentProgram != null && currentProgram.getDomainFile().canCheckin(); return currentProgram != null &&
currentProgram.getDomainFile().modifiedSinceCheckout();
} }
}; };

View file

@ -26,8 +26,8 @@ import docking.action.MenuData;
import docking.widgets.OptionDialog; import docking.widgets.OptionDialog;
import ghidra.app.CorePluginPackage; import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.main.FrontEndTool;
import ghidra.framework.main.ApplicationLevelPlugin; import ghidra.framework.main.ApplicationLevelPlugin;
import ghidra.framework.main.FrontEndTool;
import ghidra.framework.main.datatable.ProjectDataContext; import ghidra.framework.main.datatable.ProjectDataContext;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.*;
@ -316,18 +316,20 @@ public final class LanguageProviderPlugin extends Plugin implements ApplicationL
try { try {
SwingUtilities.invokeAndWait(() -> { SwingUtilities.invokeAndWait(() -> {
ToolServices toolServices = tool.getToolServices(); ToolServices toolServices = tool.getToolServices();
String defaultToolName = toolServices.getDefaultToolTemplate(file).getName(); ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(file);
if (defaultToolTemplate != null) {
String defaultToolName = defaultToolTemplate.getName();
for (PluginTool t : toolServices.getRunningTools()) { for (PluginTool t : toolServices.getRunningTools()) {
if (t.getName().equals(defaultToolName)) { if (t.getName().equals(defaultToolName)) {
openTool = t; openTool = t;
break; break;
} }
} }
if (openTool != null) {
openTool.acceptDomainFiles(new DomainFile[] { file });
} }
else { if (openTool == null ||
openTool = tool.getToolServices().launchDefaultTool(file); !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) { catch (InvocationTargetException e) {
Throwable t = e.getCause(); 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); "An error occurred while attempting to launch your default tool!", t);
} }
} }

View file

@ -30,6 +30,7 @@ import ghidra.app.events.*;
import ghidra.app.nav.Navigatable; import ghidra.app.nav.Navigatable;
import ghidra.app.services.*; import ghidra.app.services.*;
import ghidra.app.util.task.OpenProgramTask; import ghidra.app.util.task.OpenProgramTask;
import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.framework.data.DomainObjectAdapterDB; import ghidra.framework.data.DomainObjectAdapterDB;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
@ -42,9 +43,6 @@ import ghidra.util.task.TaskLauncher;
class MultiProgramManager implements DomainObjectListener, TransactionListener { 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 ProgramManagerPlugin plugin;
private PluginTool tool; private PluginTool tool;
private ProgramInfo currentInfo; private ProgramInfo currentInfo;
@ -82,26 +80,32 @@ 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); ProgramInfo oldInfo = getInfo(p);
if (oldInfo == null) { if (oldInfo == null) {
oldInfo = programInfo;
p.addConsumer(tool); p.addConsumer(tool);
ProgramInfo info = new ProgramInfo(p, state != ProgramManager.OPEN_HIDDEN); openPrograms.add(oldInfo);
info.ghidraURL = ghidraURL;
openPrograms.add(info);
openPrograms.sort(Comparator.naturalOrder()); openPrograms.sort(Comparator.naturalOrder());
programMap.put(p, info); programMap.put(p, oldInfo);
fireOpenEvents(p); fireOpenEvents(p);
p.addListener(this); p.addListener(this);
p.addTransactionListener(this); p.addTransactionListener(this);
} }
else { else if (!oldInfo.visible && state != ProgramManager.OPEN_HIDDEN) {
if (!oldInfo.visible && state != ProgramManager.OPEN_HIDDEN) {
oldInfo.setVisible(true); oldInfo.setVisible(true);
} }
}
if (state == ProgramManager.OPEN_CURRENT) { if (state == ProgramManager.OPEN_CURRENT) {
saveLocation(); saveLocation();
setCurrentProgram(p); setCurrentProgram(p);
@ -246,12 +250,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
TransientToolState toolState = null; TransientToolState toolState = null;
if (currentInfo != null) { if (currentInfo != null) {
currentInfo.setVisible(true); currentInfo.setVisible(true);
DomainFile df = currentInfo.program.getDomainFile(); tool.setSubTitle(currentInfo.toString());
String title = df.toString();
if (df.isReadOnly()) {
title = title + " [Read-Only]";
}
tool.setSubTitle(title);
txMonitor.setProgram(currentInfo.program); txMonitor.setProgram(currentInfo.program);
if (currentInfo.lastState != null) { if (currentInfo.lastState != null) {
toolState = currentInfo.lastState; toolState = currentInfo.lastState;
@ -370,7 +369,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
return (info != null && info.owner != null); return (info != null && info.owner != null);
} }
private ProgramInfo getInfo(Program p) { ProgramInfo getInfo(Program p) {
if (p == null) { if (p == null) {
return null; return null;
} }
@ -378,9 +377,6 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
} }
Program getOpenProgram(URL ghidraURL) { Program getOpenProgram(URL ghidraURL) {
if (!GhidraURL.isServerRepositoryURL(ghidraURL)) {
return null;
}
URL normalizedURL = GhidraURL.getNormalizedURL(ghidraURL); URL normalizedURL = GhidraURL.getNormalizedURL(ghidraURL);
for (ProgramInfo info : programMap.values()) { for (ProgramInfo info : programMap.values()) {
URL url = info.ghidraURL; URL url = info.ghidraURL;
@ -392,10 +388,10 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
} }
Program getOpenProgram(DomainFile domainFile, int version) { Program getOpenProgram(DomainFile domainFile, int version) {
for (Program program : programMap.keySet()) { for (ProgramInfo info : programMap.values()) {
DomainFile programDomainFile = program.getDomainFile(); DomainFile df = info.domainFile;
if (filesMatch(domainFile, version, programDomainFile)) { if (df != null && filesMatch(domainFile, version, df)) {
return program; return info.program;
} }
} }
return null; return null;
@ -413,7 +409,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
if (!SystemUtilities.isEqual(file1.getProjectLocator(), file2.getProjectLocator())) { if (!SystemUtilities.isEqual(file1.getProjectLocator(), file2.getProjectLocator())) {
return false; return false;
} }
// TODO: version check is questionable - unclear how proxy file would work
int openVersion = file2.isReadOnly() ? file2.getVersion() : -1; int openVersion = file2.isReadOnly() ? file2.getVersion() : -1;
return version == openVersion; return version == openVersion;
} }
@ -488,28 +484,54 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
OpenProgramTask openTask = new OpenProgramTask(file, -1, this); OpenProgramTask openTask = new OpenProgramTask(file, -1, this);
openTask.setSilent(); openTask.setSilent();
new TaskLauncher(openTask, tool.getToolFrame()); new TaskLauncher(openTask, tool.getToolFrame());
Program openProgram = openTask.getOpenProgram(); OpenProgramRequest openProgramReq = openTask.getOpenProgram();
plugin.openProgram(openProgram, if (openProgramReq != null) {
plugin.openProgram(openProgramReq.getProgram(),
dataState != null ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE); dataState != null ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE);
openProgram.release(this); openProgramReq.release();
removeProgram((Program) oldObject); removeProgram((Program) oldObject);
if (dataState != null) { if (dataState != null) {
tool.restoreDataStateFromXml(dataState); tool.restoreDataStateFromXml(dataState);
} }
} }
} }
}
private class ProgramInfo implements Comparable<ProgramInfo> { class ProgramInfo implements Comparable<ProgramInfo> {
// 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 TransientToolState lastState;
private int instance; private int instance;
private boolean visible; private boolean visible = false;
private Object owner; private Object owner;
ProgramInfo(Program p, boolean visible) { private String str; // cached toString
ProgramInfo(Program p, DomainFile domainFile, boolean visible) {
this.program = p; 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; this.visible = visible;
instance = nextAvailableId.incrementAndGet(); instance = nextAvailableId.incrementAndGet();
} }
@ -523,5 +545,24 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
public int compareTo(ProgramInfo info) { public int compareTo(ProgramInfo info) {
return instance - info.instance; 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;
}
} }
} }

View file

@ -18,7 +18,6 @@ package ghidra.app.plugin.core.progmgr;
import java.awt.Component; import java.awt.Component;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.beans.PropertyEditor; import java.beans.PropertyEditor;
import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
@ -32,26 +31,26 @@ import ghidra.app.CorePluginPackage;
import ghidra.app.context.ProgramActionContext; import ghidra.app.context.ProgramActionContext;
import ghidra.app.events.*; import ghidra.app.events.*;
import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.progmgr.MultiProgramManager.ProgramInfo;
import ghidra.app.services.ProgramManager; import ghidra.app.services.ProgramManager;
import ghidra.app.util.HelpTopics; import ghidra.app.util.HelpTopics;
import ghidra.app.util.NamespaceUtils; import ghidra.app.util.NamespaceUtils;
import ghidra.app.util.task.OpenProgramTask; import ghidra.app.util.task.OpenProgramTask;
import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.framework.client.ClientUtil; import ghidra.framework.client.ClientUtil;
import ghidra.framework.data.ProjectFileManager;
import ghidra.framework.main.OpenVersionedFileDialog; import ghidra.framework.main.OpenVersionedFileDialog;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.options.*; import ghidra.framework.options.*;
import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus; import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.protocol.ghidra.*; import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.program.database.ProgramContentHandler;
import ghidra.program.model.address.*; import ghidra.program.model.address.*;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Symbol; import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolType; import ghidra.program.model.symbol.SymbolType;
import ghidra.program.util.*; import ghidra.program.util.*;
import ghidra.util.*; import ghidra.util.*;
import ghidra.util.exception.NotFoundException; import ghidra.util.exception.AssertException;
import ghidra.util.task.TaskLauncher; import ghidra.util.task.TaskLauncher;
//@formatter:off //@formatter:off
@ -121,16 +120,21 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
if (domainFile == null) { if (domainFile == null) {
continue; continue;
} }
if (!(Program.class.isAssignableFrom(domainFile.getDomainObjectClass()))) { Class<? extends DomainObject> domainObjectClass = domainFile.getDomainObjectClass();
continue; if (Program.class.isAssignableFrom(domainObjectClass)) {
}
filesToOpen.add(domainFile); filesToOpen.add(domainFile);
} }
}
openPrograms(filesToOpen); openPrograms(filesToOpen);
return !filesToOpen.isEmpty(); return !filesToOpen.isEmpty();
} }
@Override
public boolean accept(URL url) {
return openProgram(url, OPEN_CURRENT) != null;
}
@Override @Override
public Class<?>[] getSupportedDataTypes() { public Class<?>[] getSupportedDataTypes() {
return new Class[] { Program.class }; return new Class[] { Program.class };
@ -144,89 +148,53 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
return null; return null;
} }
return Swing.runNow(() -> doOpenProgram(ghidraURL, state)); // Check for URL already open and re-use
} URL url = GhidraURL.getNormalizedURL(ghidraURL);
Program p = programMgr.getOpenProgram(url);
private void messageBadProgramURL(URL ghidraURL) { if (p != null) {
Msg.showError(this, null, "Invalid Ghidra URL", showProgram(p, url, state);
"Ghidra URL does not reference a Ghidra Program: " + ghidraURL); if (state == ProgramManager.OPEN_CURRENT) {
} gotoProgramRef(p, ghidraURL.getRef());
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());
programMgr.saveLocation(); programMgr.saveLocation();
} }
contextChanged(); return p;
return openProgram;
} }
GhidraURLWrappedContent wrappedContent = null; Program program = Swing.runNow(() -> doOpenProgram(ghidraURL, state));
Object content = null;
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 { try {
GhidraURLConnection c = (GhidraURLConnection) ghidraURL.openConnection(); URL url = GhidraURL.getNormalizedURL(ghidraURL);
Object obj = c.getContent(); OpenProgramTask task = new OpenProgramTask(url, this);
if (c.getResponseCode() == GhidraURLConnection.GHIDRA_UNAUTHORIZED) { new TaskLauncher(task, tool.getToolFrame());
return null; // assume user already notified 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 { finally {
if (content != null) { if (p != null && openState == ProgramManager.OPEN_CURRENT) {
wrappedContent.release(content, this); gotoProgramRef(p, ghidraURL.getRef());
programMgr.saveLocation();
} }
} }
return null; return p;
} }
private boolean gotoProgramRef(Program program, String ref) { private boolean gotoProgramRef(Program program, String ref) {
@ -296,9 +264,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
} }
Program program = Swing.runNow(() -> { Program program = Swing.runNow(() -> {
Program p = doOpenProgram(domainFile, version, state); return doOpenProgram(domainFile, version, state);
contextChanged();
return p;
}); });
if (program != null) { if (program != null) {
@ -460,13 +426,39 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
@Override @Override
public void openProgram(final Program program, final int state) { 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) { if (locked) {
throw new IllegalStateException( throw new IllegalStateException(
"Progam manager is locked and cannot accept a new program"); "Progam manager is locked and cannot accept a new program");
} }
Runnable r = () -> { 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) { if (state == ProgramManager.OPEN_CURRENT) {
programMgr.saveLocation(); programMgr.saveLocation();
} }
@ -625,11 +617,9 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
doOpenProgram(domainFile, version, OPEN_CURRENT); doOpenProgram(domainFile, version, OPEN_CURRENT);
} }
}; };
DomainFileFilter filter = f -> { openDialog = new OpenVersionedFileDialog(tool, "Open Program", f -> {
Class<?> c = f.getDomainObjectClass(); return Program.class.isAssignableFrom(f.getDomainObjectClass());
return Program.class.isAssignableFrom(c); });
};
openDialog = new OpenVersionedFileDialog(tool, "Open Program", filter);
openDialog.setHelpLocation(new HelpLocation(HelpTopics.PROGRAM, "Open_File_Dialog")); openDialog.setHelpLocation(new HelpLocation(HelpTopics.PROGRAM, "Open_File_Dialog"));
openDialog.addOkActionListener(listener); openDialog.addOkActionListener(listener);
} }
@ -638,9 +628,12 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
} }
public void openPrograms(List<DomainFile> filesToOpen) { public void openPrograms(List<DomainFile> filesToOpen) {
Program showIfNeeded = null;
OpenProgramTask openTask = null; OpenProgramTask openTask = null;
for (DomainFile domainFile : filesToOpen) { for (DomainFile domainFile : filesToOpen) {
if (programMgr.getOpenProgram(domainFile, -1) != null) { Program p = programMgr.getOpenProgram(domainFile, -1);
if (p != null) {
showIfNeeded = p;
continue; continue;
} }
if (openTask == null) { if (openTask == null) {
@ -652,32 +645,37 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
} }
if (openTask != null) { if (openTask != null) {
new TaskLauncher(openTask, tool.getToolFrame()); new TaskLauncher(openTask, tool.getToolFrame());
List<Program> openPrograms = openTask.getOpenPrograms(); List<OpenProgramRequest> openProgramReqs = openTask.getOpenPrograms();
boolean isFirst = true;
for (Program program : openPrograms) { for (OpenProgramRequest programReq : openProgramReqs) {
openProgram(program, OPEN_VISIBLE); showProgram(programReq.getProgram(), programReq.getDomainFile(),
program.release(this); isFirst ? OPEN_CURRENT : OPEN_VISIBLE);
programReq.release();
isFirst = false;
showIfNeeded = null;
} }
if (!openPrograms.isEmpty()) {
openProgram(openPrograms.get(0), OPEN_CURRENT);
} }
if (showIfNeeded != null) {
showProgram(showIfNeeded, showIfNeeded.getDomainFile(), OPEN_CURRENT);
} }
} }
protected Program doOpenProgram(DomainFile domainFile, int version, int openState) { protected Program doOpenProgram(DomainFile domainFile, int version, int openState) {
Program openProgram = programMgr.getOpenProgram(domainFile, version); Program p = programMgr.getOpenProgram(domainFile, version);
if (openProgram != null) { if (p != null) {
openProgram(openProgram, openState); openProgram(p, openState);
return openProgram;
} }
else {
OpenProgramTask task = new OpenProgramTask(domainFile, version, this); OpenProgramTask task = new OpenProgramTask(domainFile, version, this);
new TaskLauncher(task, tool.getToolFrame()); new TaskLauncher(task, tool.getToolFrame());
openProgram = task.getOpenProgram(); OpenProgramRequest programReq = task.getOpenProgram();
if (openProgram != null) { if (programReq != null) {
openProgram(openProgram, openState); p = programReq.getProgram();
openProgram.release(this); showProgram(p, programReq.getDomainFile(), openState);
programReq.release();
} }
return openProgram; }
return p;
} }
@Override @Override
@ -705,18 +703,20 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
*/ */
@Override @Override
public void writeDataState(SaveState saveState) { public void writeDataState(SaveState saveState) {
// Only remember programs from non-transient projects
ArrayList<Program> programs = new ArrayList<>(); ArrayList<ProgramInfo> programInfos = new ArrayList<>();
for (Program p : programMgr.getAllPrograms()) { for (Program p : programMgr.getAllPrograms()) {
ProjectLocator projectLocator = p.getDomainFile().getProjectLocator(); ProgramInfo info = programMgr.getInfo(p);
if (projectLocator != null && !projectLocator.isTransient()) { if (info != null) {
programs.add(p); programInfos.add(info);
} }
} }
saveState.putInt("NUM_PROGRAMS", programs.size());
saveState.putInt("NUM_PROGRAMS", programInfos.size());
int i = 0; int i = 0;
for (Program p : programs) { for (ProgramInfo programInfo : programInfos) {
writeProgramInfo(p, saveState, i++); writeProgramInfo(programInfo, saveState, i++);
} }
Program p = programMgr.getCurrentProgram(); Program p = programMgr.getCurrentProgram();
if (p != null) { 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) { if (locked) {
return; // do not save state when locked. return; // do not save state when locked.
} }
if (programInfo.ghidraURL != null) {
saveState.putString("URL_" + index, programInfo.ghidraURL.toString());
return;
}
String projectLocation = null; String projectLocation = null;
String projectName = null; String projectName = null;
String path = null; String path = null;
Program program = programInfo.program;
DomainFile df = program.getDomainFile(); DomainFile df = program.getDomainFile();
ProjectLocator projectLocator = df.getProjectLocator(); ProjectLocator projectLocator = df.getProjectLocator();
if (projectLocator != null && !projectLocator.isTransient()) { if (projectLocator != null && !projectLocator.isTransient()) {
@ -797,28 +805,30 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
* Read in my data state. * Read in my data state.
*/ */
private void loadPrograms(SaveState saveState) { private void loadPrograms(SaveState saveState) {
int n = saveState.getInt("NUM_PROGRAMS", 0); int n = saveState.getInt("NUM_PROGRAMS", 0);
if (n == 0) { if (n == 0) {
return; return;
} }
OpenProgramTask openTask = null; OpenProgramTask openTask = new OpenProgramTask(this);
for (int index = 0; index < n; index++) { for (int index = 0; index < n; index++) {
URL url = getGhidraURL(saveState, index);
if (url != null) {
openTask.addProgramToOpen(url);
continue;
}
DomainFile domainFile = getDomainFile(saveState, index); DomainFile domainFile = getDomainFile(saveState, index);
if (domainFile == null) { if (domainFile == null) {
continue; continue;
} }
int version = getVersion(saveState, index); 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; return;
} }
@ -835,10 +845,29 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
"Can't open program", e); "Can't open program", e);
} }
List<Program> openPrograms = openTask.getOpenPrograms(); List<OpenProgramRequest> openProgramReqs = openTask.getOpenPrograms();
for (Program program : openPrograms) { for (OpenProgramRequest programReq : openProgramReqs) {
openProgram(program, OPEN_VISIBLE); DomainFile df = programReq.getDomainFile();
program.release(this); 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,21 +882,8 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
ProjectData projectData = tool.getProject().getProjectData(projectLocator); ProjectData projectData = tool.getProject().getProjectData(projectLocator);
if (projectData == null) { 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; return null;
} }
catch (IOException e) {
Msg.showError(this, tool.getToolFrame(), "Program Open Failed",
"Project error: " + e.getMessage());
return null;
}
}
DomainFile df = projectData.getFile(pathname); DomainFile df = projectData.getFile(pathname);
if (df == null) { if (df == null) {

View file

@ -45,9 +45,17 @@ class ProgramSaveManager {
ProgramSaveManager(PluginTool tool, ProgramManager programMgr) { ProgramSaveManager(PluginTool tool, ProgramManager programMgr) {
this.tool = tool; this.tool = tool;
this.programMgr = programMgr; this.programMgr = programMgr;
domainFileFilter = f -> { domainFileFilter = new DomainFileFilter() {
Class<?> c = f.getDomainObjectClass();
return Program.class.isAssignableFrom(c); @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; return;
} }
if (existingFile != null) { 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, if (OptionDialog.showOptionDialog(tool.getToolFrame(), "Duplicate Name", msg,
"Overwrite", OptionDialog.QUESTION_MESSAGE) == OptionDialog.CANCEL_OPTION) { "Overwrite", OptionDialog.QUESTION_MESSAGE) == OptionDialog.CANCEL_OPTION) {
return; return;

View file

@ -312,7 +312,7 @@ public class HeadlessAnalyzer {
Object obj = c.getContent(); Object obj = c.getContent();
if (!(obj instanceof GhidraURLWrappedContent)) { if (!(obj instanceof GhidraURLWrappedContent)) {
throw new IOException( 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; GhidraURLWrappedContent wrappedContent = (GhidraURLWrappedContent) obj;
Object content = null; Object content = null;
@ -336,7 +336,7 @@ public class HeadlessAnalyzer {
processWithImport(folder.getPathname(), filesToImport); processWithImport(folder.getPathname(), filesToImport);
} }
} }
catch (NotFoundException e) { catch (FileNotFoundException e) {
throw new IOException("Connect to repository folder failed"); throw new IOException("Connect to repository folder failed");
} }
finally { finally {
@ -369,7 +369,8 @@ public class HeadlessAnalyzer {
* @param rootFolderPath root folder for imports * @param rootFolderPath root folder for imports
* @param filesToImport directories and files to be imported (null or empty is acceptable if * @param filesToImport directories and files to be imported (null or empty is acceptable if
* we are in -process mode) * 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, public void processLocal(String projectLocation, String projectName, String rootFolderPath,
List<File> filesToImport) throws IOException { List<File> filesToImport) throws IOException {
@ -1107,6 +1108,8 @@ public class HeadlessAnalyzer {
return; 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())) { if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) {
return; // skip non-Program files return; // skip non-Program files
} }
@ -1275,6 +1278,8 @@ public class HeadlessAnalyzer {
for (DomainFile domFile : parentFolder.getFiles()) { for (DomainFile domFile : parentFolder.getFiles()) {
if (filenamePattern == null || filenamePattern.matcher(domFile.getName()).matches()) { 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())) { if (ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) {
filesProcessed = true; filesProcessed = true;
processFileNoImport(domFile); processFileNoImport(domFile);
@ -1308,6 +1313,8 @@ public class HeadlessAnalyzer {
boolean filesProcessed = false; boolean filesProcessed = false;
DomainFile domFile = parentFolder.getFile(filename); 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 && if (domFile != null &&
ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) { ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) {
filesProcessed = true; filesProcessed = true;

View file

@ -16,8 +16,9 @@
package ghidra.app.util.task; package ghidra.app.util.task;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.net.MalformedURLException;
import java.util.List; import java.net.URL;
import java.util.*;
import docking.widgets.OptionDialog; import docking.widgets.OptionDialog;
import ghidra.app.util.dialog.CheckoutDialog; import ghidra.app.util.dialog.CheckoutDialog;
@ -25,8 +26,11 @@ import ghidra.framework.client.ClientUtil;
import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.main.AppInfo; import ghidra.framework.main.AppInfo;
import ghidra.framework.model.DomainFile; 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.remote.User;
import ghidra.framework.store.ExclusiveCheckoutException; import ghidra.framework.store.ExclusiveCheckoutException;
import ghidra.program.database.ProgramLinkContentHandler;
import ghidra.program.model.lang.LanguageNotFoundException; import ghidra.program.model.lang.LanguageNotFoundException;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.util.*; import ghidra.util.*;
@ -37,8 +41,8 @@ import ghidra.util.task.TaskMonitor;
public class OpenProgramTask extends Task { public class OpenProgramTask extends Task {
private final List<DomainFileInfo> domainFileInfoList = new ArrayList<>(); private final List<OpenProgramRequest> openProgramRequests = new ArrayList<>();
private List<Program> programList = new ArrayList<>(); private List<OpenProgramRequest> openedProgramList = new ArrayList<>();
private final Object consumer; private final Object consumer;
private boolean silent; // if true operation does not permit interaction private boolean silent; // if true operation does not permit interaction
@ -46,11 +50,16 @@ public class OpenProgramTask extends Task {
private String openPromptText = "Open"; 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, public OpenProgramTask(DomainFile domainFile, int version, boolean forceReadOnly,
Object consumer) { Object consumer) {
super("Open Program(s)", true, false, true); super("Open Program(s)", true, false, true);
this.consumer = consumer; 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) { public OpenProgramTask(DomainFile domainFile, int version, Object consumer) {
@ -65,17 +74,10 @@ public class OpenProgramTask extends Task {
this(domainFile, DomainFile.DEFAULT_VERSION, false, consumer); this(domainFile, DomainFile.DEFAULT_VERSION, false, consumer);
} }
public OpenProgramTask(List<DomainFile> domainFileList, boolean forceReadOnly, public OpenProgramTask(URL ghidraURL, Object consumer) {
Object consumer) { super("Open Program(s)", true, false, true);
super("Open Program(s)", true, domainFileList.size() > 1, true);
this.consumer = consumer; this.consumer = consumer;
for (DomainFile domainFile : domainFileList) { openProgramRequests.add(new OpenProgramRequest(ghidraURL));
domainFileInfoList.add(new DomainFileInfo(domainFile, -1, forceReadOnly));
}
}
public OpenProgramTask(List<DomainFile> domainFileList, Object consumer) {
this(domainFileList, false, consumer);
} }
public void setOpenPromptText(String text) { public void setOpenPromptText(String text) {
@ -88,7 +90,16 @@ public class OpenProgramTask extends Task {
public void addProgramToOpen(DomainFile domainFile, int version, boolean forceReadOnly) { public void addProgramToOpen(DomainFile domainFile, int version, boolean forceReadOnly) {
setHasProgress(true); 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; this.noCheckout = true;
} }
public List<Program> getOpenPrograms() { /**
return programList; * Get all successful open program requests
* @return all successful open program requests
*/
public List<OpenProgramRequest> 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 null;
} }
return programList.get(0); return openedProgramList.get(0);
} }
@Override @Override
public void run(TaskMonitor monitor) { public void run(TaskMonitor monitor) {
taskMonitor.initialize(domainFileInfoList.size()); taskMonitor.initialize(openProgramRequests.size());
for (DomainFileInfo domainFileInfo : domainFileInfoList) { for (OpenProgramRequest domainFileInfo : openProgramRequests) {
if (taskMonitor.isCancelled()) { if (taskMonitor.isCancelled()) {
return; return;
} }
openDomainFile(domainFileInfo); domainFileInfo.open();
taskMonitor.incrementProgress(1); taskMonitor.incrementProgress(1);
} }
} }
private void openDomainFile(DomainFileInfo domainFileInfo) { private Object openReadOnlyFile(DomainFile domainFile, URL url, int version) {
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) {
taskMonitor.setMessage("Opening " + domainFile.getName()); 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()); taskMonitor.setMessage("Getting Version " + version + " for " + domainFile.getName());
openReadOnly(domainFile, version); return openReadOnly(domainFile, url, version);
} }
private void openReadOnly(DomainFile domainFile, int version) { private Object openReadOnly(DomainFile domainFile, URL url, int version) {
String contentType = null; String contentType = domainFile.getContentType();
String path = url != null ? url.toString() : domainFile.getPathname();
Object obj = null;
try { try {
contentType = domainFile.getContentType();
Program program =
(Program) domainFile.getReadOnlyDomainObject(consumer, version, taskMonitor);
if (program == null) { obj = domainFile.getReadOnlyDomainObject(consumer, version, taskMonitor);
String errorMessage = "Can't open program - \"" + domainFile.getPathname() + "\"";
if (obj == null) {
String errorMessage = "Can't open " + contentType + " - \"" + path + "\"";
if (version != DomainFile.DEFAULT_VERSION) { if (version != DomainFile.DEFAULT_VERSION) {
errorMessage += " version " + version; errorMessage += " version " + version;
} }
Msg.showError(this, null, "DomainFile Not Found", errorMessage); Msg.showError(this, null, "File Not Found", errorMessage);
}
else {
programList.add(program);
} }
} }
catch (CancelledException e) { catch (CancelledException e) {
// we don't care, the task has been cancelled // we don't care, the task has been cancelled
} }
catch (IOException e) { catch (IOException e) {
if (domainFile.isInWritableProject()) { if (url == null && domainFile.isInWritableProject()) {
ClientUtil.handleException(AppInfo.getActiveProject().getRepository(), e, 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 { else {
Msg.showError(this, null, "Error Getting Versioned Object", Msg.showError(this, null, "Error Getting Program",
"Could not get version " + version + " for " + domainFile.getName(), e); "Open program failed for " + path, e);
} }
} }
catch (VersionException e) { catch (VersionException e) {
VersionExceptionHandler.showVersionError(null, domainFile.getName(), contentType, VersionExceptionHandler.showVersionError(null, domainFile.getName(), contentType,
"Open", e); "Open", e);
} }
return obj;
} }
private void openUnversionedFile(DomainFile domainFile) { private Program openUnversionedFile(DomainFile domainFile) {
String filename = domainFile.getName(); String filename = domainFile.getName();
taskMonitor.setMessage("Opening " + filename); taskMonitor.setMessage("Opening " + filename);
performOptionalCheckout(domainFile); performOptionalCheckout(domainFile);
try { try {
openFileMaybeUgrade(domainFile); return openFileMaybeUgrade(domainFile);
} }
catch (VersionException e) { catch (VersionException e) {
String contentType = domainFile.getContentType(); String contentType = domainFile.getContentType();
@ -226,9 +234,10 @@ public class OpenProgramTask extends Task {
"Getting domain object failed.\n" + e.getMessage(), e); "Getting domain object failed.\n" + e.getMessage(), e);
} }
} }
return null;
} }
private void openFileMaybeUgrade(DomainFile domainFile) private Program openFileMaybeUgrade(DomainFile domainFile)
throws IOException, CancelledException, VersionException { throws IOException, CancelledException, VersionException {
boolean recoverFile = false; boolean recoverFile = false;
@ -236,24 +245,18 @@ public class OpenProgramTask extends Task {
recoverFile = askRecoverFile(domainFile.getName()); recoverFile = askRecoverFile(domainFile.getName());
} }
Program program = null;
try { try {
Program program = program =
(Program) domainFile.getDomainObject(consumer, false, recoverFile, taskMonitor); (Program) domainFile.getDomainObject(consumer, false, recoverFile, taskMonitor);
if (program != null) {
programList.add(program);
}
} }
catch (VersionException e) { catch (VersionException e) {
if (VersionExceptionHandler.isUpgradeOK(null, domainFile, openPromptText, e)) { if (VersionExceptionHandler.isUpgradeOK(null, domainFile, openPromptText, e)) {
Program program = program =
(Program) domainFile.getDomainObject(consumer, true, recoverFile, taskMonitor); (Program) domainFile.getDomainObject(consumer, true, recoverFile, taskMonitor);
if (program != null) {
programList.add(program);
}
} }
} }
return program;
} }
private boolean askRecoverFile(final String filename) { private boolean askRecoverFile(final String filename) {
@ -294,32 +297,158 @@ public class OpenProgramTask extends Task {
} }
} }
static class DomainFileInfo { public class OpenProgramRequest {
private final DomainFile domainFile;
private final int version;
private boolean forceReadOnly;
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.domainFile = domainFile;
this.ghidraURL = null;
this.version = this.version =
(domainFile.isReadOnly() && domainFile.isVersioned()) ? domainFile.getVersion() (domainFile.isReadOnly() && domainFile.isVersioned()) ? domainFile.getVersion()
: version; : version;
this.forceReadOnly = forceReadOnly; this.forceReadOnly = forceReadOnly;
} }
public boolean isReadOnly() { /**
return forceReadOnly || domainFile.isReadOnly() || * Get the {@link DomainFile} which corresponds to program open request. This will be
version != DomainFile.DEFAULT_VERSION; * null for all URL-based open requests.
} * @return {@link DomainFile} which corresponds to program open request or null.
*/
public DomainFile getDomainFile() { public DomainFile getDomainFile() {
return domainFile; 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);
}
} }
} }

View file

@ -33,6 +33,7 @@ public class Annotation {
private static final Pattern QUOTATION_PATTERN = private static final Pattern QUOTATION_PATTERN =
Pattern.compile("(?<!\\\\)[\"](.*?)(?<!\\\\)[\"]"); Pattern.compile("(?<!\\\\)[\"](.*?)(?<!\\\\)[\"]");
private static List<AnnotatedStringHandler> ANNOTATED_STRING_HANDLERS;
private static Map<String, AnnotatedStringHandler> ANNOTATED_STRING_MAP; private static Map<String, AnnotatedStringHandler> ANNOTATED_STRING_MAP;
private String annotationText; private String annotationText;
@ -40,6 +41,13 @@ public class Annotation {
private AnnotatedStringHandler annotatedStringHandler; private AnnotatedStringHandler annotatedStringHandler;
private AttributedString displayString; private AttributedString displayString;
public static List<AnnotatedStringHandler> getAnnotatedStringHandlers() {
if (ANNOTATED_STRING_HANDLERS == null) {
ANNOTATED_STRING_HANDLERS = getSupportedAnnotationHandlers();
}
return ANNOTATED_STRING_HANDLERS;
}
private static Map<String, AnnotatedStringHandler> getAnnotatedStringHandlerMap() { private static Map<String, AnnotatedStringHandler> getAnnotatedStringHandlerMap() {
if (ANNOTATED_STRING_MAP == null) { // lazy init due to our use of ClassSearcher if (ANNOTATED_STRING_MAP == null) { // lazy init due to our use of ClassSearcher
ANNOTATED_STRING_MAP = createAnnotatedStringHandlerMap(); ANNOTATED_STRING_MAP = createAnnotatedStringHandlerMap();
@ -47,24 +55,28 @@ public class Annotation {
return ANNOTATED_STRING_MAP; return ANNOTATED_STRING_MAP;
} }
// locates AnnotatedStringHandler implementations to handle annotations
private static Map<String, AnnotatedStringHandler> createAnnotatedStringHandlerMap() { private static Map<String, AnnotatedStringHandler> createAnnotatedStringHandlerMap() {
Map<String, AnnotatedStringHandler> map = new HashMap<>(); Map<String, AnnotatedStringHandler> map = new HashMap<>();
for (AnnotatedStringHandler instance : getAnnotatedStringHandlers()) {
// find all instances of AnnotatedString
List<AnnotatedStringHandler> instances =
ClassSearcher.getInstances(AnnotatedStringHandler.class);
for (AnnotatedStringHandler instance : instances) {
String[] supportedAnnotations = instance.getSupportedAnnotations(); String[] supportedAnnotations = instance.getSupportedAnnotations();
for (String supportedAnnotation : supportedAnnotations) { for (String supportedAnnotation : supportedAnnotations) {
map.put(supportedAnnotation, instance); map.put(supportedAnnotation, instance);
} }
} }
return Collections.unmodifiableMap(map); return Collections.unmodifiableMap(map);
} }
// locates AnnotatedStringHandler implementations to handle annotations
private static List<AnnotatedStringHandler> getSupportedAnnotationHandlers() {
List<AnnotatedStringHandler> list = new ArrayList<>();
for (AnnotatedStringHandler h : ClassSearcher.getInstances(AnnotatedStringHandler.class)) {
if (h.getSupportedAnnotations().length != 0) {
list.add(h);
}
}
return Collections.unmodifiableList(list);
}
/** /**
* Constructor * Constructor
* <b>Note</b>: This constructor assumes that the string starts with "{<pre>@</pre>" and ends with '}' * <b>Note</b>: This constructor assumes that the string starts with "{<pre>@</pre>" and ends with '}'
@ -184,14 +196,6 @@ public class Annotation {
return annotationText; return annotationText;
} }
public static AnnotatedStringHandler[] getAnnotatedStringHandlers() {
Set<AnnotatedStringHandler> annotations =
new HashSet<>(getAnnotatedStringHandlerMap().values());
AnnotatedStringHandler[] retVal = new AnnotatedStringHandler[annotations.size()];
annotations.toArray(retVal);
return retVal;
}
/*package*/ static Set<String> getAnnotationNames() { /*package*/ static Set<String> getAnnotationNames() {
return Collections.unmodifiableSet(getAnnotatedStringHandlerMap().keySet()); return Collections.unmodifiableSet(getAnnotatedStringHandlerMap().keySet());
} }

View file

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

View file

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

View file

@ -36,8 +36,10 @@ import ghidra.util.Msg;
* displayed. * displayed.
*/ */
public class URLAnnotatedStringHandler implements AnnotatedStringHandler { public class URLAnnotatedStringHandler implements AnnotatedStringHandler {
private static final String INVALID_SYMBOL_TEXT = 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" }; private static final String[] SUPPORTED_ANNOTATIONS = { "url", "hyperlink", "href", "link" };
@Override @Override

View file

@ -233,6 +233,13 @@ public class GhidraProject {
return project; return project;
} }
/**
* Returns the underlying ProjectData instance.
*/
public ProjectData getProjectData() {
return projectData;
}
/** /**
* Closes the ghidra project, closing (without saving!) any open programs in * Closes the ghidra project, closing (without saving!) any open programs in
* that project. Also deletes the project if created as a temporary project. * that project. Also deletes the project if created as a temporary project.

View file

@ -93,7 +93,10 @@ public class DataTreeDialog extends DialogComponentProvider
private Integer treeSelectionMode; 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 parent dialog's parent
* @param title title to use * @param title title to use
@ -101,7 +104,7 @@ public class DataTreeDialog extends DialogComponentProvider
* @throws IllegalArgumentException if invalid type is specified * @throws IllegalArgumentException if invalid type is specified
*/ */
public DataTreeDialog(Component parent, String title, int type) { 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); 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) { public void setTreeSelectionMode(int mode) {
if (treePanel != null) { if (treePanel != null) {
treePanel.getTreeSelectionModel().setSelectionMode(mode); treePanel.getTreeSelectionModel().setSelectionMode(mode);

View file

@ -18,6 +18,7 @@ package ghidra.test;
import java.awt.Dialog; import java.awt.Dialog;
import java.awt.Window; import java.awt.Window;
import java.io.*; import java.io.*;
import java.net.URL;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -47,6 +48,7 @@ import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginException; import ghidra.framework.plugintool.util.PluginException;
import ghidra.framework.project.DefaultProjectManager; import ghidra.framework.project.DefaultProjectManager;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.program.database.ProgramDB; import ghidra.program.database.ProgramDB;
import ghidra.program.model.data.FileDataTypeManager; import ghidra.program.model.data.FileDataTypeManager;
import ghidra.program.model.lang.*; import ghidra.program.model.lang.*;
@ -874,10 +876,58 @@ public class TestEnv {
* @param domainFile The domain file used to launch the tool; may be null * @param domainFile The domain file used to launch the tool; may be null
* @return the tool that is launched * @return the tool that is launched
*/ */
public PluginTool launchTool(final String toolName, final DomainFile domainFile) { public PluginTool launchTool(String toolName, DomainFile domainFile) {
AtomicReference<PluginTool> ref = new AtomicReference<>(); AtomicReference<PluginTool> ref = new AtomicReference<>();
AbstractGenericTest.runSwing(() -> { AbstractGenericTest.runSwing(() -> {
PluginTool newTool = doLaunchTool(toolName);
ref.set(newTool);
if (newTool != null) {
newTool.acceptDomainFiles(new DomainFile[] { domainFile });
}
});
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;
}
/**
* Launches a tool of the given name using the given Ghidra URL.
* <p>
* 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<PluginTool> 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(); boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
AbstractDockingTest.setErrorGUIEnabled(false); // disable the error GUI while launching the tool AbstractDockingTest.setErrorGUIEnabled(false); // disable the error GUI while launching the tool
FrontEndTool frontEndToolInstance = getFrontEndTool(); FrontEndTool frontEndToolInstance = getFrontEndTool();
@ -889,21 +939,8 @@ public class TestEnv {
// couldn't find the tool in the workspace...check the test area // couldn't find the tool in the workspace...check the test area
newTool = launchDefaultToolByName(toolName); newTool = launchDefaultToolByName(toolName);
} }
ref.set(newTool);
AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled); AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
newTool.acceptDomainFiles(new DomainFile[] { domainFile }); return newTool;
});
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;
} }
/** /**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 B

View file

@ -19,6 +19,8 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.awt.*; import java.awt.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -37,6 +39,8 @@ import ghidra.framework.model.*;
import ghidra.framework.plugintool.ServiceProvider; import ghidra.framework.plugintool.ServiceProvider;
import ghidra.framework.plugintool.TestDummyServiceProvider; import ghidra.framework.plugintool.TestDummyServiceProvider;
import ghidra.framework.project.ProjectDataService; 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.ProgramBuilder;
import ghidra.program.database.ProgramDB; import ghidra.program.database.ProgramDB;
import ghidra.program.model.address.Address; import ghidra.program.model.address.Address;
@ -620,6 +624,60 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
assertTrue(spyNavigatable.navigatedTo(otherProgramPath, address)); 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 @Test
public void testUnknownAnnotation() { public void testUnknownAnnotation() {
String rawComment = "This is a symbol {@syyyybol bob} annotation"; String rawComment = "This is a symbol {@syyyybol bob} annotation";
@ -903,13 +961,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
private Set<String> openedPrograms = new HashSet<>(); private Set<String> openedPrograms = new HashSet<>();
private Set<String> closedPrograms = new HashSet<>(); private Set<String> closedPrograms = new HashSet<>();
@Override private Program generateProgram(String pathname, String name) {
public Program openProgram(DomainFile domainFile, int version, int state) {
String name = domainFile.getName();
String pathname = domainFile.getPathname();
openedPrograms.add(name);
try { try {
ProgramBuilder builder = new ProgramBuilder(); ProgramBuilder builder = new ProgramBuilder();
builder.setName(pathname); 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 @Override
public boolean closeProgram(Program p, boolean ignoreChanges) { public boolean closeProgram(Program p, boolean ignoreChanges) {
String name = FilenameUtils.getName(p.getName()); String name = FilenameUtils.getName(p.getName());

View file

@ -121,13 +121,8 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest
// If there are queued actions, then we have to kick the handling thread and // If there are queued actions, then we have to kick the handling thread and
// let it finish running. // let it finish running.
try {
assertTrue(eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS)); assertTrue(eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS));
} }
catch (InterruptedException e) {
failWithException("Interrupted waiting for filesystem events", e);
}
}
private void deleteAll(File file) { private void deleteAll(File file) {
if (file.isDirectory()) { if (file.isDirectory()) {

View file

@ -421,15 +421,10 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
assertNotNull(dialog); assertNotNull(dialog);
} }
private DomainFileFilter createStartsWithFilter(String startsWith) { private void showFiltered(final String startsWith) {
return (df) -> df.getName().startsWith(startsWith);
}
private void showFiltered(String startsWith) {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog", dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog",
DataTreeDialog.OPEN, createStartsWithFilter(startsWith)); DataTreeDialog.OPEN, f -> f.getName().startsWith(startsWith));
dialog.showComponent(); dialog.showComponent();
}); });
waitForSwing(); waitForSwing();

View file

@ -79,10 +79,10 @@ public class AddViewToProjectTest extends AbstractGhidraHeadlessIntegrationTest
try { try {
URL view = GhidraURL.makeURL(DIRECTORY_NAME, PROJECT_VIEW1); 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 // 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 // validate the view was added to project
ProjectLocator[] projViews = project.getProjectViews(); ProjectLocator[] projViews = project.getProjectViews();

View file

@ -117,7 +117,7 @@ public class CreateDomainObjectTest extends AbstractGhidraHeadedIntegrationTest
project.close(); project.close();
Project project2 = ProjectTestUtils.getProject(testDir, PROJECT_NAME2); Project project2 = ProjectTestUtils.getProject(testDir, PROJECT_NAME2);
try { try {
project2.addProjectView(GhidraURL.makeURL(testDir, PROJECT_NAME1)); project2.addProjectView(GhidraURL.makeURL(testDir, PROJECT_NAME1), true);
} }
catch (Exception e) { catch (Exception e) {
Assert.fail("View Not found"); Assert.fail("View Not found");

View file

@ -15,15 +15,18 @@
*/ */
package ghidra.base.project; package ghidra.base.project;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import generic.test.TestUtils; import generic.test.AbstractGTest;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFile;
import ghidra.framework.remote.User; import ghidra.framework.remote.User;
import ghidra.framework.store.local.IndexedV1LocalFileSystem;
import ghidra.framework.store.local.LocalFileSystem; import ghidra.framework.store.local.LocalFileSystem;
import ghidra.test.TestEnv; import ghidra.test.TestEnv;
import utilities.util.FileUtilities;
/** /**
* This class represents the idea of a shared Ghidra repository. This class is meant to be * 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<String, User> usersByName = new HashMap<>(); private Map<String, User> usersByName = new HashMap<>();
private Map<User, FakeSharedProject> projectsByUser = new HashMap<>(); private Map<User, FakeSharedProject> projectsByUser = new HashMap<>();
private File versionedFSDir;
private LocalFileSystem versionedFileSystem; private LocalFileSystem versionedFileSystem;
public FakeRepository() { public FakeRepository() throws IOException {
// validation must be enabled if both environments are utilized by a test // validation must be enabled if both environments are utilized by a test
LocalFileSystem.setValidationRequired(); 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); FakeSharedProject project = new FakeSharedProject(this, user);
projectsByUser.put(user, project); projectsByUser.put(user, project);
if (versionedFileSystem == null) {
versionedFileSystem = project.getVersionedFileSystem();
TestUtils.setInstanceField("isShared", versionedFileSystem, Boolean.TRUE);
}
return project; return project;
} }
@ -139,6 +160,8 @@ public class FakeRepository {
*/ */
public void dispose() { public void dispose() {
projectsByUser.values().forEach(p -> disposeProject(p)); projectsByUser.values().forEach(p -> disposeProject(p));
versionedFileSystem.dispose();
FileUtilities.deleteDir(versionedFSDir);
} }
private void disposeProject(FakeSharedProject p) { private void disposeProject(FakeSharedProject p) {

View file

@ -25,7 +25,7 @@ import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import generic.test.AbstractGenericTest; import generic.test.AbstractGTest;
import generic.test.TestUtils; import generic.test.TestUtils;
import ghidra.framework.data.*; import ghidra.framework.data.*;
import ghidra.framework.model.*; import ghidra.framework.model.*;
@ -39,6 +39,7 @@ import ghidra.test.TestProgramManager;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
import junit.framework.AssertionFailedError; import junit.framework.AssertionFailedError;
import utilities.util.FileUtilities;
/** /**
* This class represents the idea of a shared Ghidra project. Each project is associated with * 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 { public FakeSharedProject(FakeRepository repo, User user) throws IOException {
this.repo = repo; this.repo = repo;
String projectDirPath = AbstractGenericTest.getTestDirectoryPath(); String projectDirPath = AbstractGTest.getTestDirectoryPath();
gProject = gProject =
GhidraProject.createProject(projectDirPath, "TestProject_" + user.getName(), true); GhidraProject.createProject(projectDirPath, "TestProject_" + user.getName(), true);
gProject.setDeleteOnClose(true); gProject.setDeleteOnClose(true);
LocalFileSystem fs = repo.getSharedFileSystem(); // use local shared fake repo versioned file system
if (fs != null) { setVersionedFileSystem(repo.getSharedFileSystem());
// first project will keeps its versioned file system
setVersionedFileSystem(fs);
}
} }
FakeSharedProject(User user) throws IOException { FakeSharedProject(User user) throws IOException {
String projectDirPath = AbstractGenericTest.getTestDirectoryPath(); String projectDirPath = AbstractGTest.getTestDirectoryPath();
gProject = gProject =
GhidraProject.createProject(projectDirPath, "TestProject_" + user.getName(), true); GhidraProject.createProject(projectDirPath, "TestProject_" + user.getName(), true);
} }
@ -101,7 +99,7 @@ public class FakeSharedProject {
* @return the project file manager * @return the project file manager
*/ */
public ProjectFileManager getProjectFileManager() { public ProjectFileManager getProjectFileManager() {
return (ProjectFileManager) gProject.getProject().getProjectData(); return (ProjectFileManager) gProject.getProjectData();
} }
/** /**
@ -369,8 +367,11 @@ public class FakeSharedProject {
* @see FakeRepository#dispose() * @see FakeRepository#dispose()
*/ */
public void dispose() { public void dispose() {
ProjectLocator projectLocator = getProjectFileManager().getProjectLocator();
programManager.disposeOpenPrograms(); programManager.disposeOpenPrograms();
gProject.close(); gProject.close();
FileUtilities.deleteDir(projectLocator.getProjectDir());
projectLocator.getMarkerFile().delete();
} }
@Override @Override
@ -400,13 +401,8 @@ public class FakeSharedProject {
(FileSystemEventManager) TestUtils.getInstanceField("eventManager", (FileSystemEventManager) TestUtils.getInstanceField("eventManager",
versionedFileSystem); versionedFileSystem);
try {
eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS); eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
} }
catch (InterruptedException e) {
failWithException("Interrupted waiting for filesystem events", e);
}
}
private DomainFolder getFolder(String path) throws Exception { private DomainFolder getFolder(String path) throws Exception {
Project project = getGhidraProject().getProject(); Project project = getGhidraProject().getProject();

View file

@ -99,6 +99,10 @@ public class CollectFailedRelocations extends GhidraScript {
if (monitor.isCancelled()) { if (monitor.isCancelled()) {
return; 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)) { if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile); programs.add(domainFile);
} }

View file

@ -258,6 +258,10 @@ public class CreateMultipleLibraries extends GhidraScript {
DomainFile[] files = myFolder.getFiles(); DomainFile[] files = myFolder.getFiles();
for (DomainFile domainFile : files) { for (DomainFile domainFile : files) {
monitor.checkCanceled(); 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)) { if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile); programs.add(domainFile);
} }

View file

@ -372,6 +372,10 @@ public class FidStatistics extends GhidraScript {
DomainFile[] files = folder.getFiles(); DomainFile[] files = folder.getFiles();
for (DomainFile domainFile : files) { for (DomainFile domainFile : files) {
monitor.checkCanceled(); 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)) { if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile); programs.add(domainFile);
} }

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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, //Opens all programs under a chosen domain folder, grabs their error count,
//then sorts in increasing error order and prints them //then sorts in increasing error order and prints them
//@category FunctionID //@category FunctionID
import java.io.IOException;
import java.util.*;
import generic.stl.Pair; import generic.stl.Pair;
import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraScript;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFile;
@ -27,9 +29,6 @@ import ghidra.util.Msg;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException; import ghidra.util.exception.VersionException;
import java.io.IOException;
import java.util.*;
public class FindErrors extends GhidraScript { public class FindErrors extends GhidraScript {
@Override @Override
@ -82,6 +81,10 @@ public class FindErrors extends GhidraScript {
if (monitor.isCancelled()) { if (monitor.isCancelled()) {
return; 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)) { if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile); programs.add(domainFile);
} }

View file

@ -100,6 +100,10 @@ public class FindFunctionByHash extends GhidraScript {
if (monitor.isCancelled()) { if (monitor.isCancelled()) {
return; 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)) { if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile); programs.add(domainFile);
} }

View file

@ -16,6 +16,9 @@
//Opens all programs under a chosen domain folder, scans them for functions //Opens all programs under a chosen domain folder, scans them for functions
//that match a user supplied name, and prints info about the match. //that match a user supplied name, and prints info about the match.
//@category FunctionID //@category FunctionID
import java.io.IOException;
import java.util.ArrayList;
import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraScript;
import ghidra.feature.fid.service.FidService; import ghidra.feature.fid.service.FidService;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFile;
@ -26,9 +29,6 @@ import ghidra.util.Msg;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException; import ghidra.util.exception.VersionException;
import java.io.IOException;
import java.util.ArrayList;
public class FindNamedFunction extends GhidraScript { public class FindNamedFunction extends GhidraScript {
FidService service; FidService service;
@ -85,6 +85,10 @@ public class FindNamedFunction extends GhidraScript {
if (monitor.isCancelled()) { if (monitor.isCancelled()) {
return; 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)) { if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile); programs.add(domainFile);
} }

View file

@ -153,6 +153,10 @@ public class IngestTask extends Task {
for (DomainFile domainFile : files) { for (DomainFile domainFile : files) {
monitor.checkCanceled(); monitor.checkCanceled();
monitor.incrementProgress(1); 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)) { if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
programs.add(domainFile); programs.add(domainFile);
} }

View file

@ -21,6 +21,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import db.*; import db.*;
import ghidra.app.util.task.OpenProgramTask; 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.ImpliedMatchProgramCorrelator;
import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator; import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator;
import ghidra.feature.vt.api.impl.*; import ghidra.feature.vt.api.impl.*;
@ -309,7 +310,8 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession, VTC
TaskLauncher.launch(openTask); TaskLauncher.launch(openTask);
return openTask.getOpenProgram(); OpenProgramRequest openProgram = openTask.getOpenProgram();
return openProgram != null ? openProgram.getProgram() : null;
} }
@Override @Override

View file

@ -24,7 +24,8 @@ import db.OpenMode;
import db.buffers.BufferFile; import db.buffers.BufferFile;
import generic.theme.GIcon; import generic.theme.GIcon;
import ghidra.feature.vt.api.db.VTSessionDB; 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.ChangeSet;
import ghidra.framework.model.DomainObject; import ghidra.framework.model.DomainObject;
import ghidra.framework.store.*; import ghidra.framework.store.*;
@ -34,7 +35,8 @@ import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException; import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
public class VTSessionContentHandler extends DBContentHandler { public class VTSessionContentHandler extends DBContentHandler<VTSessionDB> {
private static Icon ICON = new GIcon("icon.version.tracking.session.content.type"); private static Icon ICON = new GIcon("icon.version.tracking.session.content.type");
public final static String CONTENT_TYPE = "VersionTracking"; public final static String CONTENT_TYPE = "VersionTracking";
@ -49,7 +51,6 @@ public class VTSessionContentHandler extends DBContentHandler {
"Unsupported domain object: " + domainObject.getClass().getName()); "Unsupported domain object: " + domainObject.getClass().getName());
} }
return createFile((VTSessionDB) domainObject, CONTENT_TYPE, fs, path, name, monitor); return createFile((VTSessionDB) domainObject, CONTENT_TYPE, fs, path, name, monitor);
} }
@Override @Override
@ -74,7 +75,7 @@ public class VTSessionContentHandler extends DBContentHandler {
} }
@Override @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) boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
throws IOException, CancelledException, VersionException { throws IOException, CancelledException, VersionException {
@ -119,7 +120,7 @@ public class VTSessionContentHandler extends DBContentHandler {
} }
@Override @Override
public Class<? extends DomainObject> getDomainObjectClass() { public Class<VTSessionDB> getDomainObjectClass() {
return VTSessionDB.class; return VTSessionDB.class;
} }
@ -129,7 +130,7 @@ public class VTSessionContentHandler extends DBContentHandler {
} }
@Override @Override
public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version, public VTSessionDB getImmutableObject(FolderItem item, Object consumer, int version,
int minChangeVersion, TaskMonitor monitor) int minChangeVersion, TaskMonitor monitor)
throws IOException, CancelledException, VersionException { throws IOException, CancelledException, VersionException {
@ -149,7 +150,7 @@ public class VTSessionContentHandler extends DBContentHandler {
} }
@Override @Override
public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, public VTSessionDB getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
Object consumer, TaskMonitor monitor) Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException { throws IOException, VersionException, CancelledException {

View file

@ -15,6 +15,10 @@
*/ */
package ghidra.feature.vt.gui.actions; 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.api.main.VTSession;
import ghidra.feature.vt.gui.plugin.VTController; import ghidra.feature.vt.gui.plugin.VTController;
import ghidra.feature.vt.gui.plugin.VTPlugin; import ghidra.feature.vt.gui.plugin.VTPlugin;
@ -23,10 +27,6 @@ import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFileFilter; import ghidra.framework.model.DomainFileFilter;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.MenuData;
import docking.tool.ToolConstants;
public class OpenVersionTrackingSessionAction extends DockingAction { public class OpenVersionTrackingSessionAction extends DockingAction {
@ -60,5 +60,10 @@ public class OpenVersionTrackingSessionAction extends DockingAction {
Class<?> c = f.getDomainObjectClass(); Class<?> c = f.getDomainObjectClass();
return VTSession.class.isAssignableFrom(c); return VTSession.class.isAssignableFrom(c);
} }
@Override
public boolean followLinkedFolders() {
return false;
}
} }
} }

View file

@ -29,6 +29,7 @@ import docking.widgets.label.GDLabel;
import docking.wizard.*; import docking.wizard.*;
import generic.theme.*; import generic.theme.*;
import ghidra.app.util.task.OpenProgramTask; import ghidra.app.util.task.OpenProgramTask;
import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
import ghidra.framework.main.DataTreeDialog; import ghidra.framework.main.DataTreeDialog;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFolder; import ghidra.framework.model.DomainFolder;
@ -383,8 +384,8 @@ public class NewSessionPanel extends AbstractMageJPanel<VTWizardStateKey> {
OpenProgramTask openProgramTask = new OpenProgramTask(programInfo.getFile(), tool); OpenProgramTask openProgramTask = new OpenProgramTask(programInfo.getFile(), tool);
new TaskLauncher(openProgramTask, tool.getActiveWindow()); new TaskLauncher(openProgramTask, tool.getActiveWindow());
Program program = openProgramTask.getOpenProgram(); OpenProgramRequest openProgram = openProgramTask.getOpenProgram();
programInfo.setProgram(program); programInfo.setProgram(openProgram != null ? openProgram.getProgram() : null);
} }
@Override @Override

View file

@ -20,12 +20,12 @@ import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import docking.widgets.OptionDialog; 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.feature.vt.gui.task.SaveTask;
import ghidra.framework.main.DataTreeDialog; import ghidra.framework.main.DataTreeDialog;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainFileFilter; import ghidra.framework.model.DomainFileFilter;
import ghidra.program.database.ProgramDB; import ghidra.program.model.listing.Program;
import ghidra.util.HTMLUtilities; import ghidra.util.HTMLUtilities;
import ghidra.util.task.TaskLauncher; import ghidra.util.task.TaskLauncher;
@ -36,23 +36,20 @@ public class VTWizardUtils {
} }
public static final DomainFileFilter VT_SESSION_FILTER = new DomainFileFilter() { public static final DomainFileFilter VT_SESSION_FILTER = new DomainFileFilter() {
@Override @Override
public boolean accept(DomainFile df) { public boolean accept(DomainFile df) {
if (VTSessionContentHandler.CONTENT_TYPE.equals(df.getContentType())) { return VTSession.class.isAssignableFrom(df.getDomainObjectClass());
return true;
} }
@Override
public boolean followLinkedFolders() {
return false; return false;
} }
}; };
public static final DomainFileFilter PROGRAM_FILTER = new DomainFileFilter() { public static final DomainFileFilter PROGRAM_FILTER = f -> {
@Override return Program.class.isAssignableFrom(f.getDomainObjectClass());
public boolean accept(DomainFile df) {
if (ProgramDB.CONTENT_TYPE.equals(df.getContentType())) {
return true;
}
return false;
}
}; };
static DomainFile chooseDomainFile(Component parent, String domainIdentifier, static DomainFile chooseDomainFile(Component parent, String domainIdentifier,

View file

@ -16,8 +16,7 @@
package db.buffers; package db.buffers;
import java.io.IOException; import java.io.IOException;
import java.rmi.NoSuchObjectException; import java.rmi.*;
import java.rmi.Remote;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import ghidra.util.Msg; import ghidra.util.Msg;
@ -104,8 +103,8 @@ public class BufferFileAdapter implements BufferFile {
bufferFileHandle.dispose(); bufferFileHandle.dispose();
} }
catch (IOException e) { catch (IOException e) {
// handle may have already been disposed // handle may have already been disposed or disconnected
if (!(e instanceof NoSuchObjectException)) { if (!(e instanceof NoSuchObjectException) && !(e instanceof ConnectException)) {
Msg.error(this, e); Msg.error(this, e);
} }
} }

View file

@ -16,6 +16,7 @@
package docking.widgets.tree; package docking.widgets.tree;
import java.util.*; import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.swing.Icon; import javax.swing.Icon;
@ -197,6 +198,22 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable<GTre
return null; return null;
} }
/**
* Returns the child node of this node with the given name which satisfies predicate filter.
*
* @param name the name of the child to be returned
* @param filter predicate filter
* @return the child with the given name
*/
public GTreeNode getChild(String name, Predicate<GTreeNode> 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. * 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<GTre
} }
} }
/**
* Determine if this node may be auto-expanded. Some special node cases may need to prevent
* or limit auto-expansion due to tree depth or other special conditions.
* @return true if this node allows auto-expansion, else false.
*/
public boolean isAutoExpandPermitted() {
return !isLeaf();
}
/** /**
* Convenience method for collapsing (closing) this node in the tree. If this node is not * Convenience method for collapsing (closing) this node in the tree. If this node is not
* currently attached to a visible tree, then this call does nothing * currently attached to a visible tree, then this call does nothing

View file

@ -42,14 +42,24 @@ public class GTreeExpandAllTask extends GTreeTask {
monitor.initialize(1000); monitor.initialize(1000);
monitor.setMessage("Expanding nodes..."); monitor.setMessage("Expanding nodes...");
try { try {
expandNode(node, monitor); expandNode(node, true, monitor);
} }
catch (CancelledException e) { catch (CancelledException e) {
// Not everything expanded which is ok // Not everything expanded which is ok
} }
} }
protected void expandNode(GTreeNode parent, TaskMonitor monitor) throws CancelledException { /**
* Expand the specified parent tree node.
* @param parent the tree node to be expanded
* @param force if parent node has auto-expand disabled
* (see {@link GTreeNode#isAutoExpandPermitted()}) passing true will force such a node to
* expand, false will respect this restriction and not expand the parent node.
* @param monitor task monitor
* @throws CancelledException if task cancelled
*/
protected void expandNode(GTreeNode parent, boolean force, TaskMonitor monitor)
throws CancelledException {
// only expand MAX number of nodes. // only expand MAX number of nodes.
if (monitor.getProgress() >= MAX) { if (monitor.getProgress() >= MAX) {
return; return;
@ -57,6 +67,9 @@ public class GTreeExpandAllTask extends GTreeTask {
if (parent.isLeaf()) { if (parent.isLeaf()) {
return; return;
} }
if (!force && !parent.isAutoExpandPermitted()) {
return;
}
monitor.checkCanceled(); monitor.checkCanceled();
List<GTreeNode> allChildren = parent.getChildren(); List<GTreeNode> allChildren = parent.getChildren();
if (allChildren.size() == 0) { if (allChildren.size() == 0) {
@ -68,9 +81,9 @@ public class GTreeExpandAllTask extends GTreeTask {
} }
for (GTreeNode child : allChildren) { for (GTreeNode child : allChildren) {
monitor.checkCanceled(); 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) { private void expandPath(final TreePath treePath, final TaskMonitor monitor) {

View file

@ -158,8 +158,10 @@ public class RepositoryAdapter implements RemoteAdapterListener {
/** /**
* Attempt to connect to the server. * 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) { synchronized (serverAdapter) {
if (repository != null) { if (repository != null) {
try { try {
@ -180,7 +182,7 @@ public class RepositoryAdapter implements RemoteAdapterListener {
unexpectedDisconnect = false; unexpectedDisconnect = false;
if (repository == null) { if (repository == null) {
noSuchRepository = true; noSuchRepository = true;
throw new IOException("Repository '" + name + "': not found"); throw new RepositoryNotFoundException("Repository '" + name + "': not found");
} }
Msg.info(this, "Connected to repository '" + name + "'"); Msg.info(this, "Connected to repository '" + name + "'");
changeDispatcher.start(); changeDispatcher.start();

View file

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

View file

@ -52,6 +52,9 @@ public class RepositoryServerAdapter {
// Keeps track of whether the connection attempt was cancelled by the user // Keeps track of whether the connection attempt was cancelled by the user
private boolean connectCancelled = false; private boolean connectCancelled = false;
// Keep track of last connect error
private Throwable lastConnectError;
private WeakSet<RemoteAdapterListener> listenerList = private WeakSet<RemoteAdapterListener> listenerList =
WeakDataStructureFactory.createCopyOnWriteWeakSet(); WeakDataStructureFactory.createCopyOnWriteWeakSet();
@ -105,6 +108,14 @@ public class RepositoryServerAdapter {
return connectCancelled; 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. * Notify listeners of a connection state change.
*/ */
@ -135,7 +146,7 @@ public class RepositoryServerAdapter {
} }
} }
Throwable cause = null; lastConnectError = null;
try { try {
try { try {
serverHandle = ClientUtil.connect(server); serverHandle = ClientUtil.connect(server);
@ -162,22 +173,22 @@ public class RepositoryServerAdapter {
catch (LoginException e) { catch (LoginException e) {
Msg.showError(this, null, "Server Error", Msg.showError(this, null, "Server Error",
"Server access denied (" + serverInfoStr + ")."); "Server access denied (" + serverInfoStr + ").");
cause = e; lastConnectError = e;
} }
catch (GeneralSecurityException e) { catch (GeneralSecurityException e) {
Msg.showError(this, null, "Server Error", Msg.showError(this, null, "Server Error",
"Server access denied (" + serverInfoStr + "): " + e.getMessage()); "Server access denied (" + serverInfoStr + "): " + e.getMessage());
cause = e; lastConnectError = e;
} }
catch (SocketTimeoutException | java.net.ConnectException | java.rmi.ConnectException e) { catch (SocketTimeoutException | java.net.ConnectException | java.rmi.ConnectException e) {
Msg.showError(this, null, "Server Error", Msg.showError(this, null, "Server Error",
"Connection to server failed (" + server + ")."); "Connection to server failed (" + server + ").");
cause = e; lastConnectError = e;
} }
catch (java.net.UnknownHostException | java.rmi.UnknownHostException e) { catch (java.net.UnknownHostException | java.rmi.UnknownHostException e) {
Msg.showError(this, null, "Server Error", Msg.showError(this, null, "Server Error",
"Server Not Found (" + server.getServerName() + ")."); "Server Not Found (" + server.getServerName() + ").");
cause = e; lastConnectError = e;
} }
catch (RemoteException e) { catch (RemoteException e) {
String msg = e.getMessage(); String msg = e.getMessage();
@ -185,7 +196,7 @@ public class RepositoryServerAdapter {
while ((t = t.getCause()) != null) { while ((t = t.getCause()) != null) {
String err = t.getMessage(); String err = t.getMessage();
msg = err != null ? err : t.toString(); msg = err != null ? err : t.toString();
cause = t; lastConnectError = t;
} }
Msg.showError(this, null, "Server Error", Msg.showError(this, null, "Server Error",
"An error occurred on the server (" + serverInfoStr + ").\n" + msg, e); "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, "An error occurred while connecting to the server (" + serverInfoStr + ").\n" + msg,
e); e);
} }
throw new NotConnectedException("Not connected to repository server", cause); throw new NotConnectedException("Not connected to repository server", lastConnectError);
} }
private void checkPasswordExpiration() { private void checkPasswordExpiration() {

View file

@ -32,7 +32,7 @@ public interface RepositoryHandle {
// TODO: NOTE! Debugging client or sever garbage collection delays could // TODO: NOTE! Debugging client or sever garbage collection delays could
// cause handle to be disposed prematurely. // 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. * Returns the name of this repository.

View file

@ -22,41 +22,69 @@ import java.util.concurrent.*;
* <code>FileSystemListenerList</code> maintains a list of FileSystemListener's. * <code>FileSystemListenerList</code> maintains a list of FileSystemListener's.
* This class, acting as a FileSystemListener, simply relays each callback to * This class, acting as a FileSystemListener, simply relays each callback to
* all FileSystemListener's within its list. Employs either a synchronous * 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 { public class FileSystemEventManager implements FileSystemListener {
private static enum ThreadState {
STOPPED, RUNNING, DISPOSED
}
private List<FileSystemListener> listeners = new CopyOnWriteArrayList<>(); private List<FileSystemListener> listeners = new CopyOnWriteArrayList<>();
private BlockingQueue<FileSystemEvent> eventQueue = new LinkedBlockingQueue<>(); private BlockingQueue<FileSystemEvent> eventQueue = new LinkedBlockingQueue<>();
private volatile boolean disposed = false; private final boolean asyncDispatchEnabled;
private volatile ThreadState state = ThreadState.STOPPED;
private Thread thread; private Thread thread;
/** /**
* Constructor * Constructor
* @param enableAsynchronousDispatching if true a separate dispatch thread will be used * @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) { public FileSystemEventManager(boolean enableAsynchronousDispatching) {
asyncDispatchEnabled = enableAsynchronousDispatching;
}
if (enableAsynchronousDispatching) { /**
* 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();
}
}
private synchronized void startDispatchThread() {
if (asyncDispatchEnabled && state == ThreadState.STOPPED) {
// only starts when first listener is added
state = ThreadState.RUNNING;
thread = new FileSystemEventProcessingThread(); thread = new FileSystemEventProcessingThread();
thread.start(); thread.start();
} }
} }
public void dispose() {
disposed = true;
if (thread != null) {
thread.interrupt();
}
}
/** /**
* Add a listener to this list. * Add a listener to this list.
* @param listener the listener * @param listener the listener
*/ */
public void add(FileSystemListener listener) { public void add(FileSystemListener listener) {
startDispatchThread(); // if asyncDispatchEnabled
listeners.add(listener); listeners.add(listener);
} }
@ -116,30 +144,34 @@ public class FileSystemEventManager implements FileSystemListener {
@Override @Override
public void syncronize() { public void syncronize() {
// Note: synchronize calls will only work when using a threaded event queue // Note: synchronize calls will only work when using a threaded event queue
if (isAsynchronous()) { if (asyncDispatchEnabled) {
add(new SynchronizeEvent()); queueEvent(new SynchronizeEvent());
} }
} }
private boolean isAsynchronous() { /**
return thread != null; * Queue specified event if listener thread is running
} * @param ev filesystm event
* @return true if queued, else false if listener thread not running
private void add(FileSystemEvent ev) { */
if (!listeners.isEmpty()) { private boolean queueEvent(FileSystemEvent ev) {
eventQueue.add(ev); if (state == ThreadState.RUNNING) {
return eventQueue.add(ev);
} }
return false;
} }
private void handleEvent(FileSystemEvent e) { private void handleEvent(FileSystemEvent e) {
if (disposed) { if (state == ThreadState.DISPOSED) {
return; return;
} }
if (isAsynchronous()) { if (asyncDispatchEnabled) {
add(e); // if there are no listeners event will be discarded (i.e., listener thread not running)
queueEvent(e);
} }
else { else {
// process in a synchronous fashion in current thread
e.process(listeners); e.process(listeners);
} }
} }
@ -154,18 +186,27 @@ public class FileSystemEventManager implements FileSystemListener {
* *
* @param timeout the maximum time to wait * @param timeout the maximum time to wait
* @param unit the time unit of the {@code time} argument * @param unit the time unit of the {@code time} argument
* @return true if the events were processed in the given timeout * @return true if the events were processed in the given timeout. A false value will be
* @throws InterruptedException if this waiting thread is interrupted * returned if either a timeout occured
*/ */
public boolean flushEvents(long timeout, TimeUnit unit) throws InterruptedException { public boolean flushEvents(long timeout, TimeUnit unit) {
if (!isAsynchronous()) { if (!asyncDispatchEnabled) {
return true; // each thread processes its own event return true; // each thread processes its own event
} }
MarkerEvent event = new MarkerEvent(); MarkerEvent event = new MarkerEvent();
eventQueue.add(event); if (!queueEvent(event)) {
// events are not queuing since there are no listeners or dispose has occured
return true;
}
try {
return event.waitForEvent(timeout, unit); return event.waitForEvent(timeout, unit);
} }
catch (InterruptedException e) {
// ignore - listener thread stopped or disposed
return true;
}
}
//================================================================================================== //==================================================================================================
// Inner Classes // Inner Classes
@ -180,18 +221,14 @@ public class FileSystemEventManager implements FileSystemListener {
@Override @Override
public void run() { public void run() {
while (!disposed) { while (state == ThreadState.RUNNING) {
FileSystemEvent event; FileSystemEvent event;
try { try {
event = eventQueue.take(); event = eventQueue.take();
event.process(listeners); event.process(listeners);
} }
catch (InterruptedException e) { catch (InterruptedException e) {
// interrupt has been cleared; if other threads rely on this interrupted state, // ignore - interrupt has been cleared
// 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
} }
} }
} }

View file

@ -916,6 +916,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
@Override @Override
public int getItemCount() throws IOException { public int getItemCount() throws IOException {
checkDisposed();
if (readOnly) { if (readOnly) {
refreshReadOnlyIndex(); refreshReadOnlyIndex();
} }
@ -930,11 +931,9 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
return count; return count;
} }
/*
* @see ghidra.framework.store.FileSystem#getFolders(java.lang.String)
*/
@Override @Override
public synchronized String[] getFolderNames(String folderPath) throws IOException { public synchronized String[] getFolderNames(String folderPath) throws IOException {
checkDisposed();
if (readOnly) { if (readOnly) {
refreshReadOnlyIndex(); refreshReadOnlyIndex();
} }
@ -948,13 +947,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
} }
} }
/*
* @see ghidra.framework.store.FileSystem#createFolder(java.lang.String, java.lang.String)
*/
@Override @Override
public synchronized void createFolder(String parentPath, String folderName) public synchronized void createFolder(String parentPath, String folderName)
throws InvalidNameException, IOException { throws InvalidNameException, IOException {
checkDisposed();
if (readOnly) { if (readOnly) {
throw new ReadOnlyException(); throw new ReadOnlyException();
} }
@ -987,12 +985,11 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
eventManager.folderCreated(parentPath, getName(path)); eventManager.folderCreated(parentPath, getName(path));
} }
/*
* @see ghidra.framework.store.FileSystem#deleteFolder(java.lang.String)
*/
@Override @Override
public synchronized void deleteFolder(String folderPath) throws IOException { public synchronized void deleteFolder(String folderPath) throws IOException {
checkDisposed();
if (readOnly) { if (readOnly) {
throw new ReadOnlyException(); throw new ReadOnlyException();
} }
@ -1059,13 +1056,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
getListener().itemCreated(destFolderPath, itemName); getListener().itemCreated(destFolderPath, itemName);
} }
/*
* @see ghidra.framework.store.FileSystem#moveItem(java.lang.String, java.lang.String, java.lang.String)
*/
@Override @Override
public synchronized void moveItem(String folderPath, String name, String newFolderPath, public synchronized void moveItem(String folderPath, String name, String newFolderPath,
String newName) throws IOException, InvalidNameException { String newName) throws IOException, InvalidNameException {
checkDisposed();
if (readOnly) { if (readOnly) {
throw new ReadOnlyException(); throw new ReadOnlyException();
} }
@ -1140,13 +1136,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
deleteEmptyVersionedFolders(folderPath); deleteEmptyVersionedFolders(folderPath);
} }
/*
* @see ghidra.framework.store.FileSystem#moveFolder(java.lang.String, java.lang.String, java.lang.String)
*/
@Override @Override
public synchronized void moveFolder(String parentPath, String folderName, String newParentPath) public synchronized void moveFolder(String parentPath, String folderName, String newParentPath)
throws InvalidNameException, IOException { throws InvalidNameException, IOException {
checkDisposed();
if (readOnly) { if (readOnly) {
throw new ReadOnlyException(); 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 @Override
public synchronized void renameFolder(String parentPath, String folderName, public synchronized void renameFolder(String parentPath, String folderName,
String newFolderName) throws InvalidNameException, IOException { String newFolderName) throws InvalidNameException, IOException {
checkDisposed();
if (readOnly) { if (readOnly) {
throw new ReadOnlyException(); throw new ReadOnlyException();
} }
@ -1247,16 +1241,14 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
eventManager.folderRenamed(parentPath, folderName, newFolderName); eventManager.folderRenamed(parentPath, folderName, newFolderName);
} }
/*
* @see ghidra.framework.store.FileSystem#folderExists(java.lang.String)
*/
@Override @Override
public synchronized boolean folderExists(String folderPath) { public synchronized boolean folderExists(String folderPath) {
try { try {
checkDisposed();
getFolder(folderPath, GetFolderOption.READ_ONLY); getFolder(folderPath, GetFolderOption.READ_ONLY);
return true; return true;
} }
catch (NotFoundException e) { catch (IOException | NotFoundException e) {
return false; return false;
} }
} }

View file

@ -46,7 +46,7 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem {
* @throws FileNotFoundException if specified rootPath does not exist * @throws FileNotFoundException if specified rootPath does not exist
* @throws IOException if error occurs while reading/writing index files * @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 { boolean enableAsyncronousDispatching, boolean create) throws IOException {
super(rootPath, isVersioned, readOnly, enableAsyncronousDispatching, create); super(rootPath, isVersioned, readOnly, enableAsyncronousDispatching, create);
} }
@ -134,6 +134,7 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem {
@Override @Override
public FolderItem getItem(String fileID) throws IOException, UnsupportedOperationException { public FolderItem getItem(String fileID) throws IOException, UnsupportedOperationException {
checkDisposed();
if (fileIdMap == null) { if (fileIdMap == null) {
return null; return null;
} }

View file

@ -71,6 +71,8 @@ public abstract class LocalFileSystem implements FileSystem {
private static boolean refreshRequired = false; private static boolean refreshRequired = false;
private boolean disposed = false;
protected final File root; protected final File root;
protected final boolean isVersioned; protected final boolean isVersioned;
protected final boolean readOnly; protected final boolean readOnly;
@ -78,9 +80,6 @@ public abstract class LocalFileSystem implements FileSystem {
private RepositoryLogger repositoryLogger; private RepositoryLogger repositoryLogger;
// Always false in production; can be manipulated by tests
private boolean isShared;
/** /**
* Construct a local filesystem for existing data * Construct a local filesystem for existing data
* @param rootPath * @param rootPath
@ -285,25 +284,16 @@ public abstract class LocalFileSystem implements FileSystem {
return refreshRequired; return refreshRequired;
} }
/*
* @see ghidra.framework.store.FileSystem#isVersioned()
*/
@Override @Override
public boolean isVersioned() { public boolean isVersioned() {
return isVersioned; return isVersioned;
} }
/*
* @see ghidra.framework.store.FileSystem#isOnline()
*/
@Override @Override
public boolean isOnline() { public boolean isOnline() {
return true; return !disposed;
} }
/*
* @see ghidra.framework.store.FileSystem#isReadOnly()
*/
@Override @Override
public boolean isReadOnly() { public boolean isReadOnly() {
return readOnly; return readOnly;
@ -388,9 +378,6 @@ public abstract class LocalFileSystem implements FileSystem {
return getItemNames(folderPath, false); return getItemNames(folderPath, false);
} }
/*
* @see ghidra.framework.store.FileSystem#getItem(java.lang.String, java.lang.String)
*/
@Override @Override
public synchronized LocalFolderItem getItem(String folderPath, String name) throws IOException { public synchronized LocalFolderItem getItem(String folderPath, String name) throws IOException {
try { try {
@ -424,9 +411,6 @@ public abstract class LocalFileSystem implements FileSystem {
throw new UnsupportedOperationException("getItem by File-ID"); 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 @Override
public synchronized LocalDatabaseItem createDatabase(String parentPath, String name, public synchronized LocalDatabaseItem createDatabase(String parentPath, String name,
String fileID, BufferFile bufferFile, String comment, String contentType, String fileID, BufferFile bufferFile, String comment, String contentType,
@ -483,9 +467,6 @@ public abstract class LocalFileSystem implements FileSystem {
return item; return item;
} }
/*
* @see ghidra.framework.store.FileSystem#createDatabase(java.lang.String, java.lang.String, java.lang.String, int, java.lang.String)
*/
@Override @Override
public LocalManagedBufferFile createDatabase(String parentPath, String name, String fileID, public LocalManagedBufferFile createDatabase(String parentPath, String name, String fileID,
String contentType, int bufferSize, String user, String projectPath) String contentType, int bufferSize, String user, String projectPath)
@ -513,9 +494,6 @@ public abstract class LocalFileSystem implements FileSystem {
return bufferFile; 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 @Override
public synchronized LocalDataFile createDataFile(String parentPath, String name, public synchronized LocalDataFile createDataFile(String parentPath, String name,
InputStream istream, String comment, String contentType, TaskMonitor monitor) InputStream istream, String comment, String contentType, TaskMonitor monitor)
@ -546,9 +524,6 @@ public abstract class LocalFileSystem implements FileSystem {
return dataFile; 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 @Override
public LocalDatabaseItem createFile(String parentPath, String name, File packedFile, public LocalDatabaseItem createFile(String parentPath, String name, File packedFile,
TaskMonitor monitor, String user) TaskMonitor monitor, String user)
@ -591,9 +566,6 @@ public abstract class LocalFileSystem implements FileSystem {
return item; return item;
} }
/*
* @see ghidra.framework.store.FileSystem#moveItem(java.lang.String, java.lang.String, java.lang.String)
*/
@Override @Override
public synchronized void moveItem(String folderPath, String name, String newFolderPath, public synchronized void moveItem(String folderPath, String name, String newFolderPath,
String newName) throws IOException, InvalidNameException { String newName) throws IOException, InvalidNameException {
@ -652,9 +624,6 @@ public abstract class LocalFileSystem implements FileSystem {
@Override @Override
public abstract boolean folderExists(String folderPath); public abstract boolean folderExists(String folderPath);
/*
* @see ghidra.framework.store.FileSystem#fileExists(java.lang.String, java.lang.String)
*/
@Override @Override
public boolean fileExists(String folderPath, String name) { public boolean fileExists(String folderPath, String name) {
try { try {
@ -669,9 +638,6 @@ public abstract class LocalFileSystem implements FileSystem {
} }
} }
/*
* @see ghidra.framework.store.FileSystem#addFileSystemListener(ghidra.framework.store.FileSystemListener)
*/
@Override @Override
public void addFileSystemListener(FileSystemListener listener) { public void addFileSystemListener(FileSystemListener listener) {
if (eventManager != null) { if (eventManager != null) {
@ -679,9 +645,6 @@ public abstract class LocalFileSystem implements FileSystem {
} }
} }
/*
* @see ghidra.framework.store.FileSystem#removeFileSystemListener(ghidra.framework.store.FileSystemListener)
*/
@Override @Override
public void removeFileSystemListener(FileSystemListener listener) { public void removeFileSystemListener(FileSystemListener listener) {
if (eventManager != null) { if (eventManager != null) {
@ -829,8 +792,7 @@ public abstract class LocalFileSystem implements FileSystem {
@Override @Override
public boolean isShared() { public boolean isShared() {
// Does not support direct sharing in production return false;
return isShared;
} }
// static void testValidPathLength(File file) throws IOException { // static void testValidPathLength(File file) throws IOException {
@ -846,6 +808,17 @@ public abstract class LocalFileSystem implements FileSystem {
if (eventManager != null) { if (eventManager != null) {
eventManager.dispose(); 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() { public boolean migrationInProgress() {

View file

@ -929,13 +929,8 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
FileSystemEventManager eventManager = FileSystemEventManager eventManager =
(FileSystemEventManager) TestUtils.getInstanceField("eventManager", fs); (FileSystemEventManager) TestUtils.getInstanceField("eventManager", fs);
try {
eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS); eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
} }
catch (InterruptedException e) {
failWithException("Interrupted waiting for filesystem events", e);
}
}
} }
class MyEvent { class MyEvent {

View file

@ -507,6 +507,41 @@ public abstract class AbstractGenericTest extends AbstractGTest {
return null; return null;
} }
public static <T extends Component> List<T> findComponents(Container parent,
Class<T> desiredClass) {
return findComponents(parent, desiredClass, false);
}
public static <T extends Component> List<T> findComponents(Container parent,
Class<T> desiredClass, boolean checkOwnedWindows) {
Component[] comps = parent.getComponents();
List<T> 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 * Get the first field object contained within object ownerInstance which
* has the type classType. This method is only really useful if it is known * has the type classType. This method is only really useful if it is known

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,8 +16,7 @@
package generic.util; package generic.util;
import java.io.*; import java.io.*;
import java.nio.channels.FileChannel; import java.nio.channels.*;
import java.nio.channels.FileLock;
public class FileChannelLock { public class FileChannelLock {
@ -51,7 +49,7 @@ public class FileChannelLock {
return isLocked; return isLocked;
} }
catch (IOException e) { catch (IOException | OverlappingFileLockException e) {
release(); release();
} }
return false; return false;

View file

@ -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/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/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/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/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/monitor.png||FAMFAMFAM Icons - CC 2.5|||silk|END|
src/main/resources/images/noneInTool.gif||GHIDRA||reviewed||END| src/main/resources/images/noneInTool.gif||GHIDRA||reviewed||END|

View file

@ -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 = icon.check
icon.project.data.file.ghidra.checked.out.exclusive = checkex.png icon.project.data.file.ghidra.checked.out.exclusive = checkex.png
icon.project.data.file.ghidra.hijacked = small_hijack.gif 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.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] [Dark Defaults]

View file

@ -19,9 +19,7 @@ import java.io.IOException;
import javax.swing.Icon; import javax.swing.Icon;
import db.DBHandle; import ghidra.framework.model.*;
import ghidra.framework.model.ChangeSet;
import ghidra.framework.model.DomainObject;
import ghidra.framework.store.FileSystem; import ghidra.framework.store.FileSystem;
import ghidra.framework.store.FolderItem; import ghidra.framework.store.FolderItem;
import ghidra.util.InvalidNameException; import ghidra.util.InvalidNameException;
@ -31,15 +29,17 @@ import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor; 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. * the ClassSearcher will not find them.
* *
* <code>ContentHandler</code> defines an application interface for converting * <code>ContentHandler</code> defines an application interface for converting
* between a specific domain object implementation and folder item storage. * between a specific domain object implementation and folder item storage.
* This interface also defines a method which provides an appropriate icon * This interface also defines a method which provides an appropriate icon
* corresponding to the content. * corresponding to the content.
*
* @param <T> {@link DomainObjectAdapter} implementation class
*/ */
public interface ContentHandler extends ExtensionPoint { public interface ContentHandler<T extends DomainObjectAdapter> extends ExtensionPoint {
public static final String UNKNOWN_CONTENT = "Unknown-File"; public static final String UNKNOWN_CONTENT = "Unknown-File";
public static final String MISSING_CONTENT = "Missing-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 domainObject the domain object to store in the newly created folder item
* @param monitor the monitor that allows the user to cancel * @param monitor the monitor that allows the user to cancel
* @return checkout ID for new item * @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 InvalidNameException if the specified name contains invalid characters
* @throws CancelledException if the user cancels * @throws CancelledException if the user cancels
*/ */
@ -77,12 +78,12 @@ public interface ContentHandler extends ExtensionPoint {
* set. * set.
* @param monitor the monitor that allows the user to cancel * @param monitor the monitor that allows the user to cancel
* @return immutable domain object * @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 CancelledException if operation is cancelled by user
* @throws VersionException if unable to handle file content due to version * @throws VersionException if unable to handle file content due to version
* difference which could not be handled. * 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) int minChangeVersion, TaskMonitor monitor)
throws IOException, CancelledException, VersionException; throws IOException, CancelledException, VersionException;
@ -98,12 +99,12 @@ public interface ContentHandler extends ExtensionPoint {
* @param consumer consumer of the returned object * @param consumer consumer of the returned object
* @param monitor the monitor that allows the user to cancel * @param monitor the monitor that allows the user to cancel
* @return read-only domain object * @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 CancelledException if operation is cancelled by user
* @throws VersionException if unable to handle file content due to version * @throws VersionException if unable to handle file content due to version
* difference which could not be handled. * 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) Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException; throws IOException, VersionException, CancelledException;
@ -121,12 +122,12 @@ public interface ContentHandler extends ExtensionPoint {
* @param consumer consumer of the returned object * @param consumer consumer of the returned object
* @param monitor cancelable task monitor * @param monitor cancelable task monitor
* @return updateable domain object * @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 CancelledException if operation is cancelled by user
* @throws VersionException if unable to handle file content due to version * @throws VersionException if unable to handle file content due to version
* difference which could not be handled. * 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) boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
throws IOException, CancelledException, VersionException; throws IOException, CancelledException, VersionException;
@ -138,7 +139,7 @@ public interface ContentHandler extends ExtensionPoint {
* @param newerVersion the newer version number * @param newerVersion the newer version number
* @return the set of changes that were made * @return the set of changes that were made
* @throws VersionException if a database version change prevents reading of data. * @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 * produced by newer version of software and can not be read
*/ */
ChangeSet getChangeSet(FolderItem versionedFolderItem, int olderVersion, int newerVersion) 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 * Returns true if the content type is always private
* (i.e., can not be added to the versioned filesystem). * (i.e., can not be added to the versioned filesystem).
* @return true if private content type, else false
*/ */
boolean isPrivateContentType(); boolean isPrivateContentType();
/** /**
* Returns list of unique content-types supported. * Returns a unique content-type identifier
* A minimum of one content-type will be returned. If more than one * @return content type identifier for associated domain object(s).
* is returned, these are considered equivalent aliases.
*/ */
String getContentType(); String getContentType();
/** /**
* A string that is meant to be presented to the user. * A string that is meant to be presented to the user.
* @return user friendly content type for associated domain object(s).
*/ */
String getContentTypeDisplayString(); String getContentTypeDisplayString();
/** /**
* Returns the Icon associated with this handlers content type. * 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(); 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(); String getDefaultToolName();
/** /**
* Returns domain object implementation class supported. * Returns domain object implementation class supported.
* @return implementation class for the associated {@link DomainObjectAdapter} implementation.
*/ */
Class<? extends DomainObject> getDomainObjectClass(); Class<T> getDomainObjectClass();
/** /**
* Create user data file associated with existing content. * If linking is supported return an instanceof the appropriate {@link LinkHandler}.
* This facilitates the lazy creation of the user data file. * @return corresponding link handler or null if not supported.
* @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
*/ */
void saveUserDataFile(DomainObject associatedDomainObj, DBHandle userDbh, FileSystem userfs, default LinkHandler<?> getLinkHandler() {
TaskMonitor monitor) throws CancelledException, IOException; return null;
}
/**
* 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;
} }

View file

@ -18,13 +18,10 @@ package ghidra.framework.data;
import java.io.IOException; import java.io.IOException;
import db.DBHandle; import db.DBHandle;
import db.buffers.BufferFile;
import db.buffers.ManagedBufferFile; import db.buffers.ManagedBufferFile;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainObject;
import ghidra.framework.store.*; import ghidra.framework.store.*;
import ghidra.util.*; import ghidra.util.InvalidNameException;
import ghidra.util.exception.AssertException; import ghidra.util.SystemUtilities;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
@ -32,8 +29,11 @@ import ghidra.util.task.TaskMonitor;
* <code>DBContentHandler</code> provides an abstract ContentHandler for * <code>DBContentHandler</code> provides an abstract ContentHandler for
* domain object content which is stored within a database file. * domain object content which is stored within a database file.
* This class provides helper methods for working with database files. * This class provides helper methods for working with database files.
*
* @param <T> {@link DomainObjectAdapterDB} implementation class
*/ */
public abstract class DBContentHandler implements ContentHandler { public abstract class DBContentHandler<T extends DomainObjectAdapterDB>
implements ContentHandler<T> {
/** /**
* Create a new database file from an open database handle. * Create a new database file from an open database handle.
@ -70,6 +70,7 @@ public abstract class DBContentHandler implements ContentHandler {
bf.delete(); bf.delete();
} }
catch (IOException e) { catch (IOException e) {
// ignore
} }
abortCreate(fs, path, name, checkoutId); abortCreate(fs, path, name, checkoutId);
} }
@ -77,7 +78,7 @@ public abstract class DBContentHandler implements ContentHandler {
return checkoutId; 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 { try {
FolderItem item = fs.getItem(path, name); FolderItem item = fs.getItem(path, name);
if (item != null) { 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);
}
} }

View file

@ -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;
/**
* <code>DBContentHandler</code> 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 <T> {@link DomainObjectAdapterDB} implementation class
*/
public abstract class DBWithUserDataContentHandler<T extends DomainObjectAdapterDB>
extends DBContentHandler<T> {
/**
* 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);
}
}

View file

@ -17,11 +17,14 @@ package ghidra.framework.data;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*; import java.util.*;
import javax.swing.Icon; import javax.swing.Icon;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.ItemCheckoutStatus; import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.framework.store.Version; import ghidra.framework.store.Version;
import ghidra.framework.store.db.PackedDatabase; import ghidra.framework.store.db.PackedDatabase;
@ -111,6 +114,29 @@ public class DomainFileProxy implements DomainFile {
return parentPath + DomainFolder.SEPARATOR + getName(); 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 @Override
public int compareTo(DomainFile df) { public int compareTo(DomainFile df) {
return getName().compareToIgnoreCase(df.getName()); return getName().compareToIgnoreCase(df.getName());
@ -143,7 +169,7 @@ public class DomainFileProxy implements DomainFile {
DomainObjectAdapter dobj = getDomainObject(); DomainObjectAdapter dobj = getDomainObject();
if (dobj != null) { if (dobj != null) {
try { try {
ContentHandler ch = DomainObjectAdapter.getContentHandler(dobj); ContentHandler<?> ch = DomainObjectAdapter.getContentHandler(dobj);
return ch.getContentType(); return ch.getContentType();
} }
catch (IOException e) { catch (IOException e) {
@ -153,6 +179,27 @@ public class DomainFileProxy implements DomainFile {
return "Unknown File"; 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 @Override
public Class<? extends DomainObject> getDomainObjectClass() { public Class<? extends DomainObject> getDomainObjectClass() {
DomainObjectAdapter dobj = getDomainObject(); DomainObjectAdapter dobj = getDomainObject();
@ -228,6 +275,7 @@ public class DomainFileProxy implements DomainFile {
dobj.release(consumer); dobj.release(consumer);
} }
catch (IllegalArgumentException e) { catch (IllegalArgumentException e) {
// ignore unknown consumer error
} }
} }
} }
@ -256,11 +304,6 @@ public class DomainFileProxy implements DomainFile {
throw new UnsupportedOperationException("Repository operations not supported"); throw new UnsupportedOperationException("Repository operations not supported");
} }
@Override
public boolean isVersionControlSupported() {
return false;
}
@Override @Override
public boolean canAddToRepository() { public boolean canAddToRepository() {
return false; return false;
@ -303,6 +346,16 @@ public class DomainFileProxy implements DomainFile {
throw new UnsupportedOperationException("Repository operations not supported"); 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 @Override
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException { 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 @Override
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor) public DomainFile copyVersionTo(int ver, DomainFolder destFolder, TaskMonitor monitor)
throws IOException, CancelledException { throws IOException, CancelledException {
throw new UnsupportedOperationException("copyVersionTo unsupported for DomainFileProxy"); 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"); throw new UnsupportedOperationException("packFile() only valid for Database files");
} }
DomainObjectAdapterDB dbObj = (DomainObjectAdapterDB) domainObj; DomainObjectAdapterDB dbObj = (DomainObjectAdapterDB) domainObj;
ContentHandler ch = DomainObjectAdapter.getContentHandler(domainObj); ContentHandler<?> ch = DomainObjectAdapter.getContentHandler(domainObj);
PackedDatabase.packDatabase(dbObj.getDBHandle(), dbObj.getName(), ch.getContentType(), file, PackedDatabase.packDatabase(dbObj.getDBHandle(), dbObj.getName(), ch.getContentType(), file,
monitor); monitor);
} }

View file

@ -38,8 +38,8 @@ public abstract class DomainObjectAdapter implements DomainObject {
protected final static String DEFAULT_NAME = "untitled"; protected final static String DEFAULT_NAME = "untitled";
private static Class<?> defaultDomainObjClass; // Domain object implementation mapped to unknown content type private static Class<?> defaultDomainObjClass; // Domain object implementation mapped to unknown content type
private static HashMap<String, ContentHandler> contentHandlerTypeMap; // maps content-type string to handler private static HashMap<String, ContentHandler<?>> contentHandlerTypeMap; // maps content-type string to handler
private static HashMap<Class<?>, ContentHandler> contentHandlerClassMap; // maps domain object class to handler private static HashMap<Class<?>, ContentHandler<?>> contentHandlerClassMap; // maps domain object class to handler
private static ChangeListener contentHandlerUpdateListener = new ChangeListener() { private static ChangeListener contentHandlerUpdateListener = new ChangeListener() {
@Override @Override
public void stateChanged(ChangeEvent e) { public void stateChanged(ChangeEvent e) {
@ -399,7 +399,7 @@ public abstract class DomainObjectAdapter implements DomainObject {
contentHandlerTypeMap.remove(null); contentHandlerTypeMap.remove(null);
} }
else { else {
ContentHandler ch = contentHandlerClassMap.get(doClass); ContentHandler<?> ch = contentHandlerClassMap.get(doClass);
if (ch != null) { if (ch != null) {
contentHandlerTypeMap.put(null, ch); contentHandlerTypeMap.put(null, ch);
} }
@ -414,9 +414,9 @@ public abstract class DomainObjectAdapter implements DomainObject {
* @return content handler * @return content handler
* @throws IOException if no content handler can be found * @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(); checkContentHandlerMaps();
ContentHandler ch = contentHandlerTypeMap.get(contentType); ContentHandler<?> ch = contentHandlerTypeMap.get(contentType);
if (ch == null) { if (ch == null) {
throw new IOException("Content handler not found for " + contentType); throw new IOException("Content handler not found for " + contentType);
} }
@ -430,10 +430,10 @@ public abstract class DomainObjectAdapter implements DomainObject {
* @return content handler * @return content handler
* @throws IOException if no content handler can be found * @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 { throws IOException {
checkContentHandlerMaps(); checkContentHandlerMaps();
ContentHandler ch = contentHandlerClassMap.get(dobj.getClass()); ContentHandler<?> ch = contentHandlerClassMap.get(dobj.getClass());
if (ch == null) { if (ch == null) {
throw new IOException("Content handler not found for " + dobj.getClass().getName()); 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() { private synchronized static void getContentHandlers() {
contentHandlerClassMap = new HashMap<Class<?>, ContentHandler>(); contentHandlerClassMap = new HashMap<Class<?>, ContentHandler<?>>();
contentHandlerTypeMap = new HashMap<String, ContentHandler>(); contentHandlerTypeMap = new HashMap<String, ContentHandler<?>>();
@SuppressWarnings("rawtypes")
List<ContentHandler> handlers = ClassSearcher.getInstances(ContentHandler.class); List<ContentHandler> handlers = ClassSearcher.getInstances(ContentHandler.class);
for (ContentHandler ch : handlers) { for (ContentHandler<?> ch : handlers) {
String type = ch.getContentType(); contentHandlerTypeMap.put(ch.getContentType(), ch);
Class<?> DOClass = ch.getDomainObjectClass(); if (!(ch instanceof LinkHandler<?>)) {
if (type != null && DOClass != null) { contentHandlerClassMap.put(ch.getDomainObjectClass(), ch);
contentHandlerClassMap.put(DOClass, ch);
contentHandlerTypeMap.put(type, ch);
continue;
} }
} }
setDefaultContentClass(defaultDomainObjClass); setDefaultContentClass(defaultDomainObjClass);

View file

@ -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<NullFolderDomainObject> {
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<NullFolderDomainObject> 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";
}
}

View file

@ -16,6 +16,7 @@
package ghidra.framework.data; package ghidra.framework.data;
import java.io.*; import java.io.*;
import java.net.URL;
import java.util.*; import java.util.*;
import javax.swing.Icon; import javax.swing.Icon;
@ -24,8 +25,7 @@ import ghidra.framework.model.*;
import ghidra.framework.store.ItemCheckoutStatus; import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.framework.store.Version; import ghidra.framework.store.Version;
import ghidra.framework.store.local.LocalFileSystem; import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.InvalidNameException; import ghidra.util.*;
import ghidra.util.ReadOnlyException;
import ghidra.util.exception.*; import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
@ -123,6 +123,17 @@ public class GhidraFile implements DomainFile {
return fileManager.getProjectLocator(); return fileManager.getProjectLocator();
} }
@Override
public URL getSharedProjectURL() {
try {
return getFileData().getSharedProjectURL();
}
catch (IOException e) {
// ignore
}
return null;
}
@Override @Override
public String getContentType() { public String getContentType() {
try { try {
@ -134,6 +145,37 @@ public class GhidraFile implements DomainFile {
return ContentHandler.UNKNOWN_CONTENT; 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 @Override
public Class<? extends DomainObject> getDomainObjectClass() { public Class<? extends DomainObject> getDomainObjectClass() {
try { try {
@ -146,7 +188,7 @@ public class GhidraFile implements DomainFile {
} }
@Override @Override
public DomainFolder getParent() { public GhidraFolder getParent() {
return parent; return parent;
} }
@ -257,7 +299,7 @@ public class GhidraFile implements DomainFile {
catch (IOException e) { catch (IOException e) {
fileError(e); fileError(e);
} }
return GhidraFileData.UNSUPPORTED_FILE_ICON; return UNSUPPORTED_FILE_ICON;
} }
@Override @Override
@ -353,17 +395,6 @@ public class GhidraFile implements DomainFile {
return true; return true;
} }
@Override
public boolean isVersionControlSupported() {
try {
return getFileData().isVersionControlSupported();
}
catch (IOException e) {
fileError(e);
}
return false;
}
@Override @Override
public boolean isVersioned() { public boolean isVersioned() {
try { try {
@ -482,6 +513,9 @@ public class GhidraFile implements DomainFile {
@Override @Override
public GhidraFile moveTo(DomainFolder newParent) throws IOException { 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; GhidraFolder newGhidraParent = (GhidraFolder) newParent;
return getFileData().moveTo(newGhidraParent.getFolderData()); return getFileData().moveTo(newGhidraParent.getFolderData());
} }
@ -489,15 +523,30 @@ public class GhidraFile implements DomainFile {
@Override @Override
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException,
CancelledException { 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(), return getFileData().copyTo(newGhidraParent.getFolderData(),
monitor != null ? monitor : TaskMonitor.DUMMY); 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 @Override
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor) public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
throws IOException, CancelledException { 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(), return getFileData().copyVersionTo(version, destGhidraFolder.getFolderData(),
monitor != null ? monitor : TaskMonitor.DUMMY); 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. * only when a non shared project is being converted to a shared project.
* @param monitor task monitor * @param monitor task monitor
* @throws IOException if an IO error occurs * @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 { void convertToPrivateFile(TaskMonitor monitor) throws IOException, CancelledException {
getFileData().convertToPrivateFile( getFileData().convertToPrivateFile(
@ -596,6 +645,10 @@ public class GhidraFile implements DomainFile {
@Override @Override
public String toString() { public String toString() {
ProjectLocator projectLocator = parent.getProjectData().getProjectLocator();
if (projectLocator.isTransient()) {
return fileManager.getProjectLocator().getName() + getPathname();
}
return fileManager.getProjectLocator().getName() + ":" + getPathname(); return fileManager.getProjectLocator().getName() + ":" + getPathname();
} }

View file

@ -17,6 +17,8 @@ package ghidra.framework.data;
import java.awt.*; import java.awt.*;
import java.io.*; import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -27,9 +29,9 @@ import db.Field;
import db.buffers.*; import db.buffers.*;
import generic.theme.GColor; import generic.theme.GColor;
import generic.theme.GIcon; import generic.theme.GIcon;
import ghidra.framework.client.ClientUtil; import ghidra.framework.client.*;
import ghidra.framework.client.NotConnectedException;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.*; import ghidra.framework.store.*;
import ghidra.framework.store.FileSystem; import ghidra.framework.store.FileSystem;
import ghidra.framework.store.local.LocalFileSystem; import ghidra.framework.store.local.LocalFileSystem;
@ -37,12 +39,14 @@ import ghidra.framework.store.local.LocalFolderItem;
import ghidra.util.*; import ghidra.util.*;
import ghidra.util.exception.*; import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
import ghidra.util.task.TaskMonitorAdapter;
import resources.MultiIcon; import resources.MultiIcon;
import resources.icons.TranslateIcon; import resources.icons.TranslateIcon;
public class GhidraFileData { 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; private static final boolean ALWAYS_MERGE = System.getProperty("ForceMerge") != null;
//@formatter:off //@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_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 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 HIJACKED_ICON = new GIcon("icon.project.data.file.ghidra.hijacked");
public static final Icon VERSION_ICON = new VersionIcon(); 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 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"); 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); 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. * Reassign a new file-ID to resolve file-ID conflict.
* Conflicts can occur as a result of a cancelled check-out. * Conflicts can occur as a result of a cancelled check-out.
* @throws IOException if an IO error occurs
*/ */
void resetFileID() throws IOException { void resetFileID() throws IOException {
synchronized (fileSystem) { synchronized (fileSystem) {
@ -262,6 +290,8 @@ public class GhidraFileData {
String getContentType() { String getContentType() {
synchronized (fileSystem) { synchronized (fileSystem) {
FolderItem item = folderItem != null ? folderItem : versionedFolderItem; 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) { if (item == null) {
return ContentHandler.MISSING_CONTENT; return ContentHandler.MISSING_CONTENT;
} }
@ -270,14 +300,27 @@ public class GhidraFileData {
} }
} }
Class<? extends DomainObject> getDomainObjectClass() { /**
* Get content handler
* @return content handler
* @throws IOException if an IO error occurs, file not found, or unsupported content
*/
ContentHandler<?> getContentHandler() throws IOException {
synchronized (fileSystem) { synchronized (fileSystem) {
FolderItem item = folderItem != null ? folderItem : versionedFolderItem; FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
try { // this can happen when we are trying to load a version file from
ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType()); // a server to which we are not connected
if (ch != null) { if (item == null) {
return ch.getDomainObjectClass(); throw new FileNotFoundException(name + " not found");
} }
return DomainObjectAdapter.getContentHandler(item.getContentType());
}
}
Class<? extends DomainObject> getDomainObjectClass() {
synchronized (fileSystem) {
try {
return getContentHandler().getDomainObjectClass();
} }
catch (IOException e) { catch (IOException e) {
// ignore missing content handler // ignore missing content handler
@ -289,8 +332,7 @@ public class GhidraFileData {
ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException { ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException {
synchronized (fileSystem) { synchronized (fileSystem) {
if (versionedFolderItem != null && folderItem != null && folderItem.isCheckedOut()) { if (versionedFolderItem != null && folderItem != null && folderItem.isCheckedOut()) {
ContentHandler ch = ContentHandler<?> ch = getContentHandler();
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
return ch.getChangeSet(versionedFolderItem, folderItem.getCheckoutVersion(), return ch.getChangeSet(versionedFolderItem, folderItem.getCheckoutVersion(),
versionedFolderItem.getCurrentVersion()); versionedFolderItem.getCurrentVersion());
} }
@ -305,10 +347,9 @@ public class GhidraFileData {
DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover, DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
TaskMonitor monitor) throws VersionException, IOException, CancelledException { TaskMonitor monitor) throws VersionException, IOException, CancelledException {
FolderItem myFolderItem; FolderItem myFolderItem;
ContentHandler ch;
DomainObjectAdapter domainObj = null; DomainObjectAdapter domainObj = null;
synchronized (fileSystem) { synchronized (fileSystem) {
if (fileSystem.isReadOnly()) { if (fileSystem.isReadOnly() || isLinkFile()) {
return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor); return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor);
} }
domainObj = getOpenedDomainObject(); domainObj = getOpenedDomainObject();
@ -321,8 +362,8 @@ public class GhidraFileData {
return domainObj; return domainObj;
} }
} }
ContentHandler<?> ch = getContentHandler();
if (folderItem == null) { if (folderItem == null) {
ch = DomainObjectAdapter.getContentHandler(versionedFolderItem.getContentType());
DomainObjectAdapter doa = ch.getReadOnlyObject(versionedFolderItem, DomainObjectAdapter doa = ch.getReadOnlyObject(versionedFolderItem,
DomainFile.DEFAULT_VERSION, true, consumer, monitor); DomainFile.DEFAULT_VERSION, true, consumer, monitor);
doa.setChanged(false); doa.setChanged(false);
@ -331,7 +372,6 @@ public class GhidraFileData {
proxy.setLastModified(getLastModifiedTime()); proxy.setLastModified(getLastModifiedTime());
return doa; return doa;
} }
ch = DomainObjectAdapter.getContentHandler(folderItem.getContentType());
myFolderItem = folderItem; myFolderItem = folderItem;
domainObj = ch.getDomainObject(myFolderItem, parent.getUserFileSystem(), domainObj = ch.getDomainObject(myFolderItem, parent.getUserFileSystem(),
@ -368,14 +408,7 @@ public class GhidraFileData {
FolderItem item = FolderItem item =
(folderItem != null && version == DomainFile.DEFAULT_VERSION) ? folderItem (folderItem != null && version == DomainFile.DEFAULT_VERSION) ? folderItem
: versionedFolderItem; : versionedFolderItem;
ContentHandler<?> ch = getContentHandler();
// 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());
DomainObjectAdapter doa = ch.getReadOnlyObject(item, version, true, consumer, monitor); DomainObjectAdapter doa = ch.getReadOnlyObject(item, version, true, consumer, monitor);
doa.setChanged(false); doa.setChanged(false);
@ -390,15 +423,12 @@ public class GhidraFileData {
throws VersionException, IOException, CancelledException { throws VersionException, IOException, CancelledException {
synchronized (fileSystem) { synchronized (fileSystem) {
DomainObjectAdapter obj = null; DomainObjectAdapter obj = null;
ContentHandler<?> ch = getContentHandler();
if (versionedFolderItem == null || if (versionedFolderItem == null ||
(version == DomainFile.DEFAULT_VERSION && folderItem != null) || isHijacked()) { (version == DomainFile.DEFAULT_VERSION && folderItem != null) || isHijacked()) {
ContentHandler ch =
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
obj = ch.getImmutableObject(folderItem, consumer, version, -1, monitor); obj = ch.getImmutableObject(folderItem, consumer, version, -1, monitor);
} }
else { else {
ContentHandler ch =
DomainObjectAdapter.getContentHandler(versionedFolderItem.getContentType());
obj = ch.getImmutableObject(versionedFolderItem, consumer, version, -1, monitor); obj = ch.getImmutableObject(versionedFolderItem, consumer, version, -1, monitor);
} }
DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), obj, DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), obj,
@ -419,8 +449,11 @@ public class GhidraFileData {
} }
boolean takeRecoverySnapshot() throws IOException { boolean takeRecoverySnapshot() throws IOException {
if (fileSystem.isReadOnly()) {
return true;
}
DomainObjectAdapter dobj = fileManager.getOpenedDomainObject(getPathname()); DomainObjectAdapter dobj = fileManager.getOpenedDomainObject(getPathname());
if (fileSystem.isReadOnly() || !(dobj instanceof DomainObjectAdapterDB) || if (!(dobj instanceof DomainObjectAdapterDB) ||
!dobj.isChanged()) { !dobj.isChanged()) {
return true; return true;
} }
@ -481,13 +514,19 @@ public class GhidraFileData {
private Icon generateIcon(boolean disabled) { private Icon generateIcon(boolean disabled) {
if (parent == null) { if (parent == null) {
// instance has been disposed // instance has been disposed
return UNSUPPORTED_FILE_ICON; return DomainFile.UNSUPPORTED_FILE_ICON;
} }
synchronized (fileSystem) { synchronized (fileSystem) {
boolean isLink = isLinkFile();
FolderItem item = folderItem != null ? folderItem : versionedFolderItem; FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
Icon baseIcon = new TranslateIcon(getBaseIcon(item), 1, 1);
if (versionedFolderItem != null) { if (versionedFolderItem != null) {
MultiIcon multiIcon = new MultiIcon(VERSION_ICON, disabled); MultiIcon multiIcon = new MultiIcon(VERSION_ICON, disabled);
multiIcon.addIcon(getBaseIcon(item)); multiIcon.addIcon(baseIcon);
if (isHijacked()) { if (isHijacked()) {
multiIcon.addIcon(HIJACKED_ICON); multiIcon.addIcon(HIJACKED_ICON);
} }
@ -504,12 +543,15 @@ public class GhidraFileData {
} }
} }
} }
if (isLink) {
multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1));
}
return multiIcon; return multiIcon;
} }
else if (folderItem != null) { 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()) { 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 (isCheckedOut()) {
if (isCheckedOutExclusive()) { if (isCheckedOutExclusive()) {
@ -519,23 +561,23 @@ public class GhidraFileData {
multiIcon.addIcon(CHECKED_OUT_ICON); multiIcon.addIcon(CHECKED_OUT_ICON);
} }
} }
if (isLink) {
multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1));
}
return multiIcon; return multiIcon;
} }
} }
return UNSUPPORTED_FILE_ICON; return DomainFile.UNSUPPORTED_FILE_ICON;
} }
private Icon getBaseIcon(FolderItem item) { private Icon getBaseIcon(FolderItem item) {
try { try {
ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType()); return getContentHandler().getIcon();
if (ch != null) {
return ch.getIcon();
}
} }
catch (IOException e) { catch (IOException e) {
// ignore missing content handler // ignore missing content handler
} }
return UNSUPPORTED_FILE_ICON; return DomainFile.UNSUPPORTED_FILE_ICON;
} }
boolean isChanged() { boolean isChanged() {
@ -593,9 +635,19 @@ public class GhidraFileData {
boolean canAddToRepository() { boolean canAddToRepository() {
synchronized (fileSystem) { synchronized (fileSystem) {
try { try {
return (!fileSystem.isReadOnly() && !versionedFileSystem.isReadOnly() && if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) {
folderItem != null && versionedFolderItem == null && return false;
!folderItem.isCheckedOut() && isVersionControlSupported()); }
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) { catch (IOException e) {
return false; return false;
@ -606,8 +658,11 @@ public class GhidraFileData {
boolean canCheckout() { boolean canCheckout() {
synchronized (fileSystem) { synchronized (fileSystem) {
try { try {
return folderItem == null && !fileSystem.isReadOnly() && if (folderItem != null || fileSystem.isReadOnly() ||
!versionedFileSystem.isReadOnly(); versionedFileSystem.isReadOnly()) {
return false;
}
return !isLinkFile();
} }
catch (IOException e) { catch (IOException e) {
return false; 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() { int getVersion() {
synchronized (fileSystem) { synchronized (fileSystem) {
try { try {
@ -714,19 +749,16 @@ public class GhidraFileData {
throws IOException, CancelledException { throws IOException, CancelledException {
DomainObjectAdapter oldDomainObj = null; DomainObjectAdapter oldDomainObj = null;
synchronized (fileSystem) { synchronized (fileSystem) {
if (!isVersionControlSupported()) { if (!canAddToRepository()) {
throw new AssertException("file type does supported version control");
}
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()) { if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) {
throw new ReadOnlyException( throw new ReadOnlyException(
"addToVersionControl permitted within writeable project and repository only"); "addToVersionControl permitted within writeable project and repository only");
} }
throw new IOException("addToVersionControl not allowed for file");
}
if (isLinkFile()) {
keepCheckedOut = false;
}
String parentPath = parent.getPathname(); String parentPath = parent.getPathname();
String user = ClientUtil.getUserName(); String user = ClientUtil.getUserName();
try { try {
@ -832,6 +864,9 @@ public class GhidraFileData {
if (!versionedFileSystem.isOnline()) { if (!versionedFileSystem.isOnline()) {
throw new NotConnectedException("Not connected to repository server"); throw new NotConnectedException("Not connected to repository server");
} }
if (isLinkFile()) {
return false;
}
String user = ClientUtil.getUserName(); String user = ClientUtil.getUserName();
ProjectLocator projectLocator = parent.getProjectLocator(); ProjectLocator projectLocator = parent.getProjectLocator();
CheckoutType checkoutType; CheckoutType checkoutType;
@ -934,8 +969,7 @@ public class GhidraFileData {
if (checkinHandler.createKeepFile()) { if (checkinHandler.createKeepFile()) {
DomainObject sourceObj = null; DomainObject sourceObj = null;
try { try {
ContentHandler ch = ContentHandler<?> ch = getContentHandler();
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
sourceObj = ch.getImmutableObject(folderItem, this, DomainFile.DEFAULT_VERSION, sourceObj = ch.getImmutableObject(folderItem, this, DomainFile.DEFAULT_VERSION,
-1, monitor); -1, monitor);
createKeepFile(sourceObj, monitor); createKeepFile(sourceObj, monitor);
@ -968,18 +1002,18 @@ public class GhidraFileData {
/** /**
* Verify that current user is the checkout user for this file * Verify that current user is the checkout user for this file
* @param caseName name of user case (e.g., checkin) * @param operationName name of user case (e.g., checkin)
* @return true if server/repository will permit current user to checkin, * @throws IOException if server/repository will not permit current user to checkin,
* or update checkout version of current file. (i.e., server login matches * or update checkout version of current file. (i.e., server login does not match
* user name used at time of initial checkout) * 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) { if (versionedFileSystem instanceof LocalFileSystem) {
return; // rely on local project ownership return; // rely on local project ownership
} }
String repoUserName = versionedFileSystem.getUserName(); String repoUserName = versionedFileSystem.getUserName();
if (repoUserName == null) { if (repoUserName == null) {
throw new IOException("File " + caseName + " not permitted (not connected)"); throw new IOException("File " + operationName + " not permitted (not connected)");
} }
ItemCheckoutStatus checkoutStatus = getCheckoutStatus(); ItemCheckoutStatus checkoutStatus = getCheckoutStatus();
if (checkoutStatus == null) { if (checkoutStatus == null) {
@ -987,7 +1021,7 @@ public class GhidraFileData {
} }
String checkoutUserName = checkoutStatus.getUser(); String checkoutUserName = checkoutStatus.getUser();
if (!repoUserName.equals(checkoutUserName)) { 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 + "'"); checkoutUserName + "' differs from repository user '" + repoUserName + "'");
} }
} }
@ -1016,7 +1050,7 @@ public class GhidraFileData {
} }
verifyRepoUser("checkin"); verifyRepoUser("checkin");
if (monitor == null) { if (monitor == null) {
monitor = TaskMonitorAdapter.DUMMY_MONITOR; monitor = TaskMonitor.DUMMY;
} }
synchronized (fileSystem) { synchronized (fileSystem) {
if (busy) { if (busy) {
@ -1036,9 +1070,7 @@ public class GhidraFileData {
Msg.info(this, "Checkin with merge for " + name); Msg.info(this, "Checkin with merge for " + name);
ContentHandler ch = ContentHandler<?> ch = getContentHandler();
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
DomainObjectAdapter checkinObj = ch.getDomainObject(versionedFolderItem, null, DomainObjectAdapter checkinObj = ch.getDomainObject(versionedFolderItem, null,
folderItem.getCheckoutId(), okToUpgrade, false, this, monitor); folderItem.getCheckoutId(), okToUpgrade, false, this, monitor);
checkinObj.setDomainFile(new DomainFileProxy(name, getParent().getPathname(), checkinObj.setDomainFile(new DomainFileProxy(name, getParent().getPathname(),
@ -1366,9 +1398,12 @@ public class GhidraFileData {
private void removeAssociatedUserDataFile() { private void removeAssociatedUserDataFile() {
try { try {
ContentHandler<?> ch = getContentHandler();
if (ch instanceof DBWithUserDataContentHandler) {
FolderItem item = folderItem != null ? folderItem : versionedFolderItem; FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType()); ((DBWithUserDataContentHandler<?>) ch).removeUserDataFile(item,
ch.removeUserDataFile(item, parent.getUserFileSystem()); parent.getUserFileSystem());
}
} }
catch (Exception e) { catch (Exception e) {
// ignore missing content handler // ignore missing content handler
@ -1403,7 +1438,7 @@ public class GhidraFileData {
} }
verifyRepoUser("merge"); verifyRepoUser("merge");
if (monitor == null) { if (monitor == null) {
monitor = TaskMonitorAdapter.DUMMY_MONITOR; monitor = TaskMonitor.DUMMY;
} }
synchronized (fileSystem) { synchronized (fileSystem) {
if (busy) { if (busy) {
@ -1425,8 +1460,7 @@ public class GhidraFileData {
"Merge failed, file merge is not supported in headless mode"); "Merge failed, file merge is not supported in headless mode");
} }
ContentHandler ch = ContentHandler<?> ch = getContentHandler();
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
// Test versioned file for VersionException // Test versioned file for VersionException
int mergeVer = versionedFolderItem.getCurrentVersion(); int mergeVer = versionedFolderItem.getCurrentVersion();
@ -1556,7 +1590,7 @@ public class GhidraFileData {
checkInUse(); checkInUse();
GhidraFolderData oldParent = parent; GhidraFolderData oldParent = parent;
String oldName = name; String oldName = name;
String newName = getTargetName(name, newParent); String newName = newParent.getTargetName(name);
try { try {
if (isHijacked()) { if (isHijacked()) {
fileSystem.moveItem(parent.getPathname(), name, newParent.getPathname(), fileSystem.moveItem(parent.getPathname(), name, newParent.getPathname(),
@ -1595,15 +1629,49 @@ public class GhidraFileData {
} }
} }
private String getTargetName(String preferredName, GhidraFolderData newParent) boolean isLinkFile() {
throws IOException { synchronized (fileSystem) {
String newName = preferredName; try {
int i = 1; return LinkHandler.class.isAssignableFrom(getContentHandler().getClass());
while (newParent.getFileData(newName, false) != null) { }
newName = preferredName + "." + i; catch (IOException e) {
i++; 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) GhidraFile copyTo(GhidraFolderData newParentData, TaskMonitor monitor)
@ -1615,7 +1683,7 @@ public class GhidraFileData {
FolderItem item = folderItem != null ? folderItem : versionedFolderItem; FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
String pathname = newParentData.getPathname(); String pathname = newParentData.getPathname();
String contentType = item.getContentType(); String contentType = item.getContentType();
String targetName = getTargetName(name, newParentData); String targetName = newParentData.getTargetName(name);
String user = ClientUtil.getUserName(); String user = ClientUtil.getUserName();
try { try {
if (item instanceof DatabaseItem) { if (item instanceof DatabaseItem) {
@ -1668,7 +1736,7 @@ public class GhidraFileData {
} }
String pathname = destFolderData.getPathname(); String pathname = destFolderData.getPathname();
String contentType = versionedFolderItem.getContentType(); String contentType = versionedFolderItem.getContentType();
String targetName = getTargetName(name + "_v" + version, destFolderData); String targetName = destFolderData.getTargetName(name + "_v" + version);
String user = ClientUtil.getUserName(); String user = ClientUtil.getUserName();
try { try {
BufferFile bufferFile = ((DatabaseItem) versionedFolderItem).open(version); 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 * 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. * 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 { void convertToPrivateFile(TaskMonitor monitor) throws IOException, CancelledException {
synchronized (fileSystem) { synchronized (fileSystem) {
@ -1749,7 +1819,10 @@ public class GhidraFileData {
Map<String, String> getMetadata() { Map<String, String> getMetadata() {
FolderItem item = (folderItem != null) ? folderItem : versionedFolderItem; FolderItem item = (folderItem != null) ? folderItem : versionedFolderItem;
return getMetadata(item);
}
static Map<String, String> getMetadata(FolderItem item) {
GenericDomainObjectDB genericDomainObj = null; GenericDomainObjectDB genericDomainObj = null;
try { try {
if (item instanceof DatabaseItem) { if (item instanceof DatabaseItem) {
@ -1767,7 +1840,7 @@ public class GhidraFileData {
// file created with newer version of Ghidra // file created with newer version of Ghidra
} }
catch (IOException e) { catch (IOException e) {
Msg.error(this, "Read meta-data error", e); Msg.error(GhidraFileData.class, "Read meta-data error", e);
} }
finally { finally {
if (genericDomainObj != null) { if (genericDomainObj != null) {
@ -1782,13 +1855,17 @@ public class GhidraFileData {
if (fileManager == null) { if (fileManager == null) {
return name + "(disposed)"; return name + "(disposed)";
} }
ProjectLocator projectLocator = fileManager.getProjectLocator();
if (projectLocator.isTransient()) {
return fileManager.getProjectLocator().getName() + getPathname();
}
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 { protected GenericDomainObjectDB(DBHandle dbh) throws IOException {
super(dbh, "Generic", 500, GhidraFileData.this); super(dbh, "Generic", 500, dbh);
loadMetadata(); loadMetadata();
} }
@ -1803,7 +1880,7 @@ public class GhidraFileData {
} }
public void release() { public void release() {
release(GhidraFileData.this); release(dbh);
} }
} }
@ -1816,8 +1893,8 @@ class VersionIcon implements Icon {
private static Color VERSION_ICON_COLOR_LIGHT = private static Color VERSION_ICON_COLOR_LIGHT =
new GColor("color.bg.ghidra.file.data.version.icon.light"); new GColor("color.bg.ghidra.file.data.version.icon.light");
private static final int WIDTH = 18; private static final int WIDTH = GhidraFileData.ICON_WIDTH;
private static final int HEIGHT = 17; private static final int HEIGHT = GhidraFileData.ICON_HEIGHT;
@Override @Override
public int getIconHeight() { public int getIconHeight() {

View file

@ -16,16 +16,19 @@
package ghidra.framework.data; package ghidra.framework.data;
import java.io.*; import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List; import java.util.List;
import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.FileSystem; import ghidra.framework.store.FileSystem;
import ghidra.framework.store.local.LocalFileSystem; import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.InvalidNameException; import ghidra.util.InvalidNameException;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
import ghidra.util.task.TaskMonitorAdapter;
public class GhidraFolder implements DomainFolder { public class GhidraFolder implements DomainFolder {
@ -83,19 +86,6 @@ public class GhidraFolder implements DomainFolder {
return fileData; 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 { GhidraFolderData getFolderData() throws FileNotFoundException {
if (parent == null) { if (parent == null) {
return fileManager.getRootFolderData(); return fileManager.getRootFolderData();
@ -140,10 +130,10 @@ public class GhidraFolder implements DomainFolder {
/** /**
* Refresh folder data - used for testing only * Refresh folder data - used for testing only
* @throws IOException * @throws IOException if an IO error occurs
*/ */
void refreshFolderData() throws IOException { void refreshFolderData() throws IOException {
getFolderData().refresh(false, true, TaskMonitorAdapter.DUMMY_MONITOR); getFolderData().refresh(false, true, TaskMonitor.DUMMY);
} }
@Override @Override
@ -193,6 +183,40 @@ public class GhidraFolder implements DomainFolder {
return path; 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 @Override
public boolean isInWritableProject() { public boolean isInWritableProject() {
return !getProjectData().getLocalFileSystem().isReadOnly(); return !getProjectData().getLocalFileSystem().isReadOnly();
@ -244,7 +268,9 @@ public class GhidraFolder implements DomainFolder {
return folderData.isEmpty(); return folderData.isEmpty();
} }
catch (FileNotFoundException e) { 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) public DomainFile createFile(String fileName, DomainObject obj, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException { throws InvalidNameException, IOException, CancelledException {
return createFolderData().createFile(fileName, obj, return createFolderData().createFile(fileName, obj,
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR); monitor != null ? monitor : TaskMonitor.DUMMY);
} }
@Override @Override
public DomainFile createFile(String fileName, File packFile, TaskMonitor monitor) public DomainFile createFile(String fileName, File packFile, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException { throws InvalidNameException, IOException, CancelledException {
return createFolderData().createFile(fileName, packFile, return createFolderData().createFile(fileName, packFile,
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR); monitor != null ? monitor : TaskMonitor.DUMMY);
} }
@Override @Override
@ -325,8 +351,11 @@ public class GhidraFolder implements DomainFolder {
if (parent == null) { if (parent == null) {
throw new UnsupportedOperationException("root folder may not be moved"); 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(); GhidraFolderData folderData = getFolderData();
GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation GhidraFolder newGhidraParent = (GhidraFolder) newParent;
return folderData.moveTo(newGhidraParent.getFolderData()); return folderData.moveTo(newGhidraParent.getFolderData());
} }
@ -334,9 +363,22 @@ public class GhidraFolder implements DomainFolder {
public GhidraFolder copyTo(DomainFolder newParent, TaskMonitor monitor) public GhidraFolder copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException { throws IOException, CancelledException {
GhidraFolderData folderData = getFolderData(); 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(), 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());
} }
/** /**

View file

@ -16,10 +16,13 @@
package ghidra.framework.data; package ghidra.framework.data;
import java.io.*; import java.io.*;
import java.net.URL;
import java.util.*; import java.util.*;
import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.model.*; 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.FileSystem;
import ghidra.framework.store.local.LocalFileSystem; import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.*; import ghidra.util.*;
@ -257,9 +260,10 @@ class GhidraFolderData {
return folderList.isEmpty() && fileList.isEmpty(); return folderList.isEmpty() && fileList.isEmpty();
} }
catch (IOException e) { 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<String> getFileNames() { List<String> getFileNames() {
@ -919,7 +923,7 @@ class GhidraFolderData {
DomainFile oldDf = doa.getDomainFile(); DomainFile oldDf = doa.getDomainFile();
try { try {
ContentHandler ch = DomainObjectAdapter.getContentHandler(doa); ContentHandler<?> ch = DomainObjectAdapter.getContentHandler(doa);
ch.createFile(fileSystem, null, getPathname(), fileName, obj, monitor); ch.createFile(fileSystem, null, getPathname(), fileName, obj, monitor);
if (oldDf != null) { 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 * used for testing
*/ */
@ -1156,6 +1242,10 @@ class GhidraFolderData {
@Override @Override
public String toString() { public String toString() {
ProjectLocator projectLocator = fileManager.getProjectLocator();
if (projectLocator.isTransient()) {
return fileManager.getProjectLocator().getName() + getPathname();
}
return fileManager.getProjectLocator().getName() + ":" + getPathname(); return fileManager.getProjectLocator().getName() + ":" + getPathname();
} }

View file

@ -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.
*
* <code>LinkHandler</code> defines an application interface for handling domain files which are
* shortcut links to another supported content type.
*
* @param <T> {@link URLLinkObject} implementation class
*/
public abstract class LinkHandler<T extends DomainObjectAdapterDB> extends DBContentHandler<T> {
// 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<String, String> 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<String, String> 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();
}

View file

@ -0,0 +1,426 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.data;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import javax.help.UnsupportedOperationException;
import javax.swing.Icon;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.*;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor;
/**
* {@code LinkedGhidraFile} corresponds to a {@link DomainFile} contained within a
* {@link LinkedGhidraFolder}.
*/
class LinkedGhidraFile implements LinkedDomainFile {
private final LinkedGhidraSubFolder parent;
private final String fileName;
LinkedGhidraFile(LinkedGhidraSubFolder parent, String fileName) {
this.parent = parent;
this.fileName = fileName;
}
@Override
public DomainFile getLinkedFile() throws IOException {
return parent.getLinkedFile(fileName);
}
private DomainFile getLinkedFileNoError() {
return parent.getLinkedFileNoError(fileName);
}
@Override
public DomainFolder getParent() {
return parent;
}
@Override
public String getName() {
return fileName;
}
@Override
public int compareTo(DomainFile df) {
return fileName.compareToIgnoreCase(df.getName());
}
@Override
public boolean exists() {
return getLinkedFileNoError() != null;
}
@Override
public String getFileID() {
DomainFile df = getLinkedFileNoError();
return df != null ? df.getFileID() : null;
}
@Override
public DomainFile setName(String newName) throws InvalidNameException, IOException {
throw new ReadOnlyException("linked file is read only");
}
@Override
public String getPathname() {
// pathname within project containing folder-link
// getParent() may return a non-linked folder
String path = getParent().getPathname();
if (path.length() != FileSystem.SEPARATOR.length()) {
path += FileSystem.SEPARATOR;
}
path += fileName;
return path;
}
@Override
public URL getSharedProjectURL() {
URL folderURL = parent.getSharedProjectURL();
if (GhidraURL.isServerRepositoryURL(folderURL)) {
// Direct URL construction done so that ghidra protocol
// extension may be supported
try {
return new URL(folderURL.toExternalForm() + fileName);
}
catch (MalformedURLException e) {
// ignore
}
}
return null;
}
@Override
public ProjectLocator getProjectLocator() {
return parent.getProjectLocator();
}
@Override
public String getContentType() {
DomainFile df = getLinkedFileNoError();
return df != null ? df.getContentType() : ContentHandler.UNKNOWN_CONTENT;
}
@Override
public Class<? extends DomainObject> getDomainObjectClass() {
DomainFile df = getLinkedFileNoError();
return df != null ? df.getDomainObjectClass() : DomainObject.class;
}
@Override
public ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException {
return null;
}
@Override
public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
TaskMonitor monitor) throws VersionException, IOException, CancelledException {
return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor);
}
@Override
public DomainObject getOpenedDomainObject(Object consumer) {
return null;
}
@Override
public DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor)
throws VersionException, IOException, CancelledException {
return getLinkedFile().getReadOnlyDomainObject(consumer, version, monitor);
}
@Override
public DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor)
throws VersionException, IOException, CancelledException {
return getLinkedFile().getImmutableDomainObject(consumer, version, monitor);
}
@Override
public void save(TaskMonitor monitor) throws IOException, CancelledException {
throw new UnsupportedOperationException();
}
@Override
public boolean canSave() {
return false;
}
@Override
public boolean canRecover() {
return false;
}
@Override
public boolean takeRecoverySnapshot() throws IOException {
return true;
}
@Override
public boolean isInWritableProject() {
return false; // While project may be writeable this folder/file is not
}
@Override
public long getLastModifiedTime() {
DomainFile df = getLinkedFileNoError();
return df != null ? df.getLastModifiedTime() : 0;
}
@Override
public Icon getIcon(boolean disabled) {
DomainFile df = getLinkedFileNoError();
return df != null ? df.getIcon(disabled) : UNSUPPORTED_FILE_ICON;
}
@Override
public boolean isCheckedOut() {
return false;
}
@Override
public boolean isCheckedOutExclusive() {
return false;
}
@Override
public boolean modifiedSinceCheckout() {
return false;
}
@Override
public boolean canCheckout() {
return false;
}
@Override
public boolean canCheckin() {
return false;
}
@Override
public boolean canMerge() {
return false;
}
@Override
public boolean canAddToRepository() {
return false;
}
@Override
public void setReadOnly(boolean state) throws IOException {
// ignore
}
@Override
public boolean isReadOnly() {
return true; // not reflected by icon
}
@Override
public boolean isVersioned() {
DomainFile df = getLinkedFileNoError();
return df != null ? df.isVersioned() : false;
}
@Override
public boolean isHijacked() {
return false;
}
@Override
public int getLatestVersion() {
DomainFile df = getLinkedFileNoError();
return df != null ? df.getLatestVersion() : DomainFile.DEFAULT_VERSION;
}
@Override
public boolean isLatestVersion() {
return true;
}
@Override
public int getVersion() {
// TODO: Do we want to reveal linked-local-project checkout details?
return getLatestVersion();
}
@Override
public Version[] getVersionHistory() throws IOException {
DomainFile df = getLinkedFileNoError();
return df != null ? df.getVersionHistory() : new Version[0];
}
@Override
public void addToVersionControl(String comment, boolean keepCheckedOut, TaskMonitor monitor)
throws IOException, CancelledException {
throw new UnsupportedOperationException();
}
@Override
public boolean checkout(boolean exclusive, TaskMonitor monitor)
throws IOException, CancelledException {
throw new UnsupportedOperationException();
}
@Override
public void checkin(CheckinHandler checkinHandler, boolean okToUpgrade, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
throw new UnsupportedOperationException();
}
@Override
public void merge(boolean okToUpgrade, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
throw new UnsupportedOperationException();
}
@Override
public void undoCheckout(boolean keep) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void undoCheckout(boolean keep, boolean force) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void terminateCheckout(long checkoutId) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public ItemCheckoutStatus[] getCheckouts() throws IOException {
DomainFile df = getLinkedFileNoError();
return df != null ? df.getCheckouts() : new ItemCheckoutStatus[0];
}
@Override
public ItemCheckoutStatus getCheckoutStatus() throws IOException {
// TODO: Do we want to reveal linked-local-project checkout details?
return null;
}
@Override
public void delete() throws IOException {
throw new ReadOnlyException("linked file is read only");
}
@Override
public void delete(int version) throws IOException {
throw new ReadOnlyException("linked file is read only");
}
@Override
public DomainFile moveTo(DomainFolder newParent) throws IOException {
throw new ReadOnlyException("linked file is read only");
}
@Override
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException {
return getLinkedFile().copyTo(newParent, monitor);
}
@Override
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
throws IOException, CancelledException {
return getLinkedFile().copyVersionTo(version, destFolder, monitor);
}
@Override
public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
return getLinkedFile().copyToAsLink(newParent);
}
@Override
public boolean isLinkingSupported() {
DomainFile df = getLinkedFileNoError();
return df != null ? df.isLinkingSupported() : false;
}
@Override
public List<?> getConsumers() {
return List.of();
}
@Override
public boolean isChanged() {
return false;
}
@Override
public boolean isOpen() {
return false; // domain file proxy always used
}
@Override
public boolean isBusy() {
return false; // domain file proxy always used
}
@Override
public void packFile(File file, TaskMonitor monitor) throws IOException, CancelledException {
getLinkedFile().packFile(file, monitor);
}
@Override
public Map<String, String> 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();
}
}

View file

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

View file

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

View file

@ -16,13 +16,12 @@
package ghidra.framework.data; package ghidra.framework.data;
import java.io.*; import java.io.*;
import java.net.URL;
import java.util.*; import java.util.*;
import docking.widgets.OptionDialog;
import generic.timer.GhidraSwinglessTimer; import generic.timer.GhidraSwinglessTimer;
import ghidra.framework.client.*; import ghidra.framework.client.*;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.remote.User; import ghidra.framework.remote.User;
import ghidra.framework.store.*; import ghidra.framework.store.*;
import ghidra.framework.store.FileSystem; import ghidra.framework.store.FileSystem;
@ -82,6 +81,7 @@ public class ProjectFileManager implements ProjectData {
private TaskMonitorAdapter projectDisposalMonitor = new TaskMonitorAdapter(); private TaskMonitorAdapter projectDisposalMonitor = new TaskMonitorAdapter();
private ProjectLock projectLock;
private String owner; private String owner;
/** /**
@ -91,12 +91,16 @@ public class ProjectFileManager implements ProjectData {
* @param resetOwner true to reset the project owner * @param resetOwner true to reset the project owner
* @throws IOException if an i/o error occurs * @throws IOException if an i/o error occurs
* @throws NotOwnerException if inProject is true and user is not owner * @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 * @throws FileNotFoundException if project directory not found
*/ */
public ProjectFileManager(ProjectLocator localStorageLocator, boolean isInWritableProject, public ProjectFileManager(ProjectLocator localStorageLocator, boolean isInWritableProject,
boolean resetOwner) throws NotOwnerException, IOException { boolean resetOwner) throws NotOwnerException, IOException, LockException {
this.localStorageLocator = localStorageLocator; this.localStorageLocator = localStorageLocator;
boolean success = false;
try {
init(false, isInWritableProject); init(false, isInWritableProject);
if (resetOwner) { if (resetOwner) {
owner = SystemUtilities.getUserName(); owner = SystemUtilities.getUserName();
@ -116,10 +120,16 @@ public class ProjectFileManager implements ProjectData {
synchronized (fileSystem) { synchronized (fileSystem) {
getVersionedFileSystem(isInWritableProject); getVersionedFileSystem(isInWritableProject);
rootFolderData = new RootGhidraFolderData(this, listenerList); rootFolderData = new RootGhidraFolderData(this, listenerList);
versionedFSListener = new MyFileSystemListener(); initVersionedFSListener();
versionedFileSystem.addFileSystemListener(versionedFSListener);
scheduleUserDataReconcilation(); 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 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 * @param isInWritableProject true if project content is writable, false if project is read-only
* @throws IOException if an i/o error occurs * @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, public ProjectFileManager(ProjectLocator localStorageLocator, RepositoryAdapter repository,
boolean isInWritableProject) throws IOException { boolean isInWritableProject) throws IOException, LockException {
this.localStorageLocator = localStorageLocator; this.localStorageLocator = localStorageLocator;
this.repository = repository; this.repository = repository;
boolean success = false;
try {
init(true, isInWritableProject); init(true, isInWritableProject);
synchronized (fileSystem) { synchronized (fileSystem) {
createVersionedFileSystem(); createVersionedFileSystem();
rootFolderData = new RootGhidraFolderData(this, listenerList); rootFolderData = new RootGhidraFolderData(this, listenerList);
versionedFSListener = new MyFileSystemListener(); initVersionedFSListener();
versionedFileSystem.addFileSystemListener(versionedFSListener); }
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"); this.localStorageLocator = new ProjectLocator(null, "Test");
owner = SystemUtilities.getUserName(); owner = SystemUtilities.getUserName();
boolean success = false;
try {
synchronized (fileSystem) { synchronized (fileSystem) {
this.fileSystem = fileSystem; this.fileSystem = fileSystem;
this.versionedFileSystem = versionedFileSystem; this.versionedFileSystem = versionedFileSystem;
rootFolderData = new RootGhidraFolderData(this, listenerList); rootFolderData = new RootGhidraFolderData(this, listenerList);
versionedFSListener = new MyFileSystemListener(); initVersionedFSListener();
versionedFileSystem.addFileSystemListener(versionedFSListener);
scheduleUserDataReconcilation(); 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(); projectDir = localStorageLocator.getProjectDir();
properties = new PropertyFile(projectDir, PROPERTY_FILENAME, "/", PROPERTY_FILENAME); properties = new PropertyFile(projectDir, PROPERTY_FILENAME, "/", PROPERTY_FILENAME);
if (create) { if (create) {
@ -163,11 +214,13 @@ public class ProjectFileManager implements ProjectData {
throw new DuplicateFileException( throw new DuplicateFileException(
"Project directory already exists: " + projectDir.getCanonicalPath()); "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(); projectDir.mkdir();
localStorageLocator.getMarkerFile().createNewFile(); localStorageLocator.getMarkerFile().createNewFile();
owner = SystemUtilities.getUserName();
properties.putString(OWNER, owner);
properties.writeState();
} }
else { else {
if (!projectDir.isDirectory()) { if (!projectDir.isDirectory()) {
@ -178,22 +231,92 @@ public class ProjectFileManager implements ProjectData {
throw new ReadOnlyException( throw new ReadOnlyException(
"Project " + localStorageLocator.getName() + " is read-only"); "Project " + localStorageLocator.getName() + " is read-only");
} }
properties.readState();
owner = properties.getString(OWNER, SystemUtilities.getUserName()); owner = properties.getString(OWNER, SystemUtilities.getUserName());
} }
else if (isInWritableProject) {
owner = SystemUtilities.getUserName();
properties.putString(OWNER, owner);
properties.writeState();
}
else { else {
owner = "<unknown>"; // Unknown owner owner = "<unknown>"; // Unknown owner
} }
} }
if (isInWritableProject) {
initLock(create);
}
getPrivateFileSystem(create, isInWritableProject); getPrivateFileSystem(create, isInWritableProject);
getUserFileSystem(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",
"<html>Project is locked. You have another instance of Ghidra<br>" +
"already running with this project open (locally or remotely).<br><br>" +
projectStr + "<br><br>" + "Lock information: " + lockInformation);
return null;
}
int userChoice = OptionDialog.showOptionDialog(null, "Project Locked - Delete Lock?",
"<html>Project is locked. You may have another instance of Ghidra<br>" +
"already running with this project opened (locally or remotely).<br>" + projectStr +
"<br><br>" + "If this is not the case, you can delete the lock file: <br><b>" +
locator.getProjectLockFile().getAbsolutePath() + "</b>.<br><br>" +
"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 @Override
public int getMaxNameLength() { public int getMaxNameLength() {
return fileSystem.getMaxNameLength(); return fileSystem.getMaxNameLength();
@ -314,8 +437,9 @@ public class ProjectFileManager implements ProjectData {
/** /**
* Change the versioned filesystem associated with this project file manager. * Change the versioned filesystem associated with this project file manager.
* This method is provided for testing. Care should be taken when using a * This method is provided for testing (see {@code FakeSharedProject}).
* LocalFileSystem in a shared capacity since locking is not supported. * Care should be taken when using a LocalFileSystem in a shared capacity since
* locking is not supported.
* @param fs versioned filesystem * @param fs versioned filesystem
* @throws IOException if an IO error occurs * @throws IOException if an IO error occurs
*/ */
@ -323,9 +447,11 @@ public class ProjectFileManager implements ProjectData {
if (!fs.isVersioned()) { if (!fs.isVersioned()) {
throw new IllegalArgumentException("versioned filesystem required"); throw new IllegalArgumentException("versioned filesystem required");
} }
if (versionedFSListener != null) {
versionedFileSystem.removeFileSystemListener(versionedFSListener); versionedFileSystem.removeFileSystemListener(versionedFSListener);
}
versionedFileSystem = fs; versionedFileSystem = fs;
versionedFileSystem.addFileSystemListener(versionedFSListener); initVersionedFSListener();
rootFolderData.setVersionedFileSystem(versionedFileSystem); rootFolderData.setVersionedFileSystem(versionedFileSystem);
} }
@ -385,19 +511,37 @@ public class ProjectFileManager implements ProjectData {
} }
@Override @Override
public GhidraFolder getFolder(String path) { public DomainFolder getFolder(String path) {
int len = path.length(); int len = path.length();
if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'"); "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) {
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; return null;
} }
} }
folder = subFolder;
}
return folder;
}
@Override @Override
public int getFileCount() { public int getFileCount() {
@ -468,19 +612,6 @@ public class ProjectFileManager implements ProjectData {
return fileIndex.getFileByID(fileID); 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) { public void releaseDomainFiles(Object consumer) {
for (DomainObjectAdapter domainObj : openDomainObjects.values()) { for (DomainObjectAdapter domainObj : openDomainObjects.values()) {
try { try {
@ -707,7 +838,11 @@ public class ProjectFileManager implements ProjectData {
monitor.checkCanceled(); monitor.checkCanceled();
LocalFolderItem item = fileSystem.getItem(folderPath, name); LocalFolderItem item = fileSystem.getItem(folderPath, name);
if (item.getCheckoutId() != FolderItem.DEFAULT_CHECKOUT_ID) { 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,15 +1172,23 @@ public class ProjectFileManager implements ProjectData {
listenerList.clearAll(); listenerList.clearAll();
} }
if (fileSystem != null) {
synchronized (fileSystem) { synchronized (fileSystem) {
rootFolderData.dispose(); if (versionedFSListener != null) {
fileSystem.dispose();
versionedFileSystem.dispose();
versionedFileSystem.removeFileSystemListener(versionedFSListener); versionedFileSystem.removeFileSystemListener(versionedFSListener);
}
if (repository != null) { if (repository != null) {
repository.disconnect(); repository.disconnect();
repository = null; repository = null;
} }
rootFolderData.dispose();
versionedFileSystem.dispose();
fileSystem.dispose();
}
}
if (projectLock != null) {
projectLock.release();
} }
} }

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.framework.project; package ghidra.framework.data;
import java.io.File; import java.io.File;

View file

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

View file

@ -40,6 +40,8 @@ import generic.theme.GIcon;
import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.GenericRunInfo; import ghidra.framework.GenericRunInfo;
import ghidra.framework.client.*; import ghidra.framework.client.*;
import ghidra.framework.data.FolderLinkContentHandler;
import ghidra.framework.data.LinkedGhidraFolder;
import ghidra.framework.main.datatable.ProjectDataTablePanel; import ghidra.framework.main.datatable.ProjectDataTablePanel;
import ghidra.framework.main.datatree.*; import ghidra.framework.main.datatree.*;
import ghidra.framework.main.projectdata.actions.*; import ghidra.framework.main.projectdata.actions.*;
@ -69,7 +71,7 @@ import ghidra.util.filechooser.GhidraFileFilter;
) )
//@formatter:on //@formatter:on
public class FrontEndPlugin extends Plugin 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 TITLE_PREFIX = "Ghidra: ";
private final static String EXPORT_TOOL_ACTION_NAME = "Export Tool"; private final static String EXPORT_TOOL_ACTION_NAME = "Export Tool";
@ -128,6 +130,7 @@ public class FrontEndPlugin extends Plugin
private ClearCutAction clearCutAction; private ClearCutAction clearCutAction;
private ProjectDataCopyAction copyAction; private ProjectDataCopyAction copyAction;
private ProjectDataPasteAction pasteAction; private ProjectDataPasteAction pasteAction;
private ProjectDataPasteLinkAction pasteLinkAction;
private ProjectDataRenameAction renameAction; private ProjectDataRenameAction renameAction;
private ProjectDataOpenDefaultToolAction openAction; private ProjectDataOpenDefaultToolAction openAction;
private ProjectDataExpandAction<FrontEndProjectTreeContext> expandAction; private ProjectDataExpandAction<FrontEndProjectTreeContext> expandAction;
@ -217,6 +220,7 @@ public class FrontEndPlugin extends Plugin
clearCutAction = new ClearCutAction(owner); clearCutAction = new ClearCutAction(owner);
copyAction = new ProjectDataCopyAction(owner, groupName); copyAction = new ProjectDataCopyAction(owner, groupName);
pasteAction = new ProjectDataPasteAction(owner, groupName); pasteAction = new ProjectDataPasteAction(owner, groupName);
pasteLinkAction = new ProjectDataPasteLinkAction(owner, groupName);
groupName = "Delete/Rename"; groupName = "Delete/Rename";
renameAction = new ProjectDataRenameAction(owner, groupName); renameAction = new ProjectDataRenameAction(owner, groupName);
@ -239,6 +243,7 @@ public class FrontEndPlugin extends Plugin
tool.addAction(clearCutAction); tool.addAction(clearCutAction);
tool.addAction(copyAction); tool.addAction(copyAction);
tool.addAction(pasteAction); tool.addAction(pasteAction);
tool.addAction(pasteLinkAction);
tool.addAction(deleteAction); tool.addAction(deleteAction);
tool.addAction(openAction); tool.addAction(openAction);
tool.addAction(renameAction); tool.addAction(renameAction);
@ -393,6 +398,10 @@ public class FrontEndPlugin extends Plugin
toolChest.addToolChestChangeListener(toolBar); toolChest.addToolChestChangeListener(toolBar);
toolChest.addToolChestChangeListener(toolChestChangeListener); toolChest.addToolChestChangeListener(toolChestChangeListener);
createToolSpecificOpenActions(); createToolSpecificOpenActions();
// Add project view listener
activeProject.addProjectViewListener(this);
// Add the repository listener // Add the repository listener
RepositoryAdapter repository = activeProject.getRepository(); RepositoryAdapter repository = activeProject.getRepository();
if (repository != null) { if (repository != null) {
@ -403,6 +412,16 @@ public class FrontEndPlugin extends Plugin
// gui.validate(); // 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 * sets the name of the project, using the default name if no project is active
*/ */
@ -1074,12 +1093,23 @@ public class FrontEndPlugin extends Plugin
} }
public void openDomainFile(DomainFile domainFile) { public void openDomainFile(DomainFile domainFile) {
if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(domainFile.getContentType())) {
showLinkedFolder(domainFile);
return;
}
Project project = tool.getProject(); Project project = tool.getProject();
final ToolServices toolServices = project.getToolServices(); final ToolServices toolServices = project.getToolServices();
ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(domainFile); ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(domainFile);
if (defaultToolTemplate != null) {
ToolButton button = toolBar.getToolButtonForToolConfig(defaultToolTemplate);
if (button != null) {
button.launchTool(domainFile);
return;
}
}
if (defaultToolTemplate == null) {
// assume no tools in the tool chest
Msg.showInfo(this, tool.getToolFrame(), "Cannot Find Tool", Msg.showInfo(this, tool.getToolFrame(), "Cannot Find Tool",
"<html>Cannot find tool to open file: <b>" + "<html>Cannot find tool to open file: <b>" +
HTMLUtilities.escapeHTML(domainFile.getName()) + HTMLUtilities.escapeHTML(domainFile.getName()) +
@ -1087,11 +1117,32 @@ public class FrontEndPlugin extends Plugin
"<b>Tools->Import Default Tools...</b> menu. Alternatively, you can " + "<b>Tools->Import Default Tools...</b> menu. Alternatively, you can " +
"use <b>Tool->Set Tool Associations</b> menu to change how Ghidra " + "use <b>Tool->Set Tool Associations</b> menu to change how Ghidra " +
"opens this type of file"); "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; return;
} }
ToolButton button = toolBar.getToolButtonForToolConfig(defaultToolTemplate); DomainFolder domainFolder = linkedFolder.getLinkedFolder();
button.launchTool(domainFile); 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);
}
} }

View file

@ -554,10 +554,15 @@ class ProjectActionManager {
return; return;
} }
ProjectDataPanel pdp = plugin.getProjectDataPanel(); try {
pdp.openView(view); activeProject.addProjectView(view, true); // listener will trigger data panel panel display
// also update the recent views menu }
plugin.rebuildRecentMenus(); 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() { private void editProjectAccess() {

View file

@ -27,6 +27,7 @@ import javax.swing.*;
import docking.ActionContext; import docking.ActionContext;
import docking.ComponentProvider; import docking.ComponentProvider;
import docking.widgets.tabbedpane.DockingTabRenderer; import docking.widgets.tabbedpane.DockingTabRenderer;
import ghidra.framework.client.NotConnectedException;
import ghidra.framework.main.datatable.ProjectDataTablePanel; import ghidra.framework.main.datatable.ProjectDataTablePanel;
import ghidra.framework.main.datatree.ProjectDataTreePanel; import ghidra.framework.main.datatree.ProjectDataTreePanel;
import ghidra.framework.model.*; import ghidra.framework.model.*;
@ -40,7 +41,7 @@ import help.HelpService;
* Manages the data tree for the active project, and the trees for the * Manages the data tree for the active project, and the trees for the
* project views. * project views.
*/ */
class ProjectDataPanel extends JSplitPane { class ProjectDataPanel extends JSplitPane implements ProjectViewListener {
private final static String BORDER_PREFIX = "Active Project: "; private final static String BORDER_PREFIX = "Active Project: ";
private final static String READ_ONLY_BORDER = "READ-ONLY Project Data"; private final static String READ_ONLY_BORDER = "READ-ONLY Project Data";
private final static int TYPICAL_NUM_VIEWS = 2; private final static int TYPICAL_NUM_VIEWS = 2;
@ -155,6 +156,21 @@ class ProjectDataPanel extends JSplitPane {
setViewsVisible(views.length > 0); 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() { private void clearReadOnlyViews() {
readOnlyTab.removeAll(); readOnlyTab.removeAll();
readOnlyViews.clear(); readOnlyViews.clear();
@ -167,7 +183,13 @@ class ProjectDataPanel extends JSplitPane {
this.setDividerLocation(visible ? DIVIDER_LOCATION : 1.0); 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(); ProjectManager projectManager = tool.getProjectManager();
Project activeProject = tool.getProject(); Project activeProject = tool.getProject();
@ -176,19 +198,23 @@ class ProjectDataPanel extends JSplitPane {
if (dtp != null) { if (dtp != null) {
readOnlyTab.setSelectedComponent(dtp); readOnlyTab.setSelectedComponent(dtp);
try { try {
activeProject.addProjectView(projectView); activeProject.addProjectView(projectView, true);
projectManager.rememberViewedProject(projectView); projectManager.rememberViewedProject(projectView);
return dtp;
} }
catch (Exception e) { catch (Exception e) {
projectManager.forgetViewedProject(projectView); projectManager.forgetViewedProject(projectView);
Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View", e.toString()); Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View", e.toString());
} }
return; return null;
} }
try { try {
// TODO: addProjectView should be done in a model task // 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); projectManager.rememberViewedProject(projectView);
String viewName = projectData.getProjectLocator().getName(); String viewName = projectData.getProjectLocator().getName();
final ProjectDataTreePanel newPanel = final ProjectDataTreePanel newPanel =
@ -204,14 +230,21 @@ class ProjectDataPanel extends JSplitPane {
readOnlyTab.setSelectedIndex(0); readOnlyTab.setSelectedIndex(0);
readOnlyViews.put(projectData.getProjectLocator(), newPanel); readOnlyViews.put(projectData.getProjectLocator(), newPanel);
setViewsVisible(true); setViewsVisible(true);
return newPanel;
}
catch (NotConnectedException e) {
// already handled (e..g, cancelled login) - ignore
} }
catch (Exception e) { catch (Exception e) {
projectManager.forgetViewedProject(projectView); projectManager.forgetViewedProject(projectView);
Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View", Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View",
"Failed to view project/repository: " + e.getMessage()); "Failed to view project/repository: " + e.getMessage(), e);
} }
finally {
validate(); validate();
} }
return null;
}
ProjectLocator[] getProjectViews() { ProjectLocator[] getProjectViews() {
int numViews = readOnlyViews.size(); int numViews = readOnlyViews.size();
@ -315,6 +348,7 @@ class ProjectDataPanel extends JSplitPane {
treePanel.setProjectData(project.getName(), project.getProjectData()); treePanel.setProjectData(project.getName(), project.getProjectData());
tablePanel.setProjectData(project.getName(), project.getProjectData()); tablePanel.setProjectData(project.getName(), project.getProjectData());
populateReadOnlyViews(project); populateReadOnlyViews(project);
project.addProjectViewListener(this);
} }
else { else {
tablePanel.setProjectData("No Active Project", null); tablePanel.setProjectData("No Active Project", null);

View file

@ -182,9 +182,12 @@ public class ProjectDataTablePanel extends JPanel {
this.projectData.removeDomainFolderChangeListener(changeListener); this.projectData.removeDomainFolderChangeListener(changeListener);
model.setProjectData(null); model.setProjectData(null);
SystemUtilities.runSwingLater(() -> { SystemUtilities.runSwingLater(() -> {
GGlassPane glassPane = (GGlassPane) gTable.getRootPane().getGlassPane(); JRootPane rootPane = gTable.getRootPane();
if (rootPane != null) {
GGlassPane glassPane = (GGlassPane) rootPane.getGlassPane();
glassPane.removePainter(painter); glassPane.removePainter(painter);
glassPane.addPainter(painter); glassPane.addPainter(painter);
}
}); });
} }
} }

View file

@ -70,7 +70,7 @@ public abstract class ProjectTreeAction extends DockingAction {
@Override @Override
public boolean isAddToPopup(ActionContext context) { public boolean isAddToPopup(ActionContext context) {
if (!isEnabledForContext(context)) { if (!(context instanceof FrontEndProjectTreeContext)) {
return false; return false;
} }
return isAddToPopup((FrontEndProjectTreeContext) context); return isAddToPopup((FrontEndProjectTreeContext) context);

View file

@ -176,40 +176,15 @@ class ChangeManager implements DomainFolderChangeListener {
if (lazy && !folderNode.isLoaded()) { if (lazy && !folderNode.isLoaded()) {
return null; // not visited return null; // not visited
} }
// must look at all children since a folder and file may have the same name folderNode =
boolean found = false; (DomainFolderNode) folderNode.getChild(name, n -> (n instanceof DomainFolderNode));
for (GTreeNode node : folderNode.getChildren()) { if (folderNode == null) {
if (!(node instanceof DomainFolderNode)) {
continue;
}
if (name.equals(node.getName())) {
folderNode = (DomainFolderNode) node;
found = true;
break;
}
}
if (!found) {
return null; return null;
} }
} }
return folderNode; 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) { private DomainFileNode findDomainFileNode(DomainFile domainFile, boolean lazy) {
DomainFolderNode folderNode = findDomainFolderNode(domainFile.getParent(), lazy); DomainFolderNode folderNode = findDomainFolderNode(domainFile.getParent(), lazy);
if (folderNode == null) { if (folderNode == null) {
@ -219,11 +194,8 @@ class ChangeManager implements DomainFolderChangeListener {
return null; // not visited return null; // not visited
} }
GTreeNode child = folderNode.getChild(domainFile.getName()); return (DomainFileNode) folderNode.getChild(domainFile.getName(),
if (child instanceof DomainFileNode) { n -> (n instanceof DomainFileNode));
return (DomainFileNode) child;
}
return null;
} }
private void updateFolderNode(DomainFolder parent) { private void updateFolderNode(DomainFolder parent) {

View file

@ -58,7 +58,7 @@ public class CheckInTask extends VersionControlTask implements CheckinHandler {
throw new CancelledException(); throw new CancelledException();
} }
if (actionID != VersionControlDialog.APPLY_TO_ALL) { 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 // adding to version control
if (actionID == VersionControlDialog.CANCEL) { if (actionID == VersionControlDialog.CANCEL) {
monitor.cancel(); monitor.cancel();

View file

@ -16,6 +16,7 @@
package ghidra.framework.main.datatree; package ghidra.framework.main.datatree;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@ -24,6 +25,8 @@ import javax.swing.SwingWorker;
import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon; import generic.theme.GIcon;
import ghidra.framework.data.FolderLinkContentHandler;
import ghidra.framework.data.LinkHandler;
import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFile;
import ghidra.framework.store.ItemCheckoutStatus; import ghidra.framework.store.ItemCheckoutStatus;
import ghidra.util.*; import ghidra.util.*;
@ -174,7 +177,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
if (domainFile.isHijacked()) { if (domainFile.isHijacked()) {
newDisplayName += " (hijacked)"; newDisplayName += " (hijacked)";
} }
else if (domainFile.isVersioned()) { else if (domainFile.isVersioned() && !domainFile.isLinkFile()) {
int versionNumber = domainFile.getVersion(); int versionNumber = domainFile.getVersion();
String versionStr = "" + versionNumber; String versionStr = "" + versionNumber;
@ -208,20 +211,34 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
} }
private void setToolTipText() { private void setToolTipText() {
String newToolTipText = toolTipText; String newToolTipText = null;
if (domainFile.isInWritableProject() && domainFile.isHijacked()) { if (domainFile.isInWritableProject() && domainFile.isHijacked()) {
newToolTipText = "Hijacked file should be deleted or renamed"; newToolTipText = "Hijacked file should be deleted or renamed";
} }
else { else {
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(); long lastModified = domainFile.getLastModifiedTime();
newToolTipText = "Last Modified " + formatter.format(new Date(lastModified)); newToolTipText = "Last Modified " + formatter.format(new Date(lastModified));
}
if (domainFile.isCheckedOut()) { if (domainFile.isCheckedOut()) {
try { try {
ItemCheckoutStatus status = domainFile.getCheckoutStatus(); ItemCheckoutStatus status = domainFile.getCheckoutStatus();
if (status != null) { if (status != null) {
newToolTipText = HTMLUtilities.toHTML( newToolTipText = "Checked out " +
"Checked out " + formatter.format(new Date(status.getCheckoutTime())) + formatter.format(new Date(status.getCheckoutTime())) +
";\n" + newToolTipText); "\n" + newToolTipText;
} }
} }
catch (IOException e) { catch (IOException e) {
@ -232,6 +249,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
if (domainFile.isReadOnly()) { if (domainFile.isReadOnly()) {
newToolTipText += " (read only)"; newToolTipText += " (read only)";
} }
newToolTipText = HTMLUtilities.toLiteralHTML(newToolTipText, 0);
} }
toolTipText = newToolTipText; toolTipText = newToolTipText;
} }
@ -243,11 +261,37 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
@Override @Override
public int compareTo(GTreeNode node) { public int compareTo(GTreeNode node) {
// Goal is to sort folder link-files similar to a folder
if (node instanceof DomainFolderNode) { 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; return 1;
} }
if (node instanceof DomainFileNode) {
DomainFileNode otherFileNode = (DomainFileNode) node;
if (isFolderLink()) {
if (otherFileNode.isFolderLink()) {
return super.compareTo(node); 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 @Override
public void valueChanged(Object newValue) { public void valueChanged(Object newValue) {

View file

@ -22,20 +22,18 @@ import javax.swing.Icon;
import docking.widgets.tree.GTreeLazyNode; import docking.widgets.tree.GTreeLazyNode;
import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.util.InvalidNameException; import ghidra.util.*;
import ghidra.util.Msg;
import resources.ResourceManager; import resources.ResourceManager;
/** /**
* Class to represent a node in the Data tree. * Class to represent a node in the Data tree.
*/ */
public class DomainFolderNode extends GTreeLazyNode implements Cuttable { 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_OPEN_FOLDER = DomainFolder.OPEN_FOLDER_ICON;
private static final Icon ENABLED_CLOSED_FOLDER = private static final Icon ENABLED_CLOSED_FOLDER = DomainFolder.CLOSED_FOLDER_ICON;
new GIcon("icon.datatree.node.domain.folder.closed");
private static final Icon DISABLED_OPEN_FOLDER = private static final Icon DISABLED_OPEN_FOLDER =
ResourceManager.getDisabledIcon(ENABLED_OPEN_FOLDER); ResourceManager.getDisabledIcon(ENABLED_OPEN_FOLDER);
private static final Icon DISABLED_CLOSED_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 // TODO: how can the folder be null?...doesn't really make sense...I don't think it ever is
if (domainFolder != null) { if (domainFolder != null) {
toolTipText = domainFolder.getPathname(); toolTipText = StringUtilities.trimMiddle(domainFolder.getPathname(), 120);
toolTipText = HTMLUtilities.toLiteralHTML(toolTipText, 0);
isEditable = domainFolder.isInWritableProject(); 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. * 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 @Override
public Icon getIcon(boolean expanded) { public Icon getIcon(boolean expanded) {
if (domainFolder instanceof LinkedDomainFolder) {
// NOTE: cut operation not supported
return ((LinkedDomainFolder) domainFolder).getIcon(expanded);
}
if (expanded) { if (expanded) {
return isCut ? DISABLED_OPEN_FOLDER : ENABLED_OPEN_FOLDER; return isCut ? DISABLED_OPEN_FOLDER : ENABLED_OPEN_FOLDER;
} }
@ -119,8 +128,12 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
@Override @Override
protected List<GTreeNode> generateChildren() { protected List<GTreeNode> generateChildren() {
List<GTreeNode> children = new ArrayList<>(); List<GTreeNode> 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(); DomainFolder[] folders = domainFolder.getFolders();
for (DomainFolder folder : folders) { for (DomainFolder folder : folders) {
children.add(new DomainFolderNode(folder, filter)); children.add(new DomainFolderNode(folder, filter));
@ -128,6 +141,13 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
DomainFile[] files = domainFolder.getFiles(); DomainFile[] files = domainFolder.getFiles();
for (DomainFile domainFile : files) { 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)) { if (filter == null || filter.accept(domainFile)) {
children.add(new DomainFileNode(domainFile)); children.add(new DomainFileNode(domainFile));
} }
@ -173,7 +193,8 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
@Override @Override
public int compareTo(GTreeNode node) { public int compareTo(GTreeNode node) {
if (node instanceof DomainFileNode) { if (node instanceof DomainFileNode) {
return -1; // defer to DomainFileNode for comparison
return -((DomainFileNode) node).compareTo(this);
} }
return super.compareTo(node); return super.compareTo(node);
} }

View file

@ -423,6 +423,9 @@ public class ProjectDataTreePanel extends JPanel {
root = createRootNode(projectName); root = createRootNode(projectName);
tree = new DataTree(tool, root); tree = new DataTree(tool, root);
if (!isActiveProject) {
tree.setName(tree.getName() + ": " + projectName);
}
if (plugin != null) { if (plugin != null) {
tree.addGTreeSelectionListener(e -> { tree.addGTreeSelectionListener(e -> {

View file

@ -117,12 +117,6 @@ public class VersionControlDialog extends DialogComponentProvider {
return keepFileCB.isSelected(); return keepFileCB.isSelected();
} }
void setCreateKeepFile(boolean selected) {
if (!addToVersionControl) {
keepFileCB.setSelected(selected);
}
}
/** /**
* Return the comments for the add to version control. * Return the comments for the add to version control.
* @return may be the empty string * @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. * 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.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() { private JPanel buildMainPanel() {

Some files were not shown because too many files have changed in this diff Show more