GP-2509 GP-2644 Improved Ghidra URL support. Added support for Ghidra

URL linked files and folders within project.
This commit is contained in:
ghidra1 2022-08-30 18:31:11 -04:00
parent 8d6cf5e310
commit 5a422c4502
153 changed files with 7083 additions and 1732 deletions

View File

@ -27,6 +27,7 @@ import ghidra.framework.data.ProjectFileManager;
import ghidra.framework.model.*;
import ghidra.framework.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;

View File

@ -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 =

View File

@ -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!_

View File

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

View File

@ -0,0 +1,71 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.trace.database;
import java.io.IOException;
import javax.swing.Icon;
import ghidra.framework.data.LinkHandler;
import ghidra.framework.data.URLLinkObject;
import ghidra.framework.model.DomainObject;
import ghidra.framework.store.FileSystem;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class DBTraceLinkContentHandler extends LinkHandler<DBTrace> {
public static final String TRACE_LINK_CONTENT_TYPE = "TraceLink";
@Override
public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
DomainObject obj, TaskMonitor monitor)
throws IOException, InvalidNameException, CancelledException {
if (!(obj instanceof URLLinkObject)) {
throw new IOException("Unsupported domain object: " + obj.getClass().getName());
}
return createFile((URLLinkObject) obj, TRACE_LINK_CONTENT_TYPE, fs, path, name,
monitor);
}
@Override
public String getContentType() {
return TRACE_LINK_CONTENT_TYPE;
}
@Override
public String getContentTypeDisplayString() {
return TRACE_LINK_CONTENT_TYPE;
}
@Override
public Class<DBTrace> getDomainObjectClass() {
// return linked content class
return DBTraceContentHandler.TRACE_DOMAIN_OBJECT_CLASS;
}
@Override
public Icon getIcon() {
return DBTraceContentHandler.TRACE_ICON;
}
@Override
public String getDefaultToolName() {
return DBTraceContentHandler.TRACE_CONTENT_DEFAULT_TOOL;
}
}

View File

@ -351,6 +351,7 @@ src/main/help/help/topics/FrontEndPlugin/images/DeleteProject.png||GHIDRA||||END
src/main/help/help/topics/FrontEndPlugin/images/EditPluginPath.png||GHIDRA||||END|
src/main/help/help/topics/FrontEndPlugin/images/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|

View File

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

View File

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

View File

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

View File

@ -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://&lt;host&gt;[:&lt;port&gt;]/&lt;repository-name&gt;/&lt;program-path&gt;[#&lt;address-or-symbol-ref&gt;]</i>
an attempt will be made to open the corresponding program from the referenced Ghidra Server (e.g.,
<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://&lt;host&gt;[:&lt;port&gt;]/&lt;repository-name&gt;/&lt;program-path&gt;[#&lt;address-or-symbol-ref&gt;]</i><BR>
Example: <i>ghidra://hostname/Repo/notepad.exe#entry</i>
</P>
<P><U>Local Ghidra project file</U><BR>
<i>ghidra:/[&lt;project-path&gt/]&lt;project-name&gt;?/&lt;program-path&gt;[#&lt;address-or-symbol-ref&gt;]</i><BR>
Example: <i>ghidra:/share/MyProject?/notepad.exe#entry</i>
</P>
</BLOCKQUOTE>
<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://&lt;host...</i>) or
local project (<i>ghidra:/&lt;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>

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -38,6 +38,7 @@ import ghidra.app.plugin.core.datamgr.tree.ArchiveNode;
import ghidra.app.plugin.core.datamgr.tree.DataTypeNode;
import ghidra.app.plugin.core.datamgr.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" +

View File

@ -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(

View File

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

View File

@ -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",

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.util.viewer.field;
/**
* This implementation expands {@link URLAnnotatedStringHandler} providing an example form
* of a local project Ghidra URL.
*/
public class GhidraLocalURLAnnotatedStringHandler extends URLAnnotatedStringHandler {
@Override
public String getDisplayString() {
return "Ghidra-URL(local)";
}
@Override
public String getPrototypeString() {
return "{@url \"ghidra:/dirpath/myproject?/folder/program.exe#symbol\" \"display string\"}";
}
}

View File

@ -0,0 +1,34 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.util.viewer.field;
/**
* This implementation expands {@link URLAnnotatedStringHandler} providing an example form
* of a Ghidra Server URL.
*/
public class GhidraServerURLAnnotatedStringHandler extends URLAnnotatedStringHandler {
@Override
public String getDisplayString() {
return "Ghidra-URL(remote)";
}
@Override
public String getPrototypeString() {
return "{@url \"ghidra://myserver/myrepo/folder/program.exe#symbol\" \"display string\"}";
}
}

View File

@ -36,8 +36,10 @@ import ghidra.util.Msg;
* displayed.
*/
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

View File

@ -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.

View File

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

View File

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

View File

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

View File

@ -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) {

View 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();

View File

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

View File

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

View File

@ -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) {

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

@ -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) {

View File

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

View File

@ -0,0 +1,31 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.client;
import java.io.IOException;
/**
* {@code RepositoryNotFoundException} thrown when a failed connection occurs to a
* non-existing repository. A valid server connection is required to make this
* determination.
*/
public class RepositoryNotFoundException extends IOException {
public RepositoryNotFoundException(String msg) {
super(msg);
}
}

View File

@ -52,6 +52,9 @@ public class RepositoryServerAdapter {
// Keeps track of whether the connection attempt was cancelled by the user
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() {

View File

@ -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.

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

@ -36,6 +36,7 @@ src/main/resources/images/disconnected.gif||GHIDRA||reviewed||END|
src/main/resources/images/disk.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/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|

View File

@ -6,10 +6,14 @@ icon.project.data.file.ghidra.unsupported = unknownFile.gif
icon.project.data.file.ghidra.checked.out = icon.check
icon.project.data.file.ghidra.checked.out.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]

View File

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

View File

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

View File

@ -0,0 +1,144 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.data;
import java.io.IOException;
import db.DBHandle;
import db.buffers.BufferFile;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainObject;
import ghidra.framework.store.*;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* <code>DBContentHandler</code> provides an abstract ContentHandler for
* domain object content which is stored within a database file.
* This class provides helper methods for working with database files.
*
* @param <T> {@link DomainObjectAdapterDB} implementation class
*/
public abstract class DBWithUserDataContentHandler<T extends DomainObjectAdapterDB>
extends DBContentHandler<T> {
/**
* Return user data content type corresponding to associatedContentType.
*/
private static String getUserDataContentType(String associatedContentType) {
return associatedContentType + "UserData";
}
/**
* Create user data file associated with existing content.
* This facilitates the lazy creation of the user data file.
* @param associatedDomainObj associated domain object corresponding to this content handler
* @param userDbh user data handle
* @param userfs private user data filesystem
* @param monitor task monitor
* @throws IOException if an IO or access error occurs
* @throws CancelledException if operation is cancelled by user
*/
public final void saveUserDataFile(DomainObject associatedDomainObj, DBHandle userDbh,
FileSystem userfs,
TaskMonitor monitor) throws CancelledException, IOException {
if (userfs.isVersioned()) {
throw new IllegalArgumentException("User data file-system may not be versioned");
}
String associatedContentType = getContentType();
DomainFile associatedDf = associatedDomainObj.getDomainFile();
if (associatedDf == null) {
throw new IllegalStateException("associated " + associatedContentType +
" file must be saved before user data can be saved");
}
String associatedFileID = associatedDf.getFileID();
if (associatedFileID == null) {
Msg.error(this, associatedContentType + " '" + associatedDf.getName() +
"' has not been assigned a file ID, user settings can not be saved!");
return;
}
String path = "/";
String name = ProjectFileManager.getUserDataFilename(associatedFileID);
BufferFile bf = null;
boolean success = false;
try {
bf =
userfs.createDatabase(path, name, FileIDFactory.createFileID(),
getUserDataContentType(associatedContentType), userDbh.getBufferSize(),
SystemUtilities.getUserName(), null);
userDbh.saveAs(bf, true, monitor);
success = true;
}
catch (InvalidNameException e) {
throw new AssertException("Unexpected Error", e);
}
finally {
if (bf != null && !success) {
try {
bf.delete();
}
catch (IOException e) {
// ignore
}
abortCreate(userfs, path, name, FolderItem.DEFAULT_CHECKOUT_ID);
}
}
}
/**
* Remove user data file associated with an existing folder item.
* @param associatedItem associated folder item
* @param userFilesystem user data file system from which corresponding data should be removed.
* @throws IOException if an access error occurs
*/
public final void removeUserDataFile(FolderItem associatedItem, FileSystem userFilesystem)
throws IOException {
String path = "/";
String name = ProjectFileManager.getUserDataFilename(associatedItem.getFileID());
FolderItem item = userFilesystem.getItem(path, name);
if (item != null) {
item.delete(-1, null);
}
}
/**
* Open user data file associatedDbh
* @param associatedFileID
* @param associatedContentType
* @param userfs
* @param monitor
* @return user data file database handle
* @throws IOException
* @throws CancelledException
*/
protected final DBHandle openAssociatedUserFile(String associatedFileID,
String associatedContentType, FileSystem userfs, TaskMonitor monitor)
throws IOException, CancelledException {
String path = "/";
String name = ProjectFileManager.getUserDataFilename(associatedFileID);
FolderItem item = userfs.getItem(path, name);
if (item == null || !(item instanceof DatabaseItem) ||
!getUserDataContentType(associatedContentType).equals(item.getContentType())) {
return null;
}
DatabaseItem dbItem = (DatabaseItem) item;
BufferFile bf = dbItem.openForUpdate(FolderItem.DEFAULT_CHECKOUT_ID);
return new DBHandle(bf, false, monitor);
}
}

View File

@ -17,11 +17,14 @@ package ghidra.framework.data;
import java.io.File;
import java.io.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);
}

View File

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

View File

@ -0,0 +1,119 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.data;
import java.io.IOException;
import java.net.URL;
import javax.swing.Icon;
import ghidra.framework.main.AppInfo;
import ghidra.framework.model.*;
import ghidra.framework.store.FileSystem;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* {@code FolderLinkContentHandler} provide folder-link support.
* Implementation relies on {@link AppInfo#getActiveProject()} to provide life-cycle
* management for related transient-projects opened while following folder-links.
*/
public class FolderLinkContentHandler extends LinkHandler<NullFolderDomainObject> {
public static FolderLinkContentHandler INSTANCE = new FolderLinkContentHandler();
public static final String FOLDER_LINK_CONTENT_TYPE = "FolderLink";
@Override
public long createFile(FileSystem fs, FileSystem userfs, String path, String name,
DomainObject obj, TaskMonitor monitor)
throws IOException, InvalidNameException, CancelledException {
if (!(obj instanceof URLLinkObject)) {
throw new IOException("Unsupported domain object: " + obj.getClass().getName());
}
return createFile((URLLinkObject) obj, FOLDER_LINK_CONTENT_TYPE, fs, path, name,
monitor);
}
@Override
public String getContentType() {
return FOLDER_LINK_CONTENT_TYPE;
}
@Override
public String getContentTypeDisplayString() {
return FOLDER_LINK_CONTENT_TYPE;
}
@Override
public Class<NullFolderDomainObject> getDomainObjectClass() {
return NullFolderDomainObject.class; // special case since link corresponds to a Domain Folder
}
@Override
public Icon getIcon() {
return DomainFolder.CLOSED_FOLDER_ICON;
}
@Override
public String getDefaultToolName() {
return null;
}
/**
* Get linked domain folder
* @param folderLinkFile folder-link file.
* @return {@link LinkedGhidraFolder} referenced by specified folder-link file or null if
* folderLinkFile content type is not {@value #FOLDER_LINK_CONTENT_TYPE}.
* @throws IOException if an IO or folder item access error occurs
*/
public static LinkedGhidraFolder getReadOnlyLinkedFolder(DomainFile folderLinkFile)
throws IOException {
if (!FOLDER_LINK_CONTENT_TYPE.equals(folderLinkFile.getContentType())) {
return null;
}
URL url = getURL(folderLinkFile);
Project activeProject = AppInfo.getActiveProject();
GhidraFolder parent = ((GhidraFile) folderLinkFile).getParent();
return new LinkedGhidraFolder(activeProject, parent, folderLinkFile.getName(), url);
}
}
/**
* Dummy domain object to satisfy {@link FolderLinkContentHandler#getDomainObjectClass()}
*/
final class NullFolderDomainObject extends DomainObjectAdapterDB {
private NullFolderDomainObject() {
// this object may not be instantiated
super(null, null, 0, NullFolderDomainObject.class);
throw new RuntimeException("Object may not be instantiated");
}
@Override
public boolean isChangeable() {
return false;
}
@Override
public String getDescription() {
return "Dummy FolderLink Domain Object";
}
}

View File

@ -16,6 +16,7 @@
package ghidra.framework.data;
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();
}

View File

@ -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() {

View File

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

View File

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

View File

@ -0,0 +1,210 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.data;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import javax.help.UnsupportedOperationException;
import javax.swing.Icon;
import generic.theme.GIcon;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.*;
import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode;
import ghidra.framework.store.FileSystem;
import ghidra.framework.store.FolderItem;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.InvalidNameException;
import ghidra.util.exception.*;
import ghidra.util.task.TaskMonitor;
/**
* NOTE: ALL ContentHandler implementations MUST END IN "ContentHandler". If not,
* the ClassSearcher will not find them.
*
* <code>LinkHandler</code> defines an application interface for handling domain files which are
* shortcut links to another supported content type.
*
* @param <T> {@link URLLinkObject} implementation class
*/
public abstract class LinkHandler<T extends DomainObjectAdapterDB> extends DBContentHandler<T> {
// TODO: Need to improve by making this meta data on file instead of database content.
// Metadata use would eliminate need for DB but we lack support for non-DB files.
public static final String URL_METADATA_KEY = "link.url";
// 16x16 link icon where link is placed in lower-left corner
public static final Icon LINK_ICON = new GIcon("icon.content.handler.link.overlay");
/**
* Create a link file using the specified URL
* @param ghidraUrl link URL (must be a Ghidra URL - see {@link GhidraURL}).
* @param fs filesystem where link file should be created
* @param folderPath folder path which should contain link file
* @param linkFilename link filename
* @throws IOException if an IO error occurs
* @throws InvalidNameException if invalid folderPath or linkFilename specified
*/
protected final void createLink(URL ghidraUrl, LocalFileSystem fs, String folderPath,
String linkFilename) throws IOException, InvalidNameException {
URLLinkObject link = new URLLinkObject(linkFilename, ghidraUrl, this);
try {
createFile(fs, null, folderPath, linkFilename, link, TaskMonitor.DUMMY);
}
catch (CancelledException e) {
throw new AssertException(e); // won't happen
}
finally {
link.release(this);
}
}
@SuppressWarnings("unchecked")
@Override
public final T getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade,
Object consumer, TaskMonitor monitor)
throws IOException, VersionException, CancelledException {
if (!okToUpgrade) {
throw new IllegalArgumentException("okToUpgrade must be true");
}
URL url = getURL(item);
Class<?> domainObjectClass = getDomainObjectClass();
if (domainObjectClass == null) {
throw new UnsupportedOperationException("");
}
GhidraURLWrappedContent wrappedContent = null;
Object content = null;
try {
GhidraURLConnection c = (GhidraURLConnection) url.openConnection();
Object obj = c.getContent(); // read-only access
if (c.getStatusCode() == StatusCode.UNAUTHORIZED) {
throw new IOException("Authorization failure");
}
if (!(obj instanceof GhidraURLWrappedContent)) {
throw new IOException("Unsupported linked content");
}
wrappedContent = (GhidraURLWrappedContent) obj;
content = wrappedContent.getContent(consumer);
if (!(content instanceof DomainFile)) {
throw new IOException("Unsupported linked content: " + content.getClass());
}
DomainFile linkedFile = (DomainFile) content;
if (!getDomainObjectClass().isAssignableFrom(linkedFile.getDomainObjectClass())) {
throw new BadLinkException(
"Excepted " + getDomainObjectClass() + " but linked to " +
linkedFile.getDomainObjectClass());
}
return (T) linkedFile.getReadOnlyDomainObject(consumer, version, monitor);
}
finally {
if (content != null) {
wrappedContent.release(content, consumer);
}
}
}
@Override
public final T getDomainObject(FolderItem item, FileSystem userfs, long checkoutId,
boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor)
throws IOException, CancelledException, VersionException {
// Always upgrade if needed for read-only object
return getReadOnlyObject(item, DomainFile.DEFAULT_VERSION, true, consumer, monitor);
}
@Override
public T getImmutableObject(FolderItem item, Object consumer, int version, int minChangeVersion,
TaskMonitor monitor) throws IOException, CancelledException, VersionException {
throw new UnsupportedOperationException("link-file does not support getImmutableObject");
}
@Override
public final ChangeSet getChangeSet(FolderItem versionedFolderItem, int olderVersion,
int newerVersion) throws VersionException, IOException {
return null;
}
@Override
public final DomainObjectMergeManager getMergeManager(DomainObject resultsObj,
DomainObject sourceObj,
DomainObject originalObj, DomainObject latestObj) {
return null;
}
@Override
public final boolean isPrivateContentType() {
// NOTE: URL must be checked - only repository-based links may be versioned
return true;
}
/**
* Get the link URL which corresponds to the specified link file.
* See {@link DomainFile#isLinkFile()}.
* @param linkFile link-file domain file
* @return link URL
* @throws MalformedURLException if link is bad or unsupported.
* @throws IOException if IO error or supported link file not specified
*/
public static URL getURL(DomainFile linkFile) throws IOException {
String contentType = linkFile.getContentType();
ContentHandler<?> ch = DomainObjectAdapter.getContentHandler(contentType);
if (ch instanceof LinkHandler) {
Map<String, String> metadata = linkFile.getMetadata();
String urlStr = metadata.get(URL_METADATA_KEY);
if (urlStr != null) {
return new URL(urlStr);
}
}
throw new IOException("Invalid link file: " + contentType);
}
/**
* Get the link URL which corresponds to the specified link file.
* See {@link DomainFile#isLinkFile()}.
* @param linkFile link-file folder item
* @return link URL
* @throws MalformedURLException if link is bad or unsupported.
* @throws IOException if IO error or supported link file not specified
*/
static URL getURL(FolderItem linkFile) throws IOException {
String contentType = linkFile.getContentType();
ContentHandler<?> ch = DomainObjectAdapter.getContentHandler(contentType);
if (ch instanceof LinkHandler) {
Map<String, String> metadata = GhidraFileData.getMetadata(linkFile);
String urlStr = metadata.get(URL_METADATA_KEY);
if (urlStr != null) {
return new URL(urlStr);
}
}
throw new IOException("Invalid link file: " + contentType);
}
/**
* Get the base icon for this link-file which does not include the
* link overlay icon.
*/
@Override
abstract public Icon getIcon();
}

View File

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

View File

@ -0,0 +1,138 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.data;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import javax.swing.Icon;
import generic.theme.GIcon;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.FileSystem;
/**
* {@code LinkedGhidraFolder} provides the base {@link LinkedDomainFolder} implementation which
* corresponds to a project folder-link (see {@link FolderLinkContentHandler}).
*/
public class LinkedGhidraFolder extends LinkedGhidraSubFolder {
public static Icon FOLDER_LINK_CLOSED_ICON =
new GIcon("icon.content.handler.linked.folder.closed");
public static Icon FOLDER_LINK_OPEN_ICON =
new GIcon("icon.content.handler.linked.folder.open");
private final Project activeProject;
private final DomainFolder localParent;
private final URL folderUrl;
private String linkedPathname;
private URL projectUrl;
/**
* Construct a linked-folder.
* @param activeProject active project responsible for linked project life-cycle management.
* @param localParent local domain folder which contains folder-link or corresponds directly to
* folder-link (name=null).
* @param linkFilename folder-link filename
* @param folderUrl linked folder URL
*/
LinkedGhidraFolder(Project activeProject, DomainFolder localParent, String linkFilename,
URL folderUrl) {
super(linkFilename);
if (!GhidraURL.isServerRepositoryURL(folderUrl) &&
!GhidraURL.isLocalProjectURL(folderUrl)) {
throw new IllegalArgumentException("Invalid Ghidra URL: " + folderUrl);
}
this.activeProject = activeProject;
this.localParent = localParent;
this.folderUrl = folderUrl;
linkedPathname = GhidraURL.getProjectPathname(folderUrl);
if (linkedPathname.length() > 0 && linkedPathname.endsWith(FileSystem.SEPARATOR)) {
linkedPathname = linkedPathname.substring(0, linkedPathname.length() - 1);
}
}
/**
* Get the Ghidra URL associated with this linked folder's project or repository
* @return Ghidra URL associated with this linked folder's project or repository
*/
public URL getProjectURL() {
if (projectUrl == null) {
projectUrl = GhidraURL.getProjectURL(folderUrl);
}
return projectUrl;
}
LinkedGhidraFolder getLinkedRootFolder() {
return this;
}
DomainFolder getLinkedFolder(String linkedPath) throws IOException {
ProjectData projectData = activeProject.addProjectView(getProjectURL(), false);
if (projectData == null) {
throw new FileNotFoundException();
}
DomainFolder folder = projectData.getFolder(linkedPath);
if (folder == null) {
throw new FileNotFoundException(folderUrl.toExternalForm());
}
return folder;
}
@Override
public String getLinkedPathname() {
return linkedPathname;
}
@Override
public ProjectLocator getProjectLocator() {
return activeProject.getProjectLocator();
}
@Override
public ProjectData getProjectData() {
return activeProject.getProjectData();
}
@Override
public DomainFolder getParent() {
return localParent;
}
@Override
public String toString() {
return "LinkedGhidraFolder: " + getPathname();
}
@Override
public Icon getIcon(boolean isOpen) {
return isOpen ? FOLDER_LINK_OPEN_ICON : FOLDER_LINK_CLOSED_ICON;
}
@Override
public boolean isLinked() {
return true;
}
}

View File

@ -0,0 +1,294 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.data;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import javax.swing.Icon;
import ghidra.framework.model.*;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.framework.store.FileSystem;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
class LinkedGhidraSubFolder implements LinkedDomainFolder {
private final LinkedGhidraFolder linkedRootFolder;
private final LinkedGhidraSubFolder parent;
private final String folderName;
LinkedGhidraSubFolder(String folderName) {
this.linkedRootFolder = getLinkedRootFolder();
this.parent = null; // must override getParent()
this.folderName = folderName;
}
LinkedGhidraSubFolder(LinkedGhidraSubFolder parent, String folderName) {
this.linkedRootFolder = parent.getLinkedRootFolder();
this.parent = parent;
this.folderName = folderName;
}
/**
* Get the linked root folder which corresponds to a folder-link
* (see {@link FolderLinkContentHandler}).
* @return linked root folder
*/
LinkedGhidraFolder getLinkedRootFolder() {
return linkedRootFolder;
}
@Override
public boolean isInWritableProject() {
return false; // While project may be writeable this folder is not
}
@Override
public DomainFolder getParent() {
return parent;
}
@Override
public String getName() {
return folderName;
}
@Override
public DomainFolder getLinkedFolder() throws IOException {
return linkedRootFolder.getLinkedFolder(getLinkedPathname());
}
@Override
public int compareTo(DomainFolder df) {
return getName().compareToIgnoreCase(df.getName());
}
@Override
public DomainFolder setName(String newName) throws InvalidNameException, IOException {
throw new ReadOnlyException("linked folder is read only");
}
@Override
public URL getSharedProjectURL() {
URL projectURL = getLinkedRootFolder().getProjectURL();
if (GhidraURL.isServerRepositoryURL(projectURL)) {
String urlStr = projectURL.toExternalForm();
if (urlStr.endsWith(FileSystem.SEPARATOR)) {
urlStr = urlStr.substring(0, urlStr.length() - 1);
}
String path = getLinkedPathname();
if (!path.endsWith(FileSystem.SEPARATOR)) {
path += FileSystem.SEPARATOR;
}
try {
return new URL(urlStr + path);
}
catch (MalformedURLException e) {
// ignore
}
}
return null;
}
@Override
public ProjectLocator getProjectLocator() {
return parent.getProjectLocator();
}
@Override
public ProjectData getProjectData() {
return parent.getProjectData();
}
@Override
public String getPathname() {
// pathname within project containing folder-link
// getParent() may return a non-linked folder
String path = getParent().getPathname();
if (path.length() != FileSystem.SEPARATOR.length()) {
path += FileSystem.SEPARATOR;
}
path += folderName;
return path;
}
/**
* Get the pathname of this folder within the linked-project/repository
* @return absolute linked folder path within the linked-project/repository
*/
public String getLinkedPathname() {
String path = parent.getLinkedPathname();
if (!path.endsWith(FileSystem.SEPARATOR)) {
path += FileSystem.SEPARATOR;
}
path += folderName;
return path;
}
@Override
public LinkedGhidraSubFolder[] getFolders() {
try {
DomainFolder linkedFolder = getLinkedFolder();
DomainFolder[] folders = linkedFolder.getFolders();
LinkedGhidraSubFolder[] linkedSubFolders = new LinkedGhidraSubFolder[folders.length];
for (int i = 0; i < folders.length; i++) {
linkedSubFolders[i] = new LinkedGhidraSubFolder(this, folders[i].getName());
}
return linkedSubFolders;
}
catch (IOException e) {
Msg.error(this, "Linked folder failure: " + e.getMessage());
return new LinkedGhidraSubFolder[0];
}
}
@Override
public LinkedGhidraSubFolder getFolder(String name) {
try {
DomainFolder linkedFolder = getLinkedFolder();
DomainFolder f = linkedFolder.getFolder(name);
if (f != null) {
return new LinkedGhidraSubFolder(this, name);
}
}
catch (IOException e) {
Msg.error(this, "Linked folder failure: " + e.getMessage());
}
return null;
}
@Override
public DomainFile[] getFiles() {
try {
DomainFolder linkedFolder = getLinkedFolder();
DomainFile[] files = linkedFolder.getFiles();
LinkedGhidraFile[] linkedSubFolders = new LinkedGhidraFile[files.length];
for (int i = 0; i < files.length; i++) {
linkedSubFolders[i] = new LinkedGhidraFile(this, files[i].getName());
}
return linkedSubFolders;
}
catch (IOException e) {
Msg.error(this, "Linked folder failure: " + e.getMessage());
return new LinkedGhidraFile[0];
}
}
/**
* Get the true file within this linked folder.
* @param name file name
* @return file or null if not found or error occurs
*/
public DomainFile getLinkedFileNoError(String name) {
try {
DomainFolder linkedFolder = getLinkedFolder();
return linkedFolder.getFile(name);
}
catch (IOException e) {
Msg.error(this, "Linked folder failure: " + e.getMessage());
}
return null;
}
DomainFile getLinkedFile(String name) throws IOException {
DomainFolder linkedFolder = getLinkedFolder();
DomainFile df = linkedFolder.getFile(name);
if (df == null) {
throw new FileNotFoundException("linked-file '" + name + "' not found");
}
return df;
}
@Override
public DomainFile getFile(String name) {
DomainFile f = getLinkedFileNoError(name);
return f != null ? new LinkedGhidraFile(this, name) : null;
}
@Override
public boolean isEmpty() {
try {
DomainFolder linkedFolder = getLinkedFolder();
return linkedFolder.isEmpty();
}
catch (IOException e) {
Msg.error(this, "Linked folder failure: " + e.getMessage());
// TODO: what should we return if folder not found or error occurs?
// True is returned to allow this method to be used to avoid continued access.
return true;
}
}
@Override
public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
throw new ReadOnlyException("linked folder is read only");
}
@Override
public DomainFile createFile(String name, File packFile, TaskMonitor monitor)
throws InvalidNameException, IOException, CancelledException {
throw new ReadOnlyException("linked folder is read only");
}
@Override
public DomainFolder createFolder(String name) throws InvalidNameException, IOException {
throw new ReadOnlyException("linked folder is read only");
}
@Override
public void delete() throws IOException {
throw new ReadOnlyException("linked folder is read only");
}
@Override
public DomainFolder moveTo(DomainFolder newParent) throws IOException {
throw new ReadOnlyException("linked folder is read only");
}
@Override
public DomainFolder copyTo(DomainFolder newParent, TaskMonitor monitor)
throws IOException, CancelledException {
DomainFolder linkedFolder = getLinkedFolder();
return linkedFolder.copyTo(newParent, monitor);
}
@Override
public DomainFile copyToAsLink(DomainFolder newParent) throws IOException {
DomainFolder linkedFolder = getLinkedFolder();
return linkedFolder.copyToAsLink(newParent);
}
@Override
public void setActive() {
// do nothing
}
@Override
public String toString() {
return "LinkedGhidraSubFolder: " + getPathname();
}
@Override
public Icon getIcon(boolean isOpen) {
return isOpen ? OPEN_FOLDER_ICON : CLOSED_FOLDER_ICON;
}
}

View File

@ -16,13 +16,12 @@
package ghidra.framework.data;
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() {

View File

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

View File

@ -0,0 +1,101 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.data;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import javax.help.UnsupportedOperationException;
import db.DBHandle;
import ghidra.framework.model.DomainFile;
import ghidra.framework.model.DomainObject;
import ghidra.framework.protocol.ghidra.GhidraURL;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* {@code DomainObjectAdapterLink} object provides a Ghidra URL (see {@link GhidraURL}) wrapper
* where the URL is intended to refer to a {@link DomainFile} within another local or remote
* project/repository. Link files which correspond to this type of {@link DomainObject} are
* not intended to be modified and should be created or deleted. A checkout may be used when
* an offline copy is required but otherwise serves no purpose since a modification and checkin
* is not supported.
*/
public class URLLinkObject extends DomainObjectAdapterDB {
// Use a reduced DB buffer size to reduce file size for minimal content.
// This will allow a 4-KByte DB buffer file to hold a URL upto ~470 bytes long.
// Longer URLs will rely on 1-KByte chained buffers which will increase file length.
private static final int DB_BUFFER_SIZE = 1024;
private URL url;
/**
* Constructs a new link file object
* @param name link name
* @param ghidraUrl link URL
* @param consumer the object that is using this program.
* @throws IOException if there is an error accessing the database or invalid URL specified.
*/
public URLLinkObject(String name, URL ghidraUrl, Object consumer) throws IOException {
super(new DBHandle(DB_BUFFER_SIZE), name, 500, consumer);
metadata.put(LinkHandler.URL_METADATA_KEY, ghidraUrl.toString());
updateMetadata();
}
/**
* Constructs a link file object from a DBHandle (read-only)
* @param dbh a handle to an open program database.
* @param consumer the object that keeping the program open.
* @throws IOException if an error accessing the database occurs.
*/
public URLLinkObject(DBHandle dbh, Object consumer) throws IOException {
super(dbh, "Untitled", 500, consumer);
loadMetadata();
String urlText = metadata.get(LinkHandler.URL_METADATA_KEY);
if (urlText == null) {
throw new IOException("Null link object");
}
url = new URL(urlText);
}
@Override
public String getDescription() {
return "Link-File";
}
/**
* Get link URL
* @return link URL
*/
public URL getLink() {
return url;
}
@Override
public final boolean isChangeable() {
return false;
}
@Override
public final void saveToPackedFile(File outputFile, TaskMonitor monitor)
throws IOException, CancelledException {
throw new UnsupportedOperationException();
}
}

View File

@ -40,6 +40,8 @@ import generic.theme.GIcon;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.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);
}
}

View File

@ -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() {

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

@ -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())) {

View File

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

View File

@ -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 -> {

View File

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