mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-01-31 13:31:08 +00:00
GP-2509 GP-2644 Improved Ghidra URL support. Added support for Ghidra
URL linked files and folders within project.
This commit is contained in:
parent
8d6cf5e310
commit
5a422c4502
@ -27,6 +27,7 @@ import ghidra.framework.data.ProjectFileManager;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.framework.store.LockException;
|
||||
import ghidra.trace.database.DBTraceContentHandler;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.trace.model.Trace;
|
||||
@ -643,6 +644,7 @@ public class DebuggerCoordinates {
|
||||
ProjectData projData = tool.getProject().getProjectData(projLoc);
|
||||
if (projData == null) {
|
||||
try {
|
||||
// FIXME! orphaned instance - transient in nature
|
||||
projData = new ProjectFileManager(projLoc, false, false);
|
||||
}
|
||||
catch (NotOwnerException e) {
|
||||
@ -650,7 +652,7 @@ public class DebuggerCoordinates {
|
||||
"Not project owner: " + projLoc + "(" + pathname + ")");
|
||||
return null;
|
||||
}
|
||||
catch (IOException e) {
|
||||
catch (IOException | LockException e) {
|
||||
Msg.showError(DebuggerCoordinates.class, tool.getToolFrame(), "Trace Open Failed",
|
||||
"Project error: " + e.getMessage());
|
||||
return null;
|
||||
|
@ -368,7 +368,18 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
|
||||
if (traceChooserDialog != null) {
|
||||
return traceChooserDialog;
|
||||
}
|
||||
DomainFileFilter filter = df -> Trace.class.isAssignableFrom(df.getDomainObjectClass());
|
||||
DomainFileFilter filter = new DomainFileFilter() {
|
||||
|
||||
@Override
|
||||
public boolean accept(DomainFile df) {
|
||||
return Trace.class.isAssignableFrom(df.getDomainObjectClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean followLinkedFolders() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO regarding the hack note below, I believe this issue ahs been fixed, but not sure how to test
|
||||
return traceChooserDialog =
|
||||
|
@ -176,7 +176,7 @@ public class ProjectExperimentsTest extends AbstractGhidraHeadedIntegrationTest
|
||||
|
||||
assertNotNull(proj2 = pm.openProject(loc2, false, false));
|
||||
|
||||
ProjectData data1 = proj2.addProjectView(loc1.getURL());
|
||||
ProjectData data1 = proj2.addProjectView(loc1.getURL(), true);
|
||||
assertNotNull(data1);
|
||||
|
||||
// It's a cryin' shame. I don't get *any* callbacks. _ANY!_
|
||||
|
@ -18,11 +18,13 @@ package ghidra.trace.database;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
import db.DBHandle;
|
||||
import db.buffers.BufferFile;
|
||||
import db.buffers.ManagedBufferFile;
|
||||
import ghidra.framework.data.*;
|
||||
import ghidra.framework.data.DBWithUserDataContentHandler;
|
||||
import ghidra.framework.data.DomainObjectMergeManager;
|
||||
import ghidra.framework.model.ChangeSet;
|
||||
import ghidra.framework.model.DomainObject;
|
||||
import ghidra.framework.store.*;
|
||||
@ -34,9 +36,16 @@ import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.exception.VersionException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
public class DBTraceContentHandler extends DBContentHandler {
|
||||
public class DBTraceContentHandler extends DBWithUserDataContentHandler<DBTrace> {
|
||||
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
|
||||
public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
|
||||
DomainObject obj, TaskMonitor monitor)
|
||||
@ -48,7 +57,7 @@ public class DBTraceContentHandler extends DBContentHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version,
|
||||
public DBTrace getImmutableObject(FolderItem item, Object consumer, int version,
|
||||
int minChangeVersion, TaskMonitor monitor)
|
||||
throws IOException, CancelledException, VersionException {
|
||||
String contentType = item.getContentType();
|
||||
@ -96,7 +105,7 @@ public class DBTraceContentHandler extends DBContentHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
|
||||
public DBTrace getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
|
||||
Object consumer, TaskMonitor monitor)
|
||||
throws IOException, VersionException, CancelledException {
|
||||
String contentType = item.getContentType();
|
||||
@ -146,7 +155,7 @@ public class DBTraceContentHandler extends DBContentHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
|
||||
public DBTrace getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
|
||||
boolean okToUpgrade, boolean recover, Object consumer, TaskMonitor monitor)
|
||||
throws IOException, CancelledException, VersionException {
|
||||
String contentType = item.getContentType();
|
||||
@ -296,8 +305,8 @@ public class DBTraceContentHandler extends DBContentHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends DomainObject> getDomainObjectClass() {
|
||||
return DBTrace.class;
|
||||
public Class<DBTrace> getDomainObjectClass() {
|
||||
return TRACE_DOMAIN_OBJECT_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -312,12 +321,12 @@ public class DBTraceContentHandler extends DBContentHandler {
|
||||
|
||||
@Override
|
||||
public String getDefaultToolName() {
|
||||
return "Debugger";
|
||||
return TRACE_CONTENT_DEFAULT_TOOL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return Trace.TRACE_ICON;
|
||||
return TRACE_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -331,4 +340,9 @@ public class DBTraceContentHandler extends DBContentHandler {
|
||||
// TODO:
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DBTraceLinkContentHandler getLinkHandler() {
|
||||
return linkHandler;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -351,6 +351,7 @@ src/main/help/help/topics/FrontEndPlugin/images/DeleteProject.png||GHIDRA||||END
|
||||
src/main/help/help/topics/FrontEndPlugin/images/EditPluginPath.png||GHIDRA||||END|
|
||||
src/main/help/help/topics/FrontEndPlugin/images/EditProjectAccessList.png||GHIDRA||||END|
|
||||
src/main/help/help/topics/FrontEndPlugin/images/EditProjectAccessPanel.png||GHIDRA||||END|
|
||||
src/main/help/help/topics/FrontEndPlugin/images/LinkOtherProject.png||GHIDRA||||END|
|
||||
src/main/help/help/topics/FrontEndPlugin/images/MemoryUsage.png||GHIDRA||||END|
|
||||
src/main/help/help/topics/FrontEndPlugin/images/NonSharedProjectInfo.png||GHIDRA||||END|
|
||||
src/main/help/help/topics/FrontEndPlugin/images/OpenProject.png||GHIDRA||||END|
|
||||
@ -1089,7 +1090,6 @@ src/main/resources/images/layout_add.png||FAMFAMFAM Icons - CC 2.5|||famfamfam s
|
||||
src/main/resources/images/ledgreen.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
|
||||
src/main/resources/images/ledred.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
|
||||
src/main/resources/images/ledyellow.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
|
||||
src/main/resources/images/link.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END|
|
||||
src/main/resources/images/lock.gif||GHIDRA||||END|
|
||||
src/main/resources/images/magnifier.png||FAMFAMFAM Icons - CC 2.5||||END|
|
||||
src/main/resources/images/media-flash.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
|
||||
|
@ -17,6 +17,8 @@
|
||||
// NOTE: Script will only process unversioned and checked-out files.
|
||||
//@category Examples
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.app.script.GhidraState;
|
||||
import ghidra.framework.model.*;
|
||||
@ -25,8 +27,6 @@ import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.exception.VersionException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CallAnotherScriptForAllPrograms extends GhidraScript {
|
||||
|
||||
// The script referenced in the following line should be replaced with the script to be called
|
||||
@ -59,6 +59,10 @@ public class CallAnotherScriptForAllPrograms extends GhidraScript {
|
||||
}
|
||||
|
||||
private void processDomainFile(DomainFile domainFile) throws CancelledException, IOException {
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this. If program links should be considered
|
||||
// "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
|
||||
// should be used.
|
||||
if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domainFile.getContentType())) {
|
||||
return; // skip non-Program files
|
||||
}
|
||||
|
@ -143,6 +143,8 @@ public class RepositoryFileUpgradeScript extends GhidraScript {
|
||||
}
|
||||
|
||||
private boolean performProgramUpgrade(DomainFile df) throws IOException, CancelledException {
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this.
|
||||
if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(df.getContentType())) {
|
||||
return false;
|
||||
}
|
||||
|
@ -48,7 +48,10 @@ public class VersionControl_AddAll extends GhidraScript {
|
||||
if (monitor.isCancelled()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this. If program links should be considered
|
||||
// "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
|
||||
// should be used. It may also be appropriate to handle other content types.
|
||||
if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(file.getContentType()) ||
|
||||
file.isVersioned()) {
|
||||
continue;// skip
|
||||
|
@ -22,13 +22,15 @@
|
||||
hyperlink.</P>
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
@ -47,10 +49,21 @@
|
||||
<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
|
||||
Ghidra, a web browser is launched and attempts to load the corresponding web page. If the URL text
|
||||
is of the form <i>ghidra://<host>[:<port>]/<repository-name>/<program-path>[#<address-or-symbol-ref>]</i>
|
||||
an attempt will be made to open the corresponding program from the referenced Ghidra Server (e.g.,
|
||||
<i>ghidra://myserver/Repo/notepad.exe#entry</i>).</P>
|
||||
Ghidra, a web browser is launched and attempts to load the corresponding web page. </P>
|
||||
|
||||
<P>If the URL text corresponds to a Ghidra URL and attempt will be made to open the referenced
|
||||
Program file within the Code Browser. Such a URL may refer to a Program file from a
|
||||
local project or Ghidra Server. The Ghidra URL forms supported include:</P>
|
||||
|
||||
<P><U>Remote Ghidra Server file</U><BR>
|
||||
<i>ghidra://<host>[:<port>]/<repository-name>/<program-path>[#<address-or-symbol-ref>]</i><BR>
|
||||
Example: <i>ghidra://hostname/Repo/notepad.exe#entry</i>
|
||||
</P>
|
||||
|
||||
<P><U>Local Ghidra project file</U><BR>
|
||||
<i>ghidra:/[<project-path>/]<project-name>?/<program-path>[#<address-or-symbol-ref>]</i><BR>
|
||||
Example: <i>ghidra:/share/MyProject?/notepad.exe#entry</i>
|
||||
</P>
|
||||
</BLOCKQUOTE>
|
||||
|
||||
<H2>Valid Annotations</H2>
|
||||
@ -174,9 +187,10 @@
|
||||
<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
|
||||
than that of the URL.<BR><BR>
|
||||
References to <i>ghidra://</i>, which refer to a program within a Ghidra Server repository,
|
||||
will be opened within the Listing display, while all other URL protocols (e.g., <i>http://, https://,
|
||||
file://,</i> etc.) will be launched via an external web browser (see
|
||||
References to a program file on a Ghidra Server (<i>ghidra://<host...</i>) or
|
||||
local project (<i>ghidra:/<project-...</i>) will be opened within the Listing display,
|
||||
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
|
||||
for Processor Manuals</a>).
|
||||
</TD>
|
||||
@ -216,6 +230,12 @@
|
||||
<LI>{@url "ghidra://myserver/Repo/notepad.exe#entry"}</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>
|
||||
</TD>
|
||||
</TR>
|
||||
@ -224,8 +244,9 @@
|
||||
<TD valign="top" width="5%">Program<BR>
|
||||
</TD>
|
||||
|
||||
<TD valign="top" width="15%">Displays a hyperlink to the given Ghidra program name that
|
||||
will open that program in a new Listing tab when clicked.<BR>
|
||||
<TD valign="top" width="15%">Displays a hyperlink to the given Ghidra program pathname
|
||||
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
|
||||
opened by appending to the program name an '@' character, followed by an address or
|
||||
symbol name.</TD>
|
||||
|
@ -688,6 +688,54 @@
|
||||
|
||||
<P>The tabbed pane for read-only Project data is removed from the Project Window.</P>
|
||||
</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>
|
||||
|
||||
<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 |
@ -1266,19 +1266,24 @@ public class CallTreeProvider extends ComponentProviderAdapter implements Domain
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void expandNode(GTreeNode node, TaskMonitor monitor) throws CancelledException {
|
||||
protected void expandNode(GTreeNode node, boolean force, TaskMonitor monitor)
|
||||
throws CancelledException {
|
||||
TreePath treePath = node.getTreePath();
|
||||
Object[] path = treePath.getPath();
|
||||
if (path.length > maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && !node.isAutoExpandPermitted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
CallNode callNode = (CallNode) node;
|
||||
if (callNode.functionIsInPath()) {
|
||||
return; // this path hit a function that is already in the path
|
||||
}
|
||||
|
||||
super.expandNode(node, monitor);
|
||||
super.expandNode(node, false, monitor);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,9 +106,33 @@ public class ClearFlowAndRepairCmd extends BackgroundCommand {
|
||||
CodeUnit cu = cuIter.next();
|
||||
if (cu instanceof Instruction) {
|
||||
Instruction instr = (Instruction) cu;
|
||||
|
||||
// check for function on delay slot
|
||||
if (listing.getFunctionAt(instr.getMinAddress()) != null) {
|
||||
continue; // skip since it will be picked-up by flow if appropriate
|
||||
}
|
||||
|
||||
// check for fallthrough to instruction
|
||||
Address ffAddr = instr.getFallFrom();
|
||||
if (ffAddr != null && startAddrs.contains(ffAddr)) {
|
||||
continue; // skip since it will be picked-up by flow
|
||||
continue; // skip since it will be picked-up by flow if appropriate
|
||||
}
|
||||
|
||||
// check for flow into delay slot
|
||||
if (instr.isInDelaySlot()) {
|
||||
boolean skip = false;
|
||||
ReferenceIterator refToIter = instr.getReferenceIteratorTo();
|
||||
while (refToIter.hasNext()) {
|
||||
Reference ref = refToIter.next();
|
||||
RefType refType = ref.getReferenceType();
|
||||
if (refType.isJump() || refType.isCall()) {
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (skip) {
|
||||
continue; // skip since it will be picked-up by flow if appropriate
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -240,10 +240,11 @@ public class CommentsDialog extends DialogComponentProvider implements KeyListen
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
private AnnotationAdapterWrapper[] getAnnotationAdapterWrappers() {
|
||||
AnnotatedStringHandler[] annotations = Annotation.getAnnotatedStringHandlers();
|
||||
AnnotationAdapterWrapper[] retVal = new AnnotationAdapterWrapper[annotations.length];
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
retVal[i] = new AnnotationAdapterWrapper(annotations[i]);
|
||||
List<AnnotatedStringHandler> annotations = Annotation.getAnnotatedStringHandlers();
|
||||
int count = annotations.size();
|
||||
AnnotationAdapterWrapper[] retVal = new AnnotationAdapterWrapper[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
retVal[i] = new AnnotationAdapterWrapper(annotations.get(i));
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ import ghidra.framework.options.SaveState;
|
||||
import ghidra.framework.plugintool.PluginInfo;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.program.database.DataTypeArchiveContentHandler;
|
||||
import ghidra.program.database.data.ProgramDataTypeManager;
|
||||
import ghidra.program.model.address.AddressSetView;
|
||||
import ghidra.program.model.data.*;
|
||||
@ -245,8 +244,7 @@ public class DataTypeManagerPlugin extends ProgramPlugin
|
||||
Project project = tool.getProjectManager().getActiveProject();
|
||||
if (project != null && project.getName().equals(projectName)) {
|
||||
DomainFile df = project.getProjectData().getFile(pathname);
|
||||
if (df != null && DataTypeArchiveContentHandler.DATA_TYPE_ARCHIVE_CONTENT_TYPE
|
||||
.equals(df.getContentType())) {
|
||||
if (DataTypeArchive.class.isAssignableFrom(df.getDomainObjectClass())) {
|
||||
return df;
|
||||
}
|
||||
}
|
||||
@ -588,12 +586,9 @@ public class DataTypeManagerPlugin extends ProgramPlugin
|
||||
openArchive(domainFile, version);
|
||||
}
|
||||
};
|
||||
DomainFileFilter filter = f -> {
|
||||
Class<?> c = f.getDomainObjectClass();
|
||||
return DataTypeArchive.class.isAssignableFrom(c);
|
||||
};
|
||||
openDialog =
|
||||
new OpenVersionedFileDialog(tool, "Open Project Data Type Archive", filter);
|
||||
new OpenVersionedFileDialog(tool, "Open Project Data Type Archive",
|
||||
df -> DataTypeArchive.class.isAssignableFrom(df.getDomainObjectClass()));
|
||||
openDialog.setHelpLocation(new HelpLocation(HelpTopics.PROGRAM, "Open_File_Dialog"));
|
||||
openDialog.addOkActionListener(listener);
|
||||
}
|
||||
|
@ -608,8 +608,15 @@ public class DataTypesProvider extends ComponentProviderAdapter {
|
||||
ArchiveNode archiveNode = dataTypeNode.getArchiveNode();
|
||||
|
||||
if (archiveNode instanceof ProjectArchiveNode && !archiveNode.isModifiable()) {
|
||||
Msg.showInfo(getClass(), archiveGTree, "Archive Not Checked Out",
|
||||
"You must checkout this archive before you may edit data types.");
|
||||
ProjectArchiveNode projectArchive = (ProjectArchiveNode) archiveNode;
|
||||
if (projectArchive.getDomainFile().isReadOnly()) {
|
||||
Msg.showInfo(getClass(), archiveGTree, "Read-Only Archive",
|
||||
"You may not edit data type within a read-only project archive.");
|
||||
}
|
||||
else {
|
||||
Msg.showInfo(getClass(), archiveGTree, "Archive Not Checked Out",
|
||||
"You must checkout this archive before you may edit data types.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,7 @@ import ghidra.app.plugin.core.datamgr.tree.ArchiveNode;
|
||||
import ghidra.app.plugin.core.datamgr.tree.DataTypeNode;
|
||||
import ghidra.app.plugin.core.datamgr.util.DataTypeTreeCopyMoveTask;
|
||||
import ghidra.app.plugin.core.datamgr.util.DataTypeTreeCopyMoveTask.ActionType;
|
||||
import ghidra.app.plugin.core.datamgr.util.DataTypeUtils;
|
||||
import ghidra.program.model.data.*;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.layout.PairLayout;
|
||||
@ -85,26 +86,21 @@ public class AssociateDataTypeAction extends DockingAction {
|
||||
return !nodes.isEmpty();
|
||||
}
|
||||
|
||||
private boolean hasSingleModifiableSourceArchive(List<GTreeNode> nodes) {
|
||||
private Archive getSingleDTArchive(List<GTreeNode> nodes) {
|
||||
|
||||
Archive sourceArchive = null;
|
||||
Archive dtArchive = null;
|
||||
for (GTreeNode node : nodes) {
|
||||
Archive archive = findArchive(node);
|
||||
if (sourceArchive == null) {
|
||||
sourceArchive = archive;
|
||||
if (dtArchive == null) {
|
||||
dtArchive = archive;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sourceArchive != archive) {
|
||||
return false;
|
||||
if (dtArchive != archive) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceArchive != null && sourceArchive.isModifiable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return dtArchive;
|
||||
}
|
||||
|
||||
private static Archive findArchive(GTreeNode node) {
|
||||
@ -134,13 +130,20 @@ public class AssociateDataTypeAction extends DockingAction {
|
||||
|
||||
List<GTreeNode> nodes = ((DataTypesActionContext) context).getSelectedNodes();
|
||||
|
||||
if (!hasSingleModifiableSourceArchive(nodes)) {
|
||||
Msg.showInfo(this, getProviderComponent(), "Multiple Source Archives",
|
||||
Archive dtArchive = getSingleDTArchive(nodes);
|
||||
if (dtArchive == null) {
|
||||
Msg.showInfo(this, getProviderComponent(), "Multiple Data Type Archives",
|
||||
"The currently selected nodes are from multiple archives.\n" +
|
||||
"Please select only nodes from a single archvie.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dtArchive.isModifiable()) {
|
||||
DataTypeUtils.showUnmodifiableArchiveErrorMessage(context.getSourceComponent(),
|
||||
"Disassociate Failed", dtArchive.getDataTypeManager());
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAlreadyAssociated((DataTypesActionContext) context)) {
|
||||
Msg.showInfo(this, getProviderComponent(), "Already Associated",
|
||||
"One or more of the currently selected nodes are already associated\n" +
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -76,7 +76,7 @@ public class DataTypeManagerHandler {
|
||||
private DataTreeDialog dataTreeSaveDialog;
|
||||
private CreateDataTypeArchiveDataTreeDialog dataTreeCreateDialog;
|
||||
private boolean treeDialogCancelled = false;
|
||||
private DomainFileFilter domainFileFilter;
|
||||
private DomainFileFilter createArchiveFileFilter;
|
||||
|
||||
private DataTypeIndexer dataTypeIndexer;
|
||||
private List<ArchiveManagerListener> archiveManagerlisteners = new ArrayList<>();
|
||||
@ -102,9 +102,17 @@ public class DataTypeManagerHandler {
|
||||
dataTypeIndexer.addDataTypeManager(builtInDataTypesManager);
|
||||
openArchives.add(new BuiltInArchive(this, builtInDataTypesManager));
|
||||
|
||||
domainFileFilter = f -> {
|
||||
Class<?> c = f.getDomainObjectClass();
|
||||
return DataTypeArchive.class.isAssignableFrom(c);
|
||||
createArchiveFileFilter = new DomainFileFilter() {
|
||||
|
||||
@Override
|
||||
public boolean accept(DomainFile df) {
|
||||
return DataTypeArchive.class.isAssignableFrom(df.getDomainObjectClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean followLinkedFolders() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
folderListener = new MyFolderListener();
|
||||
@ -1425,7 +1433,7 @@ public class DataTypeManagerHandler {
|
||||
}
|
||||
};
|
||||
dataTreeSaveDialog =
|
||||
new DataTreeDialog(null, "Save As", DataTreeDialog.SAVE, domainFileFilter);
|
||||
new DataTreeDialog(null, "Save As", DataTreeDialog.SAVE, createArchiveFileFilter);
|
||||
|
||||
dataTreeSaveDialog.addOkActionListener(listener);
|
||||
dataTreeSaveDialog
|
||||
@ -1465,7 +1473,7 @@ public class DataTypeManagerHandler {
|
||||
};
|
||||
|
||||
dataTreeCreateDialog = new CreateDataTypeArchiveDataTreeDialog(null, "Create",
|
||||
DataTreeDialog.CREATE, domainFileFilter);
|
||||
DataTreeDialog.CREATE, createArchiveFileFilter);
|
||||
|
||||
dataTreeCreateDialog.addOkActionListener(listener);
|
||||
dataTreeCreateDialog.setHelpLocation(
|
||||
|
@ -24,6 +24,7 @@ import javax.swing.Icon;
|
||||
import generic.theme.GColor;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.app.services.DataTypeQueryService;
|
||||
import ghidra.program.database.data.ProjectDataTypeManager;
|
||||
import ghidra.program.model.data.*;
|
||||
import ghidra.program.model.data.Enum;
|
||||
import ghidra.util.Msg;
|
||||
@ -438,9 +439,18 @@ public class DataTypeUtils {
|
||||
msg = "The archive file is not modifiable!\nYou must open the archive for editing\n" +
|
||||
"before performing this operation.\n" + dtm.getName();
|
||||
}
|
||||
else if (dtm instanceof ProjectDataTypeManager) {
|
||||
ProjectDataTypeManager projectDtm = (ProjectDataTypeManager) dtm;
|
||||
if (!projectDtm.isUpdatable() && !projectDtm.getDomainFile().canCheckout()) {
|
||||
msg = "The project archive is not modifiable!\n" + dtm.getName();
|
||||
}
|
||||
else {
|
||||
msg = "The project archive is not modifiable!\nYou must check out the archive\n" +
|
||||
"before performing this operation.\n" + dtm.getName();
|
||||
}
|
||||
}
|
||||
else {
|
||||
msg = "The project archive is not modifiable!\nYou must check out the archive\n" +
|
||||
"before performing this operation.\n" + dtm.getName();
|
||||
msg = "The Archive is not modifiable!\n";
|
||||
}
|
||||
Msg.showInfo(DataTypeUtils.class, parent, title, msg);
|
||||
}
|
||||
|
@ -295,11 +295,15 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
|
||||
return comboBox;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Exporter> getApplicableExporters() {
|
||||
List<Exporter> list = new ArrayList<>(ClassSearcher.getInstances(Exporter.class));
|
||||
Class<? extends DomainObject> domainObjectClass = domainFile.getDomainObjectClass();
|
||||
list.removeIf(exporter -> !exporter.canExportDomainObject(domainObjectClass));
|
||||
Collections.sort(list, (o1, o2) -> o1.toString().compareTo(o2.toString()));
|
||||
Class<?> domainObjectClass = domainFile.getDomainObjectClass();
|
||||
if (DomainObject.class.isAssignableFrom(domainObjectClass)) {
|
||||
list.removeIf(exporter -> !exporter
|
||||
.canExportDomainObject((Class<? extends DomainObject>) domainObjectClass));
|
||||
Collections.sort(list, (o1, o2) -> o1.toString().compareTo(o2.toString()));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@ -422,8 +426,16 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa
|
||||
|
||||
private void doOpenFile(TaskMonitor monitor) {
|
||||
try {
|
||||
domainObject = domainFile.getImmutableDomainObject(this, DomainFile.DEFAULT_VERSION,
|
||||
TaskMonitor.DUMMY);
|
||||
if (domainFile.isLinkFile()) {
|
||||
// Linked files are always subject to upgrade if needed and do not support
|
||||
// getImmutableDomainObject
|
||||
domainObject =
|
||||
domainFile.getReadOnlyDomainObject(this, DomainFile.DEFAULT_VERSION, monitor);
|
||||
}
|
||||
else {
|
||||
domainObject =
|
||||
domainFile.getImmutableDomainObject(this, DomainFile.DEFAULT_VERSION, monitor);
|
||||
}
|
||||
}
|
||||
catch (VersionException | CancelledException | IOException e) {
|
||||
Msg.showError(this, getComponent(), "Error Opening File",
|
||||
|
@ -147,7 +147,8 @@ public class MyProgramChangesDisplayPlugin extends ProgramPlugin implements Doma
|
||||
|
||||
@Override
|
||||
public boolean isEnabledForContext(ActionContext context) {
|
||||
return currentProgram != null && currentProgram.getDomainFile().canCheckin();
|
||||
return currentProgram != null &&
|
||||
currentProgram.getDomainFile().modifiedSinceCheckout();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -26,8 +26,8 @@ import docking.action.MenuData;
|
||||
import docking.widgets.OptionDialog;
|
||||
import ghidra.app.CorePluginPackage;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.framework.main.FrontEndTool;
|
||||
import ghidra.framework.main.ApplicationLevelPlugin;
|
||||
import ghidra.framework.main.FrontEndTool;
|
||||
import ghidra.framework.main.datatable.ProjectDataContext;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.plugintool.*;
|
||||
@ -316,18 +316,20 @@ public final class LanguageProviderPlugin extends Plugin implements ApplicationL
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
ToolServices toolServices = tool.getToolServices();
|
||||
String defaultToolName = toolServices.getDefaultToolTemplate(file).getName();
|
||||
for (PluginTool t : toolServices.getRunningTools()) {
|
||||
if (t.getName().equals(defaultToolName)) {
|
||||
openTool = t;
|
||||
break;
|
||||
ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(file);
|
||||
if (defaultToolTemplate != null) {
|
||||
String defaultToolName = defaultToolTemplate.getName();
|
||||
for (PluginTool t : toolServices.getRunningTools()) {
|
||||
if (t.getName().equals(defaultToolName)) {
|
||||
openTool = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (openTool != null) {
|
||||
openTool.acceptDomainFiles(new DomainFile[] { file });
|
||||
}
|
||||
else {
|
||||
openTool = tool.getToolServices().launchDefaultTool(file);
|
||||
if (openTool == null ||
|
||||
!openTool.acceptDomainFiles(new DomainFile[] { file })) {
|
||||
Msg.showError(this, tool.getToolFrame(), "Failed to Open Program",
|
||||
"A suitable default tool could not found!");
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -336,7 +338,7 @@ public final class LanguageProviderPlugin extends Plugin implements ApplicationL
|
||||
}
|
||||
catch (InvocationTargetException e) {
|
||||
Throwable t = e.getCause();
|
||||
Msg.showError(this, tool.getToolFrame(), "Tool Launch Failed",
|
||||
Msg.showError(this, tool.getToolFrame(), "Failed to Open Program",
|
||||
"An error occurred while attempting to launch your default tool!", t);
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import ghidra.app.events.*;
|
||||
import ghidra.app.nav.Navigatable;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.app.util.task.OpenProgramTask;
|
||||
import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
|
||||
import ghidra.framework.data.DomainObjectAdapterDB;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
@ -42,9 +43,6 @@ import ghidra.util.task.TaskLauncher;
|
||||
|
||||
class MultiProgramManager implements DomainObjectListener, TransactionListener {
|
||||
|
||||
// arbitrary counter for given ProgramInfo objects and ID to use for sorting
|
||||
private static final AtomicInteger nextAvailableId = new AtomicInteger();
|
||||
|
||||
private ProgramManagerPlugin plugin;
|
||||
private PluginTool tool;
|
||||
private ProgramInfo currentInfo;
|
||||
@ -82,25 +80,31 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
|
||||
};
|
||||
}
|
||||
|
||||
void addProgram(Program p, URL ghidraURL, int state) {
|
||||
void addProgram(Program p, DomainFile domainFile, int state) {
|
||||
addProgram(new ProgramInfo(p, domainFile, state != ProgramManager.OPEN_HIDDEN), state);
|
||||
}
|
||||
|
||||
void addProgram(Program p, URL ghidraUrl, int state) {
|
||||
addProgram(new ProgramInfo(p, ghidraUrl, state != ProgramManager.OPEN_HIDDEN), state);
|
||||
}
|
||||
|
||||
private void addProgram(ProgramInfo programInfo, int state) {
|
||||
Program p = programInfo.program;
|
||||
ProgramInfo oldInfo = getInfo(p);
|
||||
if (oldInfo == null) {
|
||||
oldInfo = programInfo;
|
||||
p.addConsumer(tool);
|
||||
ProgramInfo info = new ProgramInfo(p, state != ProgramManager.OPEN_HIDDEN);
|
||||
info.ghidraURL = ghidraURL;
|
||||
openPrograms.add(info);
|
||||
openPrograms.add(oldInfo);
|
||||
openPrograms.sort(Comparator.naturalOrder());
|
||||
programMap.put(p, info);
|
||||
programMap.put(p, oldInfo);
|
||||
|
||||
fireOpenEvents(p);
|
||||
|
||||
p.addListener(this);
|
||||
p.addTransactionListener(this);
|
||||
}
|
||||
else {
|
||||
if (!oldInfo.visible && state != ProgramManager.OPEN_HIDDEN) {
|
||||
oldInfo.setVisible(true);
|
||||
}
|
||||
else if (!oldInfo.visible && state != ProgramManager.OPEN_HIDDEN) {
|
||||
oldInfo.setVisible(true);
|
||||
}
|
||||
if (state == ProgramManager.OPEN_CURRENT) {
|
||||
saveLocation();
|
||||
@ -246,12 +250,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
|
||||
TransientToolState toolState = null;
|
||||
if (currentInfo != null) {
|
||||
currentInfo.setVisible(true);
|
||||
DomainFile df = currentInfo.program.getDomainFile();
|
||||
String title = df.toString();
|
||||
if (df.isReadOnly()) {
|
||||
title = title + " [Read-Only]";
|
||||
}
|
||||
tool.setSubTitle(title);
|
||||
tool.setSubTitle(currentInfo.toString());
|
||||
txMonitor.setProgram(currentInfo.program);
|
||||
if (currentInfo.lastState != null) {
|
||||
toolState = currentInfo.lastState;
|
||||
@ -370,7 +369,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
|
||||
return (info != null && info.owner != null);
|
||||
}
|
||||
|
||||
private ProgramInfo getInfo(Program p) {
|
||||
ProgramInfo getInfo(Program p) {
|
||||
if (p == null) {
|
||||
return null;
|
||||
}
|
||||
@ -378,9 +377,6 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
|
||||
}
|
||||
|
||||
Program getOpenProgram(URL ghidraURL) {
|
||||
if (!GhidraURL.isServerRepositoryURL(ghidraURL)) {
|
||||
return null;
|
||||
}
|
||||
URL normalizedURL = GhidraURL.getNormalizedURL(ghidraURL);
|
||||
for (ProgramInfo info : programMap.values()) {
|
||||
URL url = info.ghidraURL;
|
||||
@ -392,10 +388,10 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
|
||||
}
|
||||
|
||||
Program getOpenProgram(DomainFile domainFile, int version) {
|
||||
for (Program program : programMap.keySet()) {
|
||||
DomainFile programDomainFile = program.getDomainFile();
|
||||
if (filesMatch(domainFile, version, programDomainFile)) {
|
||||
return program;
|
||||
for (ProgramInfo info : programMap.values()) {
|
||||
DomainFile df = info.domainFile;
|
||||
if (df != null && filesMatch(domainFile, version, df)) {
|
||||
return info.program;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@ -413,7 +409,7 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
|
||||
if (!SystemUtilities.isEqual(file1.getProjectLocator(), file2.getProjectLocator())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: version check is questionable - unclear how proxy file would work
|
||||
int openVersion = file2.isReadOnly() ? file2.getVersion() : -1;
|
||||
return version == openVersion;
|
||||
}
|
||||
@ -488,28 +484,54 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
|
||||
OpenProgramTask openTask = new OpenProgramTask(file, -1, this);
|
||||
openTask.setSilent();
|
||||
new TaskLauncher(openTask, tool.getToolFrame());
|
||||
Program openProgram = openTask.getOpenProgram();
|
||||
plugin.openProgram(openProgram,
|
||||
dataState != null ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE);
|
||||
openProgram.release(this);
|
||||
removeProgram((Program) oldObject);
|
||||
if (dataState != null) {
|
||||
tool.restoreDataStateFromXml(dataState);
|
||||
OpenProgramRequest openProgramReq = openTask.getOpenProgram();
|
||||
if (openProgramReq != null) {
|
||||
plugin.openProgram(openProgramReq.getProgram(),
|
||||
dataState != null ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE);
|
||||
openProgramReq.release();
|
||||
removeProgram((Program) oldObject);
|
||||
if (dataState != null) {
|
||||
tool.restoreDataStateFromXml(dataState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ProgramInfo implements Comparable<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 int instance;
|
||||
private boolean visible;
|
||||
private boolean visible = false;
|
||||
private Object owner;
|
||||
|
||||
ProgramInfo(Program p, boolean visible) {
|
||||
private String str; // cached toString
|
||||
|
||||
ProgramInfo(Program p, DomainFile domainFile, boolean visible) {
|
||||
this.program = p;
|
||||
this.domainFile = domainFile;
|
||||
this.ghidraURL = null;
|
||||
this.visible = visible;
|
||||
instance = nextAvailableId.incrementAndGet();
|
||||
}
|
||||
|
||||
ProgramInfo(Program p, URL ghidraURL, boolean visible) {
|
||||
this.program = p;
|
||||
this.domainFile = null;
|
||||
this.ghidraURL = ghidraURL;
|
||||
this.visible = visible;
|
||||
instance = nextAvailableId.incrementAndGet();
|
||||
}
|
||||
@ -523,5 +545,24 @@ class MultiProgramManager implements DomainObjectListener, TransactionListener {
|
||||
public int compareTo(ProgramInfo info) {
|
||||
return instance - info.instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (str != null) {
|
||||
return str;
|
||||
}
|
||||
StringBuilder buf = new StringBuilder();
|
||||
DomainFile df = program.getDomainFile();
|
||||
if (domainFile != null && domainFile.isLinkFile()) {
|
||||
buf.append(domainFile.getName());
|
||||
buf.append("->");
|
||||
}
|
||||
buf.append(df.toString());
|
||||
if (df.isReadOnly()) {
|
||||
buf.append(" [Read-Only]");
|
||||
}
|
||||
str = buf.toString();
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ package ghidra.app.plugin.core.progmgr;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.beans.PropertyEditor;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
@ -32,26 +31,26 @@ import ghidra.app.CorePluginPackage;
|
||||
import ghidra.app.context.ProgramActionContext;
|
||||
import ghidra.app.events.*;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.plugin.core.progmgr.MultiProgramManager.ProgramInfo;
|
||||
import ghidra.app.services.ProgramManager;
|
||||
import ghidra.app.util.HelpTopics;
|
||||
import ghidra.app.util.NamespaceUtils;
|
||||
import ghidra.app.util.task.OpenProgramTask;
|
||||
import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
|
||||
import ghidra.framework.client.ClientUtil;
|
||||
import ghidra.framework.data.ProjectFileManager;
|
||||
import ghidra.framework.main.OpenVersionedFileDialog;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.options.*;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.framework.protocol.ghidra.*;
|
||||
import ghidra.program.database.ProgramContentHandler;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURL;
|
||||
import ghidra.program.model.address.*;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.program.model.symbol.SymbolType;
|
||||
import ghidra.program.util.*;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.NotFoundException;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.task.TaskLauncher;
|
||||
|
||||
//@formatter:off
|
||||
@ -121,16 +120,21 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
if (domainFile == null) {
|
||||
continue;
|
||||
}
|
||||
if (!(Program.class.isAssignableFrom(domainFile.getDomainObjectClass()))) {
|
||||
continue;
|
||||
Class<? extends DomainObject> domainObjectClass = domainFile.getDomainObjectClass();
|
||||
if (Program.class.isAssignableFrom(domainObjectClass)) {
|
||||
filesToOpen.add(domainFile);
|
||||
}
|
||||
filesToOpen.add(domainFile);
|
||||
}
|
||||
openPrograms(filesToOpen);
|
||||
|
||||
return !filesToOpen.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(URL url) {
|
||||
return openProgram(url, OPEN_CURRENT) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?>[] getSupportedDataTypes() {
|
||||
return new Class[] { Program.class };
|
||||
@ -144,89 +148,53 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Swing.runNow(() -> doOpenProgram(ghidraURL, state));
|
||||
}
|
||||
|
||||
private void messageBadProgramURL(URL ghidraURL) {
|
||||
Msg.showError(this, null, "Invalid Ghidra URL",
|
||||
"Ghidra URL does not reference a Ghidra Program: " + ghidraURL);
|
||||
}
|
||||
|
||||
protected Program doOpenProgram(URL ghidraURL, int openState) {
|
||||
if (!GhidraURL.isServerRepositoryURL(ghidraURL)) {
|
||||
Msg.showError(this, null, "Invalid Ghidra URL",
|
||||
"Ghidra URL does not reference a Ghidra Program: " + ghidraURL);
|
||||
return null;
|
||||
}
|
||||
Program openProgram = programMgr.getOpenProgram(ghidraURL);
|
||||
if (openProgram != null) {
|
||||
programMgr.addProgram(openProgram, GhidraURL.getNormalizedURL(ghidraURL), openState);
|
||||
if (openState == ProgramManager.OPEN_CURRENT) {
|
||||
gotoProgramRef(openProgram, ghidraURL.getRef());
|
||||
// Check for URL already open and re-use
|
||||
URL url = GhidraURL.getNormalizedURL(ghidraURL);
|
||||
Program p = programMgr.getOpenProgram(url);
|
||||
if (p != null) {
|
||||
showProgram(p, url, state);
|
||||
if (state == ProgramManager.OPEN_CURRENT) {
|
||||
gotoProgramRef(p, ghidraURL.getRef());
|
||||
programMgr.saveLocation();
|
||||
}
|
||||
contextChanged();
|
||||
return openProgram;
|
||||
return p;
|
||||
}
|
||||
|
||||
GhidraURLWrappedContent wrappedContent = null;
|
||||
Object content = null;
|
||||
Program program = Swing.runNow(() -> doOpenProgram(ghidraURL, state));
|
||||
|
||||
if (program != null) {
|
||||
Msg.info(this, "Opened program in " + tool.getName() + " tool: " + ghidraURL);
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open GhidraURL which corresponds to {@code ghidra://} remote URLs which correspond to a
|
||||
* repository program file.
|
||||
* @param ghidraURL Ghidra URL which specified Program to be opened which optional ref
|
||||
* @param openState open state
|
||||
* @return program instance of null if open failed
|
||||
*/
|
||||
private Program doOpenProgram(URL ghidraURL, int openState) {
|
||||
Program p = null;
|
||||
try {
|
||||
GhidraURLConnection c = (GhidraURLConnection) ghidraURL.openConnection();
|
||||
Object obj = c.getContent();
|
||||
if (c.getResponseCode() == GhidraURLConnection.GHIDRA_UNAUTHORIZED) {
|
||||
return null; // assume user already notified
|
||||
URL url = GhidraURL.getNormalizedURL(ghidraURL);
|
||||
OpenProgramTask task = new OpenProgramTask(url, this);
|
||||
new TaskLauncher(task, tool.getToolFrame());
|
||||
OpenProgramRequest openProgramReq = task.getOpenProgram();
|
||||
if (openProgramReq != null) {
|
||||
p = openProgramReq.getProgram();
|
||||
showProgram(p, url, openState);
|
||||
openProgramReq.release();
|
||||
}
|
||||
if (!(obj instanceof GhidraURLWrappedContent)) {
|
||||
messageBadProgramURL(ghidraURL);
|
||||
return null;
|
||||
}
|
||||
wrappedContent = (GhidraURLWrappedContent) obj;
|
||||
content = wrappedContent.getContent(this);
|
||||
if (!(content instanceof DomainFile)) {
|
||||
messageBadProgramURL(ghidraURL);
|
||||
return null;
|
||||
}
|
||||
DomainFile df = (DomainFile) content;
|
||||
if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(df.getContentType())) {
|
||||
messageBadProgramURL(ghidraURL);
|
||||
return null;
|
||||
}
|
||||
|
||||
OpenProgramTask task = new OpenProgramTask(df, true, this);
|
||||
TaskLauncher.launch(task);
|
||||
|
||||
openProgram = task.getOpenProgram();
|
||||
if (openProgram == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
programMgr.addProgram(openProgram, GhidraURL.getNormalizedURL(ghidraURL), openState);
|
||||
contextChanged();
|
||||
openProgram.release(this);
|
||||
if (openState == ProgramManager.OPEN_CURRENT) {
|
||||
gotoProgramRef(openProgram, ghidraURL.getRef());
|
||||
programMgr.saveLocation();
|
||||
}
|
||||
return openProgram;
|
||||
}
|
||||
catch (NotFoundException e) {
|
||||
messageBadProgramURL(ghidraURL);
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
Msg.showError(this, null, "Invalid Ghidra URL",
|
||||
"Improperly formed Ghidra URL: " + ghidraURL);
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.showError(this, null, "Program Open Failed",
|
||||
"Failed to open Ghidra URL: " + e.getMessage());
|
||||
}
|
||||
finally {
|
||||
if (content != null) {
|
||||
wrappedContent.release(content, this);
|
||||
if (p != null && openState == ProgramManager.OPEN_CURRENT) {
|
||||
gotoProgramRef(p, ghidraURL.getRef());
|
||||
programMgr.saveLocation();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return p;
|
||||
}
|
||||
|
||||
private boolean gotoProgramRef(Program program, String ref) {
|
||||
@ -296,9 +264,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
}
|
||||
|
||||
Program program = Swing.runNow(() -> {
|
||||
Program p = doOpenProgram(domainFile, version, state);
|
||||
contextChanged();
|
||||
return p;
|
||||
return doOpenProgram(domainFile, version, state);
|
||||
});
|
||||
|
||||
if (program != null) {
|
||||
@ -460,13 +426,39 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
|
||||
@Override
|
||||
public void openProgram(final Program program, final int state) {
|
||||
showProgram(program, program.getDomainFile(), state);
|
||||
}
|
||||
|
||||
private void showProgram(Program p, URL ghidraUrl, final int state) {
|
||||
if (p == null || p.isClosed()) {
|
||||
throw new AssertException("Opened program required");
|
||||
}
|
||||
if (locked) {
|
||||
throw new IllegalStateException(
|
||||
"Progam manager is locked and cannot accept a new program");
|
||||
}
|
||||
|
||||
Runnable r = () -> {
|
||||
programMgr.addProgram(program, null, state);
|
||||
programMgr.addProgram(p, ghidraUrl, state);
|
||||
if (state == ProgramManager.OPEN_CURRENT) {
|
||||
programMgr.saveLocation();
|
||||
}
|
||||
contextChanged();
|
||||
};
|
||||
Swing.runNow(r);
|
||||
}
|
||||
|
||||
private void showProgram(Program p, DomainFile domainFile, final int state) {
|
||||
if (p == null || p.isClosed()) {
|
||||
throw new AssertException("Opened program required");
|
||||
}
|
||||
if (locked) {
|
||||
throw new IllegalStateException(
|
||||
"Progam manager is locked and cannot accept a new program");
|
||||
}
|
||||
|
||||
Runnable r = () -> {
|
||||
programMgr.addProgram(p, domainFile, state);
|
||||
if (state == ProgramManager.OPEN_CURRENT) {
|
||||
programMgr.saveLocation();
|
||||
}
|
||||
@ -625,11 +617,9 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
doOpenProgram(domainFile, version, OPEN_CURRENT);
|
||||
}
|
||||
};
|
||||
DomainFileFilter filter = f -> {
|
||||
Class<?> c = f.getDomainObjectClass();
|
||||
return Program.class.isAssignableFrom(c);
|
||||
};
|
||||
openDialog = new OpenVersionedFileDialog(tool, "Open Program", filter);
|
||||
openDialog = new OpenVersionedFileDialog(tool, "Open Program", f -> {
|
||||
return Program.class.isAssignableFrom(f.getDomainObjectClass());
|
||||
});
|
||||
openDialog.setHelpLocation(new HelpLocation(HelpTopics.PROGRAM, "Open_File_Dialog"));
|
||||
openDialog.addOkActionListener(listener);
|
||||
}
|
||||
@ -638,9 +628,12 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
}
|
||||
|
||||
public void openPrograms(List<DomainFile> filesToOpen) {
|
||||
Program showIfNeeded = null;
|
||||
OpenProgramTask openTask = null;
|
||||
for (DomainFile domainFile : filesToOpen) {
|
||||
if (programMgr.getOpenProgram(domainFile, -1) != null) {
|
||||
Program p = programMgr.getOpenProgram(domainFile, -1);
|
||||
if (p != null) {
|
||||
showIfNeeded = p;
|
||||
continue;
|
||||
}
|
||||
if (openTask == null) {
|
||||
@ -652,32 +645,37 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
}
|
||||
if (openTask != null) {
|
||||
new TaskLauncher(openTask, tool.getToolFrame());
|
||||
List<Program> openPrograms = openTask.getOpenPrograms();
|
||||
|
||||
for (Program program : openPrograms) {
|
||||
openProgram(program, OPEN_VISIBLE);
|
||||
program.release(this);
|
||||
}
|
||||
if (!openPrograms.isEmpty()) {
|
||||
openProgram(openPrograms.get(0), OPEN_CURRENT);
|
||||
List<OpenProgramRequest> openProgramReqs = openTask.getOpenPrograms();
|
||||
boolean isFirst = true;
|
||||
for (OpenProgramRequest programReq : openProgramReqs) {
|
||||
showProgram(programReq.getProgram(), programReq.getDomainFile(),
|
||||
isFirst ? OPEN_CURRENT : OPEN_VISIBLE);
|
||||
programReq.release();
|
||||
isFirst = false;
|
||||
showIfNeeded = null;
|
||||
}
|
||||
}
|
||||
if (showIfNeeded != null) {
|
||||
showProgram(showIfNeeded, showIfNeeded.getDomainFile(), OPEN_CURRENT);
|
||||
}
|
||||
}
|
||||
|
||||
protected Program doOpenProgram(DomainFile domainFile, int version, int openState) {
|
||||
Program openProgram = programMgr.getOpenProgram(domainFile, version);
|
||||
if (openProgram != null) {
|
||||
openProgram(openProgram, openState);
|
||||
return openProgram;
|
||||
Program p = programMgr.getOpenProgram(domainFile, version);
|
||||
if (p != null) {
|
||||
openProgram(p, openState);
|
||||
}
|
||||
OpenProgramTask task = new OpenProgramTask(domainFile, version, this);
|
||||
new TaskLauncher(task, tool.getToolFrame());
|
||||
openProgram = task.getOpenProgram();
|
||||
if (openProgram != null) {
|
||||
openProgram(openProgram, openState);
|
||||
openProgram.release(this);
|
||||
else {
|
||||
OpenProgramTask task = new OpenProgramTask(domainFile, version, this);
|
||||
new TaskLauncher(task, tool.getToolFrame());
|
||||
OpenProgramRequest programReq = task.getOpenProgram();
|
||||
if (programReq != null) {
|
||||
p = programReq.getProgram();
|
||||
showProgram(p, programReq.getDomainFile(), openState);
|
||||
programReq.release();
|
||||
}
|
||||
}
|
||||
return openProgram;
|
||||
return p;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -705,18 +703,20 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
*/
|
||||
@Override
|
||||
public void writeDataState(SaveState saveState) {
|
||||
// Only remember programs from non-transient projects
|
||||
ArrayList<Program> programs = new ArrayList<>();
|
||||
|
||||
ArrayList<ProgramInfo> programInfos = new ArrayList<>();
|
||||
for (Program p : programMgr.getAllPrograms()) {
|
||||
ProjectLocator projectLocator = p.getDomainFile().getProjectLocator();
|
||||
if (projectLocator != null && !projectLocator.isTransient()) {
|
||||
programs.add(p);
|
||||
ProgramInfo info = programMgr.getInfo(p);
|
||||
if (info != null) {
|
||||
programInfos.add(info);
|
||||
}
|
||||
}
|
||||
saveState.putInt("NUM_PROGRAMS", programs.size());
|
||||
|
||||
saveState.putInt("NUM_PROGRAMS", programInfos.size());
|
||||
|
||||
int i = 0;
|
||||
for (Program p : programs) {
|
||||
writeProgramInfo(p, saveState, i++);
|
||||
for (ProgramInfo programInfo : programInfos) {
|
||||
writeProgramInfo(programInfo, saveState, i++);
|
||||
}
|
||||
Program p = programMgr.getCurrentProgram();
|
||||
if (p != null) {
|
||||
@ -768,13 +768,21 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeProgramInfo(Program program, SaveState saveState, int index) {
|
||||
private void writeProgramInfo(ProgramInfo programInfo, SaveState saveState, int index) {
|
||||
if (locked) {
|
||||
return; // do not save state when locked.
|
||||
}
|
||||
|
||||
if (programInfo.ghidraURL != null) {
|
||||
saveState.putString("URL_" + index, programInfo.ghidraURL.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
String projectLocation = null;
|
||||
String projectName = null;
|
||||
String path = null;
|
||||
|
||||
Program program = programInfo.program;
|
||||
DomainFile df = program.getDomainFile();
|
||||
ProjectLocator projectLocator = df.getProjectLocator();
|
||||
if (projectLocator != null && !projectLocator.isTransient()) {
|
||||
@ -797,28 +805,30 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
* Read in my data state.
|
||||
*/
|
||||
private void loadPrograms(SaveState saveState) {
|
||||
|
||||
int n = saveState.getInt("NUM_PROGRAMS", 0);
|
||||
if (n == 0) {
|
||||
return;
|
||||
}
|
||||
OpenProgramTask openTask = null;
|
||||
OpenProgramTask openTask = new OpenProgramTask(this);
|
||||
|
||||
for (int index = 0; index < n; index++) {
|
||||
|
||||
URL url = getGhidraURL(saveState, index);
|
||||
if (url != null) {
|
||||
openTask.addProgramToOpen(url);
|
||||
continue;
|
||||
}
|
||||
|
||||
DomainFile domainFile = getDomainFile(saveState, index);
|
||||
if (domainFile == null) {
|
||||
continue;
|
||||
}
|
||||
int version = getVersion(saveState, index);
|
||||
|
||||
if (openTask == null) {
|
||||
openTask = new OpenProgramTask(domainFile, version, this);
|
||||
}
|
||||
else {
|
||||
openTask.addProgramToOpen(domainFile, version);
|
||||
}
|
||||
openTask.addProgramToOpen(domainFile, version);
|
||||
}
|
||||
|
||||
if (openTask == null) {
|
||||
if (!openTask.hasOpenProgramRequests()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -835,10 +845,29 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
"Can't open program", e);
|
||||
}
|
||||
|
||||
List<Program> openPrograms = openTask.getOpenPrograms();
|
||||
for (Program program : openPrograms) {
|
||||
openProgram(program, OPEN_VISIBLE);
|
||||
program.release(this);
|
||||
List<OpenProgramRequest> openProgramReqs = openTask.getOpenPrograms();
|
||||
for (OpenProgramRequest programReq : openProgramReqs) {
|
||||
DomainFile df = programReq.getDomainFile();
|
||||
if (df != null) {
|
||||
showProgram(programReq.getProgram(), df, OPEN_VISIBLE);
|
||||
}
|
||||
else {
|
||||
showProgram(programReq.getProgram(), programReq.getGhidraURL(), OPEN_VISIBLE);
|
||||
}
|
||||
programReq.release();
|
||||
}
|
||||
}
|
||||
|
||||
private URL getGhidraURL(SaveState saveState, int index) {
|
||||
String url = saveState.getString("URL_" + index, null);
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new URL(url);
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -853,20 +882,7 @@ public class ProgramManagerPlugin extends Plugin implements ProgramManager {
|
||||
|
||||
ProjectData projectData = tool.getProject().getProjectData(projectLocator);
|
||||
if (projectData == null) {
|
||||
// Viewed project not available
|
||||
try {
|
||||
projectData = new ProjectFileManager(projectLocator, false, false);
|
||||
}
|
||||
catch (NotOwnerException e) {
|
||||
Msg.showError(this, tool.getToolFrame(), "Program Open Failed",
|
||||
"Not project owner: " + projectLocator + "(" + pathname + ")");
|
||||
return null;
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.showError(this, tool.getToolFrame(), "Program Open Failed",
|
||||
"Project error: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DomainFile df = projectData.getFile(pathname);
|
||||
|
@ -45,9 +45,17 @@ class ProgramSaveManager {
|
||||
ProgramSaveManager(PluginTool tool, ProgramManager programMgr) {
|
||||
this.tool = tool;
|
||||
this.programMgr = programMgr;
|
||||
domainFileFilter = f -> {
|
||||
Class<?> c = f.getDomainObjectClass();
|
||||
return Program.class.isAssignableFrom(c);
|
||||
domainFileFilter = new DomainFileFilter() {
|
||||
|
||||
@Override
|
||||
public boolean accept(DomainFile df) {
|
||||
return Program.class.isAssignableFrom(df.getDomainObjectClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean followLinkedFolders() {
|
||||
return false; // can't save to linked-folder (read-only)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -244,7 +252,8 @@ class ProgramSaveManager {
|
||||
return;
|
||||
}
|
||||
if (existingFile != null) {
|
||||
String msg = "Program " + name + " already exists.\n" + "Do you want to overwrite it?";
|
||||
String msg = existingFile.getContentType() + " file " + name + " already exists.\n" +
|
||||
"Do you want to overwrite it?";
|
||||
if (OptionDialog.showOptionDialog(tool.getToolFrame(), "Duplicate Name", msg,
|
||||
"Overwrite", OptionDialog.QUESTION_MESSAGE) == OptionDialog.CANCEL_OPTION) {
|
||||
return;
|
||||
|
@ -312,7 +312,7 @@ public class HeadlessAnalyzer {
|
||||
Object obj = c.getContent();
|
||||
if (!(obj instanceof GhidraURLWrappedContent)) {
|
||||
throw new IOException(
|
||||
"Connect to repository folder failed. Response code: " + c.getResponseCode());
|
||||
"Connect to repository folder failed. Response code: " + c.getStatusCode());
|
||||
}
|
||||
GhidraURLWrappedContent wrappedContent = (GhidraURLWrappedContent) obj;
|
||||
Object content = null;
|
||||
@ -336,7 +336,7 @@ public class HeadlessAnalyzer {
|
||||
processWithImport(folder.getPathname(), filesToImport);
|
||||
}
|
||||
}
|
||||
catch (NotFoundException e) {
|
||||
catch (FileNotFoundException e) {
|
||||
throw new IOException("Connect to repository folder failed");
|
||||
}
|
||||
finally {
|
||||
@ -369,7 +369,8 @@ public class HeadlessAnalyzer {
|
||||
* @param rootFolderPath root folder for imports
|
||||
* @param filesToImport directories and files to be imported (null or empty is acceptable if
|
||||
* we are in -process mode)
|
||||
* @throws IOException if there was an IO-related problem
|
||||
* @throws IOException if there was an IO-related problem. If caused by a failure to obtain a
|
||||
* write-lock on the project the exception cause will a {@code LockException}.
|
||||
*/
|
||||
public void processLocal(String projectLocation, String projectName, String rootFolderPath,
|
||||
List<File> filesToImport) throws IOException {
|
||||
@ -1107,6 +1108,8 @@ public class HeadlessAnalyzer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this.
|
||||
if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) {
|
||||
return; // skip non-Program files
|
||||
}
|
||||
@ -1275,6 +1278,8 @@ public class HeadlessAnalyzer {
|
||||
|
||||
for (DomainFile domFile : parentFolder.getFiles()) {
|
||||
if (filenamePattern == null || filenamePattern.matcher(domFile.getName()).matches()) {
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this.
|
||||
if (ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) {
|
||||
filesProcessed = true;
|
||||
processFileNoImport(domFile);
|
||||
@ -1308,6 +1313,8 @@ public class HeadlessAnalyzer {
|
||||
boolean filesProcessed = false;
|
||||
|
||||
DomainFile domFile = parentFolder.getFile(filename);
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this.
|
||||
if (domFile != null &&
|
||||
ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) {
|
||||
filesProcessed = true;
|
||||
|
@ -16,8 +16,9 @@
|
||||
package ghidra.app.util.task;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
import docking.widgets.OptionDialog;
|
||||
import ghidra.app.util.dialog.CheckoutDialog;
|
||||
@ -25,8 +26,11 @@ import ghidra.framework.client.ClientUtil;
|
||||
import ghidra.framework.client.RepositoryAdapter;
|
||||
import ghidra.framework.main.AppInfo;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.protocol.ghidra.*;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
|
||||
import ghidra.framework.remote.User;
|
||||
import ghidra.framework.store.ExclusiveCheckoutException;
|
||||
import ghidra.program.database.ProgramLinkContentHandler;
|
||||
import ghidra.program.model.lang.LanguageNotFoundException;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.*;
|
||||
@ -37,8 +41,8 @@ import ghidra.util.task.TaskMonitor;
|
||||
|
||||
public class OpenProgramTask extends Task {
|
||||
|
||||
private final List<DomainFileInfo> domainFileInfoList = new ArrayList<>();
|
||||
private List<Program> programList = new ArrayList<>();
|
||||
private final List<OpenProgramRequest> openProgramRequests = new ArrayList<>();
|
||||
private List<OpenProgramRequest> openedProgramList = new ArrayList<>();
|
||||
|
||||
private final Object consumer;
|
||||
private boolean silent; // if true operation does not permit interaction
|
||||
@ -46,11 +50,16 @@ public class OpenProgramTask extends Task {
|
||||
|
||||
private String openPromptText = "Open";
|
||||
|
||||
public OpenProgramTask(Object consumer) {
|
||||
super("Open Program(s)", true, false, true);
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
public OpenProgramTask(DomainFile domainFile, int version, boolean forceReadOnly,
|
||||
Object consumer) {
|
||||
super("Open Program(s)", true, false, true);
|
||||
this.consumer = consumer;
|
||||
domainFileInfoList.add(new DomainFileInfo(domainFile, version, forceReadOnly));
|
||||
openProgramRequests.add(new OpenProgramRequest(domainFile, version, forceReadOnly));
|
||||
}
|
||||
|
||||
public OpenProgramTask(DomainFile domainFile, int version, Object consumer) {
|
||||
@ -65,17 +74,10 @@ public class OpenProgramTask extends Task {
|
||||
this(domainFile, DomainFile.DEFAULT_VERSION, false, consumer);
|
||||
}
|
||||
|
||||
public OpenProgramTask(List<DomainFile> domainFileList, boolean forceReadOnly,
|
||||
Object consumer) {
|
||||
super("Open Program(s)", true, domainFileList.size() > 1, true);
|
||||
public OpenProgramTask(URL ghidraURL, Object consumer) {
|
||||
super("Open Program(s)", true, false, true);
|
||||
this.consumer = consumer;
|
||||
for (DomainFile domainFile : domainFileList) {
|
||||
domainFileInfoList.add(new DomainFileInfo(domainFile, -1, forceReadOnly));
|
||||
}
|
||||
}
|
||||
|
||||
public OpenProgramTask(List<DomainFile> domainFileList, Object consumer) {
|
||||
this(domainFileList, false, consumer);
|
||||
openProgramRequests.add(new OpenProgramRequest(ghidraURL));
|
||||
}
|
||||
|
||||
public void setOpenPromptText(String text) {
|
||||
@ -88,7 +90,16 @@ public class OpenProgramTask extends Task {
|
||||
|
||||
public void addProgramToOpen(DomainFile domainFile, int version, boolean forceReadOnly) {
|
||||
setHasProgress(true);
|
||||
domainFileInfoList.add(new DomainFileInfo(domainFile, version, forceReadOnly));
|
||||
openProgramRequests.add(new OpenProgramRequest(domainFile, version, forceReadOnly));
|
||||
}
|
||||
|
||||
public void addProgramToOpen(URL ghidraURL) {
|
||||
setHasProgress(true);
|
||||
openProgramRequests.add(new OpenProgramRequest(ghidraURL));
|
||||
}
|
||||
|
||||
public boolean hasOpenProgramRequests() {
|
||||
return !openProgramRequests.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,100 +121,97 @@ public class OpenProgramTask extends Task {
|
||||
this.noCheckout = true;
|
||||
}
|
||||
|
||||
public List<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 programList.get(0);
|
||||
return openedProgramList.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(TaskMonitor monitor) {
|
||||
|
||||
taskMonitor.initialize(domainFileInfoList.size());
|
||||
taskMonitor.initialize(openProgramRequests.size());
|
||||
|
||||
for (DomainFileInfo domainFileInfo : domainFileInfoList) {
|
||||
for (OpenProgramRequest domainFileInfo : openProgramRequests) {
|
||||
if (taskMonitor.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
openDomainFile(domainFileInfo);
|
||||
|
||||
domainFileInfo.open();
|
||||
taskMonitor.incrementProgress(1);
|
||||
}
|
||||
}
|
||||
|
||||
private void openDomainFile(DomainFileInfo domainFileInfo) {
|
||||
int version = domainFileInfo.getVersion();
|
||||
DomainFile domainFile = domainFileInfo.getDomainFile();
|
||||
if (version != DomainFile.DEFAULT_VERSION) {
|
||||
openVersionedFile(domainFile, version);
|
||||
}
|
||||
else if (domainFileInfo.isReadOnly()) {
|
||||
openReadOnlyFile(domainFile, version);
|
||||
}
|
||||
else {
|
||||
openUnversionedFile(domainFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void openReadOnlyFile(DomainFile domainFile, int version) {
|
||||
private Object openReadOnlyFile(DomainFile domainFile, URL url, int version) {
|
||||
taskMonitor.setMessage("Opening " + domainFile.getName());
|
||||
openReadOnly(domainFile, version);
|
||||
return openReadOnly(domainFile, url, version);
|
||||
}
|
||||
|
||||
private void openVersionedFile(DomainFile domainFile, int version) {
|
||||
private Object openVersionedFile(DomainFile domainFile, URL url, int version) {
|
||||
taskMonitor.setMessage("Getting Version " + version + " for " + domainFile.getName());
|
||||
openReadOnly(domainFile, version);
|
||||
return openReadOnly(domainFile, url, version);
|
||||
}
|
||||
|
||||
private void openReadOnly(DomainFile domainFile, int version) {
|
||||
String contentType = null;
|
||||
private Object openReadOnly(DomainFile domainFile, URL url, int version) {
|
||||
String contentType = domainFile.getContentType();
|
||||
String path = url != null ? url.toString() : domainFile.getPathname();
|
||||
Object obj = null;
|
||||
try {
|
||||
contentType = domainFile.getContentType();
|
||||
Program program =
|
||||
(Program) domainFile.getReadOnlyDomainObject(consumer, version, taskMonitor);
|
||||
|
||||
if (program == null) {
|
||||
String errorMessage = "Can't open program - \"" + domainFile.getPathname() + "\"";
|
||||
obj = domainFile.getReadOnlyDomainObject(consumer, version, taskMonitor);
|
||||
|
||||
if (obj == null) {
|
||||
String errorMessage = "Can't open " + contentType + " - \"" + path + "\"";
|
||||
if (version != DomainFile.DEFAULT_VERSION) {
|
||||
errorMessage += " version " + version;
|
||||
}
|
||||
|
||||
Msg.showError(this, null, "DomainFile Not Found", errorMessage);
|
||||
}
|
||||
else {
|
||||
programList.add(program);
|
||||
Msg.showError(this, null, "File Not Found", errorMessage);
|
||||
}
|
||||
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
// we don't care, the task has been cancelled
|
||||
}
|
||||
catch (IOException e) {
|
||||
if (domainFile.isInWritableProject()) {
|
||||
if (url == null && domainFile.isInWritableProject()) {
|
||||
ClientUtil.handleException(AppInfo.getActiveProject().getRepository(), e,
|
||||
"Get Versioned Object", null);
|
||||
"Get " + contentType, null);
|
||||
}
|
||||
else if (version != DomainFile.DEFAULT_VERSION) {
|
||||
Msg.showError(this, null, "Error Getting Versioned Program",
|
||||
"Could not get version " + version + " for " + path, e);
|
||||
}
|
||||
else {
|
||||
Msg.showError(this, null, "Error Getting Versioned Object",
|
||||
"Could not get version " + version + " for " + domainFile.getName(), e);
|
||||
Msg.showError(this, null, "Error Getting Program",
|
||||
"Open program failed for " + path, e);
|
||||
}
|
||||
}
|
||||
catch (VersionException e) {
|
||||
VersionExceptionHandler.showVersionError(null, domainFile.getName(), contentType,
|
||||
"Open", e);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
private void openUnversionedFile(DomainFile domainFile) {
|
||||
private Program openUnversionedFile(DomainFile domainFile) {
|
||||
String filename = domainFile.getName();
|
||||
taskMonitor.setMessage("Opening " + filename);
|
||||
performOptionalCheckout(domainFile);
|
||||
try {
|
||||
openFileMaybeUgrade(domainFile);
|
||||
return openFileMaybeUgrade(domainFile);
|
||||
}
|
||||
catch (VersionException e) {
|
||||
String contentType = domainFile.getContentType();
|
||||
@ -226,9 +234,10 @@ public class OpenProgramTask extends Task {
|
||||
"Getting domain object failed.\n" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void openFileMaybeUgrade(DomainFile domainFile)
|
||||
private Program openFileMaybeUgrade(DomainFile domainFile)
|
||||
throws IOException, CancelledException, VersionException {
|
||||
|
||||
boolean recoverFile = false;
|
||||
@ -236,24 +245,18 @@ public class OpenProgramTask extends Task {
|
||||
recoverFile = askRecoverFile(domainFile.getName());
|
||||
}
|
||||
|
||||
Program program = null;
|
||||
try {
|
||||
Program program =
|
||||
program =
|
||||
(Program) domainFile.getDomainObject(consumer, false, recoverFile, taskMonitor);
|
||||
|
||||
if (program != null) {
|
||||
programList.add(program);
|
||||
}
|
||||
|
||||
}
|
||||
catch (VersionException e) {
|
||||
if (VersionExceptionHandler.isUpgradeOK(null, domainFile, openPromptText, e)) {
|
||||
Program program =
|
||||
program =
|
||||
(Program) domainFile.getDomainObject(consumer, true, recoverFile, taskMonitor);
|
||||
if (program != null) {
|
||||
programList.add(program);
|
||||
}
|
||||
}
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
private boolean askRecoverFile(final String filename) {
|
||||
@ -294,32 +297,158 @@ public class OpenProgramTask extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
static class DomainFileInfo {
|
||||
private final DomainFile domainFile;
|
||||
private final int version;
|
||||
private boolean forceReadOnly;
|
||||
public class OpenProgramRequest {
|
||||
|
||||
public DomainFileInfo(DomainFile domainFile, int version, boolean forceReadOnly) {
|
||||
// ghidraURL and domainFile use are mutually exclusive
|
||||
private final URL ghidraURL;
|
||||
private final DomainFile domainFile;
|
||||
|
||||
private URL linkURL; // link URL read from domainFile
|
||||
|
||||
private final int version;
|
||||
private final boolean forceReadOnly;
|
||||
private Program program;
|
||||
|
||||
public OpenProgramRequest(URL ghidraURL) {
|
||||
if (!GhidraURL.PROTOCOL.equals(ghidraURL.getProtocol())) {
|
||||
throw new IllegalArgumentException(
|
||||
"unsupported protocol: " + ghidraURL.getProtocol());
|
||||
}
|
||||
this.ghidraURL = ghidraURL;
|
||||
this.domainFile = null;
|
||||
this.version = -1;
|
||||
this.forceReadOnly = true;
|
||||
}
|
||||
|
||||
public OpenProgramRequest(DomainFile domainFile, int version, boolean forceReadOnly) {
|
||||
this.domainFile = domainFile;
|
||||
this.ghidraURL = null;
|
||||
this.version =
|
||||
(domainFile.isReadOnly() && domainFile.isVersioned()) ? domainFile.getVersion()
|
||||
: version;
|
||||
this.forceReadOnly = forceReadOnly;
|
||||
}
|
||||
|
||||
public boolean isReadOnly() {
|
||||
return forceReadOnly || domainFile.isReadOnly() ||
|
||||
version != DomainFile.DEFAULT_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link DomainFile} which corresponds to program open request. This will be
|
||||
* null for all URL-based open requests.
|
||||
* @return {@link DomainFile} which corresponds to program open request or null.
|
||||
*/
|
||||
public DomainFile getDomainFile() {
|
||||
return domainFile;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
/**
|
||||
* Get the {@link URL} which corresponds to program open request. This will be
|
||||
* null for all non-URL-based open requests. URL will be a {@link GhidraURL}.
|
||||
* @return {@link URL} which corresponds to program open request or null.
|
||||
*/
|
||||
public URL getGhidraURL() {
|
||||
return ghidraURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link URL} which corresponds to the link domainFile used to open a program.
|
||||
* @return {@link URL} which corresponds to the link domainFile used to open a program.
|
||||
*/
|
||||
public URL getLinkURL() {
|
||||
return linkURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the open Program instance which corresponds to this open request.
|
||||
* @return program instance or null if never opened.
|
||||
*/
|
||||
public Program getProgram() {
|
||||
return program;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release opened program. This must be done once, and only once, on a successful
|
||||
* open request. If handing ownership off to another consumer, they should be added
|
||||
* as a program consumer prior to invoking this method. Releasing the last consumer
|
||||
* will close the program instance.
|
||||
*/
|
||||
public void release() {
|
||||
if (program != null) {
|
||||
program.release(consumer);
|
||||
}
|
||||
}
|
||||
|
||||
private Program openProgram(DomainFile df, URL url) {
|
||||
if (version != DomainFile.DEFAULT_VERSION) {
|
||||
return (Program) openVersionedFile(df, url, version);
|
||||
}
|
||||
if (forceReadOnly) {
|
||||
return (Program) openReadOnlyFile(df, url, version);
|
||||
}
|
||||
return openUnversionedFile(df);
|
||||
}
|
||||
|
||||
void open() {
|
||||
DomainFile df = domainFile;
|
||||
URL url = ghidraURL;
|
||||
GhidraURLWrappedContent wrappedContent = null;
|
||||
Object content = null;
|
||||
try {
|
||||
if (df == null && url != null) {
|
||||
GhidraURLConnection c = (GhidraURLConnection) url.openConnection();
|
||||
Object obj = c.getContent(); // read-only access
|
||||
if (c.getStatusCode() == StatusCode.UNAUTHORIZED) {
|
||||
return; // assume user already notified
|
||||
}
|
||||
if (!(obj instanceof GhidraURLWrappedContent)) {
|
||||
messageBadProgramURL(url);
|
||||
return;
|
||||
}
|
||||
wrappedContent = (GhidraURLWrappedContent) obj;
|
||||
content = wrappedContent.getContent(this);
|
||||
if (!(content instanceof DomainFile)) {
|
||||
messageBadProgramURL(url);
|
||||
return;
|
||||
}
|
||||
df = (DomainFile) content;
|
||||
|
||||
if (ProgramLinkContentHandler.PROGRAM_LINK_CONTENT_TYPE
|
||||
.equals(df.getContentType())) {
|
||||
Msg.showError(this, null, "Program Multi-Link Error",
|
||||
"Multi-link Program access not supported: " + url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Program.class.isAssignableFrom(df.getDomainObjectClass())) {
|
||||
Msg.showError(this, null, "Error Opening Program",
|
||||
"File does not correspond to a Ghidra Program: " + df.getPathname());
|
||||
return;
|
||||
}
|
||||
|
||||
program = openProgram(df, url);
|
||||
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
Msg.showError(this, null, "Invalid Ghidra URL",
|
||||
"Improperly formed Ghidra URL: " + url);
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.showError(this, null, "Program Open Failed",
|
||||
"Failed to open Ghidra URL: " + e.getMessage());
|
||||
}
|
||||
finally {
|
||||
if (content != null) {
|
||||
wrappedContent.release(content, this);
|
||||
}
|
||||
}
|
||||
|
||||
if (program != null) {
|
||||
openedProgramList.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void messageBadProgramURL(URL url) {
|
||||
Msg.error("Invalid Ghidra URL",
|
||||
"Ghidra URL does not reference a Ghidra Program: " + url);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ public class Annotation {
|
||||
private static final Pattern QUOTATION_PATTERN =
|
||||
Pattern.compile("(?<!\\\\)[\"](.*?)(?<!\\\\)[\"]");
|
||||
|
||||
private static List<AnnotatedStringHandler> ANNOTATED_STRING_HANDLERS;
|
||||
private static Map<String, AnnotatedStringHandler> ANNOTATED_STRING_MAP;
|
||||
|
||||
private String annotationText;
|
||||
@ -40,6 +41,13 @@ public class Annotation {
|
||||
private AnnotatedStringHandler annotatedStringHandler;
|
||||
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() {
|
||||
if (ANNOTATED_STRING_MAP == null) { // lazy init due to our use of ClassSearcher
|
||||
ANNOTATED_STRING_MAP = createAnnotatedStringHandlerMap();
|
||||
@ -47,24 +55,28 @@ public class Annotation {
|
||||
return ANNOTATED_STRING_MAP;
|
||||
}
|
||||
|
||||
// locates AnnotatedStringHandler implementations to handle annotations
|
||||
private static Map<String, AnnotatedStringHandler> createAnnotatedStringHandlerMap() {
|
||||
Map<String, AnnotatedStringHandler> map = new HashMap<>();
|
||||
|
||||
// find all instances of AnnotatedString
|
||||
List<AnnotatedStringHandler> instances =
|
||||
ClassSearcher.getInstances(AnnotatedStringHandler.class);
|
||||
|
||||
for (AnnotatedStringHandler instance : instances) {
|
||||
for (AnnotatedStringHandler instance : getAnnotatedStringHandlers()) {
|
||||
String[] supportedAnnotations = instance.getSupportedAnnotations();
|
||||
for (String supportedAnnotation : supportedAnnotations) {
|
||||
map.put(supportedAnnotation, instance);
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
// locates AnnotatedStringHandler implementations to handle annotations
|
||||
private static List<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
|
||||
* <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;
|
||||
}
|
||||
|
||||
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() {
|
||||
return Collections.unmodifiableSet(getAnnotatedStringHandlerMap().keySet());
|
||||
}
|
||||
|
@ -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\"}";
|
||||
}
|
||||
|
||||
}
|
@ -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\"}";
|
||||
}
|
||||
|
||||
}
|
@ -36,8 +36,10 @@ import ghidra.util.Msg;
|
||||
* displayed.
|
||||
*/
|
||||
public class URLAnnotatedStringHandler implements AnnotatedStringHandler {
|
||||
|
||||
private static final String INVALID_SYMBOL_TEXT =
|
||||
"@url annotation must have a URL string " + "optionally followed by a display string";
|
||||
"@url annotation must have a URL string optionally followed by a display string";
|
||||
|
||||
private static final String[] SUPPORTED_ANNOTATIONS = { "url", "hyperlink", "href", "link" };
|
||||
|
||||
@Override
|
||||
|
@ -233,6 +233,13 @@ public class GhidraProject {
|
||||
return project;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying ProjectData instance.
|
||||
*/
|
||||
public ProjectData getProjectData() {
|
||||
return projectData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the ghidra project, closing (without saving!) any open programs in
|
||||
* that project. Also deletes the project if created as a temporary project.
|
||||
|
@ -93,7 +93,10 @@ public class DataTreeDialog extends DialogComponentProvider
|
||||
private Integer treeSelectionMode;
|
||||
|
||||
/**
|
||||
* Construct a new DataTreeDialog.
|
||||
* Construct a new DataTreeDialog. This chooser will show all project files.
|
||||
* Following linked-folders will only be allowed if a type of {@link #CHOOSE_FOLDER}
|
||||
* or {@link #OPEN} is specified. If different behavior is required a filter should
|
||||
* be specified using the other constructor.
|
||||
*
|
||||
* @param parent dialog's parent
|
||||
* @param title title to use
|
||||
@ -101,7 +104,7 @@ public class DataTreeDialog extends DialogComponentProvider
|
||||
* @throws IllegalArgumentException if invalid type is specified
|
||||
*/
|
||||
public DataTreeDialog(Component parent, String title, int type) {
|
||||
this(parent, title, type, null);
|
||||
this(parent, title, type, getDefaultFilter(type));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,6 +122,20 @@ public class DataTreeDialog extends DialogComponentProvider
|
||||
initDataTreeDialog(type, filter);
|
||||
}
|
||||
|
||||
private static DomainFileFilter getDefaultFilter(int type) {
|
||||
if (type == CHOOSE_FOLDER || type == OPEN) {
|
||||
// return filter which forces folder selection and allow navigation into linked-folders
|
||||
return new DomainFileFilter() {
|
||||
|
||||
@Override
|
||||
public boolean accept(DomainFile df) {
|
||||
return true; // show all files (legacy behavior)
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setTreeSelectionMode(int mode) {
|
||||
if (treePanel != null) {
|
||||
treePanel.getTreeSelectionModel().setSelectionMode(mode);
|
||||
|
@ -18,6 +18,7 @@ package ghidra.test;
|
||||
import java.awt.Dialog;
|
||||
import java.awt.Window;
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
@ -47,6 +48,7 @@ import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.framework.plugintool.util.PluginException;
|
||||
import ghidra.framework.project.DefaultProjectManager;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURL;
|
||||
import ghidra.program.database.ProgramDB;
|
||||
import ghidra.program.model.data.FileDataTypeManager;
|
||||
import ghidra.program.model.lang.*;
|
||||
@ -874,26 +876,15 @@ public class TestEnv {
|
||||
* @param domainFile The domain file used to launch the tool; may be null
|
||||
* @return the tool that is launched
|
||||
*/
|
||||
public PluginTool launchTool(final String toolName, final DomainFile domainFile) {
|
||||
public PluginTool launchTool(String toolName, DomainFile domainFile) {
|
||||
AtomicReference<PluginTool> ref = new AtomicReference<>();
|
||||
|
||||
AbstractGenericTest.runSwing(() -> {
|
||||
boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
|
||||
AbstractDockingTest.setErrorGUIEnabled(false); // disable the error GUI while launching the tool
|
||||
FrontEndTool frontEndToolInstance = getFrontEndTool();
|
||||
|
||||
Project project = frontEndToolInstance.getProject();
|
||||
ToolServices toolServices = project.getToolServices();
|
||||
PluginTool newTool = toolServices.launchTool(toolName, null);
|
||||
if (newTool == null) {
|
||||
// couldn't find the tool in the workspace...check the test area
|
||||
newTool = launchDefaultToolByName(toolName);
|
||||
}
|
||||
|
||||
PluginTool newTool = doLaunchTool(toolName);
|
||||
ref.set(newTool);
|
||||
|
||||
AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
|
||||
newTool.acceptDomainFiles(new DomainFile[] { domainFile });
|
||||
if (newTool != null) {
|
||||
newTool.acceptDomainFiles(new DomainFile[] { domainFile });
|
||||
}
|
||||
});
|
||||
|
||||
PluginTool launchedTool = ref.get();
|
||||
@ -906,6 +897,52 @@ public class TestEnv {
|
||||
return launchedTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a tool of the given name using the given Ghidra URL.
|
||||
* <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();
|
||||
AbstractDockingTest.setErrorGUIEnabled(false); // disable the error GUI while launching the tool
|
||||
FrontEndTool frontEndToolInstance = getFrontEndTool();
|
||||
|
||||
Project project = frontEndToolInstance.getProject();
|
||||
ToolServices toolServices = project.getToolServices();
|
||||
PluginTool newTool = toolServices.launchTool(toolName, null);
|
||||
if (newTool == null) {
|
||||
// couldn't find the tool in the workspace...check the test area
|
||||
newTool = launchDefaultToolByName(toolName);
|
||||
}
|
||||
AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
|
||||
return newTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the auto-save feature for all tool instances running under the {@link FrontEndTool}
|
||||
* created by this TestEnv instance. Auto-save is off by default when testing.
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 410 B |
@ -19,6 +19,8 @@ import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -37,6 +39,8 @@ import ghidra.framework.model.*;
|
||||
import ghidra.framework.plugintool.ServiceProvider;
|
||||
import ghidra.framework.plugintool.TestDummyServiceProvider;
|
||||
import ghidra.framework.project.ProjectDataService;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURLConnection;
|
||||
import ghidra.framework.store.FileSystem;
|
||||
import ghidra.program.database.ProgramBuilder;
|
||||
import ghidra.program.database.ProgramDB;
|
||||
import ghidra.program.model.address.Address;
|
||||
@ -620,6 +624,60 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
assertTrue(spyNavigatable.navigatedTo(otherProgramPath, address));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGhidraLocalUrlAnnotation_Program_WithAddress() {
|
||||
|
||||
SpyNavigatable spyNavigatable = new SpyNavigatable();
|
||||
SpyServiceProvider spyServiceProvider = new SpyServiceProvider();
|
||||
|
||||
String addresstring = "1001000";
|
||||
|
||||
String pathname = "/a/b/prog";
|
||||
String url = "ghidra:/folder/project?" + pathname + "#" + addresstring;
|
||||
String annotationText = "{@url \"" + url + "\"}";
|
||||
String rawComment = "My comment - " + annotationText;
|
||||
AttributedString prototype = prototype();
|
||||
FieldElement element =
|
||||
CommentUtils.parseTextForAnnotations(rawComment, program, prototype, 0);
|
||||
|
||||
String displayString = element.getText();
|
||||
assertEquals("My comment - " + url, displayString);
|
||||
|
||||
AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element);
|
||||
click(spyNavigatable, spyServiceProvider, annotatedElement);
|
||||
|
||||
assertTrue(spyServiceProvider.programOpened(pathname));
|
||||
|
||||
// Navigation performed by ProgramManager not tested due to use of spyServiceProvider
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGhidraServerUrlAnnotation_Program_WithAddress() {
|
||||
|
||||
SpyNavigatable spyNavigatable = new SpyNavigatable();
|
||||
SpyServiceProvider spyServiceProvider = new SpyServiceProvider();
|
||||
|
||||
String addresstring = "1001000";
|
||||
|
||||
String pathname = "/a/b/prog";
|
||||
String url = "ghidra://server/repo" + pathname + "#" + addresstring;
|
||||
String annotationText = "{@url \"" + url + "\"}";
|
||||
String rawComment = "My comment - " + annotationText;
|
||||
AttributedString prototype = prototype();
|
||||
FieldElement element =
|
||||
CommentUtils.parseTextForAnnotations(rawComment, program, prototype, 0);
|
||||
|
||||
String displayString = element.getText();
|
||||
assertEquals("My comment - " + url, displayString);
|
||||
|
||||
AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element);
|
||||
click(spyNavigatable, spyServiceProvider, annotatedElement);
|
||||
|
||||
assertTrue(spyServiceProvider.programOpened(pathname));
|
||||
|
||||
// Navigation performed by ProgramManager not tested due to use of spyServiceProvider
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnknownAnnotation() {
|
||||
String rawComment = "This is a symbol {@syyyybol bob} annotation";
|
||||
@ -903,13 +961,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
private Set<String> openedPrograms = new HashSet<>();
|
||||
private Set<String> closedPrograms = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public Program openProgram(DomainFile domainFile, int version, int state) {
|
||||
String name = domainFile.getName();
|
||||
String pathname = domainFile.getPathname();
|
||||
|
||||
openedPrograms.add(name);
|
||||
|
||||
private Program generateProgram(String pathname, String name) {
|
||||
try {
|
||||
ProgramBuilder builder = new ProgramBuilder();
|
||||
builder.setName(pathname);
|
||||
@ -923,6 +975,39 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Program openProgram(URL ghidraURL, int state) {
|
||||
try {
|
||||
GhidraURLConnection c = new GhidraURLConnection(ghidraURL);
|
||||
String folderpath = c.getFolderPath();
|
||||
String name = c.getFolderItemName();
|
||||
String pathname = folderpath;
|
||||
if (!pathname.endsWith(FileSystem.SEPARATOR)) {
|
||||
pathname += FileSystem.SEPARATOR;
|
||||
}
|
||||
pathname += name;
|
||||
openedPrograms.add(name);
|
||||
|
||||
Program p = generateProgram(pathname, name);
|
||||
|
||||
// NOTE: URL ref navigation not performed
|
||||
|
||||
return p;
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
failWithException("Bad URL", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Program openProgram(DomainFile domainFile, int version, int state) {
|
||||
String name = domainFile.getName();
|
||||
String pathname = domainFile.getPathname();
|
||||
openedPrograms.add(name);
|
||||
return generateProgram(pathname, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean closeProgram(Program p, boolean ignoreChanges) {
|
||||
String name = FilenameUtils.getName(p.getName());
|
||||
|
@ -121,12 +121,7 @@ public class ProjectFileManagerTest extends AbstractGhidraHeadedIntegrationTest
|
||||
// If there are queued actions, then we have to kick the handling thread and
|
||||
// let it finish running.
|
||||
|
||||
try {
|
||||
assertTrue(eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
failWithException("Interrupted waiting for filesystem events", e);
|
||||
}
|
||||
assertTrue(eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
private void deleteAll(File file) {
|
||||
|
@ -421,15 +421,10 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
assertNotNull(dialog);
|
||||
}
|
||||
|
||||
private DomainFileFilter createStartsWithFilter(String startsWith) {
|
||||
return (df) -> df.getName().startsWith(startsWith);
|
||||
}
|
||||
|
||||
private void showFiltered(String startsWith) {
|
||||
private void showFiltered(final String startsWith) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog",
|
||||
DataTreeDialog.OPEN, createStartsWithFilter(startsWith));
|
||||
|
||||
DataTreeDialog.OPEN, f -> f.getName().startsWith(startsWith));
|
||||
dialog.showComponent();
|
||||
});
|
||||
waitForSwing();
|
||||
|
@ -79,10 +79,10 @@ public class AddViewToProjectTest extends AbstractGhidraHeadlessIntegrationTest
|
||||
|
||||
try {
|
||||
URL view = GhidraURL.makeURL(DIRECTORY_NAME, PROJECT_VIEW1);
|
||||
project.addProjectView(view);
|
||||
project.addProjectView(view, true);
|
||||
|
||||
// add another view that will be removed to test the remove
|
||||
project.addProjectView(GhidraURL.makeURL(DIRECTORY_NAME, PROJECT_VIEW2));
|
||||
project.addProjectView(GhidraURL.makeURL(DIRECTORY_NAME, PROJECT_VIEW2), true);
|
||||
|
||||
// validate the view was added to project
|
||||
ProjectLocator[] projViews = project.getProjectViews();
|
||||
|
@ -117,7 +117,7 @@ public class CreateDomainObjectTest extends AbstractGhidraHeadedIntegrationTest
|
||||
project.close();
|
||||
Project project2 = ProjectTestUtils.getProject(testDir, PROJECT_NAME2);
|
||||
try {
|
||||
project2.addProjectView(GhidraURL.makeURL(testDir, PROJECT_NAME1));
|
||||
project2.addProjectView(GhidraURL.makeURL(testDir, PROJECT_NAME1), true);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Assert.fail("View Not found");
|
||||
|
@ -15,15 +15,18 @@
|
||||
*/
|
||||
package ghidra.base.project;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import generic.test.TestUtils;
|
||||
import generic.test.AbstractGTest;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.remote.User;
|
||||
import ghidra.framework.store.local.IndexedV1LocalFileSystem;
|
||||
import ghidra.framework.store.local.LocalFileSystem;
|
||||
import ghidra.test.TestEnv;
|
||||
import utilities.util.FileUtilities;
|
||||
|
||||
/**
|
||||
* This class represents the idea of a shared Ghidra repository. This class is meant to be
|
||||
@ -54,11 +57,34 @@ public class FakeRepository {
|
||||
private Map<String, User> usersByName = new HashMap<>();
|
||||
private Map<User, FakeSharedProject> projectsByUser = new HashMap<>();
|
||||
|
||||
private File versionedFSDir;
|
||||
private LocalFileSystem versionedFileSystem;
|
||||
|
||||
public FakeRepository() {
|
||||
public FakeRepository() throws IOException {
|
||||
// validation must be enabled if both environments are utilized by a test
|
||||
LocalFileSystem.setValidationRequired();
|
||||
|
||||
versionedFSDir =
|
||||
new File(AbstractGTest.getTestDirectoryPath() + File.separator + "TestRepo.rep");
|
||||
if (versionedFSDir.exists()) {
|
||||
FileUtilities.deleteDir(versionedFSDir);
|
||||
}
|
||||
if (versionedFSDir.exists() || !FileUtilities.createDir(versionedFSDir)) {
|
||||
throw new IOException("Failed to create clean repo dir: " + versionedFSDir);
|
||||
}
|
||||
versionedFileSystem = new MyVersionedFileSystem(versionedFSDir.getPath());
|
||||
}
|
||||
|
||||
private static class MyVersionedFileSystem extends IndexedV1LocalFileSystem {
|
||||
MyVersionedFileSystem(String rootPath) throws IOException {
|
||||
super(rootPath, true, false, true, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShared() {
|
||||
// Enables use of asyncronous event dispatching thread
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -109,11 +135,6 @@ public class FakeRepository {
|
||||
|
||||
FakeSharedProject project = new FakeSharedProject(this, user);
|
||||
projectsByUser.put(user, project);
|
||||
|
||||
if (versionedFileSystem == null) {
|
||||
versionedFileSystem = project.getVersionedFileSystem();
|
||||
TestUtils.setInstanceField("isShared", versionedFileSystem, Boolean.TRUE);
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
@ -139,6 +160,8 @@ public class FakeRepository {
|
||||
*/
|
||||
public void dispose() {
|
||||
projectsByUser.values().forEach(p -> disposeProject(p));
|
||||
versionedFileSystem.dispose();
|
||||
FileUtilities.deleteDir(versionedFSDir);
|
||||
}
|
||||
|
||||
private void disposeProject(FakeSharedProject p) {
|
||||
|
@ -25,7 +25,7 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import generic.test.AbstractGenericTest;
|
||||
import generic.test.AbstractGTest;
|
||||
import generic.test.TestUtils;
|
||||
import ghidra.framework.data.*;
|
||||
import ghidra.framework.model.*;
|
||||
@ -39,6 +39,7 @@ import ghidra.test.TestProgramManager;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import junit.framework.AssertionFailedError;
|
||||
import utilities.util.FileUtilities;
|
||||
|
||||
/**
|
||||
* This class represents the idea of a shared Ghidra project. Each project is associated with
|
||||
@ -61,21 +62,18 @@ public class FakeSharedProject {
|
||||
public FakeSharedProject(FakeRepository repo, User user) throws IOException {
|
||||
|
||||
this.repo = repo;
|
||||
String projectDirPath = AbstractGenericTest.getTestDirectoryPath();
|
||||
String projectDirPath = AbstractGTest.getTestDirectoryPath();
|
||||
gProject =
|
||||
GhidraProject.createProject(projectDirPath, "TestProject_" + user.getName(), true);
|
||||
gProject.setDeleteOnClose(true);
|
||||
|
||||
LocalFileSystem fs = repo.getSharedFileSystem();
|
||||
if (fs != null) {
|
||||
// first project will keeps its versioned file system
|
||||
setVersionedFileSystem(fs);
|
||||
}
|
||||
// use local shared fake repo versioned file system
|
||||
setVersionedFileSystem(repo.getSharedFileSystem());
|
||||
}
|
||||
|
||||
FakeSharedProject(User user) throws IOException {
|
||||
|
||||
String projectDirPath = AbstractGenericTest.getTestDirectoryPath();
|
||||
String projectDirPath = AbstractGTest.getTestDirectoryPath();
|
||||
gProject =
|
||||
GhidraProject.createProject(projectDirPath, "TestProject_" + user.getName(), true);
|
||||
}
|
||||
@ -101,7 +99,7 @@ public class FakeSharedProject {
|
||||
* @return the project file manager
|
||||
*/
|
||||
public ProjectFileManager getProjectFileManager() {
|
||||
return (ProjectFileManager) gProject.getProject().getProjectData();
|
||||
return (ProjectFileManager) gProject.getProjectData();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -369,8 +367,11 @@ public class FakeSharedProject {
|
||||
* @see FakeRepository#dispose()
|
||||
*/
|
||||
public void dispose() {
|
||||
ProjectLocator projectLocator = getProjectFileManager().getProjectLocator();
|
||||
programManager.disposeOpenPrograms();
|
||||
gProject.close();
|
||||
FileUtilities.deleteDir(projectLocator.getProjectDir());
|
||||
projectLocator.getMarkerFile().delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -400,12 +401,7 @@ public class FakeSharedProject {
|
||||
(FileSystemEventManager) TestUtils.getInstanceField("eventManager",
|
||||
versionedFileSystem);
|
||||
|
||||
try {
|
||||
eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
failWithException("Interrupted waiting for filesystem events", e);
|
||||
}
|
||||
eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private DomainFolder getFolder(String path) throws Exception {
|
||||
|
@ -99,6 +99,10 @@ public class CollectFailedRelocations extends GhidraScript {
|
||||
if (monitor.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this. If program links should be considered
|
||||
// "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
|
||||
// should be used.
|
||||
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
|
||||
programs.add(domainFile);
|
||||
}
|
||||
|
@ -258,6 +258,10 @@ public class CreateMultipleLibraries extends GhidraScript {
|
||||
DomainFile[] files = myFolder.getFiles();
|
||||
for (DomainFile domainFile : files) {
|
||||
monitor.checkCanceled();
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this. If program links should be considered
|
||||
// "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
|
||||
// should be used.
|
||||
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
|
||||
programs.add(domainFile);
|
||||
}
|
||||
|
@ -372,6 +372,10 @@ public class FidStatistics extends GhidraScript {
|
||||
DomainFile[] files = folder.getFiles();
|
||||
for (DomainFile domainFile : files) {
|
||||
monitor.checkCanceled();
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this. If program links should be considered
|
||||
// "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
|
||||
// should be used.
|
||||
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
|
||||
programs.add(domainFile);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -17,6 +16,9 @@
|
||||
//Opens all programs under a chosen domain folder, grabs their error count,
|
||||
//then sorts in increasing error order and prints them
|
||||
//@category FunctionID
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
import generic.stl.Pair;
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
@ -27,9 +29,6 @@ import ghidra.util.Msg;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.exception.VersionException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class FindErrors extends GhidraScript {
|
||||
|
||||
@Override
|
||||
@ -82,6 +81,10 @@ public class FindErrors extends GhidraScript {
|
||||
if (monitor.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this. If program links should be considered
|
||||
// "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
|
||||
// should be used.
|
||||
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
|
||||
programs.add(domainFile);
|
||||
}
|
||||
|
@ -100,6 +100,10 @@ public class FindFunctionByHash extends GhidraScript {
|
||||
if (monitor.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this. If program links should be considered
|
||||
// "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
|
||||
// should be used.
|
||||
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
|
||||
programs.add(domainFile);
|
||||
}
|
||||
|
@ -16,6 +16,9 @@
|
||||
//Opens all programs under a chosen domain folder, scans them for functions
|
||||
//that match a user supplied name, and prints info about the match.
|
||||
//@category FunctionID
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.feature.fid.service.FidService;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
@ -26,9 +29,6 @@ import ghidra.util.Msg;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.exception.VersionException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class FindNamedFunction extends GhidraScript {
|
||||
|
||||
FidService service;
|
||||
@ -85,6 +85,10 @@ public class FindNamedFunction extends GhidraScript {
|
||||
if (monitor.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this. If program links should be considered
|
||||
// "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
|
||||
// should be used.
|
||||
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
|
||||
programs.add(domainFile);
|
||||
}
|
||||
|
@ -153,6 +153,10 @@ public class IngestTask extends Task {
|
||||
for (DomainFile domainFile : files) {
|
||||
monitor.checkCanceled();
|
||||
monitor.incrementProgress(1);
|
||||
// Do not follow folder-links or consider program links. Using content type
|
||||
// to filter is best way to control this. If program links should be considered
|
||||
// "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())"
|
||||
// should be used.
|
||||
if (domainFile.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) {
|
||||
programs.add(domainFile);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import db.*;
|
||||
import ghidra.app.util.task.OpenProgramTask;
|
||||
import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
|
||||
import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator;
|
||||
import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator;
|
||||
import ghidra.feature.vt.api.impl.*;
|
||||
@ -309,7 +310,8 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession, VTC
|
||||
|
||||
TaskLauncher.launch(openTask);
|
||||
|
||||
return openTask.getOpenProgram();
|
||||
OpenProgramRequest openProgram = openTask.getOpenProgram();
|
||||
return openProgram != null ? openProgram.getProgram() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -24,7 +24,8 @@ import db.OpenMode;
|
||||
import db.buffers.BufferFile;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.feature.vt.api.db.VTSessionDB;
|
||||
import ghidra.framework.data.*;
|
||||
import ghidra.framework.data.DBContentHandler;
|
||||
import ghidra.framework.data.DomainObjectMergeManager;
|
||||
import ghidra.framework.model.ChangeSet;
|
||||
import ghidra.framework.model.DomainObject;
|
||||
import ghidra.framework.store.*;
|
||||
@ -34,7 +35,8 @@ import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.exception.VersionException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
public class VTSessionContentHandler extends DBContentHandler {
|
||||
public class VTSessionContentHandler extends DBContentHandler<VTSessionDB> {
|
||||
|
||||
private static Icon ICON = new GIcon("icon.version.tracking.session.content.type");
|
||||
|
||||
public final static String CONTENT_TYPE = "VersionTracking";
|
||||
@ -49,7 +51,6 @@ public class VTSessionContentHandler extends DBContentHandler {
|
||||
"Unsupported domain object: " + domainObject.getClass().getName());
|
||||
}
|
||||
return createFile((VTSessionDB) domainObject, CONTENT_TYPE, fs, path, name, monitor);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -74,7 +75,7 @@ public class VTSessionContentHandler extends DBContentHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
|
||||
public VTSessionDB getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
|
||||
boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
|
||||
throws IOException, CancelledException, VersionException {
|
||||
|
||||
@ -119,7 +120,7 @@ public class VTSessionContentHandler extends DBContentHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends DomainObject> getDomainObjectClass() {
|
||||
public Class<VTSessionDB> getDomainObjectClass() {
|
||||
return VTSessionDB.class;
|
||||
}
|
||||
|
||||
@ -129,7 +130,7 @@ public class VTSessionContentHandler extends DBContentHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version,
|
||||
public VTSessionDB getImmutableObject(FolderItem item, Object consumer, int version,
|
||||
int minChangeVersion, TaskMonitor monitor)
|
||||
throws IOException, CancelledException, VersionException {
|
||||
|
||||
@ -149,7 +150,7 @@ public class VTSessionContentHandler extends DBContentHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
|
||||
public VTSessionDB getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
|
||||
Object consumer, TaskMonitor monitor)
|
||||
throws IOException, VersionException, CancelledException {
|
||||
|
||||
|
@ -15,6 +15,10 @@
|
||||
*/
|
||||
package ghidra.feature.vt.gui.actions;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.action.DockingAction;
|
||||
import docking.action.MenuData;
|
||||
import docking.tool.ToolConstants;
|
||||
import ghidra.feature.vt.api.main.VTSession;
|
||||
import ghidra.feature.vt.gui.plugin.VTController;
|
||||
import ghidra.feature.vt.gui.plugin.VTPlugin;
|
||||
@ -23,10 +27,6 @@ import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.model.DomainFileFilter;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.util.HelpLocation;
|
||||
import docking.ActionContext;
|
||||
import docking.action.DockingAction;
|
||||
import docking.action.MenuData;
|
||||
import docking.tool.ToolConstants;
|
||||
|
||||
public class OpenVersionTrackingSessionAction extends DockingAction {
|
||||
|
||||
@ -60,5 +60,10 @@ public class OpenVersionTrackingSessionAction extends DockingAction {
|
||||
Class<?> c = f.getDomainObjectClass();
|
||||
return VTSession.class.isAssignableFrom(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean followLinkedFolders() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import docking.widgets.label.GDLabel;
|
||||
import docking.wizard.*;
|
||||
import generic.theme.*;
|
||||
import ghidra.app.util.task.OpenProgramTask;
|
||||
import ghidra.app.util.task.OpenProgramTask.OpenProgramRequest;
|
||||
import ghidra.framework.main.DataTreeDialog;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.model.DomainFolder;
|
||||
@ -383,8 +384,8 @@ public class NewSessionPanel extends AbstractMageJPanel<VTWizardStateKey> {
|
||||
|
||||
OpenProgramTask openProgramTask = new OpenProgramTask(programInfo.getFile(), tool);
|
||||
new TaskLauncher(openProgramTask, tool.getActiveWindow());
|
||||
Program program = openProgramTask.getOpenProgram();
|
||||
programInfo.setProgram(program);
|
||||
OpenProgramRequest openProgram = openProgramTask.getOpenProgram();
|
||||
programInfo.setProgram(openProgram != null ? openProgram.getProgram() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -20,12 +20,12 @@ import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
import docking.widgets.OptionDialog;
|
||||
import ghidra.feature.vt.api.impl.VTSessionContentHandler;
|
||||
import ghidra.feature.vt.api.main.VTSession;
|
||||
import ghidra.feature.vt.gui.task.SaveTask;
|
||||
import ghidra.framework.main.DataTreeDialog;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.model.DomainFileFilter;
|
||||
import ghidra.program.database.ProgramDB;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.HTMLUtilities;
|
||||
import ghidra.util.task.TaskLauncher;
|
||||
|
||||
@ -36,23 +36,20 @@ public class VTWizardUtils {
|
||||
}
|
||||
|
||||
public static final DomainFileFilter VT_SESSION_FILTER = new DomainFileFilter() {
|
||||
|
||||
@Override
|
||||
public boolean accept(DomainFile df) {
|
||||
if (VTSessionContentHandler.CONTENT_TYPE.equals(df.getContentType())) {
|
||||
return true;
|
||||
}
|
||||
return VTSession.class.isAssignableFrom(df.getDomainObjectClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean followLinkedFolders() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
public static final DomainFileFilter PROGRAM_FILTER = new DomainFileFilter() {
|
||||
@Override
|
||||
public boolean accept(DomainFile df) {
|
||||
if (ProgramDB.CONTENT_TYPE.equals(df.getContentType())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public static final DomainFileFilter PROGRAM_FILTER = f -> {
|
||||
return Program.class.isAssignableFrom(f.getDomainObjectClass());
|
||||
};
|
||||
|
||||
static DomainFile chooseDomainFile(Component parent, String domainIdentifier,
|
||||
|
@ -16,8 +16,7 @@
|
||||
package db.buffers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.rmi.NoSuchObjectException;
|
||||
import java.rmi.Remote;
|
||||
import java.rmi.*;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import ghidra.util.Msg;
|
||||
@ -104,8 +103,8 @@ public class BufferFileAdapter implements BufferFile {
|
||||
bufferFileHandle.dispose();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// handle may have already been disposed
|
||||
if (!(e instanceof NoSuchObjectException)) {
|
||||
// handle may have already been disposed or disconnected
|
||||
if (!(e instanceof NoSuchObjectException) && !(e instanceof ConnectException)) {
|
||||
Msg.error(this, e);
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package docking.widgets.tree;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.swing.Icon;
|
||||
@ -197,6 +198,22 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable<GTre
|
||||
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.
|
||||
*
|
||||
@ -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
|
||||
* currently attached to a visible tree, then this call does nothing
|
||||
|
@ -42,14 +42,24 @@ public class GTreeExpandAllTask extends GTreeTask {
|
||||
monitor.initialize(1000);
|
||||
monitor.setMessage("Expanding nodes...");
|
||||
try {
|
||||
expandNode(node, monitor);
|
||||
expandNode(node, true, monitor);
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
// 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.
|
||||
if (monitor.getProgress() >= MAX) {
|
||||
return;
|
||||
@ -57,6 +67,9 @@ public class GTreeExpandAllTask extends GTreeTask {
|
||||
if (parent.isLeaf()) {
|
||||
return;
|
||||
}
|
||||
if (!force && !parent.isAutoExpandPermitted()) {
|
||||
return;
|
||||
}
|
||||
monitor.checkCanceled();
|
||||
List<GTreeNode> allChildren = parent.getChildren();
|
||||
if (allChildren.size() == 0) {
|
||||
@ -68,9 +81,9 @@ public class GTreeExpandAllTask extends GTreeTask {
|
||||
}
|
||||
for (GTreeNode child : allChildren) {
|
||||
monitor.checkCanceled();
|
||||
expandNode(child, monitor);
|
||||
expandNode(child, false, monitor);
|
||||
}
|
||||
monitor.incrementProgress(1);
|
||||
monitor.incrementProgress(1); // TODO: total node count is unknown
|
||||
}
|
||||
|
||||
private void expandPath(final TreePath treePath, final TaskMonitor monitor) {
|
||||
|
@ -158,8 +158,10 @@ public class RepositoryAdapter implements RemoteAdapterListener {
|
||||
|
||||
/**
|
||||
* Attempt to connect to the server.
|
||||
* @throws RepositoryNotFoundException if named repository does not exist
|
||||
* @throws IOException if IO error occurs
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
public void connect() throws RepositoryNotFoundException, IOException {
|
||||
synchronized (serverAdapter) {
|
||||
if (repository != null) {
|
||||
try {
|
||||
@ -180,7 +182,7 @@ public class RepositoryAdapter implements RemoteAdapterListener {
|
||||
unexpectedDisconnect = false;
|
||||
if (repository == null) {
|
||||
noSuchRepository = true;
|
||||
throw new IOException("Repository '" + name + "': not found");
|
||||
throw new RepositoryNotFoundException("Repository '" + name + "': not found");
|
||||
}
|
||||
Msg.info(this, "Connected to repository '" + name + "'");
|
||||
changeDispatcher.start();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -52,6 +52,9 @@ public class RepositoryServerAdapter {
|
||||
// Keeps track of whether the connection attempt was cancelled by the user
|
||||
private boolean connectCancelled = false;
|
||||
|
||||
// Keep track of last connect error
|
||||
private Throwable lastConnectError;
|
||||
|
||||
private WeakSet<RemoteAdapterListener> listenerList =
|
||||
WeakDataStructureFactory.createCopyOnWriteWeakSet();
|
||||
|
||||
@ -105,6 +108,14 @@ public class RepositoryServerAdapter {
|
||||
return connectCancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last error associated with a failed connection attempt.
|
||||
* @return last connect error or null
|
||||
*/
|
||||
public Throwable getLastConnectError() {
|
||||
return lastConnectError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify listeners of a connection state change.
|
||||
*/
|
||||
@ -135,7 +146,7 @@ public class RepositoryServerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
Throwable cause = null;
|
||||
lastConnectError = null;
|
||||
try {
|
||||
try {
|
||||
serverHandle = ClientUtil.connect(server);
|
||||
@ -162,22 +173,22 @@ public class RepositoryServerAdapter {
|
||||
catch (LoginException e) {
|
||||
Msg.showError(this, null, "Server Error",
|
||||
"Server access denied (" + serverInfoStr + ").");
|
||||
cause = e;
|
||||
lastConnectError = e;
|
||||
}
|
||||
catch (GeneralSecurityException e) {
|
||||
Msg.showError(this, null, "Server Error",
|
||||
"Server access denied (" + serverInfoStr + "): " + e.getMessage());
|
||||
cause = e;
|
||||
lastConnectError = e;
|
||||
}
|
||||
catch (SocketTimeoutException | java.net.ConnectException | java.rmi.ConnectException e) {
|
||||
Msg.showError(this, null, "Server Error",
|
||||
"Connection to server failed (" + server + ").");
|
||||
cause = e;
|
||||
lastConnectError = e;
|
||||
}
|
||||
catch (java.net.UnknownHostException | java.rmi.UnknownHostException e) {
|
||||
Msg.showError(this, null, "Server Error",
|
||||
"Server Not Found (" + server.getServerName() + ").");
|
||||
cause = e;
|
||||
lastConnectError = e;
|
||||
}
|
||||
catch (RemoteException e) {
|
||||
String msg = e.getMessage();
|
||||
@ -185,7 +196,7 @@ public class RepositoryServerAdapter {
|
||||
while ((t = t.getCause()) != null) {
|
||||
String err = t.getMessage();
|
||||
msg = err != null ? err : t.toString();
|
||||
cause = t;
|
||||
lastConnectError = t;
|
||||
}
|
||||
Msg.showError(this, null, "Server Error",
|
||||
"An error occurred on the server (" + serverInfoStr + ").\n" + msg, e);
|
||||
@ -200,7 +211,7 @@ public class RepositoryServerAdapter {
|
||||
"An error occurred while connecting to the server (" + serverInfoStr + ").\n" + msg,
|
||||
e);
|
||||
}
|
||||
throw new NotConnectedException("Not connected to repository server", cause);
|
||||
throw new NotConnectedException("Not connected to repository server", lastConnectError);
|
||||
}
|
||||
|
||||
private void checkPasswordExpiration() {
|
||||
|
@ -32,7 +32,7 @@ public interface RepositoryHandle {
|
||||
|
||||
// TODO: NOTE! Debugging client or sever garbage collection delays could
|
||||
// cause handle to be disposed prematurely.
|
||||
public final static int CLIENT_CHECK_PERIOD = SystemUtilities.isInTestingMode() ? 1000 : 30000;
|
||||
public final static int CLIENT_CHECK_PERIOD = SystemUtilities.isInTestingMode() ? 2000 : 30000;
|
||||
|
||||
/**
|
||||
* Returns the name of this repository.
|
||||
|
@ -22,33 +22,60 @@ import java.util.concurrent.*;
|
||||
* <code>FileSystemListenerList</code> maintains a list of FileSystemListener's.
|
||||
* This class, acting as a FileSystemListener, simply relays each callback to
|
||||
* all FileSystemListener's within its list. Employs either a synchronous
|
||||
* and asynchronous notification mechanism.
|
||||
* and asynchronous notification mechanism. Once disposed event dispatching will
|
||||
* discontinue.
|
||||
*/
|
||||
public class FileSystemEventManager implements FileSystemListener {
|
||||
|
||||
private static enum ThreadState {
|
||||
STOPPED, RUNNING, DISPOSED
|
||||
}
|
||||
|
||||
private List<FileSystemListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private BlockingQueue<FileSystemEvent> eventQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
private volatile boolean disposed = false;
|
||||
private final boolean asyncDispatchEnabled;
|
||||
|
||||
private volatile ThreadState state = ThreadState.STOPPED;
|
||||
private Thread thread;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param enableAsynchronousDispatching if true a separate dispatch thread will be used
|
||||
* to notify listeners. If false, blocking notification will be performed.
|
||||
* to notify listeners. If false, blocking notification will be performed. Events are
|
||||
* immediately discarded in the absence of any listener(s).
|
||||
*/
|
||||
public FileSystemEventManager(boolean enableAsynchronousDispatching) {
|
||||
asyncDispatchEnabled = enableAsynchronousDispatching;
|
||||
}
|
||||
|
||||
if (enableAsynchronousDispatching) {
|
||||
thread = new FileSystemEventProcessingThread();
|
||||
thread.start();
|
||||
/**
|
||||
* Return true if asynchornous event processing is enabled.
|
||||
* @return true if asynchornous event processing is enabled, else false
|
||||
*/
|
||||
public boolean isAsynchronous() {
|
||||
return asyncDispatchEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discontinue event dispatching and terminate dispatch thread if it exists.
|
||||
*/
|
||||
public synchronized void dispose() {
|
||||
state = ThreadState.DISPOSED;
|
||||
if (asyncDispatchEnabled) {
|
||||
if (thread != null && thread.isAlive()) {
|
||||
thread.interrupt();
|
||||
}
|
||||
eventQueue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
disposed = true;
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
private synchronized void startDispatchThread() {
|
||||
if (asyncDispatchEnabled && state == ThreadState.STOPPED) {
|
||||
// only starts when first listener is added
|
||||
state = ThreadState.RUNNING;
|
||||
thread = new FileSystemEventProcessingThread();
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +84,7 @@ public class FileSystemEventManager implements FileSystemListener {
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void add(FileSystemListener listener) {
|
||||
startDispatchThread(); // if asyncDispatchEnabled
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@ -116,30 +144,34 @@ public class FileSystemEventManager implements FileSystemListener {
|
||||
@Override
|
||||
public void syncronize() {
|
||||
// Note: synchronize calls will only work when using a threaded event queue
|
||||
if (isAsynchronous()) {
|
||||
add(new SynchronizeEvent());
|
||||
if (asyncDispatchEnabled) {
|
||||
queueEvent(new SynchronizeEvent());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAsynchronous() {
|
||||
return thread != null;
|
||||
}
|
||||
|
||||
private void add(FileSystemEvent ev) {
|
||||
if (!listeners.isEmpty()) {
|
||||
eventQueue.add(ev);
|
||||
/**
|
||||
* Queue specified event if listener thread is running
|
||||
* @param ev filesystm event
|
||||
* @return true if queued, else false if listener thread not running
|
||||
*/
|
||||
private boolean queueEvent(FileSystemEvent ev) {
|
||||
if (state == ThreadState.RUNNING) {
|
||||
return eventQueue.add(ev);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleEvent(FileSystemEvent e) {
|
||||
if (disposed) {
|
||||
if (state == ThreadState.DISPOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAsynchronous()) {
|
||||
add(e);
|
||||
if (asyncDispatchEnabled) {
|
||||
// if there are no listeners event will be discarded (i.e., listener thread not running)
|
||||
queueEvent(e);
|
||||
}
|
||||
else {
|
||||
// process in a synchronous fashion in current thread
|
||||
e.process(listeners);
|
||||
}
|
||||
}
|
||||
@ -154,17 +186,26 @@ public class FileSystemEventManager implements FileSystemListener {
|
||||
*
|
||||
* @param timeout the maximum time to wait
|
||||
* @param unit the time unit of the {@code time} argument
|
||||
* @return true if the events were processed in the given timeout
|
||||
* @throws InterruptedException if this waiting thread is interrupted
|
||||
* @return true if the events were processed in the given timeout. A false value will be
|
||||
* returned if either a timeout occured
|
||||
*/
|
||||
public boolean flushEvents(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
if (!isAsynchronous()) {
|
||||
public boolean flushEvents(long timeout, TimeUnit unit) {
|
||||
if (!asyncDispatchEnabled) {
|
||||
return true; // each thread processes its own event
|
||||
}
|
||||
|
||||
MarkerEvent event = new MarkerEvent();
|
||||
eventQueue.add(event);
|
||||
return event.waitForEvent(timeout, unit);
|
||||
if (!queueEvent(event)) {
|
||||
// events are not queuing since there are no listeners or dispose has occured
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return event.waitForEvent(timeout, unit);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
// ignore - listener thread stopped or disposed
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
@ -180,18 +221,14 @@ public class FileSystemEventManager implements FileSystemListener {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (!disposed) {
|
||||
|
||||
while (state == ThreadState.RUNNING) {
|
||||
FileSystemEvent event;
|
||||
try {
|
||||
event = eventQueue.take();
|
||||
event.process(listeners);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
// interrupt has been cleared; if other threads rely on this interrupted state,
|
||||
// then mark the thread as interrupted again by calling:
|
||||
// Thread.currentThread().interrupt();
|
||||
// For now, this code relies on the 'alive' flag to know when to terminate
|
||||
// ignore - interrupt has been cleared
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -916,6 +916,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
|
||||
|
||||
@Override
|
||||
public int getItemCount() throws IOException {
|
||||
checkDisposed();
|
||||
if (readOnly) {
|
||||
refreshReadOnlyIndex();
|
||||
}
|
||||
@ -930,11 +931,9 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
|
||||
return count;
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#getFolders(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public synchronized String[] getFolderNames(String folderPath) throws IOException {
|
||||
checkDisposed();
|
||||
if (readOnly) {
|
||||
refreshReadOnlyIndex();
|
||||
}
|
||||
@ -948,13 +947,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#createFolder(java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public synchronized void createFolder(String parentPath, String folderName)
|
||||
throws InvalidNameException, IOException {
|
||||
|
||||
checkDisposed();
|
||||
|
||||
if (readOnly) {
|
||||
throw new ReadOnlyException();
|
||||
}
|
||||
@ -987,12 +985,11 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
|
||||
eventManager.folderCreated(parentPath, getName(path));
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#deleteFolder(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public synchronized void deleteFolder(String folderPath) throws IOException {
|
||||
|
||||
checkDisposed();
|
||||
|
||||
if (readOnly) {
|
||||
throw new ReadOnlyException();
|
||||
}
|
||||
@ -1059,13 +1056,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
|
||||
getListener().itemCreated(destFolderPath, itemName);
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#moveItem(java.lang.String, java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public synchronized void moveItem(String folderPath, String name, String newFolderPath,
|
||||
String newName) throws IOException, InvalidNameException {
|
||||
|
||||
checkDisposed();
|
||||
|
||||
if (readOnly) {
|
||||
throw new ReadOnlyException();
|
||||
}
|
||||
@ -1140,13 +1136,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
|
||||
deleteEmptyVersionedFolders(folderPath);
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#moveFolder(java.lang.String, java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public synchronized void moveFolder(String parentPath, String folderName, String newParentPath)
|
||||
throws InvalidNameException, IOException {
|
||||
|
||||
checkDisposed();
|
||||
|
||||
if (readOnly) {
|
||||
throw new ReadOnlyException();
|
||||
}
|
||||
@ -1206,13 +1201,12 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#renameFolder(java.lang.String, java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public synchronized void renameFolder(String parentPath, String folderName,
|
||||
String newFolderName) throws InvalidNameException, IOException {
|
||||
|
||||
checkDisposed();
|
||||
|
||||
if (readOnly) {
|
||||
throw new ReadOnlyException();
|
||||
}
|
||||
@ -1247,16 +1241,14 @@ public class IndexedLocalFileSystem extends LocalFileSystem {
|
||||
eventManager.folderRenamed(parentPath, folderName, newFolderName);
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#folderExists(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public synchronized boolean folderExists(String folderPath) {
|
||||
try {
|
||||
checkDisposed();
|
||||
getFolder(folderPath, GetFolderOption.READ_ONLY);
|
||||
return true;
|
||||
}
|
||||
catch (NotFoundException e) {
|
||||
catch (IOException | NotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem {
|
||||
* @throws FileNotFoundException if specified rootPath does not exist
|
||||
* @throws IOException if error occurs while reading/writing index files
|
||||
*/
|
||||
IndexedV1LocalFileSystem(String rootPath, boolean isVersioned, boolean readOnly,
|
||||
protected IndexedV1LocalFileSystem(String rootPath, boolean isVersioned, boolean readOnly,
|
||||
boolean enableAsyncronousDispatching, boolean create) throws IOException {
|
||||
super(rootPath, isVersioned, readOnly, enableAsyncronousDispatching, create);
|
||||
}
|
||||
@ -134,6 +134,7 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem {
|
||||
|
||||
@Override
|
||||
public FolderItem getItem(String fileID) throws IOException, UnsupportedOperationException {
|
||||
checkDisposed();
|
||||
if (fileIdMap == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -71,6 +71,8 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
|
||||
private static boolean refreshRequired = false;
|
||||
|
||||
private boolean disposed = false;
|
||||
|
||||
protected final File root;
|
||||
protected final boolean isVersioned;
|
||||
protected final boolean readOnly;
|
||||
@ -78,9 +80,6 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
|
||||
private RepositoryLogger repositoryLogger;
|
||||
|
||||
// Always false in production; can be manipulated by tests
|
||||
private boolean isShared;
|
||||
|
||||
/**
|
||||
* Construct a local filesystem for existing data
|
||||
* @param rootPath
|
||||
@ -285,25 +284,16 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
return refreshRequired;
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#isVersioned()
|
||||
*/
|
||||
@Override
|
||||
public boolean isVersioned() {
|
||||
return isVersioned;
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#isOnline()
|
||||
*/
|
||||
@Override
|
||||
public boolean isOnline() {
|
||||
return true;
|
||||
return !disposed;
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#isReadOnly()
|
||||
*/
|
||||
@Override
|
||||
public boolean isReadOnly() {
|
||||
return readOnly;
|
||||
@ -388,9 +378,6 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
return getItemNames(folderPath, false);
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#getItem(java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public synchronized LocalFolderItem getItem(String folderPath, String name) throws IOException {
|
||||
try {
|
||||
@ -424,9 +411,6 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
throw new UnsupportedOperationException("getItem by File-ID");
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#createDatabase(java.lang.String, java.lang.String, java.lang.String, db.buffers.BufferFile, java.lang.String, java.lang.String, boolean, ghidra.util.task.TaskMonitor, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public synchronized LocalDatabaseItem createDatabase(String parentPath, String name,
|
||||
String fileID, BufferFile bufferFile, String comment, String contentType,
|
||||
@ -483,9 +467,6 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
return item;
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#createDatabase(java.lang.String, java.lang.String, java.lang.String, int, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public LocalManagedBufferFile createDatabase(String parentPath, String name, String fileID,
|
||||
String contentType, int bufferSize, String user, String projectPath)
|
||||
@ -513,9 +494,6 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
return bufferFile;
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#createDataFile(java.lang.String, java.lang.String, java.io.InputStream, java.lang.String, java.lang.String, ghidra.util.task.TaskMonitor)
|
||||
*/
|
||||
@Override
|
||||
public synchronized LocalDataFile createDataFile(String parentPath, String name,
|
||||
InputStream istream, String comment, String contentType, TaskMonitor monitor)
|
||||
@ -546,9 +524,6 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
return dataFile;
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#createFile(java.lang.String, java.lang.String, java.io.File, ghidra.util.task.TaskMonitor, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public LocalDatabaseItem createFile(String parentPath, String name, File packedFile,
|
||||
TaskMonitor monitor, String user)
|
||||
@ -591,9 +566,6 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
return item;
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#moveItem(java.lang.String, java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public synchronized void moveItem(String folderPath, String name, String newFolderPath,
|
||||
String newName) throws IOException, InvalidNameException {
|
||||
@ -652,9 +624,6 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
@Override
|
||||
public abstract boolean folderExists(String folderPath);
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#fileExists(java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public boolean fileExists(String folderPath, String name) {
|
||||
try {
|
||||
@ -669,9 +638,6 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#addFileSystemListener(ghidra.framework.store.FileSystemListener)
|
||||
*/
|
||||
@Override
|
||||
public void addFileSystemListener(FileSystemListener listener) {
|
||||
if (eventManager != null) {
|
||||
@ -679,9 +645,6 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @see ghidra.framework.store.FileSystem#removeFileSystemListener(ghidra.framework.store.FileSystemListener)
|
||||
*/
|
||||
@Override
|
||||
public void removeFileSystemListener(FileSystemListener listener) {
|
||||
if (eventManager != null) {
|
||||
@ -829,8 +792,7 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
|
||||
@Override
|
||||
public boolean isShared() {
|
||||
// Does not support direct sharing in production
|
||||
return isShared;
|
||||
return false;
|
||||
}
|
||||
|
||||
// static void testValidPathLength(File file) throws IOException {
|
||||
@ -846,6 +808,17 @@ public abstract class LocalFileSystem implements FileSystem {
|
||||
if (eventManager != null) {
|
||||
eventManager.dispose();
|
||||
}
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if file-system has been disposed.
|
||||
* @throws IOException if file-system has been disposed
|
||||
*/
|
||||
protected void checkDisposed() throws IOException {
|
||||
if (disposed) {
|
||||
throw new IOException("File-system has been disposed");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean migrationInProgress() {
|
||||
|
@ -929,12 +929,7 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest {
|
||||
FileSystemEventManager eventManager =
|
||||
(FileSystemEventManager) TestUtils.getInstanceField("eventManager", fs);
|
||||
|
||||
try {
|
||||
eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
failWithException("Interrupted waiting for filesystem events", e);
|
||||
}
|
||||
eventManager.flushEvents(DEFAULT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -507,6 +507,41 @@ public abstract class AbstractGenericTest extends AbstractGTest {
|
||||
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
|
||||
* has the type classType. This method is only really useful if it is known
|
||||
|
@ -1,6 +1,5 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -17,8 +16,7 @@
|
||||
package generic.util;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.*;
|
||||
|
||||
public class FileChannelLock {
|
||||
|
||||
@ -51,7 +49,7 @@ public class FileChannelLock {
|
||||
|
||||
return isLocked;
|
||||
}
|
||||
catch (IOException e) {
|
||||
catch (IOException | OverlappingFileLockException e) {
|
||||
release();
|
||||
}
|
||||
return false;
|
||||
|
@ -36,6 +36,7 @@ src/main/resources/images/disconnected.gif||GHIDRA||reviewed||END|
|
||||
src/main/resources/images/disk.png||FAMFAMFAM Icons - CC 2.5||||END|
|
||||
src/main/resources/images/face-glasses.png||Tango Icons - Public Domain|||tango icon set|END|
|
||||
src/main/resources/images/folder_add.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
||||
src/main/resources/images/link.png||Crystal Clear Icons - LGPL 2.1||||END|
|
||||
src/main/resources/images/lock.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
||||
src/main/resources/images/monitor.png||FAMFAMFAM Icons - CC 2.5|||silk|END|
|
||||
src/main/resources/images/noneInTool.gif||GHIDRA||reviewed||END|
|
||||
|
@ -6,10 +6,14 @@ icon.project.data.file.ghidra.unsupported = unknownFile.gif
|
||||
icon.project.data.file.ghidra.checked.out = icon.check
|
||||
icon.project.data.file.ghidra.checked.out.exclusive = checkex.png
|
||||
icon.project.data.file.ghidra.hijacked = small_hijack.gif
|
||||
icon.project.data.file.ghidra.read.only = user-busy.png [size(10,10)]
|
||||
icon.project.data.file.ghidra.read.only = user-busy.png [size(8,8)]
|
||||
icon.project.data.file.ghidra.not.latest = checkNotLatest.gif
|
||||
|
||||
icon.content.handler.link = link.png
|
||||
icon.content.handler.link.overlay = EMPTY_ICON[size(16,16)]{icon.content.handler.link[move(0,8)]} // lower-left of 16x16 icon
|
||||
|
||||
icon.content.handler.linked.folder.open = icon.datatree.node.domain.folder.open{icon.content.handler.link.overlay}
|
||||
icon.content.handler.linked.folder.closed = icon.datatree.node.domain.folder.closed{icon.content.handler.link.overlay}
|
||||
|
||||
[Dark Defaults]
|
||||
|
||||
|
@ -19,9 +19,7 @@ import java.io.IOException;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import db.DBHandle;
|
||||
import ghidra.framework.model.ChangeSet;
|
||||
import ghidra.framework.model.DomainObject;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.store.FileSystem;
|
||||
import ghidra.framework.store.FolderItem;
|
||||
import ghidra.util.InvalidNameException;
|
||||
@ -31,15 +29,17 @@ import ghidra.util.exception.VersionException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
/**
|
||||
* NOTE: ALL ContentHandler CLASSES MUST END IN "ContentHandler". If not,
|
||||
* NOTE: ALL ContentHandler implementations MUST END IN "ContentHandler". If not,
|
||||
* the ClassSearcher will not find them.
|
||||
*
|
||||
* <code>ContentHandler</code> defines an application interface for converting
|
||||
* between a specific domain object implementation and folder item storage.
|
||||
* This interface also defines a method which provides an appropriate icon
|
||||
* corresponding to the content.
|
||||
*
|
||||
* @param <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 MISSING_CONTENT = "Missing-File";
|
||||
@ -56,7 +56,8 @@ public interface ContentHandler extends ExtensionPoint {
|
||||
* @param domainObject the domain object to store in the newly created folder item
|
||||
* @param monitor the monitor that allows the user to cancel
|
||||
* @return checkout ID for new item
|
||||
* @throws IOException if an i/o error occurs
|
||||
* @throws IOException if an IO error occurs or an unsupported {@code domainObject}
|
||||
* implementation is specified.
|
||||
* @throws InvalidNameException if the specified name contains invalid characters
|
||||
* @throws CancelledException if the user cancels
|
||||
*/
|
||||
@ -77,12 +78,12 @@ public interface ContentHandler extends ExtensionPoint {
|
||||
* set.
|
||||
* @param monitor the monitor that allows the user to cancel
|
||||
* @return immutable domain object
|
||||
* @throws IOException if a folder item access error occurs
|
||||
* @throws IOException if an IO or folder item access error occurs
|
||||
* @throws CancelledException if operation is cancelled by user
|
||||
* @throws VersionException if unable to handle file content due to version
|
||||
* difference which could not be handled.
|
||||
*/
|
||||
DomainObjectAdapter getImmutableObject(FolderItem item, Object consumer, int version,
|
||||
T getImmutableObject(FolderItem item, Object consumer, int version,
|
||||
int minChangeVersion, TaskMonitor monitor)
|
||||
throws IOException, CancelledException, VersionException;
|
||||
|
||||
@ -98,12 +99,12 @@ public interface ContentHandler extends ExtensionPoint {
|
||||
* @param consumer consumer of the returned object
|
||||
* @param monitor the monitor that allows the user to cancel
|
||||
* @return read-only domain object
|
||||
* @throws IOException if a folder item access error occurs
|
||||
* @throws IOException if an IO or folder item access error occurs
|
||||
* @throws CancelledException if operation is cancelled by user
|
||||
* @throws VersionException if unable to handle file content due to version
|
||||
* difference which could not be handled.
|
||||
*/
|
||||
DomainObjectAdapter getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
|
||||
T getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
|
||||
Object consumer, TaskMonitor monitor)
|
||||
throws IOException, VersionException, CancelledException;
|
||||
|
||||
@ -121,12 +122,12 @@ public interface ContentHandler extends ExtensionPoint {
|
||||
* @param consumer consumer of the returned object
|
||||
* @param monitor cancelable task monitor
|
||||
* @return updateable domain object
|
||||
* @throws IOException if a folder item access error occurs
|
||||
* @throws IOException if an IO or folder item access error occurs
|
||||
* @throws CancelledException if operation is cancelled by user
|
||||
* @throws VersionException if unable to handle file content due to version
|
||||
* difference which could not be handled.
|
||||
*/
|
||||
DomainObjectAdapter getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
|
||||
T getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
|
||||
boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
|
||||
throws IOException, CancelledException, VersionException;
|
||||
|
||||
@ -138,7 +139,7 @@ public interface ContentHandler extends ExtensionPoint {
|
||||
* @param newerVersion the newer version number
|
||||
* @return the set of changes that were made
|
||||
* @throws VersionException if a database version change prevents reading of data.
|
||||
* @throws IOException if a folder item access error occurs or change set was
|
||||
* @throws IOException if an IO or folder item access error occurs or change set was
|
||||
* produced by newer version of software and can not be read
|
||||
*/
|
||||
ChangeSet getChangeSet(FolderItem versionedFolderItem, int olderVersion, int newerVersion)
|
||||
@ -161,55 +162,46 @@ public interface ContentHandler extends ExtensionPoint {
|
||||
/**
|
||||
* Returns true if the content type is always private
|
||||
* (i.e., can not be added to the versioned filesystem).
|
||||
* @return true if private content type, else false
|
||||
*/
|
||||
boolean isPrivateContentType();
|
||||
|
||||
/**
|
||||
* Returns list of unique content-types supported.
|
||||
* A minimum of one content-type will be returned. If more than one
|
||||
* is returned, these are considered equivalent aliases.
|
||||
* Returns a unique content-type identifier
|
||||
* @return content type identifier for associated domain object(s).
|
||||
*/
|
||||
String getContentType();
|
||||
|
||||
/**
|
||||
* A string that is meant to be presented to the user.
|
||||
* @return user friendly content type for associated domain object(s).
|
||||
*/
|
||||
String getContentTypeDisplayString();
|
||||
|
||||
/**
|
||||
* Returns the Icon associated with this handlers content type.
|
||||
* @return base icon to be used for a {@link DomainFile} with the associated content type.
|
||||
*/
|
||||
Icon getIcon();
|
||||
|
||||
/**
|
||||
* Returns the name of the default tool that should be used to open this content type
|
||||
* Returns the name of the default tool that should be used to open this content type.
|
||||
* @return associated default tool for this content type
|
||||
*/
|
||||
String getDefaultToolName();
|
||||
|
||||
/**
|
||||
* Returns domain object implementation class supported.
|
||||
* @return implementation class for the associated {@link DomainObjectAdapter} implementation.
|
||||
*/
|
||||
Class<? extends DomainObject> getDomainObjectClass();
|
||||
Class<T> getDomainObjectClass();
|
||||
|
||||
/**
|
||||
* Create user data file associated with existing content.
|
||||
* This facilitates the lazy creation of the user data file.
|
||||
* @param associatedDomainObj associated domain object corresponding to this content handler
|
||||
* @param userDbh user data handle
|
||||
* @param userfs private user data filesystem
|
||||
* @param monitor task monitor
|
||||
* @throws IOException if an access error occurs
|
||||
* @throws CancelledException if operation is cancelled by user
|
||||
* If linking is supported return an instanceof the appropriate {@link LinkHandler}.
|
||||
* @return corresponding link handler or null if not supported.
|
||||
*/
|
||||
void saveUserDataFile(DomainObject associatedDomainObj, DBHandle userDbh, FileSystem userfs,
|
||||
TaskMonitor monitor) throws CancelledException, IOException;
|
||||
|
||||
/**
|
||||
* Remove user data file associated with an existing folder item.
|
||||
* @param item folder item
|
||||
* @param userFilesystem
|
||||
* @throws IOException if an access error occurs
|
||||
*/
|
||||
void removeUserDataFile(FolderItem item, FileSystem userFilesystem) throws IOException;
|
||||
default LinkHandler<?> getLinkHandler() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,13 +18,10 @@ package ghidra.framework.data;
|
||||
import java.io.IOException;
|
||||
|
||||
import db.DBHandle;
|
||||
import db.buffers.BufferFile;
|
||||
import db.buffers.ManagedBufferFile;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.model.DomainObject;
|
||||
import ghidra.framework.store.*;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.InvalidNameException;
|
||||
import ghidra.util.SystemUtilities;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
@ -32,8 +29,11 @@ import ghidra.util.task.TaskMonitor;
|
||||
* <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 DBContentHandler implements ContentHandler {
|
||||
public abstract class DBContentHandler<T extends DomainObjectAdapterDB>
|
||||
implements ContentHandler<T> {
|
||||
|
||||
/**
|
||||
* Create a new database file from an open database handle.
|
||||
@ -70,6 +70,7 @@ public abstract class DBContentHandler implements ContentHandler {
|
||||
bf.delete();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
abortCreate(fs, path, name, checkoutId);
|
||||
}
|
||||
@ -77,7 +78,7 @@ public abstract class DBContentHandler implements ContentHandler {
|
||||
return checkoutId;
|
||||
}
|
||||
|
||||
private void abortCreate(FileSystem fs, String path, String name, long checkoutId) {
|
||||
protected void abortCreate(FileSystem fs, String path, String name, long checkoutId) {
|
||||
try {
|
||||
FolderItem item = fs.getItem(path, name);
|
||||
if (item != null) {
|
||||
@ -92,98 +93,4 @@ public abstract class DBContentHandler implements ContentHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return user data content type corresponding to associatedContentType.
|
||||
*/
|
||||
private static String getUserDataContentType(String associatedContentType) {
|
||||
return associatedContentType + "UserData";
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ghidra.framework.data.ContentHandler#saveUserDataFile(ghidra.framework.model.DomainObject, db.DBHandle, ghidra.framework.store.FileSystem, ghidra.util.task.TaskMonitor)
|
||||
*/
|
||||
@Override
|
||||
public final void saveUserDataFile(DomainObject domainObj, DBHandle userDbh, FileSystem userfs,
|
||||
TaskMonitor monitor) throws CancelledException, IOException {
|
||||
if (userfs.isVersioned()) {
|
||||
throw new IllegalArgumentException("User data file-system may not be versioned");
|
||||
}
|
||||
String associatedContentType = getContentType();
|
||||
DomainFile associatedDf = domainObj.getDomainFile();
|
||||
if (associatedDf == null) {
|
||||
throw new IllegalStateException("associated " + associatedContentType +
|
||||
" file must be saved before user data can be saved");
|
||||
}
|
||||
String associatedFileID = associatedDf.getFileID();
|
||||
if (associatedFileID == null) {
|
||||
Msg.error(this, associatedContentType + " '" + associatedDf.getName() +
|
||||
"' has not been assigned a file ID, user settings can not be saved!");
|
||||
return;
|
||||
}
|
||||
String path = "/";
|
||||
String name = ProjectFileManager.getUserDataFilename(associatedFileID);
|
||||
BufferFile bf = null;
|
||||
boolean success = false;
|
||||
try {
|
||||
bf =
|
||||
userfs.createDatabase(path, name, FileIDFactory.createFileID(),
|
||||
getUserDataContentType(associatedContentType), userDbh.getBufferSize(),
|
||||
SystemUtilities.getUserName(), null);
|
||||
userDbh.saveAs(bf, true, monitor);
|
||||
success = true;
|
||||
}
|
||||
catch (InvalidNameException e) {
|
||||
throw new AssertException("Unexpected Error", e);
|
||||
}
|
||||
finally {
|
||||
if (bf != null && !success) {
|
||||
try {
|
||||
bf.delete();
|
||||
}
|
||||
catch (IOException e) {
|
||||
}
|
||||
abortCreate(userfs, path, name, FolderItem.DEFAULT_CHECKOUT_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ghidra.framework.data.ContentHandler#removeUserDataFile(ghidra.framework.store.FolderItem, ghidra.framework.store.FileSystem)
|
||||
*/
|
||||
@Override
|
||||
public final void removeUserDataFile(FolderItem associatedItem, FileSystem userfs)
|
||||
throws IOException {
|
||||
String path = "/";
|
||||
String name = ProjectFileManager.getUserDataFilename(associatedItem.getFileID());
|
||||
FolderItem item = userfs.getItem(path, name);
|
||||
if (item != null) {
|
||||
item.delete(-1, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open user data file associatedDbh
|
||||
* @param associatedFileID
|
||||
* @param associatedContentType
|
||||
* @param userfs
|
||||
* @param monitor
|
||||
* @return user data file database handle
|
||||
* @throws IOException
|
||||
* @throws CancelledException
|
||||
*/
|
||||
protected final DBHandle openAssociatedUserFile(String associatedFileID,
|
||||
String associatedContentType, FileSystem userfs, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
String path = "/";
|
||||
String name = ProjectFileManager.getUserDataFilename(associatedFileID);
|
||||
FolderItem item = userfs.getItem(path, name);
|
||||
if (item == null || !(item instanceof DatabaseItem) ||
|
||||
!getUserDataContentType(associatedContentType).equals(item.getContentType())) {
|
||||
return null;
|
||||
}
|
||||
DatabaseItem dbItem = (DatabaseItem) item;
|
||||
BufferFile bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID);
|
||||
return new DBHandle(bf, false, monitor);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -17,11 +17,14 @@ package ghidra.framework.data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURL;
|
||||
import ghidra.framework.store.ItemCheckoutStatus;
|
||||
import ghidra.framework.store.Version;
|
||||
import ghidra.framework.store.db.PackedDatabase;
|
||||
@ -111,6 +114,29 @@ public class DomainFileProxy implements DomainFile {
|
||||
return parentPath + DomainFolder.SEPARATOR + getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getSharedProjectURL() {
|
||||
if (projectLocation != null && version == DomainFile.DEFAULT_VERSION) {
|
||||
URL projectURL = projectLocation.getURL();
|
||||
if (GhidraURL.isServerRepositoryURL(projectURL)) {
|
||||
try {
|
||||
// Direct URL construction done so that ghidra protocol
|
||||
// extension may be supported
|
||||
String urlStr = projectURL.toExternalForm();
|
||||
if (urlStr.endsWith("/")) {
|
||||
urlStr = urlStr.substring(0, urlStr.length() - 1);
|
||||
}
|
||||
urlStr += getPathname();
|
||||
return new URL(urlStr);
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(DomainFile df) {
|
||||
return getName().compareToIgnoreCase(df.getName());
|
||||
@ -143,7 +169,7 @@ public class DomainFileProxy implements DomainFile {
|
||||
DomainObjectAdapter dobj = getDomainObject();
|
||||
if (dobj != null) {
|
||||
try {
|
||||
ContentHandler ch = DomainObjectAdapter.getContentHandler(dobj);
|
||||
ContentHandler<?> ch = DomainObjectAdapter.getContentHandler(dobj);
|
||||
return ch.getContentType();
|
||||
}
|
||||
catch (IOException e) {
|
||||
@ -153,6 +179,27 @@ public class DomainFileProxy implements DomainFile {
|
||||
return "Unknown File";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLinkFile() {
|
||||
DomainObjectAdapter dobj = getDomainObject();
|
||||
if (dobj != null) {
|
||||
ContentHandler<?> ch;
|
||||
try {
|
||||
ch = DomainObjectAdapter.getContentHandler(dobj);
|
||||
return LinkHandler.class.isAssignableFrom(ch.getClass());
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFolder followLink() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends DomainObject> getDomainObjectClass() {
|
||||
DomainObjectAdapter dobj = getDomainObject();
|
||||
@ -228,6 +275,7 @@ public class DomainFileProxy implements DomainFile {
|
||||
dobj.release(consumer);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// ignore unknown consumer error
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,11 +304,6 @@ public class DomainFileProxy implements DomainFile {
|
||||
throw new UnsupportedOperationException("Repository operations not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVersionControlSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canAddToRepository() {
|
||||
return false;
|
||||
@ -303,6 +346,16 @@ public class DomainFileProxy implements DomainFile {
|
||||
throw new UnsupportedOperationException("Repository operations not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLinkingSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
|
||||
return null; // not supported by proxy file
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
@ -318,11 +371,8 @@ public class DomainFileProxy implements DomainFile {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ghidra.framework.model.DomainFile#copyVersionTo(int, ghidra.framework.model.DomainFolder, ghidra.util.task.TaskMonitor)
|
||||
*/
|
||||
@Override
|
||||
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
|
||||
public DomainFile copyVersionTo(int ver, DomainFolder destFolder, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
throw new UnsupportedOperationException("copyVersionTo unsupported for DomainFileProxy");
|
||||
}
|
||||
@ -420,7 +470,7 @@ public class DomainFileProxy implements DomainFile {
|
||||
throw new UnsupportedOperationException("packFile() only valid for Database files");
|
||||
}
|
||||
DomainObjectAdapterDB dbObj = (DomainObjectAdapterDB) domainObj;
|
||||
ContentHandler ch = DomainObjectAdapter.getContentHandler(domainObj);
|
||||
ContentHandler<?> ch = DomainObjectAdapter.getContentHandler(domainObj);
|
||||
PackedDatabase.packDatabase(dbObj.getDBHandle(), dbObj.getName(), ch.getContentType(), file,
|
||||
monitor);
|
||||
}
|
||||
|
@ -38,8 +38,8 @@ public abstract class DomainObjectAdapter implements DomainObject {
|
||||
protected final static String DEFAULT_NAME = "untitled";
|
||||
|
||||
private static Class<?> defaultDomainObjClass; // Domain object implementation mapped to unknown content type
|
||||
private static HashMap<String, ContentHandler> contentHandlerTypeMap; // maps content-type string to handler
|
||||
private static HashMap<Class<?>, ContentHandler> contentHandlerClassMap; // maps domain object class 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 ChangeListener contentHandlerUpdateListener = new ChangeListener() {
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
@ -399,7 +399,7 @@ public abstract class DomainObjectAdapter implements DomainObject {
|
||||
contentHandlerTypeMap.remove(null);
|
||||
}
|
||||
else {
|
||||
ContentHandler ch = contentHandlerClassMap.get(doClass);
|
||||
ContentHandler<?> ch = contentHandlerClassMap.get(doClass);
|
||||
if (ch != null) {
|
||||
contentHandlerTypeMap.put(null, ch);
|
||||
}
|
||||
@ -414,9 +414,9 @@ public abstract class DomainObjectAdapter implements DomainObject {
|
||||
* @return content handler
|
||||
* @throws IOException if no content handler can be found
|
||||
*/
|
||||
static synchronized ContentHandler getContentHandler(String contentType) throws IOException {
|
||||
static synchronized ContentHandler<?> getContentHandler(String contentType) throws IOException {
|
||||
checkContentHandlerMaps();
|
||||
ContentHandler ch = contentHandlerTypeMap.get(contentType);
|
||||
ContentHandler<?> ch = contentHandlerTypeMap.get(contentType);
|
||||
if (ch == null) {
|
||||
throw new IOException("Content handler not found for " + contentType);
|
||||
}
|
||||
@ -430,10 +430,10 @@ public abstract class DomainObjectAdapter implements DomainObject {
|
||||
* @return content handler
|
||||
* @throws IOException if no content handler can be found
|
||||
*/
|
||||
public static synchronized ContentHandler getContentHandler(DomainObject dobj)
|
||||
public static synchronized ContentHandler<?> getContentHandler(DomainObject dobj)
|
||||
throws IOException {
|
||||
checkContentHandlerMaps();
|
||||
ContentHandler ch = contentHandlerClassMap.get(dobj.getClass());
|
||||
ContentHandler<?> ch = contentHandlerClassMap.get(dobj.getClass());
|
||||
if (ch == null) {
|
||||
throw new IOException("Content handler not found for " + dobj.getClass().getName());
|
||||
}
|
||||
@ -450,17 +450,15 @@ public abstract class DomainObjectAdapter implements DomainObject {
|
||||
}
|
||||
|
||||
private synchronized static void getContentHandlers() {
|
||||
contentHandlerClassMap = new HashMap<Class<?>, ContentHandler>();
|
||||
contentHandlerTypeMap = new HashMap<String, ContentHandler>();
|
||||
contentHandlerClassMap = new HashMap<Class<?>, ContentHandler<?>>();
|
||||
contentHandlerTypeMap = new HashMap<String, ContentHandler<?>>();
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
List<ContentHandler> handlers = ClassSearcher.getInstances(ContentHandler.class);
|
||||
for (ContentHandler ch : handlers) {
|
||||
String type = ch.getContentType();
|
||||
Class<?> DOClass = ch.getDomainObjectClass();
|
||||
if (type != null && DOClass != null) {
|
||||
contentHandlerClassMap.put(DOClass, ch);
|
||||
contentHandlerTypeMap.put(type, ch);
|
||||
continue;
|
||||
for (ContentHandler<?> ch : handlers) {
|
||||
contentHandlerTypeMap.put(ch.getContentType(), ch);
|
||||
if (!(ch instanceof LinkHandler<?>)) {
|
||||
contentHandlerClassMap.put(ch.getDomainObjectClass(), ch);
|
||||
}
|
||||
}
|
||||
setDefaultContentClass(defaultDomainObjClass);
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
package ghidra.framework.data;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.Icon;
|
||||
@ -24,8 +25,7 @@ import ghidra.framework.model.*;
|
||||
import ghidra.framework.store.ItemCheckoutStatus;
|
||||
import ghidra.framework.store.Version;
|
||||
import ghidra.framework.store.local.LocalFileSystem;
|
||||
import ghidra.util.InvalidNameException;
|
||||
import ghidra.util.ReadOnlyException;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.*;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
@ -123,6 +123,17 @@ public class GhidraFile implements DomainFile {
|
||||
return fileManager.getProjectLocator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getSharedProjectURL() {
|
||||
try {
|
||||
return getFileData().getSharedProjectURL();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
try {
|
||||
@ -134,6 +145,37 @@ public class GhidraFile implements DomainFile {
|
||||
return ContentHandler.UNKNOWN_CONTENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLinkFile() {
|
||||
try {
|
||||
return getFileData().isLinkFile();
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFolder followLink() {
|
||||
try {
|
||||
return FolderLinkContentHandler.getReadOnlyLinkedFolder(this);
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.error(this, "Failed to following folder-link: " + getPathname());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLinkingSupported() {
|
||||
try {
|
||||
return getFileData().isLinkingSupported();
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends DomainObject> getDomainObjectClass() {
|
||||
try {
|
||||
@ -146,7 +188,7 @@ public class GhidraFile implements DomainFile {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFolder getParent() {
|
||||
public GhidraFolder getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@ -257,7 +299,7 @@ public class GhidraFile implements DomainFile {
|
||||
catch (IOException e) {
|
||||
fileError(e);
|
||||
}
|
||||
return GhidraFileData.UNSUPPORTED_FILE_ICON;
|
||||
return UNSUPPORTED_FILE_ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -353,17 +395,6 @@ public class GhidraFile implements DomainFile {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVersionControlSupported() {
|
||||
try {
|
||||
return getFileData().isVersionControlSupported();
|
||||
}
|
||||
catch (IOException e) {
|
||||
fileError(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVersioned() {
|
||||
try {
|
||||
@ -482,6 +513,9 @@ public class GhidraFile implements DomainFile {
|
||||
|
||||
@Override
|
||||
public GhidraFile moveTo(DomainFolder newParent) throws IOException {
|
||||
if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
|
||||
throw new UnsupportedOperationException("newParent does not support moveTo");
|
||||
}
|
||||
GhidraFolder newGhidraParent = (GhidraFolder) newParent;
|
||||
return getFileData().moveTo(newGhidraParent.getFolderData());
|
||||
}
|
||||
@ -489,15 +523,30 @@ public class GhidraFile implements DomainFile {
|
||||
@Override
|
||||
public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException,
|
||||
CancelledException {
|
||||
GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation
|
||||
if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
|
||||
throw new UnsupportedOperationException("newParent does not support copyTo");
|
||||
}
|
||||
GhidraFolder newGhidraParent = (GhidraFolder) newParent;
|
||||
return getFileData().copyTo(newGhidraParent.getFolderData(),
|
||||
monitor != null ? monitor : TaskMonitor.DUMMY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
|
||||
if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
|
||||
throw new UnsupportedOperationException("newParent does not support copyToAsLink");
|
||||
}
|
||||
GhidraFolder newGhidraParent = (GhidraFolder) newParent;
|
||||
return getFileData().copyToAsLink(newGhidraParent.getFolderData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
GhidraFolder destGhidraFolder = (GhidraFolder) destFolder; // assumes single implementation
|
||||
if (!GhidraFolder.class.isAssignableFrom(destFolder.getClass())) {
|
||||
throw new UnsupportedOperationException("destFolder does not support copyVersionTo");
|
||||
}
|
||||
GhidraFolder destGhidraFolder = (GhidraFolder) destFolder;
|
||||
return getFileData().copyVersionTo(version, destGhidraFolder.getFolderData(),
|
||||
monitor != null ? monitor : TaskMonitor.DUMMY);
|
||||
}
|
||||
@ -507,7 +556,7 @@ public class GhidraFile implements DomainFile {
|
||||
* only when a non shared project is being converted to a shared project.
|
||||
* @param monitor task monitor
|
||||
* @throws IOException if an IO error occurs
|
||||
* @throws CancelledException if task cancelled
|
||||
* @throws CancelledException if task is cancelled
|
||||
*/
|
||||
void convertToPrivateFile(TaskMonitor monitor) throws IOException, CancelledException {
|
||||
getFileData().convertToPrivateFile(
|
||||
@ -596,6 +645,10 @@ public class GhidraFile implements DomainFile {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
ProjectLocator projectLocator = parent.getProjectData().getProjectLocator();
|
||||
if (projectLocator.isTransient()) {
|
||||
return fileManager.getProjectLocator().getName() + getPathname();
|
||||
}
|
||||
return fileManager.getProjectLocator().getName() + ":" + getPathname();
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,8 @@ package ghidra.framework.data;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ -27,9 +29,9 @@ import db.Field;
|
||||
import db.buffers.*;
|
||||
import generic.theme.GColor;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.framework.client.ClientUtil;
|
||||
import ghidra.framework.client.NotConnectedException;
|
||||
import ghidra.framework.client.*;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURL;
|
||||
import ghidra.framework.store.*;
|
||||
import ghidra.framework.store.FileSystem;
|
||||
import ghidra.framework.store.local.LocalFileSystem;
|
||||
@ -37,12 +39,14 @@ import ghidra.framework.store.local.LocalFolderItem;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.*;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import ghidra.util.task.TaskMonitorAdapter;
|
||||
import resources.MultiIcon;
|
||||
import resources.icons.TranslateIcon;
|
||||
|
||||
public class GhidraFileData {
|
||||
|
||||
static final int ICON_WIDTH = 18;
|
||||
static final int ICON_HEIGHT = 17;
|
||||
|
||||
private static final boolean ALWAYS_MERGE = System.getProperty("ForceMerge") != null;
|
||||
|
||||
//@formatter:off
|
||||
@ -50,6 +54,7 @@ public class GhidraFileData {
|
||||
public static final Icon CHECKED_OUT_ICON = new GIcon("icon.project.data.file.ghidra.checked.out");
|
||||
public static final Icon CHECKED_OUT_EXCLUSIVE_ICON = new GIcon("icon.project.data.file.ghidra.checked.out.exclusive");
|
||||
public static final Icon HIJACKED_ICON = new GIcon("icon.project.data.file.ghidra.hijacked");
|
||||
|
||||
public static final Icon VERSION_ICON = new VersionIcon();
|
||||
public static final Icon READ_ONLY_ICON = new GIcon("icon.project.data.file.ghidra.read.only");
|
||||
public static final Icon NOT_LATEST_CHECKED_OUT_ICON = new GIcon("icon.project.data.file.ghidra.not.latest");
|
||||
@ -202,9 +207,32 @@ public class GhidraFileData {
|
||||
return new GhidraFile(parent.getDomainFolder(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a remote Ghidra URL for this domain file if available within a remote repository.
|
||||
* @return remote Ghidra URL for this file or null
|
||||
*/
|
||||
URL getSharedProjectURL() {
|
||||
synchronized (fileSystem) {
|
||||
RepositoryAdapter repository = parent.getProjectFileManager().getRepository();
|
||||
if (versionedFolderItem != null && repository != null) {
|
||||
URL folderURL = parent.getDomainFolder().getSharedProjectURL();
|
||||
try {
|
||||
// Direct URL construction done so that ghidra protocol
|
||||
// extension may be supported
|
||||
return new URL(folderURL.toExternalForm() + name);
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reassign a new file-ID to resolve file-ID conflict.
|
||||
* Conflicts can occur as a result of a cancelled check-out.
|
||||
* @throws IOException if an IO error occurs
|
||||
*/
|
||||
void resetFileID() throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
@ -262,6 +290,8 @@ public class GhidraFileData {
|
||||
String getContentType() {
|
||||
synchronized (fileSystem) {
|
||||
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
|
||||
// this can happen when we are trying to load a version file from
|
||||
// a server to which we are not connected
|
||||
if (item == null) {
|
||||
return ContentHandler.MISSING_CONTENT;
|
||||
}
|
||||
@ -270,14 +300,27 @@ public class GhidraFileData {
|
||||
}
|
||||
}
|
||||
|
||||
Class<? extends DomainObject> getDomainObjectClass() {
|
||||
/**
|
||||
* Get content handler
|
||||
* @return content handler
|
||||
* @throws IOException if an IO error occurs, file not found, or unsupported content
|
||||
*/
|
||||
ContentHandler<?> getContentHandler() throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
|
||||
// this can happen when we are trying to load a version file from
|
||||
// a server to which we are not connected
|
||||
if (item == null) {
|
||||
throw new FileNotFoundException(name + " not found");
|
||||
}
|
||||
return DomainObjectAdapter.getContentHandler(item.getContentType());
|
||||
}
|
||||
}
|
||||
|
||||
Class<? extends DomainObject> getDomainObjectClass() {
|
||||
synchronized (fileSystem) {
|
||||
try {
|
||||
ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType());
|
||||
if (ch != null) {
|
||||
return ch.getDomainObjectClass();
|
||||
}
|
||||
return getContentHandler().getDomainObjectClass();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore missing content handler
|
||||
@ -289,8 +332,7 @@ public class GhidraFileData {
|
||||
ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException {
|
||||
synchronized (fileSystem) {
|
||||
if (versionedFolderItem != null && folderItem != null && folderItem.isCheckedOut()) {
|
||||
ContentHandler ch =
|
||||
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
|
||||
ContentHandler<?> ch = getContentHandler();
|
||||
return ch.getChangeSet(versionedFolderItem, folderItem.getCheckoutVersion(),
|
||||
versionedFolderItem.getCurrentVersion());
|
||||
}
|
||||
@ -305,10 +347,9 @@ public class GhidraFileData {
|
||||
DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover,
|
||||
TaskMonitor monitor) throws VersionException, IOException, CancelledException {
|
||||
FolderItem myFolderItem;
|
||||
ContentHandler ch;
|
||||
DomainObjectAdapter domainObj = null;
|
||||
synchronized (fileSystem) {
|
||||
if (fileSystem.isReadOnly()) {
|
||||
if (fileSystem.isReadOnly() || isLinkFile()) {
|
||||
return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor);
|
||||
}
|
||||
domainObj = getOpenedDomainObject();
|
||||
@ -321,8 +362,8 @@ public class GhidraFileData {
|
||||
return domainObj;
|
||||
}
|
||||
}
|
||||
ContentHandler<?> ch = getContentHandler();
|
||||
if (folderItem == null) {
|
||||
ch = DomainObjectAdapter.getContentHandler(versionedFolderItem.getContentType());
|
||||
DomainObjectAdapter doa = ch.getReadOnlyObject(versionedFolderItem,
|
||||
DomainFile.DEFAULT_VERSION, true, consumer, monitor);
|
||||
doa.setChanged(false);
|
||||
@ -331,7 +372,6 @@ public class GhidraFileData {
|
||||
proxy.setLastModified(getLastModifiedTime());
|
||||
return doa;
|
||||
}
|
||||
ch = DomainObjectAdapter.getContentHandler(folderItem.getContentType());
|
||||
myFolderItem = folderItem;
|
||||
|
||||
domainObj = ch.getDomainObject(myFolderItem, parent.getUserFileSystem(),
|
||||
@ -368,14 +408,7 @@ public class GhidraFileData {
|
||||
FolderItem item =
|
||||
(folderItem != null && version == DomainFile.DEFAULT_VERSION) ? folderItem
|
||||
: versionedFolderItem;
|
||||
|
||||
// this can happen when we are trying to load a version file from
|
||||
// a server to which we are not connected
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType());
|
||||
ContentHandler<?> ch = getContentHandler();
|
||||
DomainObjectAdapter doa = ch.getReadOnlyObject(item, version, true, consumer, monitor);
|
||||
doa.setChanged(false);
|
||||
|
||||
@ -390,15 +423,12 @@ public class GhidraFileData {
|
||||
throws VersionException, IOException, CancelledException {
|
||||
synchronized (fileSystem) {
|
||||
DomainObjectAdapter obj = null;
|
||||
ContentHandler<?> ch = getContentHandler();
|
||||
if (versionedFolderItem == null ||
|
||||
(version == DomainFile.DEFAULT_VERSION && folderItem != null) || isHijacked()) {
|
||||
ContentHandler ch =
|
||||
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
|
||||
obj = ch.getImmutableObject(folderItem, consumer, version, -1, monitor);
|
||||
}
|
||||
else {
|
||||
ContentHandler ch =
|
||||
DomainObjectAdapter.getContentHandler(versionedFolderItem.getContentType());
|
||||
obj = ch.getImmutableObject(versionedFolderItem, consumer, version, -1, monitor);
|
||||
}
|
||||
DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), obj,
|
||||
@ -419,8 +449,11 @@ public class GhidraFileData {
|
||||
}
|
||||
|
||||
boolean takeRecoverySnapshot() throws IOException {
|
||||
if (fileSystem.isReadOnly()) {
|
||||
return true;
|
||||
}
|
||||
DomainObjectAdapter dobj = fileManager.getOpenedDomainObject(getPathname());
|
||||
if (fileSystem.isReadOnly() || !(dobj instanceof DomainObjectAdapterDB) ||
|
||||
if (!(dobj instanceof DomainObjectAdapterDB) ||
|
||||
!dobj.isChanged()) {
|
||||
return true;
|
||||
}
|
||||
@ -481,13 +514,19 @@ public class GhidraFileData {
|
||||
private Icon generateIcon(boolean disabled) {
|
||||
if (parent == null) {
|
||||
// instance has been disposed
|
||||
return UNSUPPORTED_FILE_ICON;
|
||||
return DomainFile.UNSUPPORTED_FILE_ICON;
|
||||
}
|
||||
synchronized (fileSystem) {
|
||||
|
||||
boolean isLink = isLinkFile();
|
||||
|
||||
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
|
||||
|
||||
Icon baseIcon = new TranslateIcon(getBaseIcon(item), 1, 1);
|
||||
|
||||
if (versionedFolderItem != null) {
|
||||
MultiIcon multiIcon = new MultiIcon(VERSION_ICON, disabled);
|
||||
multiIcon.addIcon(getBaseIcon(item));
|
||||
multiIcon.addIcon(baseIcon);
|
||||
if (isHijacked()) {
|
||||
multiIcon.addIcon(HIJACKED_ICON);
|
||||
}
|
||||
@ -504,12 +543,15 @@ public class GhidraFileData {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isLink) {
|
||||
multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1));
|
||||
}
|
||||
return multiIcon;
|
||||
}
|
||||
else if (folderItem != null) {
|
||||
MultiIcon multiIcon = new MultiIcon(getBaseIcon(item), disabled);
|
||||
MultiIcon multiIcon = new MultiIcon(baseIcon, disabled, ICON_WIDTH, ICON_HEIGHT);
|
||||
if (isReadOnly() && !fileSystem.isReadOnly()) {
|
||||
multiIcon.addIcon(new TranslateIcon(READ_ONLY_ICON, 6, 6));
|
||||
multiIcon.addIcon(new TranslateIcon(READ_ONLY_ICON, 8, 9));
|
||||
}
|
||||
if (isCheckedOut()) {
|
||||
if (isCheckedOutExclusive()) {
|
||||
@ -519,23 +561,23 @@ public class GhidraFileData {
|
||||
multiIcon.addIcon(CHECKED_OUT_ICON);
|
||||
}
|
||||
}
|
||||
if (isLink) {
|
||||
multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1));
|
||||
}
|
||||
return multiIcon;
|
||||
}
|
||||
}
|
||||
return UNSUPPORTED_FILE_ICON;
|
||||
return DomainFile.UNSUPPORTED_FILE_ICON;
|
||||
}
|
||||
|
||||
private Icon getBaseIcon(FolderItem item) {
|
||||
try {
|
||||
ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType());
|
||||
if (ch != null) {
|
||||
return ch.getIcon();
|
||||
}
|
||||
return getContentHandler().getIcon();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore missing content handler
|
||||
}
|
||||
return UNSUPPORTED_FILE_ICON;
|
||||
return DomainFile.UNSUPPORTED_FILE_ICON;
|
||||
}
|
||||
|
||||
boolean isChanged() {
|
||||
@ -593,9 +635,19 @@ public class GhidraFileData {
|
||||
boolean canAddToRepository() {
|
||||
synchronized (fileSystem) {
|
||||
try {
|
||||
return (!fileSystem.isReadOnly() && !versionedFileSystem.isReadOnly() &&
|
||||
folderItem != null && versionedFolderItem == null &&
|
||||
!folderItem.isCheckedOut() && isVersionControlSupported());
|
||||
if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) {
|
||||
return false;
|
||||
}
|
||||
if (folderItem == null || versionedFolderItem != null) {
|
||||
return false;
|
||||
}
|
||||
if (folderItem.isCheckedOut()) {
|
||||
return false;
|
||||
}
|
||||
if (isLinkFile()) {
|
||||
return GhidraURL.isServerRepositoryURL(LinkHandler.getURL(folderItem));
|
||||
}
|
||||
return !getContentHandler().isPrivateContentType();
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
@ -606,8 +658,11 @@ public class GhidraFileData {
|
||||
boolean canCheckout() {
|
||||
synchronized (fileSystem) {
|
||||
try {
|
||||
return folderItem == null && !fileSystem.isReadOnly() &&
|
||||
!versionedFileSystem.isReadOnly();
|
||||
if (folderItem != null || fileSystem.isReadOnly() ||
|
||||
versionedFileSystem.isReadOnly()) {
|
||||
return false;
|
||||
}
|
||||
return !isLinkFile();
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
@ -627,26 +682,6 @@ public class GhidraFileData {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isVersionControlSupported() {
|
||||
synchronized (fileSystem) {
|
||||
if (versionedFolderItem != null) {
|
||||
return true;
|
||||
}
|
||||
if (!(folderItem instanceof DatabaseItem)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
ContentHandler ch =
|
||||
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
|
||||
return !ch.isPrivateContentType();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore missing content handler
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int getVersion() {
|
||||
synchronized (fileSystem) {
|
||||
try {
|
||||
@ -714,18 +749,15 @@ public class GhidraFileData {
|
||||
throws IOException, CancelledException {
|
||||
DomainObjectAdapter oldDomainObj = null;
|
||||
synchronized (fileSystem) {
|
||||
if (!isVersionControlSupported()) {
|
||||
throw new AssertException("file type does supported version control");
|
||||
if (!canAddToRepository()) {
|
||||
if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) {
|
||||
throw new ReadOnlyException(
|
||||
"addToVersionControl permitted within writeable project and repository only");
|
||||
}
|
||||
throw new IOException("addToVersionControl not allowed for file");
|
||||
}
|
||||
if (versionedFolderItem != null) {
|
||||
throw new AssertException("file already versioned");
|
||||
}
|
||||
if (!versionedFileSystem.isOnline()) {
|
||||
throw new NotConnectedException("Not connected to repository server");
|
||||
}
|
||||
if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) {
|
||||
throw new ReadOnlyException(
|
||||
"addToVersionControl permitted within writeable project and repository only");
|
||||
if (isLinkFile()) {
|
||||
keepCheckedOut = false;
|
||||
}
|
||||
String parentPath = parent.getPathname();
|
||||
String user = ClientUtil.getUserName();
|
||||
@ -832,6 +864,9 @@ public class GhidraFileData {
|
||||
if (!versionedFileSystem.isOnline()) {
|
||||
throw new NotConnectedException("Not connected to repository server");
|
||||
}
|
||||
if (isLinkFile()) {
|
||||
return false;
|
||||
}
|
||||
String user = ClientUtil.getUserName();
|
||||
ProjectLocator projectLocator = parent.getProjectLocator();
|
||||
CheckoutType checkoutType;
|
||||
@ -934,8 +969,7 @@ public class GhidraFileData {
|
||||
if (checkinHandler.createKeepFile()) {
|
||||
DomainObject sourceObj = null;
|
||||
try {
|
||||
ContentHandler ch =
|
||||
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
|
||||
ContentHandler<?> ch = getContentHandler();
|
||||
sourceObj = ch.getImmutableObject(folderItem, this, DomainFile.DEFAULT_VERSION,
|
||||
-1, monitor);
|
||||
createKeepFile(sourceObj, monitor);
|
||||
@ -968,18 +1002,18 @@ public class GhidraFileData {
|
||||
|
||||
/**
|
||||
* Verify that current user is the checkout user for this file
|
||||
* @param caseName name of user case (e.g., checkin)
|
||||
* @return true if server/repository will permit current user to checkin,
|
||||
* or update checkout version of current file. (i.e., server login matches
|
||||
* @param operationName name of user case (e.g., checkin)
|
||||
* @throws IOException if server/repository will not permit current user to checkin,
|
||||
* or update checkout version of current file. (i.e., server login does not match
|
||||
* user name used at time of initial checkout)
|
||||
*/
|
||||
private void verifyRepoUser(String caseName) throws IOException {
|
||||
private void verifyRepoUser(String operationName) throws IOException {
|
||||
if (versionedFileSystem instanceof LocalFileSystem) {
|
||||
return; // rely on local project ownership
|
||||
}
|
||||
String repoUserName = versionedFileSystem.getUserName();
|
||||
if (repoUserName == null) {
|
||||
throw new IOException("File " + caseName + " not permitted (not connected)");
|
||||
throw new IOException("File " + operationName + " not permitted (not connected)");
|
||||
}
|
||||
ItemCheckoutStatus checkoutStatus = getCheckoutStatus();
|
||||
if (checkoutStatus == null) {
|
||||
@ -987,7 +1021,7 @@ public class GhidraFileData {
|
||||
}
|
||||
String checkoutUserName = checkoutStatus.getUser();
|
||||
if (!repoUserName.equals(checkoutUserName)) {
|
||||
throw new IOException("File " + caseName + " not permitted - checkout user '" +
|
||||
throw new IOException("File " + operationName + " not permitted - checkout user '" +
|
||||
checkoutUserName + "' differs from repository user '" + repoUserName + "'");
|
||||
}
|
||||
}
|
||||
@ -1016,7 +1050,7 @@ public class GhidraFileData {
|
||||
}
|
||||
verifyRepoUser("checkin");
|
||||
if (monitor == null) {
|
||||
monitor = TaskMonitorAdapter.DUMMY_MONITOR;
|
||||
monitor = TaskMonitor.DUMMY;
|
||||
}
|
||||
synchronized (fileSystem) {
|
||||
if (busy) {
|
||||
@ -1036,9 +1070,7 @@ public class GhidraFileData {
|
||||
|
||||
Msg.info(this, "Checkin with merge for " + name);
|
||||
|
||||
ContentHandler ch =
|
||||
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
|
||||
|
||||
ContentHandler<?> ch = getContentHandler();
|
||||
DomainObjectAdapter checkinObj = ch.getDomainObject(versionedFolderItem, null,
|
||||
folderItem.getCheckoutId(), okToUpgrade, false, this, monitor);
|
||||
checkinObj.setDomainFile(new DomainFileProxy(name, getParent().getPathname(),
|
||||
@ -1366,9 +1398,12 @@ public class GhidraFileData {
|
||||
|
||||
private void removeAssociatedUserDataFile() {
|
||||
try {
|
||||
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
|
||||
ContentHandler ch = DomainObjectAdapter.getContentHandler(item.getContentType());
|
||||
ch.removeUserDataFile(item, parent.getUserFileSystem());
|
||||
ContentHandler<?> ch = getContentHandler();
|
||||
if (ch instanceof DBWithUserDataContentHandler) {
|
||||
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
|
||||
((DBWithUserDataContentHandler<?>) ch).removeUserDataFile(item,
|
||||
parent.getUserFileSystem());
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore missing content handler
|
||||
@ -1403,7 +1438,7 @@ public class GhidraFileData {
|
||||
}
|
||||
verifyRepoUser("merge");
|
||||
if (monitor == null) {
|
||||
monitor = TaskMonitorAdapter.DUMMY_MONITOR;
|
||||
monitor = TaskMonitor.DUMMY;
|
||||
}
|
||||
synchronized (fileSystem) {
|
||||
if (busy) {
|
||||
@ -1425,8 +1460,7 @@ public class GhidraFileData {
|
||||
"Merge failed, file merge is not supported in headless mode");
|
||||
}
|
||||
|
||||
ContentHandler ch =
|
||||
DomainObjectAdapter.getContentHandler(folderItem.getContentType());
|
||||
ContentHandler<?> ch = getContentHandler();
|
||||
|
||||
// Test versioned file for VersionException
|
||||
int mergeVer = versionedFolderItem.getCurrentVersion();
|
||||
@ -1556,7 +1590,7 @@ public class GhidraFileData {
|
||||
checkInUse();
|
||||
GhidraFolderData oldParent = parent;
|
||||
String oldName = name;
|
||||
String newName = getTargetName(name, newParent);
|
||||
String newName = newParent.getTargetName(name);
|
||||
try {
|
||||
if (isHijacked()) {
|
||||
fileSystem.moveItem(parent.getPathname(), name, newParent.getPathname(),
|
||||
@ -1595,15 +1629,49 @@ public class GhidraFileData {
|
||||
}
|
||||
}
|
||||
|
||||
private String getTargetName(String preferredName, GhidraFolderData newParent)
|
||||
throws IOException {
|
||||
String newName = preferredName;
|
||||
int i = 1;
|
||||
while (newParent.getFileData(newName, false) != null) {
|
||||
newName = preferredName + "." + i;
|
||||
i++;
|
||||
boolean isLinkFile() {
|
||||
synchronized (fileSystem) {
|
||||
try {
|
||||
return LinkHandler.class.isAssignableFrom(getContentHandler().getClass());
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL associated with a link-file
|
||||
* @return link-file URL or null if not a link-file
|
||||
* @throws IOException if an IO error occurs
|
||||
*/
|
||||
URL getLinkFileURL() throws IOException {
|
||||
if (!isLinkFile()) {
|
||||
return null;
|
||||
}
|
||||
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
|
||||
return LinkHandler.getURL(item);
|
||||
}
|
||||
|
||||
public boolean isLinkingSupported() {
|
||||
synchronized (fileSystem) {
|
||||
try {
|
||||
return getContentHandler().getLinkHandler() != null;
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false; // ignore error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DomainFile copyToAsLink(GhidraFolderData newParentData) throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
LinkHandler<?> lh = getContentHandler().getLinkHandler();
|
||||
if (lh == null) {
|
||||
return null;
|
||||
}
|
||||
return newParentData.copyAsLink(fileManager, getPathname(), name, lh);
|
||||
}
|
||||
return newName;
|
||||
}
|
||||
|
||||
GhidraFile copyTo(GhidraFolderData newParentData, TaskMonitor monitor)
|
||||
@ -1615,7 +1683,7 @@ public class GhidraFileData {
|
||||
FolderItem item = folderItem != null ? folderItem : versionedFolderItem;
|
||||
String pathname = newParentData.getPathname();
|
||||
String contentType = item.getContentType();
|
||||
String targetName = getTargetName(name, newParentData);
|
||||
String targetName = newParentData.getTargetName(name);
|
||||
String user = ClientUtil.getUserName();
|
||||
try {
|
||||
if (item instanceof DatabaseItem) {
|
||||
@ -1668,7 +1736,7 @@ public class GhidraFileData {
|
||||
}
|
||||
String pathname = destFolderData.getPathname();
|
||||
String contentType = versionedFolderItem.getContentType();
|
||||
String targetName = getTargetName(name + "_v" + version, destFolderData);
|
||||
String targetName = destFolderData.getTargetName(name + "_v" + version);
|
||||
String user = ClientUtil.getUserName();
|
||||
try {
|
||||
BufferFile bufferFile = ((DatabaseItem) versionedFolderItem).open(version);
|
||||
@ -1697,7 +1765,9 @@ public class GhidraFileData {
|
||||
/**
|
||||
* Copy this file to make a private file if it is versioned. This method should be called
|
||||
* only when a non shared project is being converted to a shared project.
|
||||
* @throws IOException
|
||||
* @param monitor task monitor
|
||||
* @throws IOException if an IO error occurs
|
||||
* @throws CancelledException if task is cancelled
|
||||
*/
|
||||
void convertToPrivateFile(TaskMonitor monitor) throws IOException, CancelledException {
|
||||
synchronized (fileSystem) {
|
||||
@ -1749,7 +1819,10 @@ public class GhidraFileData {
|
||||
|
||||
Map<String, String> getMetadata() {
|
||||
FolderItem item = (folderItem != null) ? folderItem : versionedFolderItem;
|
||||
return getMetadata(item);
|
||||
}
|
||||
|
||||
static Map<String, String> getMetadata(FolderItem item) {
|
||||
GenericDomainObjectDB genericDomainObj = null;
|
||||
try {
|
||||
if (item instanceof DatabaseItem) {
|
||||
@ -1767,7 +1840,7 @@ public class GhidraFileData {
|
||||
// file created with newer version of Ghidra
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.error(this, "Read meta-data error", e);
|
||||
Msg.error(GhidraFileData.class, "Read meta-data error", e);
|
||||
}
|
||||
finally {
|
||||
if (genericDomainObj != null) {
|
||||
@ -1782,13 +1855,17 @@ public class GhidraFileData {
|
||||
if (fileManager == null) {
|
||||
return name + "(disposed)";
|
||||
}
|
||||
ProjectLocator projectLocator = fileManager.getProjectLocator();
|
||||
if (projectLocator.isTransient()) {
|
||||
return fileManager.getProjectLocator().getName() + getPathname();
|
||||
}
|
||||
return fileManager.getProjectLocator().getName() + ":" + getPathname();
|
||||
}
|
||||
|
||||
private class GenericDomainObjectDB extends DomainObjectAdapterDB {
|
||||
private static class GenericDomainObjectDB extends DomainObjectAdapterDB {
|
||||
|
||||
protected GenericDomainObjectDB(DBHandle dbh) throws IOException {
|
||||
super(dbh, "Generic", 500, GhidraFileData.this);
|
||||
super(dbh, "Generic", 500, dbh);
|
||||
loadMetadata();
|
||||
}
|
||||
|
||||
@ -1803,7 +1880,7 @@ public class GhidraFileData {
|
||||
}
|
||||
|
||||
public void release() {
|
||||
release(GhidraFileData.this);
|
||||
release(dbh);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1816,8 +1893,8 @@ class VersionIcon implements Icon {
|
||||
private static Color VERSION_ICON_COLOR_LIGHT =
|
||||
new GColor("color.bg.ghidra.file.data.version.icon.light");
|
||||
|
||||
private static final int WIDTH = 18;
|
||||
private static final int HEIGHT = 17;
|
||||
private static final int WIDTH = GhidraFileData.ICON_WIDTH;
|
||||
private static final int HEIGHT = GhidraFileData.ICON_HEIGHT;
|
||||
|
||||
@Override
|
||||
public int getIconHeight() {
|
||||
|
@ -16,16 +16,19 @@
|
||||
package ghidra.framework.data;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
import ghidra.framework.client.RepositoryAdapter;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURL;
|
||||
import ghidra.framework.store.FileSystem;
|
||||
import ghidra.framework.store.local.LocalFileSystem;
|
||||
import ghidra.util.InvalidNameException;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import ghidra.util.task.TaskMonitorAdapter;
|
||||
|
||||
public class GhidraFolder implements DomainFolder {
|
||||
|
||||
@ -83,19 +86,6 @@ public class GhidraFolder implements DomainFolder {
|
||||
return fileData;
|
||||
}
|
||||
|
||||
GhidraFolderData getFolderPathData(String folderPath) throws FileNotFoundException {
|
||||
GhidraFolderData parentData = (folderPath.startsWith(FileSystem.SEPARATOR))
|
||||
? fileManager.getRootFolderData()
|
||||
: getFolderData();
|
||||
GhidraFolderData folderData = parentData.getFolderPathData(folderPath, false);
|
||||
if (folderData == null) {
|
||||
String path = (folderPath.startsWith(FileSystem.SEPARATOR)) ? folderPath
|
||||
: getPathname(folderPath);
|
||||
throw new FileNotFoundException("folder " + path + " not found");
|
||||
}
|
||||
return folderData;
|
||||
}
|
||||
|
||||
GhidraFolderData getFolderData() throws FileNotFoundException {
|
||||
if (parent == null) {
|
||||
return fileManager.getRootFolderData();
|
||||
@ -140,10 +130,10 @@ public class GhidraFolder implements DomainFolder {
|
||||
|
||||
/**
|
||||
* Refresh folder data - used for testing only
|
||||
* @throws IOException
|
||||
* @throws IOException if an IO error occurs
|
||||
*/
|
||||
void refreshFolderData() throws IOException {
|
||||
getFolderData().refresh(false, true, TaskMonitorAdapter.DUMMY_MONITOR);
|
||||
getFolderData().refresh(false, true, TaskMonitor.DUMMY);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -193,6 +183,40 @@ public class GhidraFolder implements DomainFolder {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getSharedProjectURL() {
|
||||
ProjectLocator projectLocator = getProjectLocator();
|
||||
URL projectURL = projectLocator.getURL();
|
||||
if (!GhidraURL.isServerRepositoryURL(projectURL)) {
|
||||
RepositoryAdapter repository = fileManager.getRepository();
|
||||
if (repository == null) {
|
||||
return null;
|
||||
}
|
||||
// NOTE: only supports ghidra protocol without extension protocol.
|
||||
// Assumes any extension protocol use would be reflected in projectLocator URL.
|
||||
ServerInfo serverInfo = repository.getServerInfo();
|
||||
projectURL = GhidraURL.makeURL(serverInfo.getServerName(), serverInfo.getPortNumber(),
|
||||
repository.getName());
|
||||
}
|
||||
try {
|
||||
// Direct URL construction done so that ghidra protocol
|
||||
// extension may be supported
|
||||
String urlStr = projectURL.toExternalForm();
|
||||
if (urlStr.endsWith(FileSystem.SEPARATOR)) {
|
||||
urlStr = urlStr.substring(0, urlStr.length() - 1);
|
||||
}
|
||||
String path = getPathname();
|
||||
if (!path.endsWith(FileSystem.SEPARATOR)) {
|
||||
path += FileSystem.SEPARATOR;
|
||||
}
|
||||
urlStr += path;
|
||||
return new URL(urlStr);
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInWritableProject() {
|
||||
return !getProjectData().getLocalFileSystem().isReadOnly();
|
||||
@ -244,7 +268,9 @@ public class GhidraFolder implements DomainFolder {
|
||||
return folderData.isEmpty();
|
||||
}
|
||||
catch (FileNotFoundException e) {
|
||||
return false; // TODO: what should we return if folder not found
|
||||
// TODO: what should we return if folder not found or error occurs?
|
||||
// True is returned to allow this method to be used to avoid continued access.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -295,14 +321,14 @@ public class GhidraFolder implements DomainFolder {
|
||||
public DomainFile createFile(String fileName, DomainObject obj, TaskMonitor monitor)
|
||||
throws InvalidNameException, IOException, CancelledException {
|
||||
return createFolderData().createFile(fileName, obj,
|
||||
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
|
||||
monitor != null ? monitor : TaskMonitor.DUMMY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile createFile(String fileName, File packFile, TaskMonitor monitor)
|
||||
throws InvalidNameException, IOException, CancelledException {
|
||||
return createFolderData().createFile(fileName, packFile,
|
||||
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
|
||||
monitor != null ? monitor : TaskMonitor.DUMMY);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -325,8 +351,11 @@ public class GhidraFolder implements DomainFolder {
|
||||
if (parent == null) {
|
||||
throw new UnsupportedOperationException("root folder may not be moved");
|
||||
}
|
||||
if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
|
||||
throw new UnsupportedOperationException("newParent does not support moveTo");
|
||||
}
|
||||
GhidraFolderData folderData = getFolderData();
|
||||
GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation
|
||||
GhidraFolder newGhidraParent = (GhidraFolder) newParent;
|
||||
return folderData.moveTo(newGhidraParent.getFolderData());
|
||||
}
|
||||
|
||||
@ -334,9 +363,22 @@ public class GhidraFolder implements DomainFolder {
|
||||
public GhidraFolder copyTo(DomainFolder newParent, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
GhidraFolderData folderData = getFolderData();
|
||||
GhidraFolder newGhidraParent = (GhidraFolder) newParent; // assumes single implementation
|
||||
if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
|
||||
throw new UnsupportedOperationException("newParent does not support copyTo");
|
||||
}
|
||||
GhidraFolder newGhidraParent = (GhidraFolder) newParent;
|
||||
return folderData.copyTo(newGhidraParent.getFolderData(),
|
||||
monitor != null ? monitor : TaskMonitorAdapter.DUMMY_MONITOR);
|
||||
monitor != null ? monitor : TaskMonitor.DUMMY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
|
||||
GhidraFolderData folderData = getFolderData();
|
||||
if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) {
|
||||
throw new UnsupportedOperationException("newParent does not support copyToAsLink");
|
||||
}
|
||||
GhidraFolder newGhidraParent = (GhidraFolder) newParent;
|
||||
return folderData.copyToAsLink(newGhidraParent.getFolderData());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,10 +16,13 @@
|
||||
package ghidra.framework.data;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.framework.client.RepositoryAdapter;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURL;
|
||||
import ghidra.framework.protocol.ghidra.TransientProjectData;
|
||||
import ghidra.framework.store.FileSystem;
|
||||
import ghidra.framework.store.local.LocalFileSystem;
|
||||
import ghidra.util.*;
|
||||
@ -257,9 +260,10 @@ class GhidraFolderData {
|
||||
return folderList.isEmpty() && fileList.isEmpty();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore
|
||||
// TODO: what should we return if folder not found or error occurs?
|
||||
// True is returned to allow this method to be used to avoid continued access.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> getFileNames() {
|
||||
@ -919,7 +923,7 @@ class GhidraFolderData {
|
||||
|
||||
DomainFile oldDf = doa.getDomainFile();
|
||||
try {
|
||||
ContentHandler ch = DomainObjectAdapter.getContentHandler(doa);
|
||||
ContentHandler<?> ch = DomainObjectAdapter.getContentHandler(doa);
|
||||
ch.createFile(fileSystem, null, getPathname(), fileName, obj, monitor);
|
||||
|
||||
if (oldDf != null) {
|
||||
@ -1140,6 +1144,88 @@ class GhidraFolderData {
|
||||
}
|
||||
}
|
||||
|
||||
DomainFile copyToAsLink(GhidraFolderData newParentData) throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
String linkFilename = name;
|
||||
if (linkFilename == null) {
|
||||
if (fileManager instanceof TransientProjectData) {
|
||||
linkFilename = fileManager.getRepository().getName();
|
||||
}
|
||||
else {
|
||||
linkFilename = fileManager.getProjectLocator().getName();
|
||||
}
|
||||
}
|
||||
return newParentData.copyAsLink(fileManager, getPathname(), linkFilename,
|
||||
FolderLinkContentHandler.INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
DomainFile copyAsLink(ProjectData sourceProjectData, String pathname, String linkFilename,
|
||||
LinkHandler<?> lh) throws IOException {
|
||||
synchronized (fileSystem) {
|
||||
if (fileSystem.isReadOnly()) {
|
||||
throw new ReadOnlyException("copyAsLink permitted to writeable project only");
|
||||
}
|
||||
|
||||
if (sourceProjectData == fileManager) {
|
||||
// internal linking not yet supported
|
||||
Msg.error(this, "Internal file/folder links not yet supported");
|
||||
return null;
|
||||
}
|
||||
|
||||
URL ghidraUrl = null;
|
||||
if (sourceProjectData instanceof TransientProjectData) {
|
||||
RepositoryAdapter repository = sourceProjectData.getRepository();
|
||||
ServerInfo serverInfo = repository.getServerInfo();
|
||||
ghidraUrl =
|
||||
GhidraURL.makeURL(serverInfo.getServerName(), serverInfo.getPortNumber(),
|
||||
repository.getName(), pathname);
|
||||
}
|
||||
else {
|
||||
ProjectLocator projectLocator = sourceProjectData.getProjectLocator();
|
||||
if (projectLocator.equals(fileManager.getProjectLocator())) {
|
||||
return null; // local internal linking not supported
|
||||
}
|
||||
ghidraUrl = GhidraURL.makeURL(projectLocator, pathname, null);
|
||||
}
|
||||
|
||||
String newName = linkFilename;
|
||||
int i = 1;
|
||||
while (true) {
|
||||
GhidraFileData fileData = getFileData(newName, false);
|
||||
if (fileData != null) {
|
||||
// return existing file if link URL matches
|
||||
if (ghidraUrl.equals(fileData.getLinkFileURL())) {
|
||||
return getDomainFile(newName);
|
||||
}
|
||||
newName = linkFilename + "." + i;
|
||||
++i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
lh.createLink(ghidraUrl, fileSystem, getPathname(), newName);
|
||||
}
|
||||
catch (InvalidNameException e) {
|
||||
throw new IOException(e); // unexpected
|
||||
}
|
||||
|
||||
fileChanged(newName);
|
||||
return getDomainFile(newName);
|
||||
}
|
||||
}
|
||||
|
||||
String getTargetName(String preferredName) throws IOException {
|
||||
String newName = preferredName;
|
||||
int i = 1;
|
||||
while (getFileData(newName, false) != null) {
|
||||
newName = preferredName + "." + i;
|
||||
i++;
|
||||
}
|
||||
return newName;
|
||||
}
|
||||
|
||||
/**
|
||||
* used for testing
|
||||
*/
|
||||
@ -1156,6 +1242,10 @@ class GhidraFolderData {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
ProjectLocator projectLocator = fileManager.getProjectLocator();
|
||||
if (projectLocator.isTransient()) {
|
||||
return fileManager.getProjectLocator().getName() + getPathname();
|
||||
}
|
||||
return fileManager.getProjectLocator().getName() + ":" + getPathname();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -16,13 +16,12 @@
|
||||
package ghidra.framework.data;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
import docking.widgets.OptionDialog;
|
||||
import generic.timer.GhidraSwinglessTimer;
|
||||
import ghidra.framework.client.*;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.protocol.ghidra.GhidraURL;
|
||||
import ghidra.framework.remote.User;
|
||||
import ghidra.framework.store.*;
|
||||
import ghidra.framework.store.FileSystem;
|
||||
@ -82,6 +81,7 @@ public class ProjectFileManager implements ProjectData {
|
||||
|
||||
private TaskMonitorAdapter projectDisposalMonitor = new TaskMonitorAdapter();
|
||||
|
||||
private ProjectLock projectLock;
|
||||
private String owner;
|
||||
|
||||
/**
|
||||
@ -91,34 +91,44 @@ public class ProjectFileManager implements ProjectData {
|
||||
* @param resetOwner true to reset the project owner
|
||||
* @throws IOException if an i/o error occurs
|
||||
* @throws NotOwnerException if inProject is true and user is not owner
|
||||
* @throws LockException if {@code isInWritableProject} is true and unable to establish project
|
||||
* write lock (i.e., project in-use)
|
||||
* @throws FileNotFoundException if project directory not found
|
||||
*/
|
||||
public ProjectFileManager(ProjectLocator localStorageLocator, boolean isInWritableProject,
|
||||
boolean resetOwner) throws NotOwnerException, IOException {
|
||||
boolean resetOwner) throws NotOwnerException, IOException, LockException {
|
||||
|
||||
this.localStorageLocator = localStorageLocator;
|
||||
init(false, isInWritableProject);
|
||||
if (resetOwner) {
|
||||
owner = SystemUtilities.getUserName();
|
||||
properties.putString(OWNER, owner);
|
||||
properties.writeState();
|
||||
}
|
||||
else if (isInWritableProject && !SystemUtilities.getUserName().equals(owner)) {
|
||||
if (owner == null) {
|
||||
throw new NotOwnerException("Older projects may only be opened as a View.\n" +
|
||||
"You must first create a new project or open an existing current project, \n" +
|
||||
"then use the \"Project->View\" menu action to open the older project as a view.\n" +
|
||||
"You can then drag old files into your active project.");
|
||||
boolean success = false;
|
||||
try {
|
||||
init(false, isInWritableProject);
|
||||
if (resetOwner) {
|
||||
owner = SystemUtilities.getUserName();
|
||||
properties.putString(OWNER, owner);
|
||||
properties.writeState();
|
||||
}
|
||||
else if (isInWritableProject && !SystemUtilities.getUserName().equals(owner)) {
|
||||
if (owner == null) {
|
||||
throw new NotOwnerException("Older projects may only be opened as a View.\n" +
|
||||
"You must first create a new project or open an existing current project, \n" +
|
||||
"then use the \"Project->View\" menu action to open the older project as a view.\n" +
|
||||
"You can then drag old files into your active project.");
|
||||
}
|
||||
throw new NotOwnerException("Project is owned by " + owner);
|
||||
}
|
||||
throw new NotOwnerException("Project is owned by " + owner);
|
||||
}
|
||||
|
||||
synchronized (fileSystem) {
|
||||
getVersionedFileSystem(isInWritableProject);
|
||||
rootFolderData = new RootGhidraFolderData(this, listenerList);
|
||||
versionedFSListener = new MyFileSystemListener();
|
||||
versionedFileSystem.addFileSystemListener(versionedFSListener);
|
||||
scheduleUserDataReconcilation();
|
||||
synchronized (fileSystem) {
|
||||
getVersionedFileSystem(isInWritableProject);
|
||||
rootFolderData = new RootGhidraFolderData(this, listenerList);
|
||||
initVersionedFSListener();
|
||||
scheduleUserDataReconcilation();
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
finally {
|
||||
if (!success) {
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,34 +138,75 @@ public class ProjectFileManager implements ProjectData {
|
||||
* @param repository a repository if this is a shared project or null if it is a private project
|
||||
* @param isInWritableProject true if project content is writable, false if project is read-only
|
||||
* @throws IOException if an i/o error occurs
|
||||
* @throws LockException if {@code isInWritableProject} is true and unable to establish project
|
||||
* lock (i.e., project in-use)
|
||||
*/
|
||||
public ProjectFileManager(ProjectLocator localStorageLocator, RepositoryAdapter repository,
|
||||
boolean isInWritableProject) throws IOException {
|
||||
boolean isInWritableProject) throws IOException, LockException {
|
||||
this.localStorageLocator = localStorageLocator;
|
||||
this.repository = repository;
|
||||
init(true, isInWritableProject);
|
||||
synchronized (fileSystem) {
|
||||
createVersionedFileSystem();
|
||||
rootFolderData = new RootGhidraFolderData(this, listenerList);
|
||||
versionedFSListener = new MyFileSystemListener();
|
||||
versionedFileSystem.addFileSystemListener(versionedFSListener);
|
||||
boolean success = false;
|
||||
try {
|
||||
init(true, isInWritableProject);
|
||||
synchronized (fileSystem) {
|
||||
createVersionedFileSystem();
|
||||
rootFolderData = new RootGhidraFolderData(this, listenerList);
|
||||
initVersionedFSListener();
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
finally {
|
||||
if (!success) {
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProjectFileManager(LocalFileSystem fileSystem, FileSystem versionedFileSystem) {
|
||||
/**
|
||||
* Constructor for test use only. A non-existing {@link ProjectLocator} is used without
|
||||
* project locking.
|
||||
* @param fileSystem an existing non-versioned local file-system
|
||||
* @param versionedFileSystem an existing versioned file-system
|
||||
* @throws IOException if an IO error occurs
|
||||
*/
|
||||
ProjectFileManager(LocalFileSystem fileSystem, FileSystem versionedFileSystem)
|
||||
throws IOException {
|
||||
this.localStorageLocator = new ProjectLocator(null, "Test");
|
||||
owner = SystemUtilities.getUserName();
|
||||
synchronized (fileSystem) {
|
||||
this.fileSystem = fileSystem;
|
||||
this.versionedFileSystem = versionedFileSystem;
|
||||
rootFolderData = new RootGhidraFolderData(this, listenerList);
|
||||
versionedFSListener = new MyFileSystemListener();
|
||||
versionedFileSystem.addFileSystemListener(versionedFSListener);
|
||||
scheduleUserDataReconcilation();
|
||||
boolean success = false;
|
||||
try {
|
||||
synchronized (fileSystem) {
|
||||
this.fileSystem = fileSystem;
|
||||
this.versionedFileSystem = versionedFileSystem;
|
||||
rootFolderData = new RootGhidraFolderData(this, listenerList);
|
||||
initVersionedFSListener();
|
||||
scheduleUserDataReconcilation();
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (!success) {
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void init(boolean create, boolean isInWritableProject) throws IOException {
|
||||
private void initVersionedFSListener() throws IOException {
|
||||
// Listener not installed for local read-only versioned file-system
|
||||
if (versionedFileSystem.isShared() || !versionedFileSystem.isReadOnly()) {
|
||||
if (versionedFSListener == null) {
|
||||
versionedFSListener = new MyFileSystemListener();
|
||||
}
|
||||
versionedFileSystem.addFileSystemListener(versionedFSListener);
|
||||
}
|
||||
else {
|
||||
versionedFSListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void init(boolean create, boolean isInWritableProject)
|
||||
throws IOException, LockException {
|
||||
|
||||
projectDir = localStorageLocator.getProjectDir();
|
||||
properties = new PropertyFile(projectDir, PROPERTY_FILENAME, "/", PROPERTY_FILENAME);
|
||||
if (create) {
|
||||
@ -163,11 +214,13 @@ public class ProjectFileManager implements ProjectData {
|
||||
throw new DuplicateFileException(
|
||||
"Project directory already exists: " + projectDir.getCanonicalPath());
|
||||
}
|
||||
File markerFile = localStorageLocator.getMarkerFile();
|
||||
if (markerFile.exists()) {
|
||||
throw new DuplicateFileException(
|
||||
"Project marker file already exists: " + markerFile.getCanonicalPath());
|
||||
}
|
||||
projectDir.mkdir();
|
||||
localStorageLocator.getMarkerFile().createNewFile();
|
||||
owner = SystemUtilities.getUserName();
|
||||
properties.putString(OWNER, owner);
|
||||
properties.writeState();
|
||||
}
|
||||
else {
|
||||
if (!projectDir.isDirectory()) {
|
||||
@ -178,22 +231,92 @@ public class ProjectFileManager implements ProjectData {
|
||||
throw new ReadOnlyException(
|
||||
"Project " + localStorageLocator.getName() + " is read-only");
|
||||
}
|
||||
properties.readState();
|
||||
owner = properties.getString(OWNER, SystemUtilities.getUserName());
|
||||
}
|
||||
else if (isInWritableProject) {
|
||||
owner = SystemUtilities.getUserName();
|
||||
properties.putString(OWNER, owner);
|
||||
properties.writeState();
|
||||
}
|
||||
else {
|
||||
owner = "<unknown>"; // Unknown owner
|
||||
}
|
||||
}
|
||||
|
||||
if (isInWritableProject) {
|
||||
initLock(create);
|
||||
}
|
||||
|
||||
getPrivateFileSystem(create, isInWritableProject);
|
||||
getUserFileSystem(isInWritableProject);
|
||||
}
|
||||
|
||||
private void initLock(boolean creatingProject) throws LockException, IOException {
|
||||
this.projectLock = getProjectLock(localStorageLocator, !creatingProject);
|
||||
if (projectLock == null) {
|
||||
throw new LockException("Unable to lock project! " + localStorageLocator);
|
||||
}
|
||||
|
||||
if (!properties.exists()) {
|
||||
owner = SystemUtilities.getUserName();
|
||||
properties.putString(OWNER, owner);
|
||||
properties.writeState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ProjectLock and attempts to lock it. This handles the case
|
||||
* where the project was previously locked.
|
||||
*
|
||||
* @param locator the project locator
|
||||
* @param allowInteractiveForce if true, when a lock cannot be obtained, the
|
||||
* user will be prompted
|
||||
* @return A locked ProjectLock or null if lock fails
|
||||
*/
|
||||
private ProjectLock getProjectLock(ProjectLocator locator, boolean allowInteractiveForce) {
|
||||
ProjectLock lock = new ProjectLock(locator);
|
||||
if (lock.lock()) {
|
||||
return lock;
|
||||
}
|
||||
|
||||
// in headless mode, just spit out an error
|
||||
if (!allowInteractiveForce || SystemUtilities.isInHeadlessMode()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String projectStr = "Project: " + HTMLUtilities.escapeHTML(locator.getLocation()) +
|
||||
System.getProperty("file.separator") + HTMLUtilities.escapeHTML(locator.getName());
|
||||
String lockInformation = lock.getExistingLockFileInformation();
|
||||
if (!lock.canForceLock()) {
|
||||
Msg.showInfo(getClass(), null, "Project Locked",
|
||||
"<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
|
||||
public int getMaxNameLength() {
|
||||
return fileSystem.getMaxNameLength();
|
||||
@ -314,8 +437,9 @@ public class ProjectFileManager implements ProjectData {
|
||||
|
||||
/**
|
||||
* Change the versioned filesystem associated with this project file manager.
|
||||
* This method is provided for testing. Care should be taken when using a
|
||||
* LocalFileSystem in a shared capacity since locking is not supported.
|
||||
* This method is provided for testing (see {@code FakeSharedProject}).
|
||||
* Care should be taken when using a LocalFileSystem in a shared capacity since
|
||||
* locking is not supported.
|
||||
* @param fs versioned filesystem
|
||||
* @throws IOException if an IO error occurs
|
||||
*/
|
||||
@ -323,9 +447,11 @@ public class ProjectFileManager implements ProjectData {
|
||||
if (!fs.isVersioned()) {
|
||||
throw new IllegalArgumentException("versioned filesystem required");
|
||||
}
|
||||
versionedFileSystem.removeFileSystemListener(versionedFSListener);
|
||||
if (versionedFSListener != null) {
|
||||
versionedFileSystem.removeFileSystemListener(versionedFSListener);
|
||||
}
|
||||
versionedFileSystem = fs;
|
||||
versionedFileSystem.addFileSystemListener(versionedFSListener);
|
||||
initVersionedFSListener();
|
||||
rootFolderData.setVersionedFileSystem(versionedFileSystem);
|
||||
}
|
||||
|
||||
@ -385,18 +511,36 @@ public class ProjectFileManager implements ProjectData {
|
||||
}
|
||||
|
||||
@Override
|
||||
public GhidraFolder getFolder(String path) {
|
||||
public DomainFolder getFolder(String path) {
|
||||
int len = path.length();
|
||||
if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) {
|
||||
throw new IllegalArgumentException(
|
||||
"Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'");
|
||||
}
|
||||
try {
|
||||
return getRootFolder().getFolderPathData(path).getDomainFolder();
|
||||
|
||||
DomainFolder folder = getRootFolder();
|
||||
String[] split = path.split(FileSystem.SEPARATOR);
|
||||
if (split.length == 0) {
|
||||
return folder;
|
||||
}
|
||||
catch (FileNotFoundException e) {
|
||||
return null;
|
||||
|
||||
for (int i = 1; i < split.length; i++) {
|
||||
DomainFolder subFolder = folder.getFolder(split[i]);
|
||||
if (subFolder == null) {
|
||||
// Check for folder link-file if folder not found
|
||||
// NOTE: if real folder name matches link-file name it will block
|
||||
// use of folder link-file.
|
||||
DomainFile file = folder.getFile(split[i]);
|
||||
if (file != null && file.isLinkFile()) {
|
||||
subFolder = file.followLink();
|
||||
}
|
||||
if (subFolder == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
folder = subFolder;
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -468,19 +612,6 @@ public class ProjectFileManager implements ProjectData {
|
||||
return fileIndex.getFileByID(fileID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getSharedFileURL(String path) {
|
||||
if (repository != null) {
|
||||
DomainFile df = getFile(path);
|
||||
if (df != null && df.isVersioned()) {
|
||||
ServerInfo server = repository.getServerInfo();
|
||||
return GhidraURL.makeURL(server.getServerName(), server.getPortNumber(),
|
||||
repository.getName(), path);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void releaseDomainFiles(Object consumer) {
|
||||
for (DomainObjectAdapter domainObj : openDomainObjects.values()) {
|
||||
try {
|
||||
@ -707,7 +838,11 @@ public class ProjectFileManager implements ProjectData {
|
||||
monitor.checkCanceled();
|
||||
LocalFolderItem item = fileSystem.getItem(folderPath, name);
|
||||
if (item.getCheckoutId() != FolderItem.DEFAULT_CHECKOUT_ID) {
|
||||
checkoutList.add(new GhidraFile(getFolder(folderPath), name));
|
||||
GhidraFolderData folderData =
|
||||
getRootFolderData().getFolderPathData(folderPath, false);
|
||||
if (folderData != null) {
|
||||
checkoutList.add(new GhidraFile(folderData.getDomainFolder(), name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1037,16 +1172,24 @@ public class ProjectFileManager implements ProjectData {
|
||||
listenerList.clearAll();
|
||||
}
|
||||
|
||||
synchronized (fileSystem) {
|
||||
rootFolderData.dispose();
|
||||
fileSystem.dispose();
|
||||
versionedFileSystem.dispose();
|
||||
versionedFileSystem.removeFileSystemListener(versionedFSListener);
|
||||
if (repository != null) {
|
||||
repository.disconnect();
|
||||
repository = null;
|
||||
if (fileSystem != null) {
|
||||
synchronized (fileSystem) {
|
||||
if (versionedFSListener != null) {
|
||||
versionedFileSystem.removeFileSystemListener(versionedFSListener);
|
||||
}
|
||||
if (repository != null) {
|
||||
repository.disconnect();
|
||||
repository = null;
|
||||
}
|
||||
rootFolderData.dispose();
|
||||
versionedFileSystem.dispose();
|
||||
fileSystem.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (projectLock != null) {
|
||||
projectLock.release();
|
||||
}
|
||||
}
|
||||
|
||||
GhidraFolderData getRootFolderData() {
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.project;
|
||||
package ghidra.framework.data;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -40,6 +40,8 @@ import generic.theme.GIcon;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.framework.GenericRunInfo;
|
||||
import ghidra.framework.client.*;
|
||||
import ghidra.framework.data.FolderLinkContentHandler;
|
||||
import ghidra.framework.data.LinkedGhidraFolder;
|
||||
import ghidra.framework.main.datatable.ProjectDataTablePanel;
|
||||
import ghidra.framework.main.datatree.*;
|
||||
import ghidra.framework.main.projectdata.actions.*;
|
||||
@ -69,7 +71,7 @@ import ghidra.util.filechooser.GhidraFileFilter;
|
||||
)
|
||||
//@formatter:on
|
||||
public class FrontEndPlugin extends Plugin
|
||||
implements FrontEndService, RemoteAdapterListener, ProgramaticUseOnly {
|
||||
implements FrontEndService, RemoteAdapterListener, ProjectViewListener, ProgramaticUseOnly {
|
||||
|
||||
private final static String TITLE_PREFIX = "Ghidra: ";
|
||||
private final static String EXPORT_TOOL_ACTION_NAME = "Export Tool";
|
||||
@ -128,6 +130,7 @@ public class FrontEndPlugin extends Plugin
|
||||
private ClearCutAction clearCutAction;
|
||||
private ProjectDataCopyAction copyAction;
|
||||
private ProjectDataPasteAction pasteAction;
|
||||
private ProjectDataPasteLinkAction pasteLinkAction;
|
||||
private ProjectDataRenameAction renameAction;
|
||||
private ProjectDataOpenDefaultToolAction openAction;
|
||||
private ProjectDataExpandAction<FrontEndProjectTreeContext> expandAction;
|
||||
@ -217,6 +220,7 @@ public class FrontEndPlugin extends Plugin
|
||||
clearCutAction = new ClearCutAction(owner);
|
||||
copyAction = new ProjectDataCopyAction(owner, groupName);
|
||||
pasteAction = new ProjectDataPasteAction(owner, groupName);
|
||||
pasteLinkAction = new ProjectDataPasteLinkAction(owner, groupName);
|
||||
|
||||
groupName = "Delete/Rename";
|
||||
renameAction = new ProjectDataRenameAction(owner, groupName);
|
||||
@ -239,6 +243,7 @@ public class FrontEndPlugin extends Plugin
|
||||
tool.addAction(clearCutAction);
|
||||
tool.addAction(copyAction);
|
||||
tool.addAction(pasteAction);
|
||||
tool.addAction(pasteLinkAction);
|
||||
tool.addAction(deleteAction);
|
||||
tool.addAction(openAction);
|
||||
tool.addAction(renameAction);
|
||||
@ -393,6 +398,10 @@ public class FrontEndPlugin extends Plugin
|
||||
toolChest.addToolChestChangeListener(toolBar);
|
||||
toolChest.addToolChestChangeListener(toolChestChangeListener);
|
||||
createToolSpecificOpenActions();
|
||||
|
||||
// Add project view listener
|
||||
activeProject.addProjectViewListener(this);
|
||||
|
||||
// Add the repository listener
|
||||
RepositoryAdapter repository = activeProject.getRepository();
|
||||
if (repository != null) {
|
||||
@ -403,6 +412,16 @@ public class FrontEndPlugin extends Plugin
|
||||
// gui.validate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void viewedProjectAdded(URL projectView) {
|
||||
SwingUtilities.invokeLater(() -> rebuildRecentMenus());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void viewedProjectRemoved(URL projectView) {
|
||||
SwingUtilities.invokeLater(() -> rebuildRecentMenus());
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the name of the project, using the default name if no project is active
|
||||
*/
|
||||
@ -1074,24 +1093,56 @@ public class FrontEndPlugin extends Plugin
|
||||
}
|
||||
|
||||
public void openDomainFile(DomainFile domainFile) {
|
||||
Project project = tool.getProject();
|
||||
final ToolServices toolServices = project.getToolServices();
|
||||
ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(domainFile);
|
||||
|
||||
if (defaultToolTemplate == null) {
|
||||
// assume no tools in the tool chest
|
||||
Msg.showInfo(this, tool.getToolFrame(), "Cannot Find Tool",
|
||||
"<html>Cannot find tool to open file: <b>" +
|
||||
HTMLUtilities.escapeHTML(domainFile.getName()) +
|
||||
"</b>.<br><br>Make sure you have an appropriate tool installed <br>from the " +
|
||||
"<b>Tools->Import Default Tools...</b> menu. Alternatively, you can " +
|
||||
"use <b>Tool->Set Tool Associations</b> menu to change how Ghidra " +
|
||||
"opens this type of file");
|
||||
if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(domainFile.getContentType())) {
|
||||
showLinkedFolder(domainFile);
|
||||
return;
|
||||
}
|
||||
|
||||
ToolButton button = toolBar.getToolButtonForToolConfig(defaultToolTemplate);
|
||||
button.launchTool(domainFile);
|
||||
Project project = tool.getProject();
|
||||
final ToolServices toolServices = project.getToolServices();
|
||||
ToolTemplate defaultToolTemplate = toolServices.getDefaultToolTemplate(domainFile);
|
||||
if (defaultToolTemplate != null) {
|
||||
ToolButton button = toolBar.getToolButtonForToolConfig(defaultToolTemplate);
|
||||
if (button != null) {
|
||||
button.launchTool(domainFile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Msg.showInfo(this, tool.getToolFrame(), "Cannot Find Tool",
|
||||
"<html>Cannot find tool to open file: <b>" +
|
||||
HTMLUtilities.escapeHTML(domainFile.getName()) +
|
||||
"</b>.<br><br>Make sure you have an appropriate tool installed <br>from the " +
|
||||
"<b>Tools->Import Default Tools...</b> menu. Alternatively, you can " +
|
||||
"use <b>Tool->Set Tool Associations</b> menu to change how Ghidra " +
|
||||
"opens this type of file");
|
||||
}
|
||||
|
||||
private void showLinkedFolder(DomainFile domainFile) {
|
||||
|
||||
try {
|
||||
LinkedGhidraFolder linkedFolder =
|
||||
FolderLinkContentHandler.getReadOnlyLinkedFolder(domainFile);
|
||||
if (linkedFolder == null) {
|
||||
return; // unsupported use
|
||||
}
|
||||
|
||||
ProjectDataTreePanel dtp = projectDataPanel.openView(linkedFolder.getProjectURL());
|
||||
if (dtp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DomainFolder domainFolder = linkedFolder.getLinkedFolder();
|
||||
if (domainFolder != null) {
|
||||
// delayed to ensure tree is displayd
|
||||
Swing.runLater(() -> dtp.selectDomainFolder(domainFolder));
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.showError(this, projectDataPanel, "Linked-folder failure: " + domainFile.getName(),
|
||||
e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -554,10 +554,15 @@ class ProjectActionManager {
|
||||
return;
|
||||
}
|
||||
|
||||
ProjectDataPanel pdp = plugin.getProjectDataPanel();
|
||||
pdp.openView(view);
|
||||
// also update the recent views menu
|
||||
plugin.rebuildRecentMenus();
|
||||
try {
|
||||
activeProject.addProjectView(view, true); // listener will trigger data panel panel display
|
||||
}
|
||||
catch (IOException e) {
|
||||
ProjectManager projectManager = tool.getProjectManager();
|
||||
projectManager.forgetViewedProject(view);
|
||||
Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View",
|
||||
"Failed to view project/repository: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void editProjectAccess() {
|
||||
|
@ -27,6 +27,7 @@ import javax.swing.*;
|
||||
import docking.ActionContext;
|
||||
import docking.ComponentProvider;
|
||||
import docking.widgets.tabbedpane.DockingTabRenderer;
|
||||
import ghidra.framework.client.NotConnectedException;
|
||||
import ghidra.framework.main.datatable.ProjectDataTablePanel;
|
||||
import ghidra.framework.main.datatree.ProjectDataTreePanel;
|
||||
import ghidra.framework.model.*;
|
||||
@ -40,7 +41,7 @@ import help.HelpService;
|
||||
* Manages the data tree for the active project, and the trees for the
|
||||
* project views.
|
||||
*/
|
||||
class ProjectDataPanel extends JSplitPane {
|
||||
class ProjectDataPanel extends JSplitPane implements ProjectViewListener {
|
||||
private final static String BORDER_PREFIX = "Active Project: ";
|
||||
private final static String READ_ONLY_BORDER = "READ-ONLY Project Data";
|
||||
private final static int TYPICAL_NUM_VIEWS = 2;
|
||||
@ -155,6 +156,21 @@ class ProjectDataPanel extends JSplitPane {
|
||||
setViewsVisible(views.length > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void viewedProjectAdded(URL projectView) {
|
||||
SwingUtilities.invokeLater(() -> openView(projectView));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void viewedProjectRemoved(URL projectView) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
ProjectDataTreePanel dtp = getViewPanel(projectView);
|
||||
if (dtp != null) {
|
||||
viewRemoved(dtp, projectView, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void clearReadOnlyViews() {
|
||||
readOnlyTab.removeAll();
|
||||
readOnlyViews.clear();
|
||||
@ -167,7 +183,13 @@ class ProjectDataPanel extends JSplitPane {
|
||||
this.setDividerLocation(visible ? DIVIDER_LOCATION : 1.0);
|
||||
}
|
||||
|
||||
void openView(URL projectView) {
|
||||
/**
|
||||
* Open specified project URL in tabbed READ-Only project views
|
||||
* @param projectView project URL to be opened/added to view
|
||||
* @return corresponding tree panel or null on failure
|
||||
*/
|
||||
ProjectDataTreePanel openView(URL projectView) {
|
||||
|
||||
ProjectManager projectManager = tool.getProjectManager();
|
||||
Project activeProject = tool.getProject();
|
||||
|
||||
@ -176,19 +198,23 @@ class ProjectDataPanel extends JSplitPane {
|
||||
if (dtp != null) {
|
||||
readOnlyTab.setSelectedComponent(dtp);
|
||||
try {
|
||||
activeProject.addProjectView(projectView);
|
||||
activeProject.addProjectView(projectView, true);
|
||||
projectManager.rememberViewedProject(projectView);
|
||||
return dtp;
|
||||
}
|
||||
catch (Exception e) {
|
||||
projectManager.forgetViewedProject(projectView);
|
||||
Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View", e.toString());
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: addProjectView should be done in a model task
|
||||
ProjectData projectData = activeProject.addProjectView(projectView);
|
||||
ProjectData projectData = activeProject.addProjectView(projectView, true);
|
||||
if (projectData == null) {
|
||||
return null; // repository connection may have been cancelled
|
||||
}
|
||||
projectManager.rememberViewedProject(projectView);
|
||||
String viewName = projectData.getProjectLocator().getName();
|
||||
final ProjectDataTreePanel newPanel =
|
||||
@ -204,13 +230,20 @@ class ProjectDataPanel extends JSplitPane {
|
||||
readOnlyTab.setSelectedIndex(0);
|
||||
readOnlyViews.put(projectData.getProjectLocator(), newPanel);
|
||||
setViewsVisible(true);
|
||||
return newPanel;
|
||||
}
|
||||
catch (NotConnectedException e) {
|
||||
// already handled (e..g, cancelled login) - ignore
|
||||
}
|
||||
catch (Exception e) {
|
||||
projectManager.forgetViewedProject(projectView);
|
||||
Msg.showError(getClass(), tool.getToolFrame(), "Error Adding View",
|
||||
"Failed to view project/repository: " + e.getMessage());
|
||||
"Failed to view project/repository: " + e.getMessage(), e);
|
||||
}
|
||||
validate();
|
||||
finally {
|
||||
validate();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ProjectLocator[] getProjectViews() {
|
||||
@ -315,6 +348,7 @@ class ProjectDataPanel extends JSplitPane {
|
||||
treePanel.setProjectData(project.getName(), project.getProjectData());
|
||||
tablePanel.setProjectData(project.getName(), project.getProjectData());
|
||||
populateReadOnlyViews(project);
|
||||
project.addProjectViewListener(this);
|
||||
}
|
||||
else {
|
||||
tablePanel.setProjectData("No Active Project", null);
|
||||
|
@ -182,9 +182,12 @@ public class ProjectDataTablePanel extends JPanel {
|
||||
this.projectData.removeDomainFolderChangeListener(changeListener);
|
||||
model.setProjectData(null);
|
||||
SystemUtilities.runSwingLater(() -> {
|
||||
GGlassPane glassPane = (GGlassPane) gTable.getRootPane().getGlassPane();
|
||||
glassPane.removePainter(painter);
|
||||
glassPane.addPainter(painter);
|
||||
JRootPane rootPane = gTable.getRootPane();
|
||||
if (rootPane != null) {
|
||||
GGlassPane glassPane = (GGlassPane) rootPane.getGlassPane();
|
||||
glassPane.removePainter(painter);
|
||||
glassPane.addPainter(painter);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ public abstract class ProjectTreeAction extends DockingAction {
|
||||
|
||||
@Override
|
||||
public boolean isAddToPopup(ActionContext context) {
|
||||
if (!isEnabledForContext(context)) {
|
||||
if (!(context instanceof FrontEndProjectTreeContext)) {
|
||||
return false;
|
||||
}
|
||||
return isAddToPopup((FrontEndProjectTreeContext) context);
|
||||
|
@ -176,40 +176,15 @@ class ChangeManager implements DomainFolderChangeListener {
|
||||
if (lazy && !folderNode.isLoaded()) {
|
||||
return null; // not visited
|
||||
}
|
||||
// must look at all children since a folder and file may have the same name
|
||||
boolean found = false;
|
||||
for (GTreeNode node : folderNode.getChildren()) {
|
||||
if (!(node instanceof DomainFolderNode)) {
|
||||
continue;
|
||||
}
|
||||
if (name.equals(node.getName())) {
|
||||
folderNode = (DomainFolderNode) node;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
folderNode =
|
||||
(DomainFolderNode) folderNode.getChild(name, n -> (n instanceof DomainFolderNode));
|
||||
if (folderNode == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return folderNode;
|
||||
}
|
||||
|
||||
// private DomainFileNode findDomainFileNode(DomainFolder parent, String name, boolean lazy) {
|
||||
// DomainFolderNode folderNode = findDomainFolderNode(parent, lazy);
|
||||
// if (folderNode == null) {
|
||||
// return null;
|
||||
// }
|
||||
// if (lazy && !folderNode.isChildrenLoadedOrInProgress()) {
|
||||
// return null; // not visited
|
||||
// }
|
||||
// GTreeNode child = folderNode.getChild(name);
|
||||
// if (child instanceof DomainFileNode) {
|
||||
// return (DomainFileNode) child;
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
private DomainFileNode findDomainFileNode(DomainFile domainFile, boolean lazy) {
|
||||
DomainFolderNode folderNode = findDomainFolderNode(domainFile.getParent(), lazy);
|
||||
if (folderNode == null) {
|
||||
@ -219,11 +194,8 @@ class ChangeManager implements DomainFolderChangeListener {
|
||||
return null; // not visited
|
||||
}
|
||||
|
||||
GTreeNode child = folderNode.getChild(domainFile.getName());
|
||||
if (child instanceof DomainFileNode) {
|
||||
return (DomainFileNode) child;
|
||||
}
|
||||
return null;
|
||||
return (DomainFileNode) folderNode.getChild(domainFile.getName(),
|
||||
n -> (n instanceof DomainFileNode));
|
||||
}
|
||||
|
||||
private void updateFolderNode(DomainFolder parent) {
|
||||
|
@ -58,7 +58,7 @@ public class CheckInTask extends VersionControlTask implements CheckinHandler {
|
||||
throw new CancelledException();
|
||||
}
|
||||
if (actionID != VersionControlDialog.APPLY_TO_ALL) {
|
||||
showDialog(false, df.getName()); // false==> checking in vs.
|
||||
showDialog(false, df.getName(), df.isLinkFile()); // false==> checking in vs.
|
||||
// adding to version control
|
||||
if (actionID == VersionControlDialog.CANCEL) {
|
||||
monitor.cancel();
|
||||
|
@ -16,6 +16,7 @@
|
||||
package ghidra.framework.main.datatree;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
@ -24,6 +25,8 @@ import javax.swing.SwingWorker;
|
||||
|
||||
import docking.widgets.tree.GTreeNode;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.framework.data.FolderLinkContentHandler;
|
||||
import ghidra.framework.data.LinkHandler;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.store.ItemCheckoutStatus;
|
||||
import ghidra.util.*;
|
||||
@ -174,7 +177,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
|
||||
if (domainFile.isHijacked()) {
|
||||
newDisplayName += " (hijacked)";
|
||||
}
|
||||
else if (domainFile.isVersioned()) {
|
||||
else if (domainFile.isVersioned() && !domainFile.isLinkFile()) {
|
||||
int versionNumber = domainFile.getVersion();
|
||||
String versionStr = "" + versionNumber;
|
||||
|
||||
@ -208,20 +211,34 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
|
||||
}
|
||||
|
||||
private void setToolTipText() {
|
||||
String newToolTipText = toolTipText;
|
||||
String newToolTipText = null;
|
||||
if (domainFile.isInWritableProject() && domainFile.isHijacked()) {
|
||||
newToolTipText = "Hijacked file should be deleted or renamed";
|
||||
}
|
||||
else {
|
||||
long lastModified = domainFile.getLastModifiedTime();
|
||||
newToolTipText = "Last Modified " + formatter.format(new Date(lastModified));
|
||||
StringBuilder buf = new StringBuilder();
|
||||
try {
|
||||
if (domainFile.isLinkFile()) {
|
||||
URL url = LinkHandler.getURL(domainFile);
|
||||
buf.append("URL: ");
|
||||
buf.append(StringUtilities.trimMiddle(url.toString(), 120));
|
||||
newToolTipText = buf.toString();
|
||||
}
|
||||
}
|
||||
catch (IOException e1) {
|
||||
// ignore
|
||||
}
|
||||
if (newToolTipText == null) {
|
||||
long lastModified = domainFile.getLastModifiedTime();
|
||||
newToolTipText = "Last Modified " + formatter.format(new Date(lastModified));
|
||||
}
|
||||
if (domainFile.isCheckedOut()) {
|
||||
try {
|
||||
ItemCheckoutStatus status = domainFile.getCheckoutStatus();
|
||||
if (status != null) {
|
||||
newToolTipText = HTMLUtilities.toHTML(
|
||||
"Checked out " + formatter.format(new Date(status.getCheckoutTime())) +
|
||||
";\n" + newToolTipText);
|
||||
newToolTipText = "Checked out " +
|
||||
formatter.format(new Date(status.getCheckoutTime())) +
|
||||
"\n" + newToolTipText;
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
@ -232,6 +249,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
|
||||
if (domainFile.isReadOnly()) {
|
||||
newToolTipText += " (read only)";
|
||||
}
|
||||
newToolTipText = HTMLUtilities.toLiteralHTML(newToolTipText, 0);
|
||||
}
|
||||
toolTipText = newToolTipText;
|
||||
}
|
||||
@ -243,12 +261,38 @@ public class DomainFileNode extends GTreeNode implements Cuttable {
|
||||
|
||||
@Override
|
||||
public int compareTo(GTreeNode node) {
|
||||
// Goal is to sort folder link-files similar to a folder
|
||||
if (node instanceof DomainFolderNode) {
|
||||
if (isFolderLink()) {
|
||||
int c = super.compareTo(node);
|
||||
if (c != 0) {
|
||||
// A link-file name is permitted to match another folder node but
|
||||
// should not be considered equal
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (node instanceof DomainFileNode) {
|
||||
DomainFileNode otherFileNode = (DomainFileNode) node;
|
||||
if (isFolderLink()) {
|
||||
if (otherFileNode.isFolderLink()) {
|
||||
return super.compareTo(node);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
else if (otherFileNode.isFolderLink()) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return super.compareTo(node);
|
||||
}
|
||||
|
||||
boolean isFolderLink() {
|
||||
return FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE
|
||||
.equals(domainFile.getContentType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void valueChanged(Object newValue) {
|
||||
if (newValue.equals(getName())) {
|
||||
|
@ -22,20 +22,18 @@ import javax.swing.Icon;
|
||||
|
||||
import docking.widgets.tree.GTreeLazyNode;
|
||||
import docking.widgets.tree.GTreeNode;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.util.InvalidNameException;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.*;
|
||||
import resources.ResourceManager;
|
||||
|
||||
/**
|
||||
* Class to represent a node in the Data tree.
|
||||
*/
|
||||
public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
|
||||
private static final Icon ENABLED_OPEN_FOLDER =
|
||||
new GIcon("icon.datatree.node.domain.folder.open");
|
||||
private static final Icon ENABLED_CLOSED_FOLDER =
|
||||
new GIcon("icon.datatree.node.domain.folder.closed");
|
||||
|
||||
private static final Icon ENABLED_OPEN_FOLDER = DomainFolder.OPEN_FOLDER_ICON;
|
||||
private static final Icon ENABLED_CLOSED_FOLDER = DomainFolder.CLOSED_FOLDER_ICON;
|
||||
|
||||
private static final Icon DISABLED_OPEN_FOLDER =
|
||||
ResourceManager.getDisabledIcon(ENABLED_OPEN_FOLDER);
|
||||
private static final Icon DISABLED_CLOSED_FOLDER =
|
||||
@ -55,11 +53,18 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
|
||||
|
||||
// TODO: how can the folder be null?...doesn't really make sense...I don't think it ever is
|
||||
if (domainFolder != null) {
|
||||
toolTipText = domainFolder.getPathname();
|
||||
toolTipText = StringUtilities.trimMiddle(domainFolder.getPathname(), 120);
|
||||
toolTipText = HTMLUtilities.toLiteralHTML(toolTipText, 0);
|
||||
isEditable = domainFolder.isInWritableProject();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoExpandPermitted() {
|
||||
// Prevent auto-expansion through linked-folders
|
||||
return !domainFolder.isLinked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the domain folder; returns null if this node represents a domain file.
|
||||
*
|
||||
@ -96,6 +101,10 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
|
||||
|
||||
@Override
|
||||
public Icon getIcon(boolean expanded) {
|
||||
if (domainFolder instanceof LinkedDomainFolder) {
|
||||
// NOTE: cut operation not supported
|
||||
return ((LinkedDomainFolder) domainFolder).getIcon(expanded);
|
||||
}
|
||||
if (expanded) {
|
||||
return isCut ? DISABLED_OPEN_FOLDER : ENABLED_OPEN_FOLDER;
|
||||
}
|
||||
@ -119,8 +128,12 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
|
||||
|
||||
@Override
|
||||
protected List<GTreeNode> generateChildren() {
|
||||
|
||||
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();
|
||||
for (DomainFolder folder : folders) {
|
||||
children.add(new DomainFolderNode(folder, filter));
|
||||
@ -128,6 +141,13 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
|
||||
|
||||
DomainFile[] files = domainFolder.getFiles();
|
||||
for (DomainFile domainFile : files) {
|
||||
if (domainFile.isLinkFile() && filter != null && filter.followLinkedFolders()) {
|
||||
DomainFolder folder = domainFile.followLink();
|
||||
if (folder != null) {
|
||||
children.add(new DomainFolderNode(folder, filter));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (filter == null || filter.accept(domainFile)) {
|
||||
children.add(new DomainFileNode(domainFile));
|
||||
}
|
||||
@ -173,7 +193,8 @@ public class DomainFolderNode extends GTreeLazyNode implements Cuttable {
|
||||
@Override
|
||||
public int compareTo(GTreeNode node) {
|
||||
if (node instanceof DomainFileNode) {
|
||||
return -1;
|
||||
// defer to DomainFileNode for comparison
|
||||
return -((DomainFileNode) node).compareTo(this);
|
||||
}
|
||||
return super.compareTo(node);
|
||||
}
|
||||
|
@ -423,6 +423,9 @@ public class ProjectDataTreePanel extends JPanel {
|
||||
root = createRootNode(projectName);
|
||||
|
||||
tree = new DataTree(tool, root);
|
||||
if (!isActiveProject) {
|
||||
tree.setName(tree.getName() + ": " + projectName);
|
||||
}
|
||||
|
||||
if (plugin != null) {
|
||||
tree.addGTreeSelectionListener(e -> {
|
||||
|
@ -117,12 +117,6 @@ public class VersionControlDialog extends DialogComponentProvider {
|
||||
return keepFileCB.isSelected();
|
||||
}
|
||||
|
||||
void setCreateKeepFile(boolean selected) {
|
||||
if (!addToVersionControl) {
|
||||
keepFileCB.setSelected(selected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the comments for the add to version control.
|
||||
* @return may be the empty string
|
||||
@ -133,10 +127,14 @@ public class VersionControlDialog extends DialogComponentProvider {
|
||||
|
||||
/*
|
||||
* Disable the check box for "keep checked out" because some files are still in use.
|
||||
* @param enabled true if checkbox control should be enabled, false if disabled
|
||||
* @param selected true if default state should be selected, else not-selected
|
||||
* @param disabledMsg tooltip message if enabled is false, otherwise ignored.
|
||||
*/
|
||||
public void setKeepCheckboxEnabled(boolean enabled) {
|
||||
public void setKeepCheckboxEnabled(boolean enabled, boolean selected, String disabledMsg) {
|
||||
keepCB.setEnabled(enabled);
|
||||
keepCB.setToolTipText(enabled ? "" : "Must keep Checked Out because the file is in use");
|
||||
keepCB.setSelected(selected);
|
||||
keepCB.setToolTipText(enabled ? "" : disabledMsg);
|
||||
}
|
||||
|
||||
private JPanel buildMainPanel() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user