diff --git a/Ghidra/Configurations/Public_Release/src/global/docs/WhatsNew.md b/Ghidra/Configurations/Public_Release/src/global/docs/WhatsNew.md index fab2595819..e4ec81f50d 100644 --- a/Ghidra/Configurations/Public_Release/src/global/docs/WhatsNew.md +++ b/Ghidra/Configurations/Public_Release/src/global/docs/WhatsNew.md @@ -15,6 +15,43 @@ applied Ghidra SRE capabilities to a variety of problems that involve analyzing generating deep insights for NSA analysts who seek a better understanding of potential vulnerabilities in networks and systems. +# What's coming in Ghidra 11.5 +This is a preview of what is coming in the future Ghidra 11.5 release. + +**NOTE:** Ghidra Server: The Ghidra 11.5 server is compatible with Ghidra 9.2 and later Ghidra +clients although the presence of any newer link-files within a repository may not be handled properly +by client versions prior to 11.5 which lack support for the new storage format. Ghidra 11.5 clients +which introduce new link-files into a project will not be able to add such files into version +control if connected to older Ghidra Server versions. + +## Project Link Files + +Support for link-files within a Ghidra Project has been significantly expanded with this release and +with it a new file storage type has been introduced which can create some incompatibilties if projects +and repositories containing such files are used by older version of Ghidra or the Ghidra Server. + +Previously only external folder and file links were supported through the use of a Ghidra URL. +With 11.5 the ability to establish internal folder and file links has been introduced. The new +storage format avoids the use of a database and relies only on a light-weight property file. Internal +project links also allow for either absolute or relative links. Due to the fact that Ghidra allows +a folder or file to have the same pathname, some abiguities can result. It is highly recommended that +the use of conflicting folder and file pathnames be avoided. + +The use of internally linked folders and files allows batch import processing to more accurately +reflect the native file-system and its use of symbolic links which allow for the same content to +be referenced by multiple paths. Allowing this within a Ghidra project can avoid the potential for +importing content multiple times with the different paths and simply import once with additional +link-files which reference it. How best to leverage links very much depends on the end-user's +needs and project file management preferences. Special care must be taken when defining or +traversing link-files to avoid external and circular references. + +Additional Ghidra API methods have been provided or refined on the following classes to leverage +link-files: `DomainFolder`, `DomainFile`, `LinkFile`, `LinkHandler`, `DomainFileFilter`, +`DomainFileIterator`, etc. + +...TO BE CONTINUED... + + # What's New in Ghidra 11.4 This release includes new features, enhancements, performance improvements, quite a few bug fixes, and many pull-request contributions. Thanks to all those who have contributed their time, thoughts, diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyActionsPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyActionsPlugin.java index 633b47a77d..997ff971c0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyActionsPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/copying/DebuggerCopyActionsPlugin.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -116,10 +116,8 @@ public class DebuggerCopyActionsPlugin extends AbstractDebuggerPlugin { ? view.getTrace().getFixedProgramView(view.getSnap()) : view; - ExporterDialog dialog = - new ExporterDialog(tool, fixed.getDomainFile(), fixed, - getSelectionFromContext(context)); - tool.showDialog(dialog); + ExporterDialog.showExporterDialog(tool, fixed.getDomainFile(), fixed, + getSelectionFromContext(context)); } protected void activatedCopyIntoCurrentProgram(DebuggerProgramLocationActionContext context) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java index 412792ba7a..ea420ea1d8 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java @@ -26,6 +26,7 @@ import ghidra.app.plugin.core.debug.utils.ProgramURLUtils; import ghidra.framework.model.*; import ghidra.framework.options.Options; import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.ProgramContentHandler; import ghidra.program.model.address.*; import ghidra.program.model.listing.Program; import ghidra.trace.model.Lifespan; @@ -73,7 +74,8 @@ public class ProgramModuleIndexer implements DomainFolderChangeListener { // TODO: Note language and prefer those from the same processor? // Will get difficult with new OBTR, since I'd need a platform // There's also the WoW64 issue.... - protected record IndexEntry(String name, String dfID, NameSource source) {} + protected record IndexEntry(String name, String dfID, NameSource source) { + } protected class ModuleChangeListener implements DomainObjectListener, DomainObjectClosedListener { @@ -212,10 +214,13 @@ public class ProgramModuleIndexer implements DomainFolderChangeListener { if (disposed) { return; } - if (!Program.class.isAssignableFrom(file.getDomainObjectClass())) { - return; + // Folder-links and program link-files are not handled. Using content type + // to filter is the 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(file.getContentType())) { + addToIndex(file, file.getMetadata()); } - addToIndex(file, file.getMetadata()); } protected void addToIndex(DomainFile file, Map metadata) { @@ -383,9 +388,9 @@ public class ProgramModuleIndexer implements DomainFolderChangeListener { public DomainFile getBestMatch(TraceModule module, long snap, Program currentProgram, Collection entries) { Address base = module.getBase(snap); - AddressSpace space = base == null - ? module.getTrace().getBaseAddressFactory().getDefaultAddressSpace() - : base.getAddressSpace(); + AddressSpace space = + base == null ? module.getTrace().getBaseAddressFactory().getDefaultAddressSpace() + : base.getAddressSpace(); return getBestMatch(space, module, snap, currentProgram, entries); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java index 447f326bf2..7bd047d237 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java @@ -15,7 +15,7 @@ */ package ghidra.app.plugin.core.debug.service.tracemgr; -import static ghidra.framework.main.DataTreeDialogType.OPEN; +import static ghidra.framework.main.DataTreeDialogType.*; import java.io.IOException; import java.lang.invoke.MethodHandles; @@ -223,9 +223,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin public void targetWithdrawn(Target target) { Swing.runLater(() -> updateCurrentTarget()); boolean save = isSaveTracesByDefault(); - CompletableFuture flush = save - ? waitUnlockedDebounced(target) - : AsyncUtils.nil(); + CompletableFuture flush = save ? waitUnlockedDebounced(target) : AsyncUtils.nil(); flush.thenRunAsync(() -> { if (!isAutoCloseOnTerminate()) { return; @@ -416,20 +414,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } protected DataTreeDialog getTraceChooserDialog() { - - DomainFileFilter filter = new DomainFileFilter() { - - @Override - public boolean accept(DomainFile df) { - return Trace.class.isAssignableFrom(df.getDomainObjectClass()); - } - - @Override - public boolean followLinkedFolders() { - return false; - } - }; - + DomainFileFilter filter = new DefaultDomainFileFilter(Trace.class, false); return new DataTreeDialog(null, OpenTraceAction.NAME, OPEN, filter); } @@ -454,11 +439,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin @Override public void closeDeadTraces() { - checkCloseTraces(targetService == null - ? getOpenTraces() - : getOpenTraces().stream() - .filter(t -> targetService.getTarget(t) == null) - .toList(), + checkCloseTraces(targetService == null ? getOpenTraces() + : getOpenTraces().stream().filter(t -> targetService.getTarget(t) == null).toList(), false); } @@ -790,8 +772,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin varView.setSnap(snap); varView.setPlatform(coordinates.getPlatform()); fireLocationEvent(coordinates, cause); - }, cause == ActivationCause.EMU_STATE_EDIT - ? SwingExecutorService.MAYBE_NOW // ProgramView may call .get on Swing thread + }, cause == ActivationCause.EMU_STATE_EDIT ? SwingExecutorService.MAYBE_NOW // ProgramView may call .get on Swing thread : SwingExecutorService.LATER); // Respect event order } @@ -845,7 +826,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin // TODO: Support upgrading e = new VersionException(e.getVersionIndicator(), false).combine(e); VersionExceptionHandler.showVersionError(null, file.getName(), file.getContentType(), - "Open", e); + "Open", false, e); return null; } catch (IOException e) { @@ -1069,10 +1050,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin protected void checkCloseTraces(Collection traces, boolean noConfirm) { List live = - traces.stream() - .map(t -> targetService.getTarget(t)) - .filter(t -> t != null) - .toList(); + traces.stream().map(t -> targetService.getTarget(t)).filter(t -> t != null).toList(); /** * A provider may be reading a trace, likely via the Swing thread, so schedule this on the * same thread to avoid a ClosedException. diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceContentHandler.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceContentHandler.java index 4a24d5fcad..093b571e13 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceContentHandler.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceContentHandler.java @@ -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. @@ -313,7 +313,7 @@ public class DBTraceContentHandler extends DBWithUserDataContentHandler @Override public String getContentTypeDisplayString() { - return "Trace"; + return TRACE_CONTENT_TYPE; } @Override diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceLinkContentHandler.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceLinkContentHandler.java index f64526be0a..932db0e5ef 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceLinkContentHandler.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/database/DBTraceLinkContentHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,32 +15,15 @@ */ package ghidra.trace.database; -import java.io.IOException; - import javax.swing.Icon; import ghidra.framework.data.LinkHandler; -import ghidra.framework.data.URLLinkObject; -import ghidra.framework.model.DomainObject; -import ghidra.framework.store.FileSystem; -import ghidra.util.InvalidNameException; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; public class DBTraceLinkContentHandler extends LinkHandler { - public static final String TRACE_LINK_CONTENT_TYPE = "TraceLink"; + public static DBTraceLinkContentHandler INSTANCE = new DBTraceLinkContentHandler(); - @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); - } + public static final String TRACE_LINK_CONTENT_TYPE = "TraceLink"; @Override public String getContentType() { diff --git a/Ghidra/Extensions/MachineLearning/src/main/java/ghidra/machinelearning/functionfinding/FunctionStartRFParamsDialog.java b/Ghidra/Extensions/MachineLearning/src/main/java/ghidra/machinelearning/functionfinding/FunctionStartRFParamsDialog.java index 683e4575b0..85900e10f1 100644 --- a/Ghidra/Extensions/MachineLearning/src/main/java/ghidra/machinelearning/functionfinding/FunctionStartRFParamsDialog.java +++ b/Ghidra/Extensions/MachineLearning/src/main/java/ghidra/machinelearning/functionfinding/FunctionStartRFParamsDialog.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,8 +15,6 @@ */ package ghidra.machinelearning.functionfinding; -import static ghidra.framework.main.DataTreeDialogType.*; - import java.awt.BorderLayout; import java.util.*; import java.util.stream.Collectors; @@ -35,7 +33,7 @@ import docking.widgets.table.GTable; import docking.widgets.table.threaded.GThreadedTablePanel; import docking.widgets.textfield.IntegerTextField; import ghidra.app.services.ProgramManager; -import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.main.ProgramFileChooser; import ghidra.framework.model.DomainFile; import ghidra.framework.preferences.Preferences; import ghidra.program.model.address.AddressSet; @@ -485,11 +483,7 @@ public class FunctionStartRFParamsDialog extends ReusableDialogComponentProvider } private void searchOtherProgram(RandomForestRowObject modelRow) { - DataTreeDialog dtd = - new DataTreeDialog(null, "Select Program", OPEN, f -> { - Class c = f.getDomainObjectClass(); - return Program.class.isAssignableFrom(c); - }); + ProgramFileChooser dtd = new ProgramFileChooser(null, "Select Program"); dtd.show(); DomainFile dFile = dtd.getDomainFile(); if (dFile == null) { diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java index 8163027b42..0e1e11aa06 100755 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/IterateRepository.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,7 @@ import java.net.URL; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFolder; import ghidra.framework.protocol.ghidra.*; +import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl; import ghidra.program.database.ProgramContentHandler; import ghidra.program.model.listing.Program; import ghidra.util.Msg; @@ -56,7 +57,8 @@ public abstract class IterateRepository { throw new MalformedURLException("Unsupported repository URL: " + ghidraURL); } - GhidraURLQuery.queryUrl(ghidraURL, new GhidraURLResultHandlerAdapter(true) { + // Query URL - may be either file or folder (no link following) + GhidraURLQuery.queryUrl(ghidraURL, null, new GhidraURLResultHandlerAdapter(true) { @Override public void processResult(DomainFolder domainFolder, URL url, TaskMonitor m) @@ -76,7 +78,9 @@ public abstract class IterateRepository { process(domainFile, monitor); } - }, monitor); + // Link files are skipped to avoid duplicate processing + // Processing should be done on actual folder - not a linked folder + }, LinkFileControl.NO_FOLLOW, monitor); } @@ -115,12 +119,11 @@ public abstract class IterateRepository { private void process(DomainFile file, TaskMonitor monitor) throws IOException, CancelledException { - // 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())" + // Do not follow folder-links or consider program links to avoid possible duplication of + // file processing. Using content type is the best way to restrict this. If program links + // should be considered "Program.class.isAssignableFrom(domainFile.getDomainObjectClass())" // should be used. if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(file.getContentType())) { - // NOTE: linked-folders and linked-files are not currently supported return; // skip non-program file } @@ -129,6 +132,7 @@ public abstract class IterateRepository { Msg.debug(IterateRepository.class, "Processing " + file.getPathname() + "..."); monitor.setMessage("Processing: " + file.getName()); monitor.incrementProgress(1); + // NOTE: The following method invocation will follow all links if presented one program = (Program) file.getReadOnlyDomainObject(this, -1, monitor); process(program, monitor); } diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 7b6b928e40..76a4b1aa3b 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -398,6 +398,10 @@ src/main/help/help/topics/FrontEndPlugin/Project_Info.htm||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/Re-opening_a_Project.htm||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/Restore_Project.htm||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/Saving_a_Ghidra_Project.htm||GHIDRA||||END| +src/main/help/help/topics/FrontEndPlugin/images/AbsoluteBrokenFileLinkIcon.png||GHIDRA||||END| +src/main/help/help/topics/FrontEndPlugin/images/AbsoluteBrokenFolderLinkIcon.png||GHIDRA||||END| +src/main/help/help/topics/FrontEndPlugin/images/AbsoluteFileLinkIcon.png||GHIDRA||||END| +src/main/help/help/topics/FrontEndPlugin/images/AbsoluteFolderLinkIcon.png||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/ArchiveFileExists.png||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/ArchiveProject.png||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/ChangeAccessList.png||GHIDRA||||END| @@ -439,6 +443,7 @@ src/main/help/help/topics/FrontEndPlugin/images/VersionedFileIcon.png||GHIDRA||| src/main/help/help/topics/FrontEndPlugin/images/ViewOtherProjects.png||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/ViewProjectAccessPanel.png||GHIDRA||||END| src/main/help/help/topics/FrontEndPlugin/images/hijack_file.png||GHIDRA||||END| +src/main/help/help/topics/FrontEndPlugin/images/start-here_16.png||GHIDRA||||END| src/main/help/help/topics/FunctionComparison/FunctionComparison.htm||GHIDRA||||END| src/main/help/help/topics/FunctionComparison/images/AddFunctionsPanel.png||GHIDRA||||END| src/main/help/help/topics/FunctionComparison/images/AddToComparisonIcon.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/developer_scripts/FixLangId.java b/Ghidra/Features/Base/developer_scripts/FixLangId.java index 52910947cd..d68ead2229 100644 --- a/Ghidra/Features/Base/developer_scripts/FixLangId.java +++ b/Ghidra/Features/Base/developer_scripts/FixLangId.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -30,7 +30,7 @@ import ghidra.app.script.ImproperUseException; import ghidra.framework.data.GhidraFile; import ghidra.framework.data.GhidraFileData; import ghidra.framework.main.DataTreeDialog; -import ghidra.framework.model.DomainFile; +import ghidra.framework.model.*; import ghidra.framework.store.FolderItem; import ghidra.framework.store.local.LocalDatabaseItem; import ghidra.program.model.lang.LanguageDescription; @@ -115,8 +115,8 @@ public class FixLangId extends GhidraScript { if (langId != null) { Msg.warn(this, "Changing language ID from '" + record.getString(0) + "' to '" + langId + "' for program: " + df.getName()); - desc = DefaultLanguageService.getLanguageService().getLanguageDescription( - new LanguageID(langId)); + desc = DefaultLanguageService.getLanguageService() + .getLanguageDescription(new LanguageID(langId)); long txId = dbh.startTransaction(); try { record.setString(0, langId); @@ -139,7 +139,10 @@ public class FixLangId extends GhidraScript { public DomainFile askProgramFile(String title) { final DomainFile[] domainFile = new DomainFile[] { null }; - final DataTreeDialog dtd = new DataTreeDialog(null, title, OPEN); + // The file filter employed restricts selection to a program file within the active + // project where we have the ability to update file data. + final DataTreeDialog dtd = + new DataTreeDialog(null, title, OPEN, new DefaultDomainFileFilter(Program.class, true)); dtd.addOkActionListener(e -> { dtd.close(); domainFile[0] = dtd.getDomainFile(); diff --git a/Ghidra/Features/Base/ghidra_scripts/RenameProgramsInProjectScript.java b/Ghidra/Features/Base/ghidra_scripts/RenameProgramsInProjectScript.java index 25215b53bd..9fa0456dfd 100644 --- a/Ghidra/Features/Base/ghidra_scripts/RenameProgramsInProjectScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/RenameProgramsInProjectScript.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,48 +19,56 @@ import ghidra.app.script.GhidraScript; import ghidra.framework.model.*; import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.ProgramContentHandler; import ghidra.util.InvalidNameException; +import ghidra.util.exception.CancelledException; import java.io.IOException; public class RenameProgramsInProjectScript extends GhidraScript { - @Override - public void run() throws Exception { - - if ( currentProgram != null ) { - popup( "This script should be run from a tool with no open programs" ); - return; - } - - PluginTool tool = state.getTool(); - Project project = tool.getProject(); - ProjectData projectData = project.getProjectData(); - DomainFolder rootFolder = projectData.getRootFolder(); - recurseProjectFolder( rootFolder ); - } - - private void recurseProjectFolder( DomainFolder domainFolder ) { - DomainFile[] files = domainFolder.getFiles(); - for ( DomainFile domainFile : files ) { - processDomainFile( domainFile ); - } - DomainFolder[] folders = domainFolder.getFolders(); - for ( DomainFolder folder : folders ) { - recurseProjectFolder( folder ); - } - } - - private void processDomainFile( DomainFile domainFile ) { - String oldName = domainFile.getName(); - try { - domainFile.setName( oldName + "_renamed" ); - } - catch ( InvalidNameException e ) { - e.printStackTrace(); - } - catch ( IOException e ) { - e.printStackTrace(); - } - } + @Override + public void run() throws Exception { + + if (currentProgram != null) { + popup("This script should be run from a tool with no open programs.\n" + + "Warning! If using file-links to programs within this project such linkages will break."); + return; + } + + PluginTool tool = state.getTool(); + Project project = tool.getProject(); + ProjectData projectData = project.getProjectData(); + DomainFolder rootFolder = projectData.getRootFolder(); + recurseProjectFolder(rootFolder); + } + + private void recurseProjectFolder(DomainFolder domainFolder) throws CancelledException { + DomainFile[] files = domainFolder.getFiles(); + for (DomainFile domainFile : files) { + monitor.checkCancelled(); + processDomainFile(domainFile); + } + DomainFolder[] folders = domainFolder.getFolders(); + for (DomainFolder folder : folders) { + monitor.checkCancelled(); + recurseProjectFolder(folder); + } + } + + private void processDomainFile(DomainFile domainFile) { + if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domainFile.getContentType())) { + return; + } + String oldName = domainFile.getName(); + try { + domainFile.setName(oldName + "_renamed"); + } + catch (InvalidNameException e) { + e.printStackTrace(); + } + catch (IOException e) { + e.printStackTrace(); + } + } } diff --git a/Ghidra/Features/Base/ghidra_scripts/RepositoryFileUpgradeScript.java b/Ghidra/Features/Base/ghidra_scripts/RepositoryFileUpgradeScript.java index 3445104b53..1c6cc53a29 100644 --- a/Ghidra/Features/Base/ghidra_scripts/RepositoryFileUpgradeScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/RepositoryFileUpgradeScript.java @@ -102,6 +102,8 @@ public class RepositoryFileUpgradeScript extends GhidraScript { } private int listCheckouts(DomainFolder folder) throws IOException, CancelledException { + // Avoid following folder-links so we don't count the same file more than once. + // Link-files will never be in a checked-out state. int count = 0; for (DomainFile df : folder.getFiles()) { monitor.checkCancelled(); @@ -115,8 +117,8 @@ public class RepositoryFileUpgradeScript extends GhidraScript { } private int listCheckouts(DomainFile df) throws IOException { - if (!df.isVersioned()) { - return 0; + if (!df.isVersioned() || df.isLink()) { + return 0; // ignore non-versioned files and link-files } int count = 0; for (ItemCheckoutStatus checkout : df.getCheckouts()) { diff --git a/Ghidra/Features/Base/ghidra_scripts/VersionControl_ResetAll.java b/Ghidra/Features/Base/ghidra_scripts/VersionControl_ResetAll.java index fbc387c9d7..3f33158d0e 100644 --- a/Ghidra/Features/Base/ghidra_scripts/VersionControl_ResetAll.java +++ b/Ghidra/Features/Base/ghidra_scripts/VersionControl_ResetAll.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -27,7 +27,6 @@ public class VersionControl_ResetAll extends GhidraScript { public VersionControl_ResetAll() { } - @Override public void run() throws Exception { @@ -54,7 +53,8 @@ public class VersionControl_ResetAll extends GhidraScript { if (monitor.isCancelled()) { break; } - + // Do not follow folder-links or consider program links. Checking the content type + // is the best way to restrict this. if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(file.getContentType()) || !file.isVersioned() || file.getLatestVersion() < 2) { continue;// skip diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm index 8d80474130..41896f838c 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm @@ -60,6 +60,8 @@
  • Data Table
  • File Icons
  • + +
  • Ghidra URL Formats
  • Console
  • @@ -93,8 +95,10 @@

    Active Project

    -

    The Active Project view shows your programs and datatype archives in a tree view or a - table view. The tree view is useful for organizing your files into folders and sub-folders. +

    The Active Project view shows the various files associated with the current + project which has been open for update. Project files generally consist of programs and + datatype archives but may also be related to other Ghidra content. + The tree view is useful for organizing your files into folders and sub-folders. The table view is useful for sorting all your files on some particular attribute such as size, processor, or modification date. In either view, you open and perform various actions on program files or datatype archives.

    @@ -105,13 +109,23 @@

    The data tree shows all files in the project orgnanized into folders and sub-folders. - Icons for files - indicate whether they are under Icons for files indicate whether they are under version control and whether you have the file checked out. - Open this view by activating the "Tree View" tab.

    + In addition, unique icons are used to reflect content-type and if it corresponds to + a link-file referring to another file or folder (see creating links). + Open this view by activating the project window "Tree View" tab.

    + +

    Although Ghidra allows a folder and file within + the same parent folder to have the same name, it is recommended this be avoided if possible. + Allowing both a folder and file to have the same pathname can result in ambiguous path problems + when using link files and/or Ghidra URLs where only a path is used to identify either a project + resource. +

    +
    +

     

    Tree Only Actions

    @@ -124,7 +138,7 @@

    To create a new folder,

      -
    1. Select a folder that you own. 
    2. +
    3. Select a folder which should contain the new folder.
    4. Right mouse click and choose the New Folder option.
    5. @@ -133,8 +147,6 @@ editing.
    -

    You cannot create - a sub-folder of a folder that you do not own.

    Copy Folders and Files

    @@ -159,7 +171,47 @@ time.  + +

    Paste Copied Folder or File as a Link

    +
    +

    A Link may be created within the active project to a file or folder within the + same project (internal) or to a viewed project/repository (external). + Internal links may be defined using either a relative path or an absolute path. Once + a link is created its stored path will not change. The link will need to be replaced + should the referenced path need to be changed. In addition, file-links are specific + to the content-type of the referenced file at the time of link creation (e.g., + ProgramLink). +

    +

    To create a Link use the following steps from the source project data tree:

    +
      +
    1. Select a single file or folder, right mouse click and choose the  Copy option.
    2. + +
    3. Select a destination folder within the active project data tree.
    4. + +
    5. Right mouse click and choose the Paste as Link or Paste as Relative-Link + option.
    6. +
    + +

    See Create Linked Folder or File for more information + about links and creating external links. +

    + +

    An internal link in the project tree may indicate a "broken" status for + various reasons, including:

    +
      +
    • The referenced file or folder does not exist,
    • +
    • the content-type at the referenced location does not match the link type, or
    • +
    • a folder-link results in a circular path reference.
    • +
    +

    A broken link will have an icon which conveys its type but with a jagged red line + through it and a tooltip which conveys the issue detected.

    + +

    External links will never show a broken + link state since they are not evaluated for such conditions.

    + +
    +

    Move Folders and Files

    @@ -182,7 +234,7 @@

    You cannot move a - file that is in use.

    + file that is in use or a folder that contains a file that is in use.

    Drag/Drop for Copy

    @@ -217,6 +269,9 @@
  • Release the mouse button when you get a valid drop target.
  • + +

    You cannot move a + file that is in use or a folder that contains a file that is in use.

    If a folder or file @@ -241,6 +296,24 @@

  • Right mouse click and choose the Collapse All option.
  • + +

    Follow Link

    + +
    +

    Select the internal or external folder or file referenced by a selected link-file. + While internal folders may be expanded directly from a folder-link, following a link + to the actual referenced location may be useful at times. +

    + +
      +
    1. + Select a file-link or folder-link, right mouse click and choose the Follow Link + option. The referenced file or folder will be selected if possible. If associated + with an external project or repository the selection will occur in a READ-ONLY + project view once opened.
    2. +
    + +

     

    @@ -421,9 +494,9 @@ -
    - A program
    + "font-weight: bold;">Program
    @@ -434,12 +507,34 @@ -
    - A project data type archive (a data type file + style="font-weight: bold;">Data Type Archive (a data type file stored in the project)
    + + + + + -
    + + + Debugger Trace Data
    + + + + + + + -
    + + + Version Tracking Session Data
    + + @@ -521,6 +616,61 @@ not under version control, exists only on your local machine, and is not visible to other users. + + + File Link  + + + + A file link named "Example" which refers to + a Program at /data/example. File links may reference another file using either an + 1) absolute file path within the same project, 2) a relative file path within + the same project, 3) a shared repository Ghidra URL, or 4) a local project Ghidra URL. + See Ghidra URL formats below. + A file link may appear with various icon states which correspond to version control. + File links only support a single version and may not be modified. + + + + + File Link (Broken)  + + + + A file link named "Example" which refers to + a Program at /data/example and is in a "Broken" state. Hovering the mouse + on this node will display a tooltip which indicates the reason for the broken state. + External file links will never show a broken link state since they are not evaluated for such conditions. + + + + + Folder Link  + + + + A folder link named "Example" which refers + to a folder at /data/example. Folder links may reference another folder using either an + 1) absolute file path within the same project, 2) a relative file path within + the same project, 3) a shared repository Ghidra URL, or 4) a local project Ghidra URL. + See Ghidra URL formats below. + Since a folder link is stored as a file, it may appear with various icon states which + correspond to version control. Folder links only support a single version and may not + be modified. + + + + + Folder Link (Broken)  + + + + A folder link named "Example" which refers to + a folder at /data/example and is in a "Broken" state. Hovering the mouse + on this node will display a tooltip which indicates the reason for the broken state. + External folder links will never show a broken link state since they are not evaluated for such conditions. + + Hijacked File @@ -545,9 +695,44 @@ +
    -

     

    +

    Ghidra URL Formats

    + +

    The format of a remote Ghidra Server URL is distinctly different from a + Local Ghidra Project URL. These URLs have the following formats:

    + +

    Remote Ghidra Server Repository
    +

    + +
    + + + + +
    ghidra://<hostname>[:<port>]/<repository_name>[/<folder_or_file_path>]
    +
    + +

    If the default Ghidra Server port (13100) is in use it is not specified by the URL. + The hostname may specify either a Fully Qualified Domain Name (FQDN, e.g., + host.abc.com) or IP v4 Address (e.g., 1.2.3.4).

    + +

    Local Ghidra Project
    +

    + +
    + + + + +
    ghidra:[/<directory_path>]/<project_name>[?/<folder_or_file_path>]
    +
    + +

    For local project URLs, the absolute directory path containing the project + *.gpr locator file is specified with the project name but excludes any .gpr/.rep suffix. + The folder or file path within the project is conveyed with a URL query so the '?' is required.

    +

    Read-Only Project Data

    @@ -697,47 +882,54 @@

    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.

    + corresponding folder or file within your project or to a read-only viewed project. + External links are established using a Ghidra URL which references a + file or folder in its local or remote storage location. An external Ghidra URL will + be used if a link refers to a viewed project or repository. It is possible for internal links to + become broken if the referenced file or folder location has changed (e.g., no longers exists + or has the wrong content type). External links may become invalid for various reasons + but will not convey an issue until the link is used. The broken link icon does not apply + to external link files. +

    +

    To create an external folder or file link the following steps may be used:

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

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

      -
    7. +
    8. Select a destination folder in the active project data tree.
    9. +
    10. Right mouse click on the folder and choose the Paste as Link option.
    -

    A linked-file may be opened in a tool via the project window in the same fashion that +

    It is important to note that the resulting link is always stored as a file within the + project. With the exception of external links to local project content, a link may be + added to version control so that it may be shared. Once added to version control it cannot + be checked-out, since they are immutable, however they can still be deleted.

    +

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

    -

    Within a project file chooser dialog a linked-folder may be expanded in a similar fashion + connection password when accessing an external folder or file link.

    +

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

    -

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

    -

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

    +

    Currently, external file-links only provide access + to the latest file version and do not facilitate access to older file versions. An external + folder-link will allow access to file versions contained within such a folder. +

    +

    Some file chooser use cases, including the + GhidraScript API, are restricted to selecting files and folders within the active + project only and will hide all external links. +

    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.

    + same file in the viewed project.

    +

    A folder or file link will show its referenced location with either + same file in the viewed project.

    + +
    diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteBrokenFileLinkIcon.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteBrokenFileLinkIcon.png new file mode 100644 index 0000000000..fcb3254f07 Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteBrokenFileLinkIcon.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteBrokenFolderLinkIcon.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteBrokenFolderLinkIcon.png new file mode 100644 index 0000000000..1c2f983e78 Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteBrokenFolderLinkIcon.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteFileLinkIcon.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteFileLinkIcon.png new file mode 100644 index 0000000000..25575237b6 Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteFileLinkIcon.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteFolderLinkIcon.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteFolderLinkIcon.png new file mode 100644 index 0000000000..268c99b447 Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/AbsoluteFolderLinkIcon.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/CheckedOutNotLatest.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/CheckedOutNotLatest.png index dd73f99e9d..670cb9a2ec 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/CheckedOutNotLatest.png and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/CheckedOutNotLatest.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/LinkOtherProject.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/LinkOtherProject.png index 7ac46d4370..bba0ea62e9 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/LinkOtherProject.png and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/LinkOtherProject.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/PrivateFileIcon.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/PrivateFileIcon.png index 03fed24134..17ef5cdfdf 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/PrivateFileIcon.png and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/PrivateFileIcon.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileCOnoServer.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileCOnoServer.png index f89def5ef3..9aac1286bf 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileCOnoServer.png and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileCOnoServer.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileCOwithServer.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileCOwithServer.png index 13efbbafbd..d8aa4e3159 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileCOwithServer.png and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileCOwithServer.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileIcon.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileIcon.png index 3794beab40..9fa929760b 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileIcon.png and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/VersionedFileIcon.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/hijack_file.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/hijack_file.png index 23b97d2701..9a510b1172 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/hijack_file.png and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/hijack_file.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/start-here_16.png b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/start-here_16.png new file mode 100644 index 0000000000..bd516a54c5 Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/images/start-here_16.png differ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/ApplyDataArchiveAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/ApplyDataArchiveAnalyzer.java index d41425b6df..b300fbb549 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/ApplyDataArchiveAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/ApplyDataArchiveAnalyzer.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -133,8 +133,9 @@ public class ApplyDataArchiveAnalyzer extends AbstractAnalyzer { OPTION_DESCRIPTION_GDT_FILEPATH, () -> new FileChooserEditor(FileDataTypeManager.GDT_FILEFILTER)); options.registerOption(OPTION_NAME_PROJECT_PATH, OptionType.STRING_TYPE, null, null, - OPTION_DESCRIPTION_PROJECT_PATH, () -> new ProjectPathChooserEditor( - "Choose Data Type Archive", DATATYPEARCHIVE_PROJECT_FILTER)); + OPTION_DESCRIPTION_PROJECT_PATH, + () -> new ProjectPathChooserEditor("Choose Data Type Archive", + new DefaultDomainFileFilter(DataTypeArchive.class, false))); } @Override @@ -289,6 +290,4 @@ public class ApplyDataArchiveAnalyzer extends AbstractAnalyzer { .collect(Collectors.toMap(f -> f.getName(), f -> f)); } - private static final DomainFileFilter DATATYPEARCHIVE_PROJECT_FILTER = - df -> DataTypeArchive.class.isAssignableFrom(df.getDomainObjectClass()); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/ProjectPathChooserEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/ProjectPathChooserEditor.java index d8d28140e4..639be89648 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/ProjectPathChooserEditor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/ProjectPathChooserEditor.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -46,10 +46,6 @@ public class ProjectPathChooserEditor extends PropertyEditorSupport { private String title; private DomainFileFilter filter; - public ProjectPathChooserEditor() { - this(null, null); - } - public ProjectPathChooserEditor(String title, DomainFileFilter filter) { this.title = title; this.filter = filter; @@ -127,8 +123,7 @@ public class ProjectPathChooserEditor extends PropertyEditorSupport { private void displayFileChooser() { AtomicReference result = new AtomicReference<>(); - DataTreeDialog dataTreeDialog = - new DataTreeDialog(this, title, OPEN, filter); + DataTreeDialog dataTreeDialog = new DataTreeDialog(this, title, OPEN, filter); dataTreeDialog.addOkActionListener(e -> { dataTreeDialog.close(); DomainFile df = dataTreeDialog.getDomainFile(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/OpenDomainFileTask.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/OpenDomainFileTask.java index 69ee117299..b3c0bc96d7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/OpenDomainFileTask.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/OpenDomainFileTask.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -92,8 +92,7 @@ class OpenDomainFileTask extends Task { private boolean isFileOpen() { List dtArchiveList = dtmHandler.getAllArchives(); - for (int i = 0; i < dtArchiveList.size(); i++) { - Archive archive = dtArchiveList.get(i); + for (Archive archive : dtArchiveList) { if (archive instanceof ProjectArchive) { ProjectArchive projectArchive = (ProjectArchive) archive; DomainFile archiveDomainFile = projectArchive.getDomainFile(); @@ -156,7 +155,7 @@ class OpenDomainFileTask extends Task { } catch (VersionException e) { VersionExceptionHandler.showVersionError(tool.getToolFrame(), domainFile.getName(), - contentType, "Open", e); + contentType, "Open", false, e); } } @@ -179,7 +178,7 @@ class OpenDomainFileTask extends Task { } catch (VersionException e) { VersionExceptionHandler.showVersionError(null, domainFile.getName(), contentType, - "Open", e); + "Open", false, e); } catch (CancelledException e) { // we don't care, the task has been canceled diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java index 634ef9bd49..28baeeba2b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/archive/DataTypeManagerHandler.java @@ -77,7 +77,7 @@ public class DataTypeManagerHandler { private Map invalidArchives = new HashMap<>(); private boolean treeDialogCancelled = false; - private DomainFileFilter createArchiveFileFilter; + private DomainFileFilter archiveFileFilter; private DataTypeIndexer dataTypeIndexer; private List archiveManagerlisteners = new ArrayList<>(); @@ -107,18 +107,7 @@ public class DataTypeManagerHandler { dataTypeIndexer.addDataTypeManager(builtInDataTypesManager); openArchives.add(new BuiltInArchive(this, builtInDataTypesManager)); - createArchiveFileFilter = new DomainFileFilter() { - - @Override - public boolean accept(DomainFile df) { - return DataTypeArchive.class.isAssignableFrom(df.getDomainObjectClass()); - } - - @Override - public boolean followLinkedFolders() { - return false; - } - }; + archiveFileFilter = new DefaultDomainFileFilter(DataTypeArchive.class, true); folderListener = new MyFolderListener(); tool.getProject().getProjectData().addDomainFolderChangeListener(folderListener); @@ -1454,7 +1443,7 @@ public class DataTypeManagerHandler { } private DataTreeDialog getSaveDialog() { - DataTreeDialog dialog = new DataTreeDialog(null, "Save As", SAVE, createArchiveFileFilter); + DataTreeDialog dialog = new DataTreeDialog(null, "Save As", SAVE, archiveFileFilter); ActionListener listener = event -> { DomainFolder folder = dialog.getDomainFolder(); @@ -1486,7 +1475,7 @@ public class DataTypeManagerHandler { private CreateDataTypeArchiveDataTreeDialog getCreateDialog() { CreateDataTypeArchiveDataTreeDialog dialog = new CreateDataTypeArchiveDataTreeDialog(null, - "Create", CREATE, createArchiveFileFilter); + "Create", CREATE, archiveFileFilter); ActionListener listener = event -> { DomainFolder folder = dialog.getDomainFolder(); @@ -1726,7 +1715,7 @@ public class DataTypeManagerHandler { } catch (VersionException e) { VersionExceptionHandler.showVersionError(null, newDomainFile.getName(), contentType, - "Re-open", e); + "Re-open", false, e); } catch (CancelledException e) { throw new AssertException(e); @@ -1766,7 +1755,7 @@ public class DataTypeManagerHandler { Throwable cause = t.getCause(); if (cause instanceof VersionException) { VersionExceptionHandler.showVersionError(null, archiveFile.getName(), "Archive", - "open", (VersionException) cause); + "open", false, (VersionException) cause); } else { Msg.showError(plugin, plugin.getProvider().getComponent(), "Open Archive Failed", diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterDialog.java index 5b6e09a72a..d321ebb092 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterDialog.java @@ -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. @@ -78,6 +78,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa private JCheckBox selectionCheckBox; // null for FrontEnd Tool use private JTextField filePathTextField; private JButton fileChooserButton; + private List applicableExporters; private GhidraComboBox comboBox; private final DomainFile domainFile; private boolean domainObjectWasSupplied; @@ -86,39 +87,82 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa private PluginTool tool; private JLabel selectionOnlyLabel; + private boolean showNoExporterErrorIfNeeded = true; /** - * Construct a new ExporterDialog for exporting an entire program. + * Show a new ExporterDialog for exporting an entire program. + * The method {@link #hasNoApplicableExporter()} should be checked before showing the + * dilaog. If no exporters are available a popup error will be displayed and the exporter + * dialog will not be shown. * * @param tool the tool that launched this dialog. * @param domainFile the program to export */ - public ExporterDialog(PluginTool tool, DomainFile domainFile) { - this(tool, domainFile, null, null); + public static void show(PluginTool tool, DomainFile domainFile) { + showExporterDialog(tool, domainFile, null, null); } /** * Construct a new ExporterDialog for exporting a program, optionally only exported a - * selected region. + * selected region. The method {@link #hasNoApplicableExporter()} should be checked before + * showing the dilaog. If no exporters are available a popup error will be displayed and the + * exporter dialog will not be shown. + * The {@link #close()} method must always be invoked on the dialog instance even if it + * is never shown to ensure any {@link DomainObject} instance held is properly released. * * @param tool the tool that launched this dialog. * @param domainFile the program file to export. (may be proxy) * @param domainObject the program to export if already open, otherwise null. * @param selection the current program selection (ignored for FrontEnd Tool). */ - public ExporterDialog(PluginTool tool, DomainFile domainFile, DomainObject domainObject, + public static void showExporterDialog(PluginTool tool, DomainFile domainFile, + DomainObject domainObject, ProgramSelection selection) { + ExporterDialog dialog = new ExporterDialog(tool, domainFile, domainObject, selection); + if (dialog.hasNoApplicableExporter()) { + dialog.close(); + } + else { + tool.showDialog(dialog); + } + } + + /** + * Construct a new modal ExporterDialog for exporting a program, optionally only exported a + * selected region. The method {@link #hasNoApplicableExporter()} should be checked before + * showing the dilaog. If no exporters are available a popup error will be displayed. + * The {@link #close()} method must always be invoked on the dialog instance even if it + * is never shown to ensure any {@link DomainObject} instance held is properly released. + * + * @param tool the tool that launched this dialog. + * @param domainFile the program file to export. (may be proxy) + * @param domainObject the program to export if already open, otherwise null. + * @param selection the current program selection (ignored for FrontEnd Tool). + */ + private ExporterDialog(PluginTool tool, DomainFile domainFile, DomainObject domainObject, ProgramSelection selection) { super("Export " + domainFile.getName()); + + if (!Swing.isSwingThread()) { + throw new RuntimeException("ExporterDialog must be instantiated within Swing thread"); + } + this.tool = tool; this.domainFile = domainFile; this.domainObject = domainObject; this.currentSelection = selection; + if (domainObject != null) { + applicableExporters = getApplicableExporters(false); domainObjectWasSupplied = true; domainObject.addConsumer(this); } else { - domainObject = getDomainObjectIfNeeded(TaskMonitor.DUMMY); + applicableExporters = getApplicableExporters(true); + List applicableDomainFileExporters = getApplicableExporters(false); + + domainObject = getDomainObjectIfNeeded(!applicableDomainFileExporters.isEmpty()); + + applicableExporters = getApplicableExporters(false); } addWorkPanel(buildWorkPanel()); @@ -133,6 +177,11 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa // need to initialize a few things selectedFormatChanged(); validate(); + + if (showNoExporterErrorIfNeeded && hasNoApplicableExporter()) { + Msg.showError(this, tool.getToolFrame(), "Unable to Export", + "No available exporters for content type"); + } } @Override @@ -305,10 +354,9 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa private Component buildFormatChooser() { - List exporters = getApplicableExporters(false); - comboBox = new GhidraComboBox<>(exporters); + comboBox = new GhidraComboBox<>(applicableExporters); - Exporter defaultExporter = getDefaultExporter(exporters); + Exporter defaultExporter = getDefaultExporter(applicableExporters); if (defaultExporter != null) { comboBox.setSelectedItem(defaultExporter); } @@ -319,8 +367,8 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa /** * This list generation will be based upon the open domainObject if available, otherwise - * the domainFile's content class will be used. - * @return list of exporters able to handle content + * the domainFile's content class will be used. The {@code applicableExporters} variable + * is set to the applicable list of exporters. */ private List getApplicableExporters(boolean preliminaryCheck) { List list = new ArrayList<>(ClassSearcher.getInstances(Exporter.class)); @@ -330,15 +378,13 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa } private boolean canExport(Exporter exporter, boolean preliminaryCheck) { - if (exporter.canExportDomainFile(domainFile)) { - return true; + if (domainObject != null) { + return exporter.canExportDomainObject(domainObject); } - if (domainObject == null) { - return preliminaryCheck - ? exporter.canExportDomainObject(domainFile.getDomainObjectClass()) - : false; + if (preliminaryCheck) { + return exporter.canExportDomainObject(domainFile.getDomainObjectClass()); } - return exporter.canExportDomainObject(domainObject); + return exporter.canExportDomainFile(domainFile); } private Exporter getDefaultExporter(List list) { @@ -410,6 +456,10 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa setOkEnabled(true); } + public boolean hasNoApplicableExporter() { + return applicableExporters.isEmpty(); + } + private boolean hasOptions() { return options != null && !options.isEmpty(); } @@ -450,7 +500,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa } } - private DomainObject getDomainObjectIfNeeded(TaskMonitor taskMonitor) { + private DomainObject getDomainObjectIfNeeded(boolean exportPossibleWithoutOpening) { if (domainObject != null) { return domainObject; } @@ -459,7 +509,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa // direct domain file export. This avoids potential upgrade issues and preserves // database in its current state for those exporters. boolean doOpen = false; - for (Exporter exporter : getApplicableExporters(true)) { + for (Exporter exporter : applicableExporters) { if (!exporter.canExportDomainFile(domainFile)) { doOpen = true; break; @@ -469,35 +519,28 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa return null; } - if (SystemUtilities.isEventDispatchThread()) { - TaskLauncher.launchModal("Opening File: " + domainFile.getName(), - monitor -> doOpenFile(monitor)); - } - else { - doOpenFile(taskMonitor); - } + TaskLauncher.launchModal("Opening File: " + domainFile.getName(), + monitor -> doOpenFile(exportPossibleWithoutOpening, monitor)); + return domainObject; } - private void doOpenFile(TaskMonitor monitor) { + private void doOpenFile(boolean exportPossibleWithoutOpening, TaskMonitor monitor) { + showNoExporterErrorIfNeeded = false; + String linkedPrefix = domainFile.isLink() ? "linked-" : ""; try { - 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); - } + domainObject = + domainFile.getImmutableDomainObject(this, DomainFile.DEFAULT_VERSION, monitor); } catch (VersionException e) { - String msg = "Could not open file: " + domainFile.getName() + - "\n\nAvailable export options will be limited."; + String msg = "Could not open " + linkedPrefix + "file: " + domainFile.getName(); + if (exportPossibleWithoutOpening) { + msg += "\n\nAvailable export options will be limited."; + } if (e.isUpgradable()) { - msg += "\n\nA data upgrade is required. You may open file" + - "\nin a tool first then Export if a different exporter" + "\nis required."; + msg += "\n\nA " + linkedPrefix + + "content upgrade is required. You may open file in a" + + "\ntool first to complete upgrade then Export if needed."; } else { msg += "\nFile was created with a newer version of Ghidra"; @@ -505,8 +548,10 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa Msg.showError(this, getComponent(), "Error Opening File", msg); } catch (IOException e) { - String msg = "Could not open file: " + domainFile.getName() + - "\n\nAvailable export options will be limited."; + String msg = "Could not open " + linkedPrefix + "file: " + domainFile.getName(); + if (exportPossibleWithoutOpening) { + msg += "\n\nAvailable export options will be limited."; + } Msg.showError(this, getComponent(), "Error Opening File", msg, e); } catch (CancelledException e) { @@ -552,7 +597,7 @@ public class ExporterDialog extends DialogComponentProvider implements AddressFa boolean exportDomainFile = !domainObjectWasSupplied && exporter.canExportDomainFile(domainFile); - if (!exportDomainFile && domainFile == null) { + if (!exportDomainFile && (domainFile == null || domainFile.isLink())) { return; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterPlugin.java index 855de0d8ef..9e46e72e4b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/exporter/ExporterPlugin.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -70,9 +70,8 @@ public class ExporterPlugin extends Plugin implements ApplicationLevelPlugin { protected void actionPerformed(NavigatableActionContext context) { Program program = context.getProgram(); DomainFile domainFile = program.getDomainFile(); - ExporterDialog dialog = - new ExporterDialog(tool, domainFile, program, context.getSelection()); - tool.showDialog(dialog); + ExporterDialog.showExporterDialog(tool, domainFile, program, + context.getSelection()); } }; MenuData menuData = @@ -104,8 +103,7 @@ public class ExporterPlugin extends Plugin implements ApplicationLevelPlugin { @Override protected void actionPerformed(ProjectDataContext context) { DomainFile domainFile = context.getSelectedFiles().get(0); - ExporterDialog dialog = new ExporterDialog(tool, domainFile); - tool.showDialog(dialog); + ExporterDialog.show(tool, domainFile); } @Override @@ -118,6 +116,10 @@ public class ExporterPlugin extends Plugin implements ApplicationLevelPlugin { if (selectedFiles.size() != 1) { return false; } + DomainFile domainFile = context.getSelectedFiles().get(0); + if (domainFile.isLink() && domainFile.getLinkInfo().isFolderLink()) { + return false; + } return true; } }; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/gotoquery/GoToHelper.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/gotoquery/GoToHelper.java index 58355626f5..b8e8521161 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/gotoquery/GoToHelper.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/gotoquery/GoToHelper.java @@ -15,8 +15,6 @@ */ package ghidra.app.plugin.core.gotoquery; -import static ghidra.framework.main.DataTreeDialogType.*; - import java.util.Stack; import org.apache.commons.lang3.StringUtils; @@ -32,7 +30,7 @@ import ghidra.app.util.NamespaceUtils; import ghidra.app.util.SymbolPath; import ghidra.app.util.query.TableService; import ghidra.framework.cmd.Command; -import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.main.ProgramFileChooser; import ghidra.framework.model.DomainFile; import ghidra.framework.model.ProjectData; import ghidra.framework.plugintool.PluginTool; @@ -106,8 +104,10 @@ public class GoToHelper { ExternalLocation externalLoc = program.getExternalManager().getExternalLocation(externalSym); - // TODO - this seems like a mistake to always pass 'false' here; please doc why we - // wish to ignore the user options for when to navigate to external programs + // TODO - This seems like a mistake to always pass 'false' here; please doc why we + // wish to ignore the user options for when to navigate to external programs. + // It appears this was done since this method is invoked on simple external + // location node selection within symbol tree where you would not want a popup. return goToExternalLinkage(navigatable, externalLoc, false); } @@ -187,10 +187,11 @@ public class GoToHelper { * * @param nav Navigatable * @param externalLoc external location - * @param popupAllowed if true a table may be displayed when multiple linkage locations exist, - * otherwise navigation to the first linkage location will be performed + * @param popupAllowed if true a table may be displayed when multiple linkage locations exist + * or navigation to an external program, otherwise navigation to the first linkage + * location will be performed * @return true if navigation was successful or a list of possible linkage locations was - * displayed. + * displayed, false if no navigation was performed. */ protected boolean goToExternalLinkage(Navigatable nav, ExternalLocation externalLoc, boolean popupAllowed) { @@ -205,8 +206,14 @@ public class GoToHelper { NavigationUtils.getExternalLinkageAddresses(program, externalSym.getAddress()); if (externalLinkageAddresses.length == 0) { if (externalLoc.isFunction()) { - tool.setStatusInfo("Failed to identify external linkage address for " + - externalSym.getName(true) + ". Unable to perform navigation.", true); + // This assume external functions always require linkage location + tool.setStatusInfo("Failed to identify external linkage address for function " + + externalSym.getName(true), true); + } + else if (popupAllowed) { + // If there are no linkage location try to navigate to external program if a popup + // is tolerated. + return goToExternalLocation(nav, externalLoc, false); } return false; } @@ -306,7 +313,7 @@ public class GoToHelper { } ProjectData pd = tool.getProject().getProjectData(); - DomainFile domainFile = pd.getFile(pathName); + DomainFile domainFile = pd.getFile(pathName, ProgramFileChooser.PROGRAM_FILE_FILTER); ProgramManager service = tool.getService(ProgramManager.class); if (domainFile == null || service == null) { tool.setStatusInfo("Unable to navigate to external location. " + @@ -441,8 +448,8 @@ public class GoToHelper { return; } - DataTreeDialog dialog = new DataTreeDialog(null, - "Choose External Program (" + extProgName + ")", OPEN); + ProgramFileChooser dialog = + new ProgramFileChooser(null, "Choose External Program (" + extProgName + ")"); dialog.setSearchText(extProgName); dialog.setHelpLocation(new HelpLocation("ReferencesPlugin", "ChooseExternalProgram")); tool.showDialog(dialog); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/help/AboutProgramPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/help/AboutProgramPlugin.java index 905ff48471..1ae42ec8a0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/help/AboutProgramPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/help/AboutProgramPlugin.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -79,16 +79,25 @@ public class AboutProgramPlugin extends Plugin implements ApplicationLevelPlugin @Override protected void actionPerformed(ProjectDataContext context) { DomainFile domainFile = context.getSelectedFiles().get(0); - showAbout(domainFile, domainFile.getMetadata()); + Map metadata = domainFile.getMetadata(); + + showAbout(domainFile, metadata); } @Override protected boolean isAddToPopup(ProjectDataContext context) { - return context.getFileCount() == 1 && context.getFolderCount() == 0; + if (context.getFileCount() == 1 && context.getFolderCount() == 0) { + // Adjust popup menu text + DomainFile domainFile = context.getSelectedFiles().get(0); + String contentType = domainFile.getContentType(); + setPopupMenuData( + new MenuData(new String[] { "About " + contentType }, null, "AAA")); + return true; + } + return false; } }; - aboutAction.setPopupMenuData( - new MenuData(new String[] { ACTION_NAME }, null, "AAA")); + aboutAction.setPopupMenuData(new MenuData(new String[] { ACTION_NAME }, null, "AAA")); aboutAction.setEnabled(true); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/LanguageProviderPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/LanguageProviderPlugin.java index 6852142465..84ffd980be 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/LanguageProviderPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/processors/LanguageProviderPlugin.java @@ -88,6 +88,10 @@ public final class LanguageProviderPlugin extends Plugin implements ApplicationL return false; } + if (file.isLink() && file.getLinkInfo().isExternalLink()) { + return false; + } + return file.isInWritableProject() && Program.class.isAssignableFrom(file.getDomainObjectClass()); } @@ -105,12 +109,12 @@ public final class LanguageProviderPlugin extends Plugin implements ApplicationL } }; - setLanguageAction.setPopupMenuData( - new MenuData(new String[] { "Set Language..." }, "Language")); + setLanguageAction + .setPopupMenuData(new MenuData(new String[] { "Set Language..." }, "Language")); setLanguageAction.setEnabled(true); - setLanguageAction.setHelpLocation( - new HelpLocation("LanguageProviderPlugin", "set language")); + setLanguageAction + .setHelpLocation(new HelpLocation("LanguageProviderPlugin", "set language")); tool.addAction(setLanguageAction); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramLocator.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramLocator.java index 077f7dbf16..a58c60f2eb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramLocator.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramLocator.java @@ -20,10 +20,10 @@ import java.net.URL; import java.util.Objects; import ghidra.framework.data.DomainFileProxy; -import ghidra.framework.data.LinkHandler; import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.program.model.listing.Program; +import ghidra.util.Msg; /** * Programs locations can be specified from either a {@link DomainFile} or a ghidra {@link URL}. @@ -81,11 +81,19 @@ public class ProgramLocator { file = domainFile; } else { - try { - url = GhidraURL.getNormalizedURL(resolveURL(domainFile)); + if (domainFile instanceof LinkedDomainFile linkedFile) { + try { + // Attempt to resolve to actual linked-file to allow for + // direct URL reference + domainFile = linkedFile.getLinkedFile(); + } + catch (IOException e) { + Msg.error(this, "Failed to resolve linked-file", e); + } } - catch (IOException e) { - file = domainFile; + url = domainFile.getLocalProjectURL(null); + if (url == null) { + url = domainFile.getSharedProjectURL(null); } } this.domainFile = file; @@ -177,25 +185,4 @@ public class ProgramLocator { Objects.equals(ghidraURL, other.ghidraURL) && version == other.version; } - private URL resolveURL(DomainFile file) throws IOException { - if (file.isLinkFile()) { - return LinkHandler.getURL(file); - } - DomainFolder parent = file.getParent(); - if (file instanceof LinkedDomainFile linkedFile) { - return resolveLinkedDomainFile(linkedFile); - } - if (!parent.getProjectLocator().isTransient()) { - return file.getLocalProjectURL(null); - } - return file.getSharedProjectURL(null); - } - - private URL resolveLinkedDomainFile(LinkedDomainFile linkedFile) { - URL url = linkedFile.getLocalProjectURL(null); - if (url == null) { - url = linkedFile.getSharedProjectURL(null); - } - return url; - } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java index e850c639da..5a6992a38d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramSaveManager.java @@ -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. @@ -41,23 +41,12 @@ class ProgramSaveManager { private ProgramManager programMgr; private PluginTool tool; private boolean treeDialogCancelled; - private DomainFileFilter domainFileFilter; + private DomainFileFilter programFileFilter; ProgramSaveManager(PluginTool tool, ProgramManager programMgr) { this.tool = tool; this.programMgr = programMgr; - 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) - } - }; + programFileFilter = new DefaultDomainFileFilter(Program.class, true); } /** @@ -443,8 +432,7 @@ class ProgramSaveManager { } private DataTreeDialog getSaveDialog() { - DataTreeDialog dialog = - new DataTreeDialog(null, "Save As", SAVE, domainFileFilter); + DataTreeDialog dialog = new DataTreeDialog(null, "Save As", SAVE, programFileFilter); ActionListener listener = event -> { DomainFolder folder = dialog.getDomainFolder(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/references/EditExternalReferencePanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/references/EditExternalReferencePanel.java index 8682ea8bfa..b70d7a3bfd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/references/EditExternalReferencePanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/references/EditExternalReferencePanel.java @@ -15,8 +15,6 @@ */ package ghidra.app.plugin.core.references; -import static ghidra.framework.main.DataTreeDialogType.*; - import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.event.*; @@ -30,7 +28,7 @@ import javax.swing.event.DocumentListener; import docking.widgets.combobox.GhidraComboBox; import docking.widgets.label.GLabel; import ghidra.app.util.AddressInput; -import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.main.ProgramFileChooser; import ghidra.framework.model.DomainFile; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; @@ -113,8 +111,8 @@ class EditExternalReferencePanel extends EditReferencePanel { } }); - editButton = new JButton("Edit"); - editButton.setToolTipText("Edit Link to External Program"); + editButton = new JButton("Select..."); + editButton.setToolTipText("Select External Program"); editButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -187,10 +185,8 @@ class EditExternalReferencePanel extends EditReferencePanel { * Pop up the data tree dialog so the user can choose the external program. */ private void popupProgramChooser() { - DataTreeDialog d = - new DataTreeDialog(this.getParent(), "Choose External Program", OPEN); - final DataTreeDialog dialog = d; - d.addOkActionListener(new ActionListener() { + ProgramFileChooser dialog = new ProgramFileChooser(this.getParent(), "Choose External Program"); + dialog.addOkActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { DomainFile df = dialog.getDomainFile(); @@ -206,7 +202,7 @@ class EditExternalReferencePanel extends EditReferencePanel { extLibPath.setText(df.getPathname()); } }); - plugin.getTool().showDialog(d); + plugin.getTool().showDialog(dialog); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/references/ExternalReferencesProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/references/ExternalReferencesProvider.java index 883bdcf452..78b7128437 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/references/ExternalReferencesProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/references/ExternalReferencesProvider.java @@ -15,8 +15,6 @@ */ package ghidra.app.plugin.core.references; -import static ghidra.framework.main.DataTreeDialogType.*; - import java.awt.BorderLayout; import java.awt.event.MouseEvent; import java.util.*; @@ -34,7 +32,7 @@ import generic.theme.GIcon; import ghidra.app.cmd.refs.*; import ghidra.framework.cmd.Command; import ghidra.framework.cmd.CompoundCmd; -import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.main.*; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainObjectListener; import ghidra.framework.plugintool.ComponentProviderAdapter; @@ -236,8 +234,8 @@ public class ExternalReferencesProvider extends ComponentProviderAdapter { private void setExternalProgramAssociation() { List selectedExternalNames = getSelectedExternalNames(); String externalName = selectedExternalNames.get(0); // must be exactly one for us to be enabled. - DataTreeDialog dialog = new DataTreeDialog(mainPanel, - "Choose External Program (" + externalName + ")", OPEN); + DataTreeDialog dialog = new ProgramFileChooser(mainPanel, + "Choose External Program (" + externalName + ")", AppInfo.getActiveProject()); dialog.setSearchText(externalName); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/EditExternalLocationPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/EditExternalLocationPanel.java index 99e1276208..8ec68bf5e6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/EditExternalLocationPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/EditExternalLocationPanel.java @@ -15,8 +15,6 @@ */ package ghidra.app.plugin.core.symboltree; -import static ghidra.framework.main.DataTreeDialogType.*; - import java.awt.*; import java.awt.event.ItemListener; import java.util.Arrays; @@ -36,7 +34,7 @@ import docking.widgets.label.GLabel; import ghidra.app.util.AddressInput; import ghidra.app.util.NamespaceUtils; import ghidra.framework.main.AppInfo; -import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.main.ProgramFileChooser; import ghidra.framework.model.*; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSpace; @@ -270,9 +268,9 @@ class EditExternalLocationPanel extends JPanel { * Pop up the data tree dialog so the user can choose the external program. */ private void popupProgramChooser() { - DataTreeDialog d = new DataTreeDialog(this.getParent(), "Choose External Program", OPEN); - final DataTreeDialog dialog = d; - d.addOkActionListener(e -> { + ProgramFileChooser dialog = + new ProgramFileChooser(this.getParent(), "Choose External Program"); + dialog.addOkActionListener(e -> { DomainFile df = dialog.getDomainFile(); if (df == null) { return; @@ -285,7 +283,7 @@ class EditExternalLocationPanel extends JPanel { dialog.close(); extLibPathTextField.setText(df.getPathname()); }); - DockingWindowManager.showDialog(this, d); + DockingWindowManager.showDialog(this, dialog); } private void initialize() { @@ -363,7 +361,8 @@ class EditExternalLocationPanel extends JPanel { Project project = AppInfo.getActiveProject(); ProjectData projectData = project.getProjectData(); - DomainFile file = projectData.getFile(extLibPath); + DomainFile file = + projectData.getFile(extLibPath, ProgramFileChooser.PROGRAM_FILE_FILTER); if (file == null) { showInputErr("Cannot find the program for the specified library 'Path' of " + extLibPath + "."); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SetExternalProgramAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SetExternalProgramAction.java index d6030511ee..f2dccef96d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SetExternalProgramAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/symboltree/actions/SetExternalProgramAction.java @@ -15,8 +15,6 @@ */ package ghidra.app.plugin.core.symboltree.actions; -import static ghidra.framework.main.DataTreeDialogType.*; - import javax.swing.Icon; import javax.swing.SwingUtilities; import javax.swing.tree.TreePath; @@ -29,7 +27,7 @@ import ghidra.app.plugin.core.symboltree.*; import ghidra.app.plugin.core.symboltree.nodes.LibrarySymbolNode; import ghidra.framework.cmd.Command; import ghidra.framework.main.AppInfo; -import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.main.ProgramFileChooser; import ghidra.framework.model.*; import ghidra.program.model.listing.Program; import ghidra.program.model.symbol.ExternalManager; @@ -82,8 +80,8 @@ public class SetExternalProgramAction extends SymbolTreeContextAction { ExternalManager externalManager = program.getExternalManager(); final String externalLibraryPath = externalManager.getExternalLibraryPath(externalName); - final DataTreeDialog dialog = new DataTreeDialog(provider.getComponent(), - "Choose External Program (" + externalName + ")", OPEN); + ProgramFileChooser dialog = new ProgramFileChooser(provider.getComponent(), + "Choose External Program (" + externalName + ")"); dialog.setSearchText(externalName); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java index 688d61596d..de319f45e2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java @@ -2827,8 +2827,10 @@ public abstract class GhidraScript extends FlatProgramAPI { DomainFile choice = loadAskValue(this::parseDomainFile, title); if (!isRunningHeadless()) { choice = doAsk(Program.class, title, "", choice, lastValue -> { - - DataTreeDialog dtd = new DataTreeDialog(null, title, OPEN); + // File filter employed limits access to program files within the active project + // only to ensure the ability to open for update is possible. + DataTreeDialog dtd = new DataTreeDialog(null, title, OPEN, + new DefaultDomainFileFilter(Program.class, true)); dtd.show(); if (dtd.wasCancelled()) { return null; @@ -2932,8 +2934,10 @@ public abstract class GhidraScript extends FlatProgramAPI { String message = ""; DomainFile choice = doAsk(DomainFile.class, title, message, existingValue, lastValue -> { - - DataTreeDialog dtd = new DataTreeDialog(null, title, OPEN); + // File filter employed limits access to files within the active project + // only to ensure the ability to open for update is possible. + DataTreeDialog dtd = new DataTreeDialog(null, title, OPEN, + DomainFileFilter.ALL_FILES_NO_EXTERNAL_FOLDERS_FILTER); dtd.show(); if (dtd.wasCancelled()) { throw new CancelledException(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/GdtExporter.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/GdtExporter.java index 40057735f8..21d049525a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/GdtExporter.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/GdtExporter.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -47,7 +47,8 @@ public class GdtExporter extends Exporter { @Override public boolean canExportDomainFile(DomainFile domainFile) { - return canExportDomainObject(domainFile.getDomainObjectClass()); + // Avoid exporting link-file itself + return !domainFile.isLink() && canExportDomainObject(domainFile.getDomainObjectClass()); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/GzfExporter.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/GzfExporter.java index 7ed69e1672..1e735b916f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/GzfExporter.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/exporter/GzfExporter.java @@ -42,7 +42,8 @@ public class GzfExporter extends Exporter { @Override public boolean canExportDomainFile(DomainFile domainFile) { - return canExportDomainObject(domainFile.getDomainObjectClass()); + // Avoid exporting link-file itself + return !domainFile.isLink() && canExportDomainObject(domainFile.getDomainObjectClass()); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java index fb5d024e57..dd9e38face 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java @@ -43,6 +43,7 @@ import ghidra.framework.model.*; import ghidra.framework.project.DefaultProject; import ghidra.framework.project.DefaultProjectManager; import ghidra.framework.protocol.ghidra.*; +import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl; import ghidra.framework.remote.User; import ghidra.framework.store.LockException; import ghidra.framework.store.local.LocalFileSystem; @@ -354,7 +355,10 @@ public class HeadlessAnalyzer { } throw new IOException(title + ": " + message); } - }, TaskMonitor.DUMMY); + + // Link files are skipped to avoid duplicate processing + // Processing should be done on actual folder - not a linked folder + }, LinkFileControl.NO_FOLLOW, TaskMonitor.DUMMY); } catch (CancelledException e) { @@ -1338,7 +1342,7 @@ public class HeadlessAnalyzer { boolean filesProcessed = false; DomainFile domFile = parentFolder.getFile(filename); - // Do not follow folder-links or consider program links. Using content type + // Do not follow folder-links or program links. Using content type // to filter is best way to control this. if (domFile != null && ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java index 2b653374e9..bd2f805109 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/task/ProgramOpener.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -27,6 +27,7 @@ import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.main.AppInfo; import ghidra.framework.model.DomainFile; import ghidra.framework.protocol.ghidra.GhidraURLQuery; +import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl; import ghidra.framework.protocol.ghidra.GhidraURLResultHandlerAdapter; import ghidra.framework.remote.User; import ghidra.framework.store.ExclusiveCheckoutException; @@ -103,13 +104,13 @@ public class ProgramOpener { AtomicReference openedProgram = new AtomicReference<>(); try { - GhidraURLQuery.queryUrl(ghidraUrl, new GhidraURLResultHandlerAdapter() { + GhidraURLQuery.queryUrl(ghidraUrl, Program.class, new GhidraURLResultHandlerAdapter() { @Override public void processResult(DomainFile domainFile, URL url, TaskMonitor m) { Program p = openProgram(locator, domainFile, m); // may return null openedProgram.set(p); } - }, monitor); + }, LinkFileControl.FOLLOW_EXTERNAL, monitor); } catch (IOException | CancelledException e) { // IOException reported to user by GhidraURLResultHandlerAdapter @@ -148,7 +149,7 @@ public class ProgramOpener { } catch (VersionException e) { String contentType = domainFile.getContentType(); - VersionExceptionHandler.showVersionError(null, filename, contentType, "Open", e); + VersionExceptionHandler.showVersionError(null, filename, contentType, "Open", false, e); } catch (CancelledException e) { // we don't care, the task has been cancelled @@ -197,7 +198,7 @@ public class ProgramOpener { } catch (VersionException e) { VersionExceptionHandler.showVersionError(null, domainFile.getName(), contentType, - "Open", e); + "Open", false, e); } return null; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ProgramAnnotatedStringHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ProgramAnnotatedStringHandler.java index 914b095687..1e9ca4042c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ProgramAnnotatedStringHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ProgramAnnotatedStringHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -129,31 +129,37 @@ public class ProgramAnnotatedStringHandler implements AnnotatedStringHandler { serviceProvider.getService(ProjectDataService.class); ProjectData projectData = projectDataService.getProjectData(); - // default folder is the root folder - DomainFolder folder = projectData.getRootFolder(); - // Get program name and folder from program comment annotation // handles forward and back slashes and with and without first slash String programText = getProgramText(annotationParts); String programName = FilenameUtils.getName(programText); String path = FilenameUtils.getFullPathNoEndSeparator(programText); + + DomainFolder folder; if (path.length() > 0) { path = StringUtils.prependIfMissing(FilenameUtils.separatorsToUnix(path), "/"); folder = projectData.getFolder(path); + if (folder == null) { + Msg.showInfo(getClass(), null, "Folder Not Found: " + path, + "Unable to locate folder by the name \"" + path); + return true; + } + } + else { + folder = projectData.getRootFolder(); } - if (folder == null) { - Msg.showInfo(getClass(), null, "No Folder: " + path, - "Unable to locate folder by the name \"" + path); + DomainFile programFile = folder.getFile(programName); + if (programFile == null || + !Program.class.isAssignableFrom(programFile.getDomainObjectClass())) { + Msg.showInfo(getClass(), null, "Program Not Found: " + programName, + "Unable to locate program at path \"" + programText + + "\".\nNOTE: File names are case-sensitive."); return true; } - - DomainFile programFile = findProgramByName(programName, folder); - - if (programFile == null) { - Msg.showInfo(getClass(), null, "No Program: " + programName, - "Unable to locate a program by the name \"" + programName + - "\".\nNOTE: Program name is case-sensitive. "); + if (!Program.class.isAssignableFrom(programFile.getDomainObjectClass())) { + Msg.showInfo(getClass(), null, "Program Not Found: " + programName, + "File exists with incorrect content type. "); return true; } @@ -199,7 +205,7 @@ public class ProgramAnnotatedStringHandler implements AnnotatedStringHandler { return; } - Msg.showInfo(getClass(), null, "No Symbol: " + symbolName, + Msg.showInfo(getClass(), null, "Symbol Not Found: " + symbolName, "Unable to navigate to '" + symbolName + "' in the program '" + programFile.getName() + "'.\nMake sure that the given symbol/address exists."); if (!programManager.isVisible(program)) { @@ -247,27 +253,6 @@ public class ProgramAnnotatedStringHandler implements AnnotatedStringHandler { return address; } - // recursive program to find a program by the given name within the given folder - private DomainFile findProgramByName(String programText, DomainFolder folder) { - DomainFile[] files = folder.getFiles(); - for (DomainFile file : files) { - if (file.getName().equals(programText)) { - return file; - } - } - - // not at the current level, then check sub-folders - DomainFolder[] folders = folder.getFolders(); - for (DomainFolder subFolder : folders) { - DomainFile domainFile = findProgramByName(programText, subFolder); - if (domainFile != null) { - return domainFile; - } - } - - return null; - } - @Override public String getDisplayString() { return "Program"; @@ -275,7 +260,7 @@ public class ProgramAnnotatedStringHandler implements AnnotatedStringHandler { @Override public String getPrototypeString() { - return "{@program program_name.exe@symbol_name}"; + return "{@program program_path@symbol_name}"; } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/AbstractDataTreeDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/AbstractDataTreeDialog.java index c9f9accac3..389f1bc862 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/AbstractDataTreeDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/AbstractDataTreeDialog.java @@ -291,16 +291,28 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider else { domainFile = treePanel.getSelectedDomainFile(); if (domainFile != null) { - folderNameLabel.setText(domainFile.getParent().getPathname()); - nameField.setText(domainFile.getName()); - domainFolder = domainFile.getParent(); - } - else { - domainFolder = treePanel.getSelectedDomainFolder(); - if (domainFolder == null) { - domainFolder = project.getProjectData().getRootFolder(); + LinkFileInfo linkInfo = domainFile.getLinkInfo(); + if (linkInfo != null && linkInfo.isFolderLink()) { + // Ensure we don't have a folder name conflict + if (domainFile.getParent().getFolder(domainFile.getName()) == null) { + domainFolder = linkInfo.getLinkedFolder(); + domainFile = null; + } } + else { + folderNameLabel.setText(domainFile.getParent().getPathname()); + nameField.setText(domainFile.getName()); + domainFolder = domainFile.getParent(); + } + } + if (domainFile == null) { + if (domainFolder == null) { + domainFolder = treePanel.getSelectedDomainFolder(); + if (domainFolder == null) { + domainFolder = project.getProjectData().getRootFolder(); + } + } folderNameLabel.setText(domainFolder.getPathname()); if (nameField.isEditable()) { if (nameField.getText().length() > 0) { @@ -349,7 +361,9 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider * @param file the file */ public void selectDomainFile(DomainFile file) { - Swing.runLater(() -> treePanel.selectDomainFile(file)); + if (file != null) { + Swing.runLater(() -> treePanel.selectDomainFile(file)); + } } @Override @@ -584,20 +598,6 @@ public abstract class AbstractDataTreeDialog extends DialogComponentProvider } } - protected static DomainFileFilter getDefaultFilter(DataTreeDialogType 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; - } - private class FieldKeyListener extends KeyAdapter { @Override public void keyPressed(KeyEvent e) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java index 767dbdb916..0d74dd1369 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,8 +17,7 @@ package ghidra.framework.main; import java.awt.Component; -import ghidra.framework.model.DomainFileFilter; -import ghidra.framework.model.Project; +import ghidra.framework.model.*; /** * Dialog to open or save domain data items to a new location or name. @@ -33,9 +32,9 @@ public class DataTreeDialog extends AbstractDataTreeDialog { /** * Construct a new DataTreeDialog for the active project. This chooser will show all project - * files. Following linked-folders will only be allowed if a type of CHOOSE_FOLDER - * or OPEN is specified. If different behavior is required a filter should - * be specified using the other constructor. + * files and/or folders within the active project only. Broken and external links will not be + * shown. If different behavior is required a filter should be specified using the other + * constructor. * * @param parent dialog's parent * @param title title to use @@ -43,7 +42,7 @@ public class DataTreeDialog extends AbstractDataTreeDialog { * @throws IllegalArgumentException if invalid type is specified */ public DataTreeDialog(Component parent, String title, DataTreeDialogType type) { - this(parent, title, type, getDefaultFilter(type)); + this(parent, title, type, DomainFileFilter.ALL_INTERNAL_FILES_FILTER); } /** @@ -52,7 +51,9 @@ public class DataTreeDialog extends AbstractDataTreeDialog { * @param parent dialog's parent * @param title title to use * @param type specify OPEN, SAVE, CHOOSE_FOLDER, or CREATE - * @param filter filter used to control what is displayed in the data tree + * @param filter filter used to control what is displayed in the data tree. See static + * implementations provided by {@link DomainFileFilter} and a more tailored + * {@link DefaultDomainFileFilter}. * @throws IllegalArgumentException if invalid type is specified */ public DataTreeDialog(Component parent, String title, DataTreeDialogType type, @@ -66,7 +67,9 @@ public class DataTreeDialog extends AbstractDataTreeDialog { * @param parent dialog's parent * @param title title to use * @param type specify OPEN, SAVE, CHOOSE_FOLDER, or CREATE - * @param filter filter used to control what is displayed in the data tree + * @param filter filter used to control what is displayed in the data tree. See static + * implementations provided by {@link DomainFileFilter} and a more tailored + * {@link DefaultDomainFileFilter}. * @param project the project to browse * @throws IllegalArgumentException if invalid type is specified */ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/OpenVersionedFileDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/OpenVersionedFileDialog.java index 247965f391..bfef36d641 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/OpenVersionedFileDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/OpenVersionedFileDialog.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -90,9 +90,8 @@ public class OpenVersionedFileDialog extends AbstractDat */ public OpenVersionedFileDialog(PluginTool tool, String title, Class domainObjectClass, List openDomainObjects) { - super(tool.getToolFrame(), title, OPEN, f -> { - return domainObjectClass.isAssignableFrom(f.getDomainObjectClass()); - }, AppInfo.getActiveProject()); + super(tool.getToolFrame(), title, OPEN, + new DefaultDomainFileFilter(domainObjectClass, false), AppInfo.getActiveProject()); this.tool = tool; this.domainObjectClass = domainObjectClass; @@ -214,8 +213,7 @@ public class OpenVersionedFileDialog extends AbstractDat tabbedPane = new JTabbedPane(); tabbedPane.setName("Tabs"); tabbedPane.add("Project Files", projectFilePanel); - tabbedPane.add("Open " + domainObjectClass.getSimpleName() + "s", - buildOpenObjectsTable()); + tabbedPane.add("Open " + domainObjectClass.getSimpleName() + "s", buildOpenObjectsTable()); tabbedPane.addChangeListener(e -> { int selectedTabIndex = tabbedPane.getModel().getSelectedIndex(); @@ -254,8 +252,7 @@ public class OpenVersionedFileDialog extends AbstractDat openObjectsTable = new GFilterTable<>(new OpenObjectsTableModel()); GTable table = openObjectsTable.getTable(); - table.getSelectionModel() - .setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + table.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); openObjectsTable.addSelectionListener(e -> { setOkEnabled(true); okButton.setToolTipText("Use the selected " + domainObjectClass.getSimpleName()); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/ProgramFileChooser.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/ProgramFileChooser.java new file mode 100644 index 0000000000..9be017109a --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/ProgramFileChooser.java @@ -0,0 +1,65 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.main; + +import java.awt.Component; + +import ghidra.framework.model.*; +import ghidra.program.model.listing.Program; + +/** + * {@link ProgramFileChooser} facilitates selection of an existing project Program file including + * Program link-files which may link to either internal or external program files. + * This chooser operates in the {@link DataTreeDialogType#OPEN open mode} for selecting + * an existing file only. + *

    + * This chooser should not be used to facilitate an immediate or + * future save-as operation or to open a Program for update since it can return a read-only file. + * A more taylored {@link DataTreeDialog} should be used for case where the file will be written. + */ +public class ProgramFileChooser extends DataTreeDialog { + + /** + * This file filter permits selection of any program including those than can be + * found by following bother internal and external folder and files links. + */ + public static final DomainFileFilter PROGRAM_FILE_FILTER = + new DefaultDomainFileFilter(Program.class, false); + + /** + * Construct a new ProgramChooser for the active project. + * + * @param parent dialog's parent + * @param title title to use + * @throws IllegalArgumentException if invalid type is specified + */ + public ProgramFileChooser(Component parent, String title) { + super(parent, title, DataTreeDialogType.OPEN, PROGRAM_FILE_FILTER); + } + + /** + * Construct a new DataTreeDialog for the given project. + * + * @param parent dialog's parent + * @param title title to use + * @param project the project to browse + * @throws IllegalArgumentException if invalid type is specified + */ + public ProgramFileChooser(Component parent, String title, Project project) { + super(parent, title, DataTreeDialogType.OPEN, PROGRAM_FILE_FILTER, project); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/AbstractFileListFlavorHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/AbstractFileListFlavorHandler.java index 397e0c06f3..f5784576af 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/AbstractFileListFlavorHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/AbstractFileListFlavorHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,7 +19,6 @@ import java.awt.Component; import java.io.File; import java.util.List; -import docking.widgets.tree.GTreeNode; import ghidra.app.services.FileImporterService; import ghidra.app.util.FileOpenDataFlavorHandler; import ghidra.framework.model.DomainFolder; @@ -61,15 +60,4 @@ abstract class AbstractFileListFlavorHandler } }); } - - protected DomainFolder getDomainFolder(GTreeNode destinationNode) { - if (destinationNode instanceof DomainFolderNode) { - return ((DomainFolderNode) destinationNode).getDomainFolder(); - } - else if (destinationNode instanceof DomainFileNode) { - DomainFolderNode parent = (DomainFolderNode) destinationNode.getParent(); - return parent.getDomainFolder(); - } - return null; - } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/JavaFileListHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/JavaFileListHandler.java index 8ff42780bc..a365ba733d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/JavaFileListHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/JavaFileListHandler.java @@ -46,7 +46,7 @@ public final class JavaFileListHandler extends AbstractFileListFlavorHandler { if (fileList.isEmpty()) { return false; } - doImport(getDomainFolder(destinationNode), fileList, tool, dataTree); + doImport(DataTree.getRealInternalFolderForNode(destinationNode), fileList, tool, dataTree); return true; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java index 19050e5123..bd398d87e6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/datatree/LinuxFileUrlHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -38,9 +38,8 @@ public final class LinuxFileUrlHandler extends AbstractFileListFlavorHandler { * Linux URL-based file list {@link DataFlavor} to be used during handler registration * using {@link DataTreeDragNDropHandler#addActiveDataFlavorHandler}. */ - public static final DataFlavor linuxFileUrlFlavor = - new DataFlavor("application/x-java-serialized-object;class=java.lang.String", - "String file URL"); + public static final DataFlavor linuxFileUrlFlavor = new DataFlavor( + "application/x-java-serialized-object;class=java.lang.String", "String file URL"); @Override // This is for the FileOpenDataFlavorHandler for handling file drops from Linux to a Tool @@ -57,7 +56,7 @@ public final class LinuxFileUrlHandler extends AbstractFileListFlavorHandler { if (files.isEmpty()) { return false; } - doImport(getDomainFolder(destinationNode), files, tool, dataTree); + doImport(DataTree.getRealInternalFolderForNode(destinationNode), files, tool, dataTree); return true; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java index c02319c162..27998ca008 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java @@ -142,7 +142,7 @@ public class ImporterDialog extends DialogComponentProvider { */ public void setDestinationFolder(DomainFolder folder) { destinationFolder = folder; - folderNameTextField.setText(destinationFolder.toString()); + folderNameTextField.setText(destinationFolder.getPathname()); validateFormInput(); } @@ -521,7 +521,7 @@ public class ImporterDialog extends DialogComponentProvider { String parentPath = FilenameUtils.getFullPathNoEndSeparator(pathName); String fileOrFolderName = FilenameUtils.getName(pathName); DomainFolder localDestFolder = - (parentPath != null) ? ProjectDataUtils.lookupDomainPath(destinationFolder, parentPath) + (parentPath != null) ? ProjectDataUtils.getDomainFolder(destinationFolder, parentPath) : destinationFolder; if (localDestFolder != null) { if (isFolder && localDestFolder.getFolder(fileOrFolderName) != null || diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java index f51f3bddb6..63ba37eea8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java @@ -28,6 +28,7 @@ import docking.action.*; import docking.tool.ToolConstants; import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooserMode; +import docking.widgets.tree.GTreeNode; import ghidra.app.CorePluginPackage; import ghidra.app.context.ListingActionContext; import ghidra.app.events.ProgramActivatedPluginEvent; @@ -441,17 +442,14 @@ public class ImporterPlugin extends Plugin } private static DomainFolder getFolderFromContext(ActionContext context) { + DomainFolder folder = null; Object contextObj = context.getContextObject(); - if (contextObj instanceof DomainFolderNode) { - DomainFolderNode node = (DomainFolderNode) contextObj; - return node.getDomainFolder(); + if (contextObj instanceof GTreeNode dataTreeNode) { + folder = DataTree.getRealInternalFolderForNode(dataTreeNode); } - if (contextObj instanceof DomainFileNode) { - DomainFileNode node = (DomainFileNode) contextObj; - DomainFile domainFile = node.getDomainFile(); - return domainFile != null ? domainFile.getParent() : null; + if (folder != null && folder.isInWritableProject()) { + return folder; } - return AppInfo.getActiveProject().getProjectData().getRootFolder(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java b/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java index 24bc1852e0..c370d82a95 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/test/ToyProgramBuilder.java @@ -15,8 +15,7 @@ */ package ghidra.test; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import ghidra.program.database.ProgramBuilder; import ghidra.program.model.address.*; @@ -628,4 +627,21 @@ public class ToyProgramBuilder extends ProgramBuilder { disassemble(address, 1); } + /** + * Create simple Toy program with a single initialized memory block at 0x1001000-0x1002fff + * @param programName program name + * @param consumer object consumer responsible for releasing the returned program + * @return new in-memory program instance + * @throws Exception if an error occurs + */ + public static Program buildSimpleProgram(String programName, Object consumer) throws Exception { + Objects.requireNonNull(consumer); + ProgramBuilder builder = new ProgramBuilder(programName, ProgramBuilder._TOY); + builder.createMemory("test1", Long.toHexString(0x1001000), 0x2000); + Program p = builder.getProgram(); + p.addConsumer(consumer); + p.release(builder); + return p; + } + } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java index e9048fe1f7..89f84aec35 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,10 +22,10 @@ import java.awt.*; import java.net.MalformedURLException; import java.net.URL; import java.util.HashSet; -import java.util.List; import java.util.Set; import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; import org.junit.Before; import org.junit.Test; @@ -52,7 +52,6 @@ import ghidra.program.util.ProgramLocation; import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.util.bean.field.AnnotatedTextFieldElement; import ghidra.util.task.TaskMonitor; -import util.CollectionUtils; public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { @@ -365,7 +364,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element); click(spyNavigatable, spyServiceProvider, annotatedElement); - assertErrorDialog("No Symbol"); + assertErrorDialog("Symbol Not Found"); assertTrue(spyServiceProvider.programOpened(programName)); assertTrue(spyServiceProvider.programClosed(programName)); @@ -422,7 +421,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element); click(spyNavigatable, spyServiceProvider, annotatedElement); - assertErrorDialog("No Symbol"); + assertErrorDialog("Symbol Not Found"); assertTrue(spyServiceProvider.programOpened(programName)); assertTrue(spyServiceProvider.programClosed(programName)); @@ -478,7 +477,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element); click(spyNavigatable, spyServiceProvider, annotatedElement); - assertErrorDialog("No Program"); + assertErrorDialog("Program Not Found"); assertFalse(spyServiceProvider.programOpened(programName)); } @@ -495,8 +494,8 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { String otherProgramPath = "folder1/folder2/program_f1_f2.exe"; // real path - String realPath = "folder1/program_f1_f2.exe"; - addFakeProgramByPath(spyServiceProvider, realPath); + String realPath = "/folder1/program_f1_f2.exe"; + addFakeProgramByPath(spyServiceProvider, realPath, program); String annotationText = "{@program " + otherProgramPath + "@" + addresstring + "}"; String rawComment = "My comment - " + annotationText; @@ -513,7 +512,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { AnnotatedTextFieldElement annotatedElement = getAnnotatedTextFieldElement(element); click(spyNavigatable, spyServiceProvider, annotatedElement); - assertErrorDialog("No Folder"); + assertErrorDialog("Folder Not Found"); assertFalse(spyServiceProvider.programOpened(otherProgramPath)); } @@ -525,7 +524,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { SpyServiceProvider spyServiceProvider = new SpyServiceProvider(); String otherProgramPath = "/folder1/folder2/program_f1_f2.exe"; - addFakeProgramByPath(spyServiceProvider, otherProgramPath); + addFakeProgramByPath(spyServiceProvider, otherProgramPath, program); String annotationText = "{@program " + otherProgramPath + "}"; String rawComment = "My comment - " + annotationText; @@ -557,7 +556,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { Address address = program.getAddressFactory().getAddress(addresstring); String otherProgramPath = "/folder1/folder2/program_f1_f2.exe"; - addFakeProgramByPath(spyServiceProvider, otherProgramPath); + addFakeProgramByPath(spyServiceProvider, otherProgramPath, program); String annotationText = "{@program " + otherProgramPath + "@" + addresstring + "}"; String rawComment = "My comment - " + annotationText; @@ -591,7 +590,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { String otherProgramPath = "/folder1/folder2/program_f1_f2.exe"; String annotationPath = "\\folder1\\folder2\\program_f1_f2.exe"; - addFakeProgramByPath(spyServiceProvider, otherProgramPath); + addFakeProgramByPath(spyServiceProvider, otherProgramPath, program); String annotationText = "{@program " + annotationPath + "@" + addresstring + "}"; String rawComment = "My comment - " + annotationText; @@ -622,9 +621,9 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { String addresstring = "1001000"; Address address = program.getAddressFactory().getAddress(addresstring); - String otherProgramPath = "folder1/folder2/program_f1_f2.exe"; + String otherProgramPath = "/folder1/folder2/program_f1_f2.exe"; String annotationPath = "folder1\\folder2\\program_f1_f2.exe"; - addFakeProgramByPath(spyServiceProvider, otherProgramPath); + addFakeProgramByPath(spyServiceProvider, otherProgramPath, program); String annotationText = "{@program " + annotationPath + "@" + addresstring + "}"; String rawComment = "My comment - " + annotationText; @@ -656,8 +655,8 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { String addresstring = "1001000"; Address address = program.getAddressFactory().getAddress(addresstring); - String otherProgramPath = "folder1/folder2/program_f1_f2.exe"; - addFakeProgramByPath(spyServiceProvider, otherProgramPath); + String otherProgramPath = "/folder1/folder2/program_f1_f2.exe"; + addFakeProgramByPath(spyServiceProvider, otherProgramPath, program); String annotationText = "{@program " + otherProgramPath + "@" + addresstring + "}"; String rawComment = "My comment - " + annotationText; @@ -883,30 +882,29 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { return new FieldElement[] { fieldElement }; } - private void addFakeProgramByPath(SpyServiceProvider provider, String path) { + private void addFakeProgramByPath(SpyServiceProvider provider, String path, Program p) { SpyProjectDataService spyProjectData = (SpyProjectDataService) provider.getService(ProjectDataService.class); FakeRootFolder root = spyProjectData.fakeProjectData.fakeRootFolder; - String parentPath = FilenameUtils.getFullPath(path); - String programName = FilenameUtils.getName(path); - - String[] paths = parentPath.split("/"); - TestDummyDomainFolder parent = root; - String pathSoFar = root.getPathname(); - for (String folderName : paths) { - pathSoFar += folderName; - TestDummyDomainFolder folder = (TestDummyDomainFolder) root.getFolder(pathSoFar); - if (folder == null) { - folder = new TestDummyDomainFolder(parent, folderName); - root.addFolder(folder); - } - parent = folder; + if (StringUtils.isBlank(path) || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { + throw new IllegalArgumentException( + "Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'"); } + else if (path.charAt(path.length() - 1) == FileSystem.SEPARATOR_CHAR) { + throw new IllegalArgumentException("Missing file name in path"); + } + int ix = path.lastIndexOf(FileSystem.SEPARATOR); + String folderPath = "/"; + if (ix > 0) { + folderPath = path.substring(0, ix); + } + String programName = path.substring(ix + 1); try { - parent.createFile(programName, (DomainObject) null, TaskMonitor.DUMMY); + DomainFolder parent = ProjectDataUtils.createDomainFolderPath(root, folderPath); + parent.createFile(programName, p, TaskMonitor.DUMMY); } catch (Exception e) { failWithException("Unable to create a dummy domain file", e); @@ -973,41 +971,50 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { } @Override - public DomainFolder getFolder(String path) { - return fakeRootFolder.getFolder(path); + public DomainFolder getFolder(String path, DomainFolderFilter filter) { + return ProjectDataUtils.getDomainFolder(fakeRootFolder, path, filter); + } + + @Override + public DomainFile getFile(String path, DomainFileFilter filter) { + if (StringUtils.isBlank(path) || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { + throw new IllegalArgumentException( + "Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'"); + } + else if (path.charAt(path.length() - 1) == FileSystem.SEPARATOR_CHAR) { + throw new IllegalArgumentException("Missing file name in path"); + } + int ix = path.lastIndexOf(FileSystem.SEPARATOR); + + DomainFolder folder; + String fileName = path; + if (ix > 0) { + folder = getFolder(path.substring(0, ix), filter); + fileName = path.substring(ix + 1); + } + else { + folder = getRootFolder(); + } + if (folder != null) { + DomainFile file = folder.getFile(fileName); + if (file != null && filter.accept(file)) { + return file; + } + } + return null; } } private class FakeRootFolder extends TestDummyDomainFolder { - private List folders = CollectionUtils.asList(this); - - private List folderFiles = - CollectionUtils.asList(new TestDummyDomainFile(this, OTHER_PROGRAM_NAME)); - public FakeRootFolder() { super(null, "Fake Root Folder"); - } - - void addFolder(TestDummyDomainFolder f) { - folders.add(f); + files.add(new TestDummyDomainFile(this, OTHER_PROGRAM_NAME, "Program")); } @Override - public synchronized DomainFile[] getFiles() { - return folderFiles.toArray(new TestDummyDomainFile[folderFiles.size()]); - } - - @Override - public synchronized DomainFolder getFolder(String path) { - for (TestDummyDomainFolder folder : folders) { - String folderPath = folder.getPathname(); - if (folderPath.equals(path)) { - return folder; - } - } - - return null; + public boolean isInWritableProject() { + return true; } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java index 3751e477e8..9539770432 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/DataTreeDialogTest.java @@ -87,10 +87,8 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest { showFiltered("tN"); JTree tree = getJTree(); - List expectedFilteredNames = names.stream() - .filter(s -> s.startsWith("tN")) - .sorted() - .collect(Collectors.toList()); + List expectedFilteredNames = + names.stream().filter(s -> s.startsWith("tN")).sorted().collect(Collectors.toList()); TreeModel model = tree.getModel(); GTreeNode root = (GTreeNode) model.getRoot(); @@ -424,8 +422,8 @@ public class DataTreeDialogTest extends AbstractGhidraHeadedIntegrationTest { private void showFiltered(final String startsWith) { Swing.runLater(() -> { - dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog", - OPEN, f -> f.getName().startsWith(startsWith)); + dialog = new DataTreeDialog(frontEndTool.getToolFrame(), "Test Data Tree Dialog", OPEN, + f -> f.getName().startsWith(startsWith)); dialog.showComponent(); }); waitForSwing(); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginActionsTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginActionsTest.java index 2c78427209..e161b9b1cd 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginActionsTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginActionsTest.java @@ -732,11 +732,22 @@ public class FrontEndPluginActionsTest extends AbstractGhidraHeadedIntegrationTe performAction(selectAction, getTreeActionContext(), true); waitForTree(); + // NOTE: All nodes except the root node should be selected. + // Root is not selected to allow for most popup actions to + // be enabled and work as expected + BreadthFirstIterator it = new BreadthFirstIterator(rootNode); + int count = 0; while (it.hasNext()) { GTreeNode node = it.next(); - assertTrue(tree.isPathSelected(node.getTreePath())); + if (tree.isPathSelected(node.getTreePath())) { + ++count; + } + else { + assertTrue(node.isRoot()); + } } + assertEquals(7, count); } @Test @@ -954,11 +965,12 @@ public class FrontEndPluginActionsTest extends AbstractGhidraHeadedIntegrationTe for (TreePath path : paths) { GTreeNode node = (GTreeNode) path.getLastPathComponent(); - if (node instanceof DomainFileNode) { - fileList.add(((DomainFileNode) node).getDomainFile()); + if (node instanceof DomainFileNode fileNode) { + // NOTE: File may be a linked-folder. Treatment as folder or file depends on action + fileList.add(fileNode.getDomainFile()); } - else if (node instanceof DomainFolderNode) { - folderList.add(((DomainFolderNode) node).getDomainFolder()); + else if (node instanceof DomainFolderNode folderNode) { + folderList.add(folderNode.getDomainFolder()); } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/bundle/FullOatBundle.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/bundle/FullOatBundle.java index 083618ae86..cb98f5c43d 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/bundle/FullOatBundle.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/bundle/FullOatBundle.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,9 +15,8 @@ */ package ghidra.file.formats.android.oat.bundle; -import java.util.*; - import java.io.IOException; +import java.util.*; import org.apache.commons.io.FilenameUtils; @@ -30,6 +29,7 @@ import ghidra.file.formats.android.oat.OatHeader; import ghidra.file.formats.android.vdex.*; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFolder; +import ghidra.program.database.ProgramContentHandler; import ghidra.program.model.listing.Program; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -45,8 +45,7 @@ public class FullOatBundle implements OatBundle { private boolean isLittleEndian; - FullOatBundle(Program oatProgram, OatHeader oatHeader, TaskMonitor monitor, - MessageLog log) { + FullOatBundle(Program oatProgram, OatHeader oatHeader, TaskMonitor monitor, MessageLog log) { this.oatProgram = oatProgram; this.oatHeader = oatHeader; @@ -120,12 +119,11 @@ public class FullOatBundle implements OatBundle { DomainFolder parentFolder = domainFile.getParent(); //first, look in current project for VDEX file.... - if (lookInProjectFolder(HeaderType.VDEX, parentFolder, - vdexProgramName, monitor, log)) { + if (lookInProjectFolder(HeaderType.VDEX, parentFolder, vdexProgramName, monitor, log)) { return; } - if (lookInProjectFolder(HeaderType.VDEX, parentFolder.getParent(), - vdexProgramName, monitor, log)) { + if (lookInProjectFolder(HeaderType.VDEX, parentFolder.getParent(), vdexProgramName, monitor, + log)) { return; } } @@ -140,8 +138,8 @@ public class FullOatBundle implements OatBundle { break; } if (file.getName().startsWith(CLASSES) && file.getName().endsWith(DEX)) { - lookInProjectFolder(HeaderType.DEX, odexApkFolder, file.getName(), - monitor, log); + lookInProjectFolder(HeaderType.DEX, odexApkFolder, file.getName(), monitor, + log); } } } @@ -153,8 +151,8 @@ public class FullOatBundle implements OatBundle { break; } if (file.getName().startsWith(CLASSES) && file.getName().endsWith(DEX)) { - lookInProjectFolder(HeaderType.DEX, apkOrJarFolder, file.getName(), - monitor, log); + lookInProjectFolder(HeaderType.DEX, apkOrJarFolder, file.getName(), monitor, + log); } } } @@ -166,8 +164,8 @@ public class FullOatBundle implements OatBundle { break; } if (file.getName().startsWith(CDEX)) { - lookInProjectFolder(HeaderType.CDEX, appVdexFolder, file.getName(), - monitor, log); + lookInProjectFolder(HeaderType.CDEX, appVdexFolder, file.getName(), monitor, + log); } } } @@ -183,12 +181,11 @@ public class FullOatBundle implements OatBundle { DomainFolder parentFolder = domainFile.getParent(); //first, look in current project for ART file.... - if (lookInProjectFolder(HeaderType.ART, parentFolder, - artProgramName, monitor, log)) { + if (lookInProjectFolder(HeaderType.ART, parentFolder, artProgramName, monitor, log)) { return; } - if (lookInProjectFolder(HeaderType.ART, parentFolder.getParent(), - artProgramName, monitor, log)) { + if (lookInProjectFolder(HeaderType.ART, parentFolder.getParent(), artProgramName, monitor, + log)) { return; } } @@ -203,14 +200,15 @@ public class FullOatBundle implements OatBundle { * @param log the message log */ private boolean lookInProjectFolder(HeaderType type, DomainFolder parentFolder, - String programName, - TaskMonitor monitor, MessageLog log) { + String programName, TaskMonitor monitor, MessageLog log) { - DomainFile child = parentFolder.getFile(programName); - if (child != null) { + DomainFile file = parentFolder.getFile(programName); + // Constrain to Program files only and not program link-files + if (file != null && + ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(file.getContentType())) { Program program = null; try { - program = (Program) child.getDomainObject(this, true, true, monitor); + program = (Program) file.getDomainObject(this, true, true, monitor); ByteProvider provider = MemoryByteProvider.createProgramHeaderByteProvider(program, false); return makeHeader(type, programName, provider, monitor); diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/generic/iOS_KextStubFixupAnalyzer.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/generic/iOS_KextStubFixupAnalyzer.java index fed8ac4753..b906ca92f6 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/generic/iOS_KextStubFixupAnalyzer.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/generic/iOS_KextStubFixupAnalyzer.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,7 @@ import ghidra.file.analyzers.FileFormatAnalyzer; import ghidra.framework.main.AppInfo; import ghidra.framework.model.*; import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.ProgramContentHandler; import ghidra.program.model.address.*; import ghidra.program.model.data.PointerDataType; import ghidra.program.model.lang.Processor; @@ -295,6 +296,7 @@ public class iOS_KextStubFixupAnalyzer extends FileFormatAnalyzer { private DestinationProgramInfo recurseFolder(DomainFolder folder, Address destinationAddress, ProgramManager programManager, TaskMonitor monitor) { + // NOTE: All folder-links and file-links are ignored DomainFolder[] folders = folder.getFolders(); for (DomainFolder child : folders) { if (monitor.isCancelled()) { @@ -311,30 +313,31 @@ public class iOS_KextStubFixupAnalyzer extends FileFormatAnalyzer { if (monitor.isCancelled()) { break; } - DomainObject domainObject = null; + if (!file.getContentType().equals(ProgramContentHandler.PROGRAM_CONTENT_TYPE)) { + continue; + } + Program program = null; try { - domainObject = file.getDomainObject(this, true /* upgrade */, + program = (Program) file.getDomainObject(this, true /* upgrade */, false /* do not recover */, monitor); - if (domainObject instanceof Program) { - Program program = (Program) domainObject; - if (program.getMemory().contains(destinationAddress)) { - if (programManager != null) { - programManager.openProgram(program, ProgramManager.OPEN_VISIBLE);//once program is located, open it, so lookup is faster next time! - } - SymbolTable symbolTable = program.getSymbolTable(); - Symbol symbol = symbolTable.getPrimarySymbol(destinationAddress); - String symbolName = symbol == null ? null : symbol.getName(); - return new DestinationProgramInfo(program.getName(), file.getPathname(), - symbolName); + if (program.getMemory().contains(destinationAddress)) { + if (programManager != null) { + //once program is located, open it, so lookup is faster next time! + programManager.openProgram(program, ProgramManager.OPEN_VISIBLE); } + SymbolTable symbolTable = program.getSymbolTable(); + Symbol symbol = symbolTable.getPrimarySymbol(destinationAddress); + String symbolName = symbol == null ? null : symbol.getName(); + return new DestinationProgramInfo(program.getName(), file.getPathname(), + symbolName); } } catch (Exception e) { Msg.warn(this, e); } finally { - if (domainObject != null) { - domainObject.release(this); + if (program != null) { + program.release(this); } } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java index 373e554732..b60c668974 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java @@ -118,7 +118,7 @@ public class GFileSystemLoadKernelTask extends Task { ProjectIndexService projectIndex = ProjectIndexService.getIndexFor(project); DomainFile existingDF = projectIndex.findFirstByFSRL(file.getFSRL()); - if ( existingDF != null && programManager != null ) { + if (existingDF != null && programManager != null) { programManager.openProgram(existingDF); return; } @@ -138,6 +138,9 @@ public class GFileSystemLoadKernelTask extends Task { AppInfo.getActiveProject().getProjectData().getRootFolder(), file.getParentFile().getPath()); String fileName = ProjectDataUtils.getUniqueName(folder, program.getName()); + if (fileName == null) { + throw new IOException("Unable to find unique name for " + program.getName()); + } GhidraProgramUtilities.markProgramAnalyzed(program); diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java index 6e4461a77d..bbd374ff10 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -250,12 +250,12 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan } @Override - public void checkCompatibility(int serverInterfaceVersion) throws RemoteException { - if (serverInterfaceVersion > INTERFACE_VERSION) { + public void checkCompatibility(int minServerInterfaceVersion) throws RemoteException { + if (minServerInterfaceVersion > INTERFACE_VERSION) { throw new RemoteException( "Incompatible server interface, a newer Ghidra Server version is required."); } - else if (serverInterfaceVersion < INTERFACE_VERSION) { + else if (minServerInterfaceVersion < MINIMUM_INTERFACE_VERSION) { throw new RemoteException( "Incompatible server interface, the minimum supported Ghidra version is " + MIN_GHIDRA_VERSION); diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java index 8a81debdab..d868205915 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java @@ -381,6 +381,22 @@ public class RepositoryHandleImpl extends UnicastRemoteObject } } + @Override + public void createTextDataFile(String parentPath, String itemName, String fileID, + String contentType, String textData, String comment) + throws InvalidNameException, IOException { + synchronized (syncObject) { + validate(); + repository.validateWritePrivilege(currentUser); + RepositoryFolder folder = repository.getFolder(currentUser, parentPath, true); + if (folder == null) { + throw new IOException("Failed to create repository Folder " + parentPath); + } + folder.createTextDataFile(itemName, fileID, contentType, textData, comment, + currentUser); + } + } + @Override public RemoteManagedBufferFileHandle createDatabase(String parentPath, String itemName, String fileID, int bufferSize, String contentType, String projectPath) diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java index 6c399840c9..d0b9714bd9 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java @@ -39,7 +39,7 @@ public class RepositoryFile { private LocalFileSystem fileSystem; private RepositoryFolder parent; private String name; - private LocalDatabaseItem databaseItem; + private LocalFolderItem folderItem; private RepositoryItem repositoryItem; private boolean deleted = false; @@ -69,11 +69,10 @@ public class RepositoryFile { if (deleted) { throw new FileNotFoundException(getPathname() + " not found"); } - if (databaseItem == null) { + if (folderItem == null) { repositoryItem = null; - LocalFolderItem folderItem = fileSystem.getItem(parent.getPathname(), name); - if (folderItem == null || !folderItem.isVersioned() || - !(folderItem instanceof LocalDatabaseItem)) { + folderItem = fileSystem.getItem(parent.getPathname(), name); + if (folderItem == null) { // must build pathname just in case folderItem does not exist String pathname = parent.getPathname(); if (pathname.length() != 1) { @@ -84,7 +83,6 @@ public class RepositoryFile { "file is corrupt or unsupported", null); throw new FileNotFoundException(pathname + " is corrupt or unsupported"); } - this.databaseItem = (LocalDatabaseItem) folderItem; } } } @@ -127,16 +125,33 @@ public class RepositoryFile { synchronized (fileSystem) { try { validate(); - if (repositoryItem == null) { - repositoryItem = - new RepositoryItem(parent.getPathname(), name, databaseItem.getFileID(), - RepositoryItem.DATABASE, databaseItem.getContentType(), - databaseItem.getCurrentVersion(), databaseItem.lastModified()); + if (repositoryItem == null && folderItem != null) { + String textData = null; + int itemType = -1; + if (folderItem instanceof DatabaseItem) { + itemType = RepositoryItem.DATABASE; + } + else if (folderItem instanceof TextDataItem textItem) { + itemType = RepositoryItem.TEXT_DATA_FILE; + textData = textItem.getTextData(); + } + else { + repository.log(getPathname(), + "Unsupported item type: " + folderItem.getClass().getSimpleName(), + null); + } + + repositoryItem = new RepositoryItem(parent.getPathname(), name, + folderItem.getFileID(), itemType, folderItem.getContentType(), + folderItem.getCurrentVersion(), folderItem.lastModified(), textData); } } catch (IOException e) { + repository.log(getPathname(), "Item failure: " + e.getMessage(), null); + } + if (repository == null) { repositoryItem = new RepositoryItem(parent.getPathname(), name, null, - RepositoryItem.DATABASE, "INVALID", 0, 0); + RepositoryItem.FILE, "INVALID", 0, 0, null); } return repositoryItem; } @@ -157,9 +172,14 @@ public class RepositoryFile { synchronized (fileSystem) { validate(); repository.validateReadPrivilege(user); + if (!(folderItem instanceof LocalDatabaseItem databaseItem)) { + throw new IOException( + "Unsupported operation for " + folderItem.getClass().getSimpleName()); + } LocalManagedBufferFile bf = databaseItem.open(version, minChangeDataVer); - repository.log(getPathname(), "version " + - (version < 0 ? databaseItem.getCurrentVersion() : version) + " opened read-only", + repository.log( + getPathname(), "version " + + (version < 0 ? folderItem.getCurrentVersion() : version) + " opened read-only", user); return bf; } @@ -177,7 +197,11 @@ public class RepositoryFile { synchronized (fileSystem) { validate(); repository.validateWritePrivilege(user); - ItemCheckoutStatus coStatus = databaseItem.getCheckout(checkoutId); + if (!(folderItem instanceof LocalDatabaseItem databaseItem)) { + throw new IOException( + "Unsupported operation for " + folderItem.getClass().getSimpleName()); + } + ItemCheckoutStatus coStatus = folderItem.getCheckout(checkoutId); if (coStatus == null) { throw new IOException("Illegal checkin"); } @@ -202,7 +226,7 @@ public class RepositoryFile { synchronized (fileSystem) { validate(); repository.validateReadPrivilege(user); - return databaseItem.getVersions(); + return folderItem.getVersions(); } } @@ -216,7 +240,7 @@ public class RepositoryFile { public long length() throws IOException { synchronized (fileSystem) { validate(); - return databaseItem.length(); + return folderItem.length(); } } @@ -234,7 +258,7 @@ public class RepositoryFile { User userObj = repository.validateWritePrivilege(user); if (!userObj.isAdmin()) { - Version[] versions = databaseItem.getVersions(); + Version[] versions = folderItem.getVersions(); if (deleteVersion == -1) { for (Version version : versions) { if (!user.equals(version.getUser())) { @@ -259,21 +283,13 @@ public class RepositoryFile { throw new IOException("Only the oldest or latest version may be deleted"); } } - String oldPath = getPathname(); - if (databaseItem == null) { - // forced removal by repo Admin - } - else { - databaseItem.delete(deleteVersion, user); + if (folderItem != null) { + folderItem.delete(deleteVersion, user); } deleted = true; repositoryItem = null; parent.fileDeleted(this); - RepositoryFile newRf = parent.getFile(name); - if (newRf == null) { - RepositoryManager.log(repository.getName(), oldPath, "file deleted", user); - } parent = null; } } @@ -320,7 +336,7 @@ public class RepositoryFile { synchronized (fileSystem) { validate(); repository.validateWritePrivilege(user); // don't allow checkout if read-only - ItemCheckoutStatus coStatus = databaseItem.checkout(checkoutType, user, projectPath); + ItemCheckoutStatus coStatus = folderItem.checkout(checkoutType, user, projectPath); if (coStatus != null && checkoutType != CheckoutType.NORMAL && repositoryItem != null && repositoryItem.getFileID() == null) { repositoryItem = null; // force refresh since fileID should get reset @@ -340,7 +356,7 @@ public class RepositoryFile { throws IOException { synchronized (fileSystem) { validate(); - databaseItem.updateCheckoutVersion(checkoutId, checkoutVersion, user); + folderItem.updateCheckoutVersion(checkoutId, checkoutVersion, user); } } @@ -354,14 +370,14 @@ public class RepositoryFile { public void terminateCheckout(long checkoutId, String user, boolean notify) throws IOException { synchronized (fileSystem) { validate(); - ItemCheckoutStatus coStatus = databaseItem.getCheckout(checkoutId); + ItemCheckoutStatus coStatus = folderItem.getCheckout(checkoutId); if (coStatus != null) { User userObj = repository.getUser(user); if (!userObj.isAdmin() && !coStatus.getUser().equals(user)) { throw new IOException( "Undo-checkout not permitted, checkout was made by " + coStatus.getUser()); } - databaseItem.terminateCheckout(checkoutId, notify); + folderItem.terminateCheckout(checkoutId, notify); } } } @@ -378,7 +394,7 @@ public class RepositoryFile { synchronized (fileSystem) { validate(); repository.validateReadPrivilege(user); - return databaseItem.getCheckout(checkoutId); + return folderItem.getCheckout(checkoutId); } } @@ -393,7 +409,7 @@ public class RepositoryFile { synchronized (fileSystem) { validate(); repository.validateReadPrivilege(user); - return databaseItem.getCheckouts(); + return folderItem.getCheckouts(); } } @@ -405,7 +421,7 @@ public class RepositoryFile { public boolean hasCheckouts() throws IOException { synchronized (fileSystem) { validate(); - return databaseItem.hasCheckouts(); + return folderItem.hasCheckouts(); } } @@ -417,7 +433,7 @@ public class RepositoryFile { public boolean isCheckinActive() throws IOException { synchronized (fileSystem) { validate(); - return databaseItem.isCheckinActive(); + return folderItem.isCheckinActive(); } } @@ -436,7 +452,7 @@ public class RepositoryFile { void pathChanged() { synchronized (fileSystem) { repositoryItem = null; - databaseItem = null; + folderItem = null; } } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java index 5af3ff774e..01465e7121 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java @@ -24,8 +24,7 @@ import org.apache.logging.log4j.Logger; import db.buffers.LocalManagedBufferFile; import ghidra.framework.store.*; -import ghidra.framework.store.local.LocalFileSystem; -import ghidra.framework.store.local.LocalFolderItem; +import ghidra.framework.store.local.*; import ghidra.server.Repository; import ghidra.server.RepositoryManager; import ghidra.util.InvalidNameException; @@ -94,20 +93,20 @@ public class RepositoryFolder { private void init() throws IOException { String path = getPathname(); String[] names = fileSystem.getFolderNames(path); - for (String name2 : names) { - RepositoryFolder subfolder = new RepositoryFolder(repository, fileSystem, this, name2); - folderMap.put(name2, subfolder); + for (String folderName : names) { + RepositoryFolder subfolder = + new RepositoryFolder(repository, fileSystem, this, folderName); + folderMap.put(folderName, subfolder); } names = fileSystem.getItemNames(path); int badItemCount = 0; - for (String name2 : names) { - LocalFolderItem item = fileSystem.getItem(path, name2); - if (item == null || !(item instanceof DatabaseItem)) { + for (String itemName : names) { + LocalFolderItem item = fileSystem.getItem(path, itemName); + if (item == null || (item instanceof UnknownFolderItem)) { ++badItemCount; - continue; } - RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, name2); - fileMap.put(name2, rf); + RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, itemName); + fileMap.put(itemName, rf); } if (badItemCount != 0) { log.error("Repository '" + repository.getName() + "' contains " + badItemCount + @@ -217,7 +216,7 @@ public class RepositoryFolder { if (fileSystem.fileExists(getPathname(), fileName)) { try { LocalFolderItem item = fileSystem.getItem(getPathname(), fileName); - if (item == null || !(item instanceof DatabaseItem)) { + if (item == null) { log.error("Repository '" + repository.getName() + "' contains bad item: " + makePathname(getPathname(), fileName)); return null; @@ -262,6 +261,41 @@ public class RepositoryFolder { } } + /** + * Creates a new text data file within the specified parent folder. + * @param itemName new data file name + * @param fileID file ID to be associated with new file or null + * @param contentType application defined content type + * @param textData text data (required) + * @param comment file comment (may be null) + * @param user user who is initiating request + * @throws DuplicateFileException Thrown if a folderItem with that name already exists. + * @throws InvalidNameException if the name has illegal characters. + * @throws IOException if an IO error occurs. + */ + public void createTextDataFile(String itemName, String fileID, String contentType, + String textData, String comment, String user) throws InvalidNameException, IOException { + synchronized (fileSystem) { + repository.validate(); + repository.validateWritePrivilege(user); + if (getFile(itemName) != null) { + throw new DuplicateFileException(itemName + " already exists"); + } + + LocalTextDataItem textDataItem = fileSystem.createTextDataItem(getPathname(), itemName, + fileID, contentType, textData, null); // comment conveyed with Version info below + + Version singleVersion = new Version(1, System.currentTimeMillis(), user, comment); + textDataItem.setVersionInfo(singleVersion); + + RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, itemName); + fileMap.put(itemName, rf); + + RepositoryManager.log(repository.getName(), makePathname(getPathname(), itemName), + "file created", user); + } + } + /** * Create a new database file/item within this folder. * @param itemName name of new database @@ -445,4 +479,5 @@ public class RepositoryFolder { : parentPath; return path + FileSystem.SEPARATOR + childName; } + } diff --git a/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java b/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java index e53d1162a0..681c97ad4e 100644 --- a/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java +++ b/Ghidra/Features/VersionTracking/ghidra_scripts/AutoVersionTrackingScript.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -87,6 +87,7 @@ // //@category Version Tracking import ghidra.app.script.GhidraScript; +import ghidra.feature.vt.api.db.VTSessionContentHandler; import ghidra.feature.vt.api.db.VTSessionDB; import ghidra.feature.vt.api.main.VTSession; import ghidra.feature.vt.api.util.VTOptions; @@ -107,8 +108,8 @@ public class AutoVersionTrackingScript extends GhidraScript { @Override public void run() throws Exception { - - if(currentProgram == null) { + + if (currentProgram == null) { println("Please open the destination program."); return; } @@ -175,8 +176,8 @@ public class AutoVersionTrackingScript extends GhidraScript { return; } - Program sourceProgram = (Program) sourceProgramDF.getDomainObject(this, autoUpgradeIfNeeded, - false, monitor); + Program sourceProgram = + (Program) sourceProgramDF.getDomainObject(this, autoUpgradeIfNeeded, false, monitor); VTSession session = null; try { @@ -258,19 +259,8 @@ public class AutoVersionTrackingScript extends GhidraScript { * @throws CancelledException if cancelled */ private boolean hasExistingSession(String name, DomainFolder folder) throws CancelledException { - - DomainFile[] files = folder.getFiles(); - - for (DomainFile file : files) { - monitor.checkCancelled(); - - if (file.getName().equals(name)) { - if (file.getContentType().equals("VersionTracking")) { - return true; - } - } - } - return false; + DomainFile file = folder.getFile(name); + return file != null && file.getContentType().equals(VTSessionContentHandler.CONTENT_TYPE); } /** diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java index 23af40fd72..edf1778e71 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTSessionDB.java @@ -333,7 +333,7 @@ public class VTSessionDB extends DomainObjectAdapterDB implements VTSession { } catch (VersionException e) { VersionExceptionHandler.showVersionError(null, domainFile.getName(), type, "open", - e); + false, e); } catch (IOException e) { Msg.showError(this, null, "Can't open " + type + ": " + domainFile.getName(), diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java index 57bdb334b8..5109e99cb7 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/OpenVersionTrackingSessionAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,8 +25,8 @@ import ghidra.feature.vt.api.main.VTSession; import ghidra.feature.vt.gui.plugin.VTController; import ghidra.feature.vt.gui.plugin.VTPlugin; import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.model.DefaultDomainFileFilter; import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainFileFilter; import ghidra.framework.plugintool.PluginTool; import ghidra.util.HelpLocation; @@ -48,7 +48,7 @@ public class OpenVersionTrackingSessionAction extends DockingAction { PluginTool tool = controller.getTool(); DataTreeDialog dialog = new DataTreeDialog(tool.getToolFrame(), "Open Version Tracking Session", OPEN, - new VTDomainFileFilter()); + new DefaultDomainFileFilter(VTSession.class, true)); tool.showDialog(dialog); if (!dialog.wasCancelled()) { @@ -57,16 +57,4 @@ public class OpenVersionTrackingSessionAction extends DockingAction { } } - class VTDomainFileFilter implements DomainFileFilter { - @Override - public boolean accept(DomainFile f) { - Class c = f.getDomainObjectClass(); - return VTSession.class.isAssignableFrom(c); - } - - @Override - public boolean followLinkedFolders() { - return false; - } - } } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java index 2473ee06f8..c13f1d8f61 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTControllerImpl.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -217,7 +217,7 @@ public class VTControllerImpl } catch (VersionException e) { VersionExceptionHandler.showVersionError(null, domainFile.getName(), "VT Session", - "open", e); + "open", false, e); } catch (IOException e) { Msg.showError(this, null, "Can't open VT Session: " + domainFile.getName(), diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/SessionConfigurationPanel.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/SessionConfigurationPanel.java index a03f71b445..8e430dbbf2 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/SessionConfigurationPanel.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/SessionConfigurationPanel.java @@ -31,8 +31,7 @@ import generic.theme.GIcon; import generic.theme.GThemeDefaults.Ids.Fonts; import generic.theme.Gui; import ghidra.framework.main.DataTreeDialog; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainFolder; +import ghidra.framework.model.*; import ghidra.util.StringUtilities; import utility.function.Callback; @@ -230,8 +229,8 @@ public class SessionConfigurationPanel extends JPanel { JButton button = new BrowseButton(); button.setName("SOURCE_BUTTON"); button.addActionListener(e -> { - DomainFile programFile = VTWizardUtils.chooseDomainFile(SessionConfigurationPanel.this, - "a source program", VTWizardUtils.PROGRAM_FILTER, null); + DomainFile programFile = VTWizardUtils.chooseProgramFile(SessionConfigurationPanel.this, + "a source program", null); if (programFile != null) { setSourceFile(programFile); statusChangedCallback.call(); @@ -244,8 +243,8 @@ public class SessionConfigurationPanel extends JPanel { JButton button = new BrowseButton(); button.setName("DESTINATION_BUTTON"); button.addActionListener(e -> { - DomainFile programFile = VTWizardUtils.chooseDomainFile(SessionConfigurationPanel.this, - "a destination program", VTWizardUtils.PROGRAM_FILTER, null); + DomainFile programFile = VTWizardUtils.chooseProgramFile(SessionConfigurationPanel.this, + "a destination program", null); if (programFile != null) { setDestinationFile(programFile); statusChangedCallback.call(); diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/VTWizardUtils.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/VTWizardUtils.java index bd12ffbda2..ad4f58c195 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/VTWizardUtils.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/wizard/session/VTWizardUtils.java @@ -25,8 +25,7 @@ import docking.widgets.OptionDialog; import ghidra.feature.vt.api.main.VTSession; import ghidra.feature.vt.gui.task.SaveTask; import ghidra.framework.main.DataTreeDialog; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainFileFilter; +import ghidra.framework.model.*; import ghidra.program.model.listing.Program; import ghidra.util.HTMLUtilities; import ghidra.util.task.TaskLauncher; @@ -37,29 +36,13 @@ public class VTWizardUtils { DomainFile df; } - public static final DomainFileFilter VT_SESSION_FILTER = new DomainFileFilter() { + public static final DomainFileFilter VT_SESSION_FILTER = + new DefaultDomainFileFilter(VTSession.class, true); - @Override - public boolean accept(DomainFile df) { - return VTSession.class.isAssignableFrom(df.getDomainObjectClass()); - } - - @Override - public boolean followLinkedFolders() { - return false; - } - }; - - public static final DomainFileFilter PROGRAM_FILTER = f -> { - return Program.class.isAssignableFrom(f.getDomainObjectClass()); - }; - - public static DomainFile chooseDomainFile(Component parent, String domainIdentifier, - DomainFileFilter filter, DomainFile fileToSelect) { - final DataTreeDialog dataTreeDialog = filter == null - ? new DataTreeDialog(parent, "Choose " + domainIdentifier, OPEN) - : new DataTreeDialog(parent, "Choose " + domainIdentifier, OPEN, - filter); + public static DomainFile chooseProgramFile(Component parent, String domainIdentifier, + DomainFile fileToSelect) { + final DataTreeDialog dataTreeDialog = new DataTreeDialog(parent, + "Choose " + domainIdentifier, OPEN, new DefaultDomainFileFilter(Program.class, true)); final DomainFileBox box = new DomainFileBox(); dataTreeDialog.addOkActionListener(new ActionListener() { @Override diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java index 0e58b47074..337de9be54 100644 --- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java +++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTBaseTestCase.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -34,7 +34,7 @@ import ghidra.program.model.mem.Memory; import ghidra.program.model.mem.StubMemory; import ghidra.program.model.symbol.*; -public class VTBaseTestCase extends AbstractGenericTest { +public abstract class VTBaseTestCase extends AbstractGenericTest { private DomainFile sourceDomainFile = new TestDummyDomainFile(null, "SourceDomainFile") { @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java index 3260963776..93112a060f 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTreeNode.java @@ -448,7 +448,14 @@ public abstract class GTreeNode extends CoreGTreeNode implements Comparable + * NOTE: Iterator will not include children of a node where {@link GTreeNode#isAutoExpandPermitted()} + * returns false. */ public class BreadthFirstIterator implements Iterator { private Queue nodeQueue = new LinkedList(); @@ -39,7 +42,7 @@ public class BreadthFirstIterator implements Iterator { @Override public GTreeNode next() { lastNode = nodeQueue.poll(); - if (lastNode != null) { + if (lastNode != null && lastNode.isAutoExpandPermitted()) { List children = lastNode.getChildren(); nodeQueue.addAll(children); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/DepthFirstIterator.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/DepthFirstIterator.java index a23f13ae29..4aa992beda 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/DepthFirstIterator.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/DepthFirstIterator.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,6 +25,9 @@ import docking.widgets.tree.GTreeNode; /** * Implements an iterator over all GTreeNodes in some gTree (or subtree). The nodes are * return in depth first order. + *
    + * NOTE: Iterator will not include children of a node where {@link GTreeNode#isAutoExpandPermitted()} + * returns false. */ public class DepthFirstIterator implements Iterator { private Stack> stack = new Stack<>(); @@ -49,7 +52,7 @@ public class DepthFirstIterator implements Iterator { it = stack.pop(); } lastNode = it.next(); - if (lastNode.getChildCount() > 0) { + if (lastNode.isAutoExpandPermitted() && lastNode.getChildCount() > 0) { if (it.hasNext()) { stack.push(it); } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java index 819f819860..711bee3bb1 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java @@ -81,6 +81,7 @@ public class RepositoryAdapter implements RemoteAdapterListener { /** * Returns true if connection recently was lost unexpectedly + * @return true if connection recently was lost unexpectedly */ public boolean hadUnexpectedDisconnect() { return unexpectedDisconnect; @@ -151,6 +152,7 @@ public class RepositoryAdapter implements RemoteAdapterListener { /** * Returns true if connected. + * @return true if connected. */ public boolean isConnected() { return repository != null; @@ -177,7 +179,7 @@ public class RepositoryAdapter implements RemoteAdapterListener { if (repository == null) { serverAdapter.connect(); // may cause auto-reconnect of repository } - if (repository == null) { + if (repository == null && serverAdapter.isConnected()) { repository = serverAdapter.getRepositoryHandle(name); unexpectedDisconnect = false; if (repository == null) { @@ -193,8 +195,7 @@ public class RepositoryAdapter implements RemoteAdapterListener { /** * Event reader for change dispatcher. - * @return - * @throws IOException + * @return events * @throws InterruptedIOException if repository handle is closed */ RepositoryChangeEvent[] getEvents() throws InterruptedIOException { @@ -227,21 +228,24 @@ public class RepositoryAdapter implements RemoteAdapterListener { } /** - * Returns repository name + * Get the associated repository name + * @return repository name */ public String getName() { return name; } /** - * Returns server adapter + * Get the associated server adapter + * @return server adapter */ public RepositoryServerAdapter getServer() { return serverAdapter; } /** - * Returns server information + * Returns associated server information + * @return server information */ public ServerInfo getServerInfo() { return serverAdapter.getServerInfo(); @@ -280,9 +284,11 @@ public class RepositoryAdapter implements RemoteAdapterListener { } /** - * Returns repository user object. + * Returns repository connected user object. + * @return connected user object * @throws UserAccessException user no longer has any permission to use repository. * @throws NotConnectedException if server/repository connection is down (user already informed) + * @throws IOException if an IO error occurs * @see ghidra.framework.remote.RemoteRepositoryHandle#getUser() */ public User getUser() throws IOException { @@ -305,7 +311,7 @@ public class RepositoryAdapter implements RemoteAdapterListener { /** * @return true if anonymous access allowed by this repository - * @throws IOException + * @throws IOException if an IO error occurs */ public boolean anonymousAccessAllowed() throws IOException { synchronized (serverAdapter) { @@ -323,10 +329,11 @@ public class RepositoryAdapter implements RemoteAdapterListener { } /** - * Returns list of repository users. - * @throws IOException + * Returns list of repository users with repository access permission + * @return return users with repository access permission * @throws UserAccessException user no longer has any permission to use repository. * @throws NotConnectedException if server/repository connection is down (user already informed) + * @throws IOException if an IO error occurs * @see RemoteRepositoryHandle#getUserList() */ public User[] getUserList() throws IOException { @@ -345,10 +352,11 @@ public class RepositoryAdapter implements RemoteAdapterListener { } /** - * Returns list of all users known to server. - * @throws IOException + * Returns list of all user names known to server. + * @return list of all user names known to server. * @throws UserAccessException user no longer has any permission to use repository. * @throws NotConnectedException if server/repository connection is down (user already informed) + * @throws IOException if an IO error occurs * @see RemoteRepositoryHandle#getServerUserList() */ public String[] getServerUserList() throws IOException { @@ -371,8 +379,8 @@ public class RepositoryAdapter implements RemoteAdapterListener { * @param users list of user and access permissions. * @param anonymousAccessAllowed true to permit anonymous access (also requires anonymous * access to be enabled for server) - * @throws UserAccessException - * @throws IOException + * @throws UserAccessException user is not a repository Admin + * @throws IOException if an IO error occurs * @throws NotConnectedException if server/repository connection is down (user already informed) * @see RemoteRepositoryHandle#setUserList(User[], boolean) */ @@ -392,7 +400,36 @@ public class RepositoryAdapter implements RemoteAdapterListener { } } - /** + /* + * @see RepositoryHandle#createTextDataFile(String, String, String, String, String, String) + */ + public void createTextDataFile(String parentPath, String itemName, String fileID, + String contentType, String textData, String comment) + throws IOException, InvalidNameException { + synchronized (serverAdapter) { + checkRepository(); + try { + repository.createTextDataFile(parentPath, itemName, fileID, contentType, textData, + comment); + } + catch (NotConnectedException | RemoteException e) { + checkUnmarshalException(e, "createTextDataFile"); + if (recoverConnection(e)) { + try { + repository.createTextDataFile(parentPath, itemName, fileID, contentType, + textData, comment); + } + catch (RemoteException e1) { + checkUnmarshalException(e1, "createTextDataFile"); + throw e1; + } + } + throw e; + } + } + } + + /* * @see RepositoryHandle#createDatabase(String, String, String, int, String, String) */ public ManagedBufferFileAdapter createDatabase(String parentPath, String itemName, @@ -530,8 +567,8 @@ public class RepositoryAdapter implements RemoteAdapterListener { /** * Convert UnmarshalException into UnsupportedOperationException - * @param e - * @throws UnsupportedOperationException + * @param e IOException to be converted if appropriate + * @throws UnsupportedOperationException unsupported operation exception */ private void checkUnmarshalException(IOException e, String operation) throws UnsupportedOperationException { @@ -931,5 +968,4 @@ public class RepositoryAdapter implements RemoteAdapterListener { public int getOpenFileHandleCount() { return openFileHandleCount; } - } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java index c14e328eb8..b367cb3028 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java @@ -168,13 +168,14 @@ class ServerConnectTask extends Task { monitor.setCancelEnabled(false); monitor.setMessage("Connecting..."); - Registry reg = - LocateRegistry.getRegistry(server.getServerName(), server.getPortNumber(), - new SslRMIClientSocketFactory()); + Registry reg = LocateRegistry.getRegistry(server.getServerName(), + server.getPortNumber(), new SslRMIClientSocketFactory()); checkServerBindNames(reg); gsh = (GhidraServerHandle) reg.lookup(GhidraServerHandle.BIND_NAME); - gsh.checkCompatibility(GhidraServerHandle.INTERFACE_VERSION); + + // Check interface compatibility with the minimum supported version + gsh.checkCompatibility(GhidraServerHandle.MINIMUM_INTERFACE_VERSION); } catch (NotBoundException e) { throw new IOException(e.getMessage()); @@ -237,8 +238,7 @@ class ServerConnectTask extends Task { * @throws LoginException login failure */ private RemoteRepositoryServerHandle getRepositoryServerHandle(String defaultUserID, - TaskMonitor monitor) - throws IOException, LoginException, CancelledException { + TaskMonitor monitor) throws IOException, LoginException, CancelledException { GhidraServerHandle gsh = getGhidraServerHandle(server, monitor); @@ -296,7 +296,8 @@ class ServerConnectTask extends Task { "Client PKI certificate has not been installed"); } - if (ApplicationKeyManagerFactory.usingGeneratedSelfSignedCertificate()) { + if (ApplicationKeyManagerFactory + .usingGeneratedSelfSignedCertificate()) { Msg.warn(this, "Server connect - client is using self-signed PKI certificate"); } @@ -394,7 +395,7 @@ class ServerConnectTask extends Task { monitor.setCancelEnabled(true); monitor.setMessage("Checking Server Liveness..."); - + // Perform simple socket test connection with short timeout to verify connectivity. try (Socket socket = new FastConnectionFailSocket(serverName, sslRmiPort); ConnectCancelledListener cancelListener = diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java index 13277e4885..6fcc450366 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -50,8 +50,20 @@ public interface GhidraServerHandle extends Remote { * - version 9.1 switched to using SSL/TLS for RMI registry connection preventing * older clients the ability to connect to the server. Remote interface remained * unchanged allowing 9.1 clients to connect to 9.0 server. + * 12: Revised RepositoryFile serialization to facilitate support for text-data used + * for link-file storage. */ - public static final int INTERFACE_VERSION = 11; + + /** + * The server interface version that the server will use and is the maximum version that the + * client can operate with. + */ + public static final int INTERFACE_VERSION = 12; + + /** + * The minimum server interface version that the client can operate with. + */ + public static final int MINIMUM_INTERFACE_VERSION = 11; /** * Minimum version of Ghidra which utilized the current INTERFACE_VERSION diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java index 661f64513d..6a291821f5 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -70,6 +70,10 @@ public interface RemoteRepositoryHandle extends RepositoryHandle, Remote { int bufferSize, String contentType, String projectPath) throws IOException, InvalidNameException; + @Override + void createTextDataFile(String parentPath, String itemName, String fileID, String contentType, + String textData, String comment) throws InvalidNameException, IOException; + @Override ManagedBufferFileHandle openDatabase(String parentPath, String itemName, int version, int minChangeDataVer) throws IOException; diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java index f317923ae2..2240b2c8b6 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryHandle.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -123,6 +123,21 @@ public interface RepositoryHandle { */ RepositoryItem getItem(String fileID) throws IOException; + /** + * Creates a new text data file within the specified parent folder. + * @param parentPath folder path of parent + * @param itemName new data file name + * @param fileID unique file ID + * @param contentType application defined content type + * @param textData text data (required) + * @param comment file comment (may be null) + * @throws DuplicateFileException Thrown if a folderItem with that name already exists. + * @throws InvalidNameException if the name has illegal characters. + * @throws IOException if an IO error occurs. + */ + void createTextDataFile(String parentPath, String itemName, String fileID, String contentType, + String textData, String comment) throws InvalidNameException, IOException; + /** * Create a new empty database item within the repository. * @param parentPath parent folder path @@ -138,8 +153,8 @@ public interface RepositoryHandle { * @throws InvalidNameException if itemName or parentPath contains invalid characters */ ManagedBufferFileHandle createDatabase(String parentPath, String itemName, String fileID, - int bufferSize, String contentType, String projectPath) throws IOException, - InvalidNameException; + int bufferSize, String contentType, String projectPath) + throws IOException, InvalidNameException; /** * Open an existing version of a database buffer file for non-update read-only use. @@ -212,8 +227,8 @@ public interface RepositoryHandle { * @throws DuplicateFileException if target item already exists * @throws IOException if an IO error occurs */ - void moveItem(String oldParentPath, String newParentPath, String oldItemName, String newItemName) - throws InvalidNameException, IOException; + void moveItem(String oldParentPath, String newParentPath, String oldItemName, + String newItemName) throws InvalidNameException, IOException; /** * Perform a checkout on the specified item. diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryItem.java index 830a8910b1..f62055f765 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryItem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RepositoryItem.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,9 +15,12 @@ */ package ghidra.framework.remote; -import ghidra.framework.store.FileSystem; - import java.io.IOException; +import java.io.InvalidClassException; + +import org.apache.commons.lang3.StringUtils; + +import ghidra.framework.store.FileSystem; /** * RepositoryItemStatus provides status information for a @@ -25,18 +28,36 @@ import java.io.IOException; */ public class RepositoryItem implements java.io.Serializable { + // Serial version 2 supports an expandable schema which allows a newer repository server + // to remain usable by older clients, and a newer client to deserialize data from an older + // server. The optional schema version if present can be used to identify the additional + // serialized data which may following the schema version number. + public final static long serialVersionUID = 2L; - public final static int FILE = 1; - public final static int DATABASE = 2; + private static final byte SERIALIZATION_SCHEMA_VERSION = 1; - protected String folderPath; - protected String itemName; - protected String fileID; - protected int itemType; - protected String contentType; - protected int version; - protected long versionTime; + public final static int FILE = 1; // DataFileItem (not yet supported) + public final static int DATABASE = 2; // DatabaseItem + public final static int TEXT_DATA_FILE = 3; // TextDataItem + + // + // Client use can support reading from older server which presents serialVersionUID==2 + // + + private String folderPath; + private String itemName; + private String fileID; + private int itemType; + private String contentType; + private int version; + private long versionTime; + + // Variables below were added after serialVersionUID == 2 was established and rely on + // additional serialization version byte to identify the optional data fields added + // after original serialVersionUID == 2 fields. + + private String textData; // applies to TEXT_DATA_FILE introduced with GhidraServerHandle v12 /** * Default constructor needed for de-serialization @@ -53,9 +74,10 @@ public class RepositoryItem implements java.io.Serializable { * @param contentType content type associated with item * @param version repository item version or -1 if versioning not supported * @param versionTime version creation time + * @param textData related text data (may be null) */ public RepositoryItem(String folderPath, String itemName, String fileID, int itemType, - String contentType, int version, long versionTime) { + String contentType, int version, long versionTime, String textData) { this.folderPath = folderPath; this.itemName = itemName; this.fileID = fileID; @@ -63,6 +85,7 @@ public class RepositoryItem implements java.io.Serializable { this.contentType = contentType; this.version = version; this.versionTime = versionTime; + this.textData = textData; } /** @@ -71,6 +94,7 @@ public class RepositoryItem implements java.io.Serializable { * @throws IOException if an IO error occurs */ private void writeObject(java.io.ObjectOutputStream out) throws IOException { + out.writeLong(serialVersionUID); out.writeUTF(folderPath); out.writeUTF(itemName); @@ -79,6 +103,12 @@ public class RepositoryItem implements java.io.Serializable { out.writeUTF(contentType != null ? contentType : ""); out.writeInt(version); out.writeLong(versionTime); + + // Variables below were added after serialVersionUID == 2 was established + + out.writeByte(SERIALIZATION_SCHEMA_VERSION); + out.writeUTF(textData != null ? textData : ""); + } /** @@ -87,11 +117,11 @@ public class RepositoryItem implements java.io.Serializable { * @throws IOException if IO error occurs * @throws ClassNotFoundException if unrecognized serialVersionUID detected */ - private void readObject(java.io.ObjectInputStream in) throws IOException, - ClassNotFoundException { + private void readObject(java.io.ObjectInputStream in) + throws IOException, ClassNotFoundException { long serialVersion = in.readLong(); if (serialVersion != serialVersionUID) { - throw new ClassNotFoundException("Unsupported version of RepositoryItemStatus"); + throw new ClassNotFoundException("Unsupported version of RepositoryItem"); } folderPath = in.readUTF(); itemName = in.readUTF(); @@ -106,6 +136,31 @@ public class RepositoryItem implements java.io.Serializable { } version = in.readInt(); versionTime = in.readLong(); + + // Variable handling below was added after serialVersionUID == 2 was established + + int available = in.available(); + if (available == 0) { + // assume original schema before serializationSchemaVersion was employed + return; + } + + // Since we do not serialize class implementations with RMI the older client must be able to + // read the initial data sequence that was previously supported. Newer clients that have this + // class will use the presence of the version byte to handle communicating with either an + // older server (no version byte) or a newer server (version byte and subsequent data is read) + byte serializationSchemaVersion = in.readByte(); + if (serializationSchemaVersion < 1 || + serializationSchemaVersion > SERIALIZATION_SCHEMA_VERSION) { + throw new InvalidClassException("RepositoryItem", + "RepositoryItem has incompatible serialization schema version: " + + serializationSchemaVersion); + } + + textData = in.readUTF(); + if (StringUtils.isBlank(textData)) { + textData = null; + } } /** @@ -162,4 +217,11 @@ public class RepositoryItem implements java.io.Serializable { return versionTime; } + /** + * Get related text data + * @return text data or null + */ + public String getTextData() { + return textData; + } } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/DataFileItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/DataFileItem.java index 96346548a3..54c8d3f6f6 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/DataFileItem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/DataFileItem.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,19 +23,19 @@ import java.io.OutputStream; * DataFileItem corresponds to a private serialized * data file within a FileSystem. Methods are provided for opening * the underlying file as an input or output stream. - *
    + *

    * NOTE: The use of DataFile is not encouraged and is not fully * supported. */ public interface DataFileItem extends FolderItem { - + /** * Open the current version of this item for reading. * @return input stream * @throws FileNotFoundException */ InputStream getInputStream() throws FileNotFoundException; - + /** * Open a new version of this item for writing. * @return output stream. @@ -50,5 +49,5 @@ public interface DataFileItem extends FolderItem { * @throws FileNotFoundException */ InputStream getInputStream(int version) throws FileNotFoundException; - + } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystem.java index 6c67fc96c2..5345dae893 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FileSystem.java @@ -16,10 +16,13 @@ package ghidra.framework.store; import java.io.*; +import java.util.ArrayList; +import java.util.NoSuchElementException; import db.buffers.BufferFile; import db.buffers.ManagedBufferFile; -import ghidra.framework.store.local.UnknownFolderItem; +import ghidra.framework.store.local.LocalFileSystem; +import ghidra.framework.store.remote.RemoteFileSystem; import ghidra.util.InvalidNameException; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; @@ -40,38 +43,38 @@ public interface FileSystem { * Get user name associated with this filesystem. In the case of a remote filesystem * this will correspond to the name used during login/authentication. A null value may * be returned if user name unknown. + * @return user name used to authenticate or null if not-applicable */ - String getUserName(); + public String getUserName(); /** - * Returns true if the file-system requires check-outs when - * modifying folder items. + * {@return true if the file-system requires check-outs when + * modifying folder items.} */ public boolean isVersioned(); /** - * Returns true if file-system is on-line. + * {@return true if file-system is on-line.} */ public boolean isOnline(); /** - * Returns true if file-system is read-only. - * @throws IOException + * {@return true if file-system is read-only.} + * @throws IOException if IO error occurs */ public boolean isReadOnly() throws IOException; /** - * Returns the number of folder items contained within this file-system. - * @throws IOException + * {@return the number of folder items contained within this file-system.} + * @throws IOException if an IO error occurs * @throws UnsupportedOperationException if file-system does not support this operation */ public int getItemCount() throws IOException, UnsupportedOperationException; /** - * Returns a list of the folder item names contained in the given folder. + * {@return a list of the folder item names contained in the given folder.} * @param folderPath the path of the folder. - * @return a list of folder item names. - * @throws IOException + * @throws IOException if an IO error occurs */ public String[] getItemNames(String folderPath) throws IOException; @@ -81,7 +84,7 @@ public interface FileSystem { * @return a list of folder items. Null items may exist if index contained item name * while storage was not found. An {@link UnknownFolderItem} may be returned if unsupported * item storage encountered. - * @throws IOException + * @throws IOException if an IO error occurs */ public FolderItem[] getItems(String folderPath) throws IOException; @@ -105,6 +108,8 @@ public interface FileSystem { /** * Return a list of subfolders (by name) that are stored within the specified folder path. + * @param folderPath folder path + * @return subfolders names * @throws FileNotFoundException if folder path does not exist. * @throws IOException if IO error occurs. */ @@ -122,6 +127,16 @@ public interface FileSystem { public void createFolder(String parentPath, String folderName) throws InvalidNameException, IOException; + /** + * Determine if the specified folder item is supported by this filesystem's interface and + * storage. This method primarily exists to determine if a remote server can support + * the specified content. This can come into play as new storage formats are added + * to a {@link LocalFileSystem} but may not be supported by a connected {@link RemoteFileSystem}. + * @param folderItem folder item + * @return true if folder item storage is supported + */ + public boolean isSupportedItemType(FolderItem folderItem); + /** * Create a new database item within the specified parent folder using the contents * of the specified BufferFile. @@ -162,8 +177,7 @@ public interface FileSystem { * @return an empty BufferFile open for read-write. * @throws FileNotFoundException thrown if parent folder does not exist. * @throws DuplicateFileException if a folder item exists with this name - * @throws InvalidNameException if the name does not have - * all alphanumerics + * @throws InvalidNameException if the name has illegal characters. * @throws IOException if an IO error occurs. */ public ManagedBufferFile createDatabase(String parentPath, String name, String fileID, @@ -182,7 +196,6 @@ public interface FileSystem { * @return new data file * @throws DuplicateFileException Thrown if a folderItem with that name already exists. * @throws InvalidNameException if the name has illegal characters. - * all alphanumerics * @throws IOException if an IO error occurs. * @throws CancelledException if cancelled by monitor */ @@ -190,6 +203,23 @@ public interface FileSystem { String comment, String contentType, TaskMonitor monitor) throws InvalidNameException, IOException, CancelledException; + /** + * Creates a new text data file within the specified parent folder. + * @param parentPath folder path of parent + * @param name new data file name + * @param fileID file ID to be associated with new file or null + * @param contentType application defined content type + * @param textData text data (required) + * @param comment file comment (may be null, only used if versioning is enabled) + * @return new data file + * @throws DuplicateFileException Thrown if a folderItem with that name already exists. + * @throws InvalidNameException if the name has illegal characters. + * @throws IOException if an IO error occurs. + */ + public TextDataItem createTextDataItem(String parentPath, String name, String fileID, + String contentType, String textData, String comment) + throws InvalidNameException, IOException; + /** * Creates a new file item from a packed file. * The content/item type must be determined from the input stream. @@ -252,7 +282,8 @@ public interface FileSystem { * Moves the specified item to a new folder. * @param folderPath path of folder containing the item. * @param name name of the item to be moved. - * @param newFolderPath path of folder where item is to be moved. + * @param newFolderPath path of folder where item is to be moved to. + * @param newName new item name to be applied * @throws FileNotFoundException if the item does not exist. * @throws DuplicateFileException if item with the same name exists within the new parent folder. * @throws FileInUseException if the item is in-use or checked-out @@ -263,14 +294,14 @@ public interface FileSystem { throws IOException, InvalidNameException; /** - * Adds the given listener to be notified of file system changes. + * Adds a file system listener to be notified of file system changes. * @param listener the listener to be added. */ public void addFileSystemListener(FileSystemListener listener); /** - * Removes the listener from being notified of file system changes. - * @param listener + * Removes a file system listener from being notified of file system changes. + * @param listener file system listener */ public void removeFileSystemListener(FileSystemListener listener); @@ -283,7 +314,7 @@ public interface FileSystem { public boolean folderExists(String folderPath) throws IOException; /** - * Returns true if the file exists + * {@return true if the file exists} * @param folderPath the folderPath of the folder that may contain the file. * @param name the name of the file to check for existence. * @throws IOException if an IO error occurs. @@ -291,7 +322,7 @@ public interface FileSystem { public boolean fileExists(String folderPath, String name) throws IOException; /** - * Returns true if this file system is shared + * {@return true if this file system is shared} */ public boolean isShared(); @@ -300,4 +331,58 @@ public interface FileSystem { */ public void dispose(); + /** + * Normalize an absolute path, removing all "." and ".." use. + *

    + * NOTE: This method does not consider possible linked folder traversal which may + * get ignored when flattening/simplifying path. + * + * @param path absolute filesystem path which may contain "." or ".." path elements. + * @return normalized path + * @throws IllegalArgumentException if an absolute path starting with {@link #SEPARATOR} + * was not specified or an illegal path was specified. + */ + public static String normalizePath(String path) throws IllegalArgumentException { + if (!path.startsWith(SEPARATOR)) { + throw new IllegalArgumentException("Absolute path required"); + } + + String[] split = path.split(SEPARATOR); + + ArrayList elements = new ArrayList<>(); + for (int i = 1; i < split.length; i++) { + String e = split[i]; + if (e.length() == 0) { + throw new IllegalArgumentException("Invalid path with empty element: " + path); + } + if ("..".equals(e)) { + try { + // remove last element + elements.removeLast(); + } + catch (NoSuchElementException ex) { + throw new IllegalArgumentException("Invalid path: " + path); + } + } + else if (".".equals(e)) { + // ignore element + continue; + } + else { + elements.add(e); + } + } + + if (elements.isEmpty()) { + return SEPARATOR; + } + + StringBuilder buf = new StringBuilder(); + for (String e : elements) { + buf.append(SEPARATOR); + buf.append(e); + } + return buf.toString(); + } + } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FolderItem.java index 80915343b8..1aa48dee02 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FolderItem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/FolderItem.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -43,6 +43,11 @@ public interface FolderItem { */ public static final int DATAFILE_FILE_TYPE = 1; + /** + * Item type is associated with metadata only (e.g., URL) + */ + public static final int LINK_FILE_TYPE = 2; + /** * Default checkout ID used when a checkout is not applicable. */ diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/TextDataItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/TextDataItem.java new file mode 100644 index 0000000000..33a667975c --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/TextDataItem.java @@ -0,0 +1,30 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.store; + +/** + * TextDataItem corresponds to a file which contains text data only + * and relies only on property file storage (i.e., no separate database or data file). + */ +public interface TextDataItem extends FolderItem { + + /** + * Get the text data that was stored with this item + * @return text data + */ + public String getTextData(); + +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/UnknownFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/UnknownFolderItem.java new file mode 100644 index 0000000000..792ca0d379 --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/UnknownFolderItem.java @@ -0,0 +1,37 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.store; + +/** + * UnknownFolderItem corresponds to a folder item which has an unknown storage type + * or has encountered a storage failure. + */ +public interface UnknownFolderItem extends FolderItem { + + public static final String UNKNOWN_CONTENT_TYPE = "Unknown-File"; + + /** + * Get the file type: + *

      + *
    • {@link FolderItem#DATABASE_FILE_TYPE}
    • + *
    • {@link FolderItem#DATAFILE_FILE_TYPE}
    • + *
    • {@link FolderItem#LINK_FILE_TYPE}
    • + *
    + * @return file type or {@link FolderItem#UNKNOWN_FILE_TYPE} (-1) if unknown + */ + public int getFileType(); + +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/db/PackedDatabase.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/db/PackedDatabase.java index 3b53054de3..8731c57840 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/db/PackedDatabase.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/db/PackedDatabase.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -39,7 +39,7 @@ import utilities.util.FileUtilities; /** * PackedDatabase provides a packed form of Database * which compresses a single version into a file. - *
    + *

    * When opening a packed database, a PackedDBHandle is returned * after first expanding the file into a temporary Database. */ @@ -114,7 +114,7 @@ public class PackedDatabase extends Database { * @throws CancelledException is unpack is cancelled * @throws IOException if IO error occurs */ - PackedDatabase(CachedDB cachedDb, ResourceFile packedDbFile, LockFile packedDbLock, + PackedDatabase(CachedDB cachedDb, ResourceFile packedDbFile, LockFile packedDbLock, TaskMonitor monitor) throws CancelledException, IOException { super(cachedDb.dbDir, null, false); this.packedDbFile = packedDbFile; @@ -276,8 +276,8 @@ public class PackedDatabase extends Database { * @throws IOException if IO error occurs * @throws CancelledException if unpack/open is cancelled */ - public static synchronized PackedDatabase getPackedDatabase(ResourceFile packedDbFile, boolean neverCache, - TaskMonitor monitor) throws IOException, CancelledException { + public static synchronized PackedDatabase getPackedDatabase(ResourceFile packedDbFile, + boolean neverCache, TaskMonitor monitor) throws IOException, CancelledException { if (!neverCache && PackedDatabaseCache.isEnabled()) { try { return PackedDatabaseCache.getCache().getCachedDB(packedDbFile, monitor); @@ -633,7 +633,7 @@ public class PackedDatabase extends Database { tmpFile = Application.createTempFile("pack", ".tmp"); tmpFile.delete(); dbh.saveAs(tmpFile, false, monitor); - try (InputStream itemIn = new BufferedInputStream(new FileInputStream(tmpFile))){ + try (InputStream itemIn = new BufferedInputStream(new FileInputStream(tmpFile))) { ItemSerializer.outputItem(itemName, contentType, FolderItem.DATABASE_FILE_TYPE, tmpFile.length(), itemIn, outputFile, monitor); } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java index 40a28987ea..3533d45866 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedLocalFileSystem.java @@ -598,7 +598,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem { } } - private boolean addFileToIndex(PropertyFile pfile) throws IOException, NotFoundException { + private boolean addFileToIndex(ItemPropertyFile pfile) throws IOException, NotFoundException { String parentPath = pfile.getParentPath(); String name = pfile.getName(); @@ -832,7 +832,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem { catch (NotFoundException e) { // ignore - handled below } - throw new FileNotFoundException("Item not found: " + folderPath + SEPARATOR + itemName); + throw new FileNotFoundException("Item not found: " + getPath(folderPath, itemName)); } /** @@ -1207,7 +1207,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem { String newFolderPath = folder.getPathname(); for (Item item : folder.items.values()) { ItemStorage itemStorage = item.itemStorage; - PropertyFile pfile = item.itemStorage.getPropertyFile(); + ItemPropertyFile pfile = item.itemStorage.getPropertyFile(); pfile.moveTo(itemStorage.dir, itemStorage.storageName, newFolderPath, itemStorage.itemName); itemStorage.folderPath = newFolderPath; @@ -1236,7 +1236,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem { folder = getFolder(folderPath, GetFolderOption.READ_ONLY); if (folder.parent.folders.get(newFolderName) != null) { throw new DuplicateFileException( - parentPath + SEPARATOR + newFolderName + " already exists."); + getPath(parentPath, newFolderName) + " already exists."); } indexJournal.moveFolder(folderPath, getPath(parentPath, newFolderName)); @@ -1462,7 +1462,6 @@ public class IndexedLocalFileSystem extends LocalFileSystem { } private void replayJournal() throws IndexReadException { - Msg.info(this, "restoring data storage index..."); int lineNum = 0; BufferedReader journalReader = null; try { @@ -1778,7 +1777,7 @@ public class IndexedLocalFileSystem extends LocalFileSystem { } @Override - PropertyFile getPropertyFile() throws IOException { + ItemPropertyFile getPropertyFile() throws IOException { return new IndexedPropertyFile(dir, storageName, folderPath, itemName); } } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedPropertyFile.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedPropertyFile.java index 031b882fde..defd60d7cf 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedPropertyFile.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedPropertyFile.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,59 +15,68 @@ */ package ghidra.framework.store.local; -import ghidra.framework.store.FileSystem; -import ghidra.util.PropertyFile; -import ghidra.util.exception.DuplicateFileException; - import java.io.*; -public class IndexedPropertyFile extends PropertyFile { +import ghidra.util.exception.DuplicateFileException; - public final static String NAME_PROPERTY = "NAME"; - public final static String PARENT_PATH_PROPERTY = "PARENT"; +public class IndexedPropertyFile extends ItemPropertyFile { + + protected static final String NAME_PROPERTY = "NAME"; + protected static final String PARENT_PATH_PROPERTY = "PARENT"; /** * Construct a new or existing PropertyFile. - * This form ignores retained property values for NAME and PARENT path. + * This constructor ignores retained property values for NAME and PARENT path. + * This constructor will not throw an exception if the file does not exist. * @param dir parent directory * @param storageName stored property file name (without extension) * @param parentPath path to parent * @param name name of the property file - * @throws IOException + * @throws InvalidObjectException if a file parse error occurs + * @throws IOException if an IO error occurs reading an existing file */ public IndexedPropertyFile(File dir, String storageName, String parentPath, String name) throws IOException { super(dir, storageName, parentPath, name); -// if (exists() && -// (!name.equals(getString(NAME_PROPERTY, null)) || !parentPath.equals(getString( -// PARENT_PATH_PROPERTY, null)))) { -// throw new AssertException(); -// } - putString(NAME_PROPERTY, name); - putString(PARENT_PATH_PROPERTY, parentPath); + if (contains(NAME_PROPERTY) && contains(PARENT_PATH_PROPERTY)) { + this.name = getString(NAME_PROPERTY, name); + this.parentPath = getString(PARENT_PATH_PROPERTY, parentPath); + } + else { + // new property file + putString(NAME_PROPERTY, name); + putString(PARENT_PATH_PROPERTY, parentPath); + } } /** - * Construct an existing PropertyFile. + * Construct a existing PropertyFile. + * This constructor uses property values for NAME and PARENT path. * @param dir parent directory * @param storageName stored property file name (without extension) * @throws FileNotFoundException if property file does not exist + * @throws InvalidObjectException if a file parse error occurs * @throws IOException if error occurs reading property file */ public IndexedPropertyFile(File dir, String storageName) throws IOException { - super(dir, storageName, FileSystem.SEPARATOR, storageName); + super(dir, storageName, null, null); if (!exists()) { - throw new FileNotFoundException(); + throw new FileNotFoundException( + new File(dir, storageName + PROPERTY_EXT) + " not found"); } + name = getString(NAME_PROPERTY, null); + parentPath = getString(PARENT_PATH_PROPERTY, null); if (name == null || parentPath == null) { throw new IOException("Invalid indexed property file: " + propertyFile); } } /** - * Construct an existing PropertyFile. - * @param file + * Construct a existing PropertyFile. + * This constructor uses property values for NAME and PARENT path. + * @param file property file * @throws FileNotFoundException if property file does not exist + * @throws InvalidObjectException if a file parse error occurs * @throws IOException if error occurs reading property file */ public IndexedPropertyFile(File file) throws IOException { @@ -82,27 +90,17 @@ public class IndexedPropertyFile extends PropertyFile { return propertyFileName.substring(0, propertyFileName.length() - PROPERTY_EXT.length()); } - @Override - public void readState() throws IOException { - super.readState(); - name = getString(NAME_PROPERTY, null); - parentPath = getString(PARENT_PATH_PROPERTY, null); - } - @Override public void moveTo(File newParent, String newStorageName, String newParentPath, String newName) throws DuplicateFileException, IOException { - + String oldName = name; + String oldParentPath = parentPath; super.moveTo(newParent, newStorageName, newParentPath, newName); -// if (!parentPath.equals(newParentPath)) { -// throw new AssertException(); -// } -// if (!name.equals(newName)) { -// throw new AssertException(); -// } - putString(NAME_PROPERTY, newName); - putString(PARENT_PATH_PROPERTY, newParentPath); - writeState(); + if (!newParentPath.equals(oldParentPath) || !newName.equals(oldName)) { + putString(NAME_PROPERTY, name); + putString(PARENT_PATH_PROPERTY, parentPath); + writeState(); + } } } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java index 1647da5dbc..125f690ae4 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/IndexedV1LocalFileSystem.java @@ -19,7 +19,6 @@ import java.io.*; import java.util.HashMap; import ghidra.util.Msg; -import ghidra.util.PropertyFile; import ghidra.util.exception.NotFoundException; /** @@ -94,7 +93,7 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem { } @Override - protected synchronized void fileIdChanged(PropertyFile pfile, String oldFileId) + protected synchronized void fileIdChanged(ItemPropertyFile pfile, String oldFileId) throws IOException { indexJournal.open(); try { @@ -143,12 +142,19 @@ public class IndexedV1LocalFileSystem extends IndexedLocalFileSystem { if (item == null) { return null; } + ItemStorage itemStorage = item.itemStorage; try { - PropertyFile propertyFile = item.itemStorage.getPropertyFile(); + ItemPropertyFile propertyFile = itemStorage.getPropertyFile(); if (propertyFile.exists()) { return LocalFolderItem.getFolderItem(this, propertyFile); } } + catch (InvalidObjectException e) { + // Use unknown placeholder item on failure + InvalidPropertyFile invalidFile = new InvalidPropertyFile(itemStorage.dir, + itemStorage.storageName, itemStorage.folderPath, itemStorage.itemName); + return new LocalUnknownFolderItem(this, invalidFile); + } catch (FileNotFoundException e) { // ignore } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/InvalidPropertyFile.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/InvalidPropertyFile.java new file mode 100644 index 0000000000..faf24ec7a0 --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/InvalidPropertyFile.java @@ -0,0 +1,47 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.store.local; + +import java.io.File; +import java.io.IOException; + +/** + * {@link InvalidPropertyFile} provides a substitue {@link ItemPropertyFile} when one + * fails to parse. This allows the item's existance to be managed even if the item cannot + * be opened. + */ +public class InvalidPropertyFile extends ItemPropertyFile { + + /** + * Construct an invalid property file instance if it previously failed to parse. + * @param dir native directory where this file is stored + * @param storageName stored property file name (without extension) + * @param parentPath logical parent path for the associated item + * @param name name of the associated item + * @throws IOException (never thrown since file is never read) + */ + public InvalidPropertyFile(File dir, String storageName, String parentPath, String name) + throws IOException { + super(dir, storageName, parentPath, name); + // NOTE: IOException is prevented by having a do-nothing readState method below + } + + @Override + public final void readState() { + // avoid potential parse failure + } + +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/ItemPropertyFile.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/ItemPropertyFile.java new file mode 100644 index 0000000000..f9f5b37491 --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/ItemPropertyFile.java @@ -0,0 +1,145 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.store.local; + +import java.io.*; + +import javax.help.UnsupportedOperationException; + +import ghidra.framework.store.FileSystem; +import ghidra.framework.store.FolderItem; +import ghidra.util.PropertyFile; +import ghidra.util.exception.DuplicateFileException; + +/** + * {@link ItemPropertyFile} provides basic property storage which is primarily intended to + * store limited information related to a logical {@link FolderItem}. The file + * extension used is {@link #PROPERTY_EXT}. + */ +public class ItemPropertyFile extends PropertyFile { + + private static final String FILE_ID_PROPERTY = "FILE_ID"; + + protected String name; + protected String parentPath; + + /** + * Construct a new or existing PropertyFile. + * This constructor ignores retained property values for NAME and PARENT path. + * This constructor will not throw an exception if the file does not exist. + * @param dir native directory where this file is stored + * @param storageName stored property file name (without extension) + * @param parentPath logical parent path for the associated item + * @param name name of the associated item + * @throws InvalidObjectException if a file parse error occurs + * @throws IOException if an IO error occurs reading an existing file + */ + public ItemPropertyFile(File dir, String storageName, String parentPath, String name) + throws IOException { + super(dir, storageName); + this.name = name; + this.parentPath = parentPath; + } + + /** + * Return the name of the item associated with this PropertyFile. A null value may be returned + * if this is an older property file and the name was not specified at + * time of construction. + * @return associated item name or null if unknown + */ + public String getName() { + return name; + } + + /** + * Return the logical path of the item associated with this PropertyFile. A null value may be + * returned if this is an older property file and the name and parentPath was not specified at + * time of construction. + * @return logical path of the associated item or null if unknown + */ + public String getPath() { + if (parentPath == null || name == null) { + return null; + } + if (parentPath.length() == 1) { + return parentPath + name; + } + return parentPath + FileSystem.SEPARATOR_CHAR + name; + } + + /** + * Return the logical parent path containing the item descibed by this PropertyFile. + * @return logical parent directory path + */ + public String getParentPath() { + return parentPath; + } + + /** + * Returns the FileID associated with this file. + * @return FileID associated with this file or null + */ + public String getFileID() { + return getString(FILE_ID_PROPERTY, null); + } + + /** + * Set the FileID associated with this file. + * @param fileId unique file ID + */ + public void setFileID(String fileId) { + putString(FILE_ID_PROPERTY, fileId); + } + + /** + * Move this PropertyFile to the newParent file. + * @param newStorageParent new storage parent of the native file + * @param newStorageName new storage name for this property file + * @param newParentPath new logical parent path + * @param newName new logical item name + * @throws IOException thrown if there was a problem accessing the + * @throws DuplicateFileException thrown if a file with the newName + * already exists + */ + public void moveTo(File newStorageParent, String newStorageName, String newParentPath, + String newName) throws DuplicateFileException, IOException { + super.moveTo(newStorageParent, newStorageName); + if (!newParentPath.equals(parentPath) || !newName.equals(name)) { + parentPath = newParentPath; + name = newName; + } + } + + /** + * NOTE!! This method must not be used. + *

    + * Movement of an item is related to its logical pathname and must be accomplished + * with the {@link #moveTo(File, String, String, String)} method. There is no supported + * direct use of this method. + * + * @param newStorageParent new storage parent of the native file + * @param newStorageName new storage name for this property file + * @throws UnsupportedOperationException always thrown + * @deprecated method must not be used + */ + @Deprecated(forRemoval = false, since = "11.4") + @Override + public final void moveTo(File newStorageParent, String newStorageName) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFile.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFileItem.java similarity index 83% rename from Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFile.java rename to Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFileItem.java index 7778c9f8b2..c7826dcdd5 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFile.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDataFileItem.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,27 +17,38 @@ package ghidra.framework.store.local; import ghidra.framework.store.DataFileItem; import ghidra.framework.store.FolderItem; -import ghidra.util.PropertyFile; import ghidra.util.exception.CancelledException; import ghidra.util.exception.DuplicateFileException; import ghidra.util.task.TaskMonitor; import java.io.*; +import org.apache.commons.lang3.StringUtils; + /** - * LocalDataFile provides a FolderItem implementation + * LocalDataFileItem provides a FolderItem implementation * for a local serialized data file. This implementation supports * a non-versioned file-system only. *

    * This item utilizes a data directory for storing the serialized * data file. + *

    + * NOTE: The use of this file item type is not fully supported. */ -public class LocalDataFile extends LocalFolderItem implements DataFileItem { +public class LocalDataFileItem extends LocalFolderItem implements DataFileItem { private final static int IO_BUFFER_SIZE = 32 * 1024; private static final String DATA_FILE = "data.1.gdf"; - public LocalDataFile(LocalFileSystem fileSystem, PropertyFile propertyFile) throws IOException { + /** + * Constructor for an existing local serialized=data file item which corresponds to the specified + * property file. + * @param fileSystem file system + * @param propertyFile database property file + * @throws IOException if an IO Error occurs + */ + public LocalDataFileItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile) + throws IOException { super(fileSystem, propertyFile, true, false); if (fileSystem.isVersioned()) { @@ -50,7 +61,7 @@ public class LocalDataFile extends LocalFolderItem implements DataFileItem { } /** - * Create a new local data file item. + * Create a new local serialized-data file item. * @param fileSystem file system * @param propertyFile serialized data property file * @param istream data source input stream (should be a start of data and will be read to end of file). @@ -61,9 +72,9 @@ public class LocalDataFile extends LocalFolderItem implements DataFileItem { * @throws IOException if an IO Error occurs * @throws CancelledException if monitor cancels operation */ - public LocalDataFile(LocalFileSystem fileSystem, PropertyFile propertyFile, - InputStream istream, String contentType, TaskMonitor monitor) throws IOException, - CancelledException { + public LocalDataFileItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, + InputStream istream, String contentType, TaskMonitor monitor) + throws IOException, CancelledException { super(fileSystem, propertyFile, true, true); if (fileSystem.isVersioned()) { @@ -71,6 +82,11 @@ public class LocalDataFile extends LocalFolderItem implements DataFileItem { throw new UnsupportedOperationException("Versioning not yet supported for DataFiles"); } + if (StringUtils.isBlank(contentType)) { + abortCreate(); + throw new IllegalArgumentException("Missing content-type"); + } + File dataFile = getDataFile(); if (dataFile.exists()) { throw new DuplicateFileException(getName() + " already exists."); diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDatabaseItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDatabaseItem.java index d8c5288347..fcabd092c6 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDatabaseItem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalDatabaseItem.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,10 +18,13 @@ package ghidra.framework.store.local; import java.io.File; import java.io.IOException; +import org.apache.commons.lang3.StringUtils; + import db.buffers.*; import ghidra.framework.store.*; import ghidra.framework.store.db.*; -import ghidra.util.*; +import ghidra.util.Msg; +import ghidra.util.ReadOnlyException; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -49,8 +52,8 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem { * @param create if true the data directory will be created * @throws IOException */ - private LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile, boolean create) - throws IOException { + private LocalDatabaseItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, + boolean create) throws IOException { super(fileSystem, propertyFile, true, create); if (isVersioned) { versionedDbListener = new LocalVersionedDbListener(); @@ -63,7 +66,8 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem { * @param fileSystem file system * @param propertyFile database property file */ - LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile) throws IOException { + LocalDatabaseItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile) + throws IOException { super(fileSystem, propertyFile, true, false); if (isVersioned) { @@ -94,11 +98,16 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem { * @throws IOException if error occurs * @throws CancelledException if database creation cancelled by user */ - LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile, BufferFile srcFile, + LocalDatabaseItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, BufferFile srcFile, String contentType, String fileID, String comment, boolean resetDatabaseId, TaskMonitor monitor, String user) throws IOException, CancelledException { super(fileSystem, propertyFile, true, true); + if (StringUtils.isBlank(contentType)) { + abortCreate(); + throw new IllegalArgumentException("Missing content-type"); + } + boolean success = false; long checkoutId = DEFAULT_CHECKOUT_ID; try { @@ -154,7 +163,7 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem { * @throws IOException if error occurs * @throws CancelledException if database creation cancelled by user */ - LocalDatabaseItem(LocalFileSystem fileSystem, PropertyFile propertyFile, File packedFile, + LocalDatabaseItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, File packedFile, String contentType, TaskMonitor monitor, String user) throws IOException, CancelledException { super(fileSystem, propertyFile, true, true); @@ -222,7 +231,7 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem { * @throws IOException if error occurs */ static LocalManagedBufferFile create(final LocalFileSystem fileSystem, - PropertyFile propertyFile, int bufferSize, String contentType, String fileID, + ItemPropertyFile propertyFile, int bufferSize, String contentType, String fileID, String user, String projectPath) throws IOException { final LocalDatabaseItem dbItem = new LocalDatabaseItem(fileSystem, propertyFile, true); @@ -257,6 +266,7 @@ public class LocalDatabaseItem extends LocalFolderItem implements DatabaseItem { db.setSynchronizationObject(dbItem.fileSystem); dbItem.privateDb = (PrivateDatabase) db; } + dbItem.log("file created", user); dbItem.fireItemCreated(); } } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java index fbce15768b..6c74955c57 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFileSystem.java @@ -82,11 +82,12 @@ public abstract class LocalFileSystem implements FileSystem { /** * Construct a local filesystem for existing data - * @param rootPath - * @param create - * @param isVersioned - * @param readOnly - * @param enableAsyncronousDispatching + * @param rootPath filesystem root directory (the directory must exist and must not have any + * contents if {@code create} is true) + * @param create true if creating new filesystem from the empty directory at rootPath + * @param isVersioned true if creating a versioned filesystem + * @param readOnly true if file system is read-only (ignored if {@code create} is true). + * @param enableAsyncronousDispatching true if async event dispatching should be performed * @return local filesystem * @throws FileNotFoundException if specified rootPath does not exist * @throws IOException if error occurs while reading/writing index files @@ -103,10 +104,6 @@ public abstract class LocalFileSystem implements FileSystem { throw new IOException("new filesystem directory is not empty: " + rootPath); } if (create) { -// if (isCreateMangledFileSystemEnabled()) { -// return new MangledLocalFileSystem(rootPath, isVersioned, readOnly, -// enableAsyncronousDispatching); -// } return new IndexedV1LocalFileSystem(rootPath, isVersioned, readOnly, enableAsyncronousDispatching, true); } @@ -154,7 +151,7 @@ public abstract class LocalFileSystem implements FileSystem { /** * Returns true if any file found within dir whose name starts * with '~' character (e.g., ~index.dat, etc) - * @param dir + * @param dir directory to inspect * @return true if any hidden file found with '~' prefix */ private static boolean hasAnyHiddenFiles(File dir) { @@ -237,7 +234,7 @@ public abstract class LocalFileSystem implements FileSystem { /** * Associate file system with a specific repository logger - * @param repositoryLogger + * @param repositoryLogger repository logger (may be null) */ public void setAssociatedRepositoryLogger(RepositoryLogger repositoryLogger) { this.repositoryLogger = repositoryLogger; @@ -317,8 +314,14 @@ public abstract class LocalFileSystem implements FileSystem { return pfile.exists(); } - PropertyFile getPropertyFile() throws IOException { - return new PropertyFile(dir, storageName, folderPath, itemName); + /** + * Get property file associated with this item storage + * @return property file + * @throws InvalidObjectException if a file parse error occurs + * @throws IOException if an IO error occurs reading an existing file + */ + ItemPropertyFile getPropertyFile() throws IOException { + return new ItemPropertyFile(dir, storageName, folderPath, itemName); } @Override @@ -336,19 +339,19 @@ public abstract class LocalFileSystem implements FileSystem { /** * Find an existing storage location - * @param folderPath - * @param itemName + * @param folderPath folder path of item + * @param itemName item name * @return storage location. A non-null value does not guarantee that the associated * item actually exists. - * @throws FileNotFoundException + * @throws FileNotFoundException if existing storage allocation not found */ protected abstract ItemStorage findItemStorage(String folderPath, String itemName) throws FileNotFoundException; /** * Allocate a new storage location - * @param folderPath - * @param itemName + * @param folderPath folder path of item + * @param itemName item name * @return storage location * @throws DuplicateFileException if item path has previously been allocated * @throws IOException if invalid path/item name specified @@ -359,9 +362,9 @@ public abstract class LocalFileSystem implements FileSystem { /** * Deallocate item storage - * @param folderPath - * @param itemName - * @throws IOException + * @param folderPath folder path of item + * @param itemName item name + * @throws IOException if an IO error occurs */ protected abstract void deallocateItemStorage(String folderPath, String itemName) throws IOException; @@ -376,15 +379,27 @@ public abstract class LocalFileSystem implements FileSystem { @Override public synchronized LocalFolderItem getItem(String folderPath, String name) throws IOException { + ItemStorage itemStorage = null; try { - ItemStorage itemStorage = findItemStorage(folderPath, name); + itemStorage = findItemStorage(folderPath, name); if (itemStorage == null) { return null; } - PropertyFile propertyFile = itemStorage.getPropertyFile(); + ItemPropertyFile propertyFile = itemStorage.getPropertyFile(); if (propertyFile.exists()) { return LocalFolderItem.getFolderItem(this, propertyFile); } + + // force cleanup of bad storage allocation + Msg.warn(this, "Attempting item cleanup due to missing property file: " + + new File(propertyFile.getParentStorageDirectory(), propertyFile.getStorageName())); + itemDeleted(folderPath, name); + } + catch (InvalidObjectException e) { + // Use unknown placeholder item on failure + InvalidPropertyFile invalidFile = new InvalidPropertyFile(itemStorage.dir, + itemStorage.storageName, itemStorage.folderPath, itemStorage.itemName); + return new LocalUnknownFolderItem(this, invalidFile); } catch (FileNotFoundException e) { // ignore @@ -394,11 +409,12 @@ public abstract class LocalFileSystem implements FileSystem { /** * Notification that FileID has been changed within propertyFile - * @param propertyFile - * @param oldFileId - * @throws IOException + * @param propertyFile item property file + * @param oldFileId old FileId + * @throws IOException if an IO error occurs */ - protected void fileIdChanged(PropertyFile propertyFile, String oldFileId) throws IOException { + protected void fileIdChanged(ItemPropertyFile propertyFile, String oldFileId) + throws IOException { // do nothing by default } @@ -418,6 +434,12 @@ public abstract class LocalFileSystem implements FileSystem { return folderItems; } + @Override + public boolean isSupportedItemType(FolderItem folderItem) { + return (folderItem instanceof DatabaseItem) || (folderItem instanceof TextDataItem) || + (folderItem instanceof DataFileItem); + } + @Override public synchronized LocalDatabaseItem createDatabase(String parentPath, String name, String fileID, BufferFile bufferFile, String comment, String contentType, @@ -434,7 +456,7 @@ public abstract class LocalFileSystem implements FileSystem { ItemStorage itemStorage = allocateItemStorage(parentPath, name); LocalDatabaseItem item = null; try { - PropertyFile propertyFile = itemStorage.getPropertyFile(); + ItemPropertyFile propertyFile = itemStorage.getPropertyFile(); item = new LocalDatabaseItem(this, propertyFile, bufferFile, contentType, fileID, comment, resetDatabaseId, monitor, user); } @@ -462,7 +484,7 @@ public abstract class LocalFileSystem implements FileSystem { ItemStorage itemStorage = allocateItemStorage(parentPath, hiddenName); LocalDatabaseItem item = null; try { - PropertyFile propertyFile = itemStorage.getPropertyFile(); + ItemPropertyFile propertyFile = itemStorage.getPropertyFile(); item = new LocalDatabaseItem(this, propertyFile, bufferFile, contentType, fileID, null, resetDatabaseId, monitor, null); } @@ -489,7 +511,7 @@ public abstract class LocalFileSystem implements FileSystem { ItemStorage itemStorage = allocateItemStorage(parentPath, name); LocalManagedBufferFile bufferFile = null; try { - PropertyFile propertyFile = itemStorage.getPropertyFile(); + ItemPropertyFile propertyFile = itemStorage.getPropertyFile(); bufferFile = LocalDatabaseItem.create(this, propertyFile, bufferSize, contentType, fileID, user, projectPath); } @@ -502,7 +524,7 @@ public abstract class LocalFileSystem implements FileSystem { } @Override - public synchronized LocalDataFile createDataFile(String parentPath, String name, + public synchronized LocalDataFileItem createDataFile(String parentPath, String name, InputStream istream, String comment, String contentType, TaskMonitor monitor) throws InvalidNameException, IOException, CancelledException { @@ -514,11 +536,12 @@ public abstract class LocalFileSystem implements FileSystem { testValidName(name, false); ItemStorage itemStorage = allocateItemStorage(parentPath, name); - LocalDataFile dataFile = null; + LocalDataFileItem dataFile = null; try { //TODO handle comment - PropertyFile propertyFile = itemStorage.getPropertyFile(); - dataFile = new LocalDataFile(this, propertyFile, istream, contentType, monitor); + ItemPropertyFile propertyFile = itemStorage.getPropertyFile(); + dataFile = new LocalDataFileItem(this, propertyFile, istream, contentType, monitor); + dataFile.log("file created", getUserName()); } finally { if (dataFile == null) { @@ -531,6 +554,38 @@ public abstract class LocalFileSystem implements FileSystem { return dataFile; } + @Override + public synchronized LocalTextDataItem createTextDataItem(String parentPath, String name, + String fileID, String contentType, String textData, String ignoredComment) + throws InvalidNameException, IOException { + + // comment is ignored + + if (readOnly) { + throw new ReadOnlyException(); + } + + testValidName(parentPath, true); + testValidName(name, false); + + ItemStorage itemStorage = allocateItemStorage(parentPath, name); + LocalTextDataItem linkFile = null; + try { + ItemPropertyFile propertyFile = itemStorage.getPropertyFile(); + linkFile = new LocalTextDataItem(this, propertyFile, fileID, contentType, textData); + linkFile.log("file created", getUserName()); + } + finally { + if (linkFile == null) { + deallocateItemStorage(parentPath, name); + } + } + + eventManager.itemCreated(parentPath, name); + + return linkFile; + } + @Override public LocalDatabaseItem createFile(String parentPath, String name, File packedFile, TaskMonitor monitor, String user) @@ -561,7 +616,7 @@ public abstract class LocalFileSystem implements FileSystem { ItemStorage itemStorage = allocateItemStorage(parentPath, name); LocalDatabaseItem item = null; try { - PropertyFile propertyFile = itemStorage.getPropertyFile(); + ItemPropertyFile propertyFile = itemStorage.getPropertyFile(); item = new LocalDatabaseItem(this, propertyFile, packedFile, contentType, monitor, user); } @@ -661,6 +716,7 @@ public abstract class LocalFileSystem implements FileSystem { /** * Returns file system listener. + * @return file system listener or null */ FileSystemListener getListener() { return eventManager; @@ -716,6 +772,7 @@ public abstract class LocalFileSystem implements FileSystem { } /** + * @param c character to check * @return true if c is a valid character within the FileSystem. */ public static boolean isValidNameCharacter(char c) { @@ -756,9 +813,9 @@ public abstract class LocalFileSystem implements FileSystem { /** * Notify the filesystem that the property file and associated data files for * an item have been removed from the filesystem. - * @param folderPath - * @param itemName - * @throws IOException + * @param folderPath folder path of item + * @param itemName item name + * @throws IOException if an IO error occurs */ protected synchronized void itemDeleted(String folderPath, String itemName) throws IOException { // do nothing @@ -768,6 +825,7 @@ public abstract class LocalFileSystem implements FileSystem { * Returns the full path for a specific folder or item * @param parentPath full parent path * @param name child folder or item name + * @return pathname */ protected final static String getPath(String parentPath, String name) { if (parentPath.length() == 1) { @@ -848,7 +906,7 @@ public abstract class LocalFileSystem implements FileSystem { /** * Escape hidden prefix chars in name - * @param name + * @param name name to be escaped * @return escaped name */ public static final String escapeHiddenDirPrefixChars(String name) { @@ -867,7 +925,7 @@ public abstract class LocalFileSystem implements FileSystem { /** * Unescape a non-hidden directory name - * @param name + * @param name name to be unescaped * @return unescaped name or null if name is a hidden name */ public static final String unescapeHiddenDirPrefixChars(String name) { diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java index 7424607d0f..b6c90e5be5 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java @@ -21,7 +21,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import ghidra.framework.store.*; -import ghidra.util.*; +import ghidra.util.Msg; +import ghidra.util.ReadOnlyException; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; import utilities.util.FileUtilities; @@ -49,7 +50,7 @@ public abstract class LocalFolderItem implements FolderItem { static final String DATA_DIR_EXTENSION = ".db"; - final PropertyFile propertyFile; + final ItemPropertyFile propertyFile; final CheckoutManager checkoutMgr; final HistoryManager historyMgr; final LocalFileSystem fileSystem; @@ -69,7 +70,7 @@ public abstract class LocalFolderItem implements FolderItem { * @param fileSystem file system * @param propertyFile property file */ - LocalFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile) { + LocalFolderItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile) { this.fileSystem = fileSystem; this.propertyFile = propertyFile; this.isVersioned = fileSystem.isVersioned(); @@ -90,7 +91,7 @@ public abstract class LocalFolderItem implements FolderItem { * @param create if true the data directory will be created * @throws IOException */ - LocalFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile, boolean useDataDir, + LocalFolderItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, boolean useDataDir, boolean create) throws IOException { this.fileSystem = fileSystem; this.propertyFile = propertyFile; @@ -121,7 +122,7 @@ public abstract class LocalFolderItem implements FolderItem { throw new FileNotFoundException(getName() + " not found"); } - if (isVersioned) { + if (isVersioned && useDataDir) { checkoutMgr = new CheckoutManager(this, create); historyMgr = new HistoryManager(this, create); } @@ -161,7 +162,7 @@ public abstract class LocalFolderItem implements FolderItem { final File getDataDir() { synchronized (fileSystem) { // Use hidden DB directory - return new File(propertyFile.getFolder(), + return new File(propertyFile.getParentStorageDirectory(), LocalFileSystem.HIDDEN_DIR_PREFIX + LocalFileSystem.escapeHiddenDirPrefixChars(propertyFile.getStorageName()) + DATA_DIR_EXTENSION); @@ -234,6 +235,9 @@ public abstract class LocalFolderItem implements FolderItem { */ void beginCheckin(long checkoutId) throws FileInUseException { synchronized (fileSystem) { + if (checkoutMgr == null) { + throw new UnsupportedOperationException("item does not support checkin/checkout"); + } if (checkinId != DEFAULT_CHECKOUT_ID) { ItemCheckoutStatus status; try { @@ -426,7 +430,7 @@ public abstract class LocalFolderItem implements FolderItem { synchronized (fileSystem) { checkInUse(); - File oldFolder = propertyFile.getFolder(); + File oldFolder = propertyFile.getParentStorageDirectory(); String oldStorageName = propertyFile.getStorageName(); String oldPath = propertyFile.getParentPath(); File oldDbDir = getDataDir(); @@ -491,41 +495,6 @@ public abstract class LocalFolderItem implements FolderItem { return propertyFile.getName(); } -// /** -// * Change the name of this item's property file and hidden data directory -// * based upon the new item name. -// * If in-use files prevent renaming a FileInUseException will be thrown. -// * @param name new name for this item -// * @throws InvalidNameException invalid name was specified -// * @throws IOException an error occurred -// */ -// void doSetName(String name) throws InvalidNameException, IOException { -// synchronized (fileSystem) { -// File oldDbDir = getDataDir(); -// String oldName = getName(); -// -// boolean success = false; -// try { -// propertyFile.setName(name); -// File newDbDir = getDataDir(); -// if (useDataDir) { -// if (newDbDir.exists()) { -// throw new DuplicateFileException(getName() + " already exists"); -// } -// else if (!oldDbDir.renameTo(newDbDir)) { -// throw new FileInUseException(oldName + " is in use"); -// } -// } -// success = true; -// } -// finally { -// if (!success && !propertyFile.getName().equals(oldName)) { -// propertyFile.setName(oldName); -// } -// } -// } -// } - /** * @see ghidra.framework.store.FolderItem#getParentPath() */ @@ -590,6 +559,10 @@ public abstract class LocalFolderItem implements FolderItem { throw new UnsupportedOperationException( "Non-versioned item does not support getVersions"); } + if (historyMgr == null) { + throw new UnsupportedOperationException( + "getVersions not supported without history manager"); + } return historyMgr.getVersions(); } } @@ -652,12 +625,16 @@ public abstract class LocalFolderItem implements FolderItem { @Override public ItemCheckoutStatus checkout(CheckoutType checkoutType, String user, String projectPath) throws IOException { + if (checkoutMgr == null) { + throw new UnsupportedOperationException("item does not support checkin/checkout"); + } if (!isVersioned) { throw new UnsupportedOperationException("Non-versioned item does not support checkout"); } if (fileSystem.isReadOnly()) { throw new ReadOnlyException(); } + synchronized (fileSystem) { ItemCheckoutStatus coStatus = @@ -672,6 +649,9 @@ public abstract class LocalFolderItem implements FolderItem { @Override public void terminateCheckout(long checkoutId, boolean notify) throws IOException { + if (checkoutMgr == null) { + throw new UnsupportedOperationException("item does not support checkin/checkout"); + } if (!isVersioned) { throw new UnsupportedOperationException("Non-versioned item does not support checkout"); } @@ -700,6 +680,9 @@ public abstract class LocalFolderItem implements FolderItem { throw new UnsupportedOperationException( "Non-versioned item does not support checkout"); } + if (checkoutMgr == null) { + return null; + } return checkoutMgr.getCheckout(checkoutId); } } @@ -711,6 +694,9 @@ public abstract class LocalFolderItem implements FolderItem { throw new UnsupportedOperationException( "Non-versioned item does not support checkout"); } + if (checkoutMgr == null) { + return new ItemCheckoutStatus[0]; + } return checkoutMgr.getAllCheckouts(); } } @@ -802,33 +788,39 @@ public abstract class LocalFolderItem implements FolderItem { * @param propertyFile property file which identifies the folder item. * @return folder item */ - static LocalFolderItem getFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile) { + static LocalFolderItem getFolderItem(LocalFileSystem fileSystem, + ItemPropertyFile propertyFile) { int fileType = propertyFile.getInt(FILE_TYPE, UNKNOWN_FILE_TYPE); try { if (fileType == DATAFILE_FILE_TYPE) { - return new LocalDataFile(fileSystem, propertyFile); + return new LocalDataFileItem(fileSystem, propertyFile); } else if (fileType == DATABASE_FILE_TYPE) { return new LocalDatabaseItem(fileSystem, propertyFile); } + else if (fileType == LINK_FILE_TYPE) { + return new LocalTextDataItem(fileSystem, propertyFile); + } else if (fileType == UNKNOWN_FILE_TYPE) { - log.error("Folder item has unspecified file type: " + - new File(propertyFile.getFolder(), propertyFile.getStorageName())); + log.error("Folder item has unspecified file type: " + new File( + propertyFile.getParentStorageDirectory(), propertyFile.getStorageName())); } else { - log.error("Folder item has unsupported file type (" + fileType + "): " + - new File(propertyFile.getFolder(), propertyFile.getStorageName())); + log.error("Folder item has unsupported file type (" + fileType + "): " + new File( + propertyFile.getParentStorageDirectory(), propertyFile.getStorageName())); } } catch (FileNotFoundException e) { log.error("Folder item may be corrupt due to missing file: " + - new File(propertyFile.getFolder(), propertyFile.getStorageName()), e); + new File(propertyFile.getParentStorageDirectory(), propertyFile.getStorageName()), + e); } catch (IOException e) { log.error("Folder item may be corrupt: " + - new File(propertyFile.getFolder(), propertyFile.getStorageName()), e); + new File(propertyFile.getParentStorageDirectory(), propertyFile.getStorageName()), + e); } - return new UnknownFolderItem(fileSystem, propertyFile); + return new LocalUnknownFolderItem(fileSystem, propertyFile); } @Override @@ -836,7 +828,7 @@ public abstract class LocalFolderItem implements FolderItem { synchronized (fileSystem) { if (isVersioned) { try { - return checkoutMgr.isCheckedOut(); + return checkoutMgr != null && checkoutMgr.isCheckedOut(); } catch (IOException e) { Msg.error(getName() + " versioning error", e); @@ -865,6 +857,11 @@ public abstract class LocalFolderItem implements FolderItem { return false; } + @Override + public int hashCode() { + return propertyFile.hashCode(); + } + /** * Update this non-versioned item with the latest version of the specified versioned item. * @param versionedFolderItem versioned item which corresponds to this @@ -892,6 +889,9 @@ public abstract class LocalFolderItem implements FolderItem { @Override public void updateCheckoutVersion(long checkoutId, int checkoutVersion, String user) throws IOException { + if (checkoutMgr == null) { + throw new UnsupportedOperationException("item does not support checkin/checkout"); + } if (!isVersioned) { throw new UnsupportedOperationException( "updateCheckoutVersion is not applicable to non-versioned item"); @@ -907,4 +907,5 @@ public abstract class LocalFolderItem implements FolderItem { checkoutMgr.updateCheckout(checkoutId, checkoutVersion); } } + } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalTextDataItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalTextDataItem.java new file mode 100644 index 0000000000..ecf7b61f44 --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalTextDataItem.java @@ -0,0 +1,170 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.store.local; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.lang3.StringUtils; + +import ghidra.framework.store.*; +import ghidra.util.task.TaskMonitor; + +/** + * LocalTextDataItem provides a {@link LocalFolderItem} implementation + * which stores text data within the associated propertyFile and without any other data storage. + */ +public class LocalTextDataItem extends LocalFolderItem implements TextDataItem { + + private static final String TEXT_PROPERTY = "TEXT"; + private static final String VERSION_CREATE_USER = "CREATE_USER"; + private static final String VERSION_CREATE_TIME = "CREATE_TIME"; + private static final String VERSION_CREATE_COMMENT = "CREATE_COMMENT"; + + /** + * Constructor for an existing local link file item which corresponds to the specified + * property file. + * @param fileSystem file system + * @param propertyFile database property file + * @throws IOException if an IO Error occurs + */ + public LocalTextDataItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile) + throws IOException { + super(fileSystem, propertyFile, false, false); + } + + /** + * Create a new local text data file item. + * @param fileSystem file system + * @param propertyFile serialized data property file + * @param fileID file ID to be associated with new file or null + * @param contentType user content type + * @param textData text to be stored within associated property file + * @throws IOException if an IO Error occurs + */ + public LocalTextDataItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile, + String fileID, String contentType, String textData) throws IOException { + super(fileSystem, propertyFile, false, true); + + if (StringUtils.isBlank(contentType)) { + abortCreate(); + throw new IllegalArgumentException("Missing content-type"); + } + + if (StringUtils.isBlank(textData)) { + abortCreate(); + throw new IllegalArgumentException("Missing text data"); + } + + propertyFile.putInt(FILE_TYPE, LINK_FILE_TYPE); + propertyFile.putBoolean(READ_ONLY, false); + propertyFile.putString(CONTENT_TYPE, contentType); + if (fileID != null) { + propertyFile.setFileID(fileID); + } + + propertyFile.putString(TEXT_PROPERTY, textData); + + propertyFile.writeState(); + } + + /** + * Get the text data that was stored with this item + * @return text data + */ + public String getTextData() { + return propertyFile.getString(TEXT_PROPERTY, null); + } + + @Override + public long length() throws IOException { + return 0; + } + + @Override + public void updateCheckout(FolderItem versionedFolderItem, boolean updateItem, + TaskMonitor monitor) throws IOException { + throw new IOException("Versioning updates not supported"); + } + + @Override + public void updateCheckout(FolderItem item, int checkoutVersion) throws IOException { + throw new IOException("Versioning updates not supported"); + } + + @Override + void deleteMinimumVersion(String user) throws IOException { + throw new UnsupportedOperationException("Versioning updates not supported"); + } + + @Override + void deleteCurrentVersion(String user) throws IOException { + throw new UnsupportedOperationException("Versioning updates not supported"); + } + + @Override + public void output(File outputFile, int version, TaskMonitor monitor) throws IOException { + throw new IOException("Output not supported"); + } + + @Override + int getMinimumVersion() { + return getCurrentVersion(); + } + + @Override + public int getCurrentVersion() { + return 1; // only a single version of the file may exist + } + + @Override + public boolean canRecover() { + return false; + } + + /** + * Set the version info associated with this versioned file. Only a single version is + * supported. + * @param version version information (only user, create time and comment is retained) + * @throws IOException if an IO error occurs + */ + public void setVersionInfo(Version version) throws IOException { + synchronized (fileSystem) { + if (!isVersioned()) { + throw new UnsupportedOperationException("Versioning not supported"); + } + propertyFile.putString(VERSION_CREATE_USER, version.getUser()); + propertyFile.putLong(VERSION_CREATE_TIME, version.getCreateTime()); + propertyFile.putString(VERSION_CREATE_COMMENT, version.getComment()); + propertyFile.writeState(); + } + } + + @Override + public synchronized Version[] getVersions() throws IOException { + synchronized (fileSystem) { + if (!isVersioned) { + throw new UnsupportedOperationException( + "Non-versioned item does not support getVersions"); + } + String createUser = propertyFile.getString(VERSION_CREATE_USER, ""); + long createTime = propertyFile.getLong(VERSION_CREATE_TIME, 0); + String comment = propertyFile.getString(VERSION_CREATE_COMMENT, null); + return new Version[] { new Version(1, createTime, createUser, comment) }; + } + } + +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/UnknownFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalUnknownFolderItem.java similarity index 65% rename from Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/UnknownFolderItem.java rename to Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalUnknownFolderItem.java index c630e32ae3..716ed09732 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/UnknownFolderItem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalUnknownFolderItem.java @@ -19,16 +19,13 @@ import java.io.File; import java.io.IOException; import ghidra.framework.store.*; -import ghidra.util.PropertyFile; import ghidra.util.task.TaskMonitor; /** * UnknownFolderItem acts as a LocalFolderItem place-holder for * items of an unknown type. */ -public class UnknownFolderItem extends LocalFolderItem { - - public static final String UNKNOWN_CONTENT_TYPE = "Unknown-File"; +public class LocalUnknownFolderItem extends LocalFolderItem implements UnknownFolderItem { private final int fileType; @@ -37,11 +34,11 @@ public class UnknownFolderItem extends LocalFolderItem { * @param fileSystem local file system * @param propertyFile property file associated with this item */ - UnknownFolderItem(LocalFileSystem fileSystem, PropertyFile propertyFile) { + LocalUnknownFolderItem(LocalFileSystem fileSystem, ItemPropertyFile propertyFile) { super(fileSystem, propertyFile); fileType = propertyFile.getInt(FILE_TYPE, UNKNOWN_FILE_TYPE); } - + /** * Get the file type * @return file type or -1 if unspecified @@ -55,134 +52,82 @@ public class UnknownFolderItem extends LocalFolderItem { return 0; } - /* - * @see ghidra.framework.store.FolderItem#updateCheckout(ghidra.framework.store.FolderItem, boolean, ghidra.util.task.TaskMonitor) - */ @Override public void updateCheckout(FolderItem versionedFolderItem, boolean updateItem, TaskMonitor monitor) throws IOException { throw new UnsupportedOperationException(); } - /* - * @see ghidra.framework.store.FolderItem#updateCheckout(ghidra.framework.store.FolderItem, int) - */ @Override public void updateCheckout(FolderItem item, int checkoutVersion) throws IOException { throw new UnsupportedOperationException(); } - /* - * @see ghidra.framework.store.FolderItem#checkout(java.lang.String) - */ public synchronized ItemCheckoutStatus checkout(String user) throws IOException { - throw new IOException(propertyFile.getName() + - " may not be checked-out, item may be corrupt"); + throw new IOException( + propertyFile.getName() + " may not be checked-out, item may be corrupt"); } - /* - * @see ghidra.framework.store.FolderItem#terminateCheckout(long) - */ public synchronized void terminateCheckout(long checkoutId) { // Do nothing } - /* - * @see ghidra.framework.store.FolderItem#clearCheckout() - */ @Override public void clearCheckout() throws IOException { // Do nothing } - /* - * @see ghidra.framework.store.FolderItem#setCheckout(long, int, int) - */ public void setCheckout(long checkoutId, int checkoutVersion, int localVersion) { // Do nothing } - /* - * @see ghidra.framework.store.FolderItem#getCheckout(long) - */ @Override public synchronized ItemCheckoutStatus getCheckout(long checkoutId) throws IOException { return null; } - /* - * @see ghidra.framework.store.FolderItem#getCheckouts() - */ @Override public synchronized ItemCheckoutStatus[] getCheckouts() throws IOException { return new ItemCheckoutStatus[0]; } - /* - * @see ghidra.framework.store.FolderItem#getVersions() - */ @Override public synchronized Version[] getVersions() throws IOException { throw new IOException("History data is unavailable for " + propertyFile.getName()); } - /* - * @see ghidra.framework.store.FolderItem#getContentType() - */ @Override public String getContentType() { + // NOTE: We could get the content type from the property file but we don't want any + // attempt to use it return UNKNOWN_CONTENT_TYPE; } - /* - * @see ghidra.framework.store.local.LocalFolderItem#deleteMinimumVersion(java.lang.String) - */ @Override void deleteMinimumVersion(String user) throws IOException { - throw new UnsupportedOperationException("Versioning not supported for UnknownFolderItems"); - } - /* - * @see ghidra.framework.store.local.LocalFolderItem#deleteCurrentVersion(java.lang.String) - */ @Override void deleteCurrentVersion(String user) throws IOException { - throw new UnsupportedOperationException("Versioning not supported for UnknownFolderItems"); - } - /* - * @see ghidra.framework.store.FolderItem#output(java.io.File, int, ghidra.util.task.TaskMonitor) - */ @Override public void output(File outputFile, int version, TaskMonitor monitor) throws IOException { - throw new UnsupportedOperationException("Output not supported for UnknownFolderItems"); - } - /* - * @see ghidra.framework.store.local.LocalFolderItem#getMinimumVersion() - */ @Override int getMinimumVersion() throws IOException { return -1; } - /* - * @see ghidra.framework.store.FolderItem#getCurrentVersion() - */ @Override public int getCurrentVersion() { return -1; } - /* - * @see ghidra.framework.store.FolderItem#canRecover() - */ @Override public boolean canRecover() { return false; diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteDatabaseItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteDatabaseItem.java index b9e3d0b70f..43a9546403 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteDatabaseItem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteDatabaseItem.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -47,11 +47,6 @@ public class RemoteDatabaseItem extends RemoteFolderItem implements DatabaseItem return repository.getLength(parentPath, itemName); } - @Override - int getItemType() { - return RepositoryItem.DATABASE; - } - @Override public boolean canRecover() { return false; diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFileSystem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFileSystem.java index 24f10a4700..1c57805418 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFileSystem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFileSystem.java @@ -120,10 +120,13 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener { if (items[i].getItemType() == RepositoryItem.DATABASE) { folderItems[i] = new RemoteDatabaseItem(repository, items[i]); } + else if (items[i].getItemType() == RepositoryItem.TEXT_DATA_FILE) { + folderItems[i] = new RemoteTextDataItem(repository, items[i]); + } else { - Msg.error(this, - "Unsupported respository item encountered (" + items[i].getItemType() + "): " + - folderPath + items[i].getName()); + Msg.error(this, "Unsupported respository item encountered (" + + items[i].getItemType() + "): " + folderPath + items[i].getName()); + folderItems[i] = new RemoteUnknownFolderItem(repository, items[i]); } } return folderItems; @@ -138,7 +141,10 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener { if (item.getItemType() == RepositoryItem.DATABASE) { return new RemoteDatabaseItem(repository, item); } - throw new IOException("Unsupported repository item type (" + item.getItemType() + ")"); + if (item.getItemType() == RepositoryItem.TEXT_DATA_FILE) { + return new RemoteTextDataItem(repository, item); + } + return new RemoteUnknownFolderItem(repository, item); } @Override @@ -150,7 +156,10 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener { if (item.getItemType() == RepositoryItem.DATABASE) { return new RemoteDatabaseItem(repository, item); } - throw new IOException("Unsupported repository item type (" + item.getItemType() + ")"); + if (item.getItemType() == RepositoryItem.TEXT_DATA_FILE) { + return new RemoteTextDataItem(repository, item); + } + return new RemoteUnknownFolderItem(repository, item); } @Override @@ -163,6 +172,17 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener { throw new UnsupportedOperationException(); } + @Override + public boolean isSupportedItemType(FolderItem folderItem) { + if (folderItem instanceof DatabaseItem) { + return true; // assume this is always supported + } + if (folderItem instanceof TextDataItem) { + return true; + } + return false; + } + @Override public ManagedBufferFile createDatabase(String parentPath, String name, String fileID, String contentType, int bufferSize, String user, String projectPath) @@ -207,6 +227,14 @@ public class RemoteFileSystem implements FileSystem, RemoteAdapterListener { return (DataFileItem) getItem(parentPath, name); } + @Override + public TextDataItem createTextDataItem(String parentPath, String name, String fileID, + String contentType, String textData, String comment) + throws InvalidNameException, IOException { + repository.createTextDataFile(parentPath, name, fileID, contentType, textData, comment); + return (TextDataItem) getItem(parentPath, name); + } + @Override public FolderItem createFile(String parentPath, String name, File packedFile, TaskMonitor monitor, String user) diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFolderItem.java index 1da09bb400..aa077eee3c 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFolderItem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteFolderItem.java @@ -16,11 +16,11 @@ package ghidra.framework.store.remote; import java.io.IOException; +import java.util.Objects; import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.remote.RepositoryItem; import ghidra.framework.store.*; -import ghidra.framework.store.local.UnknownFolderItem; /** * RemoteFolderItem provides an abstract FolderItem implementation @@ -36,6 +36,8 @@ public abstract class RemoteFolderItem implements FolderItem { protected int version; protected long versionTime; + protected String textData; // applies to TextDataItem only + protected RepositoryAdapter repository; /** @@ -56,15 +58,9 @@ public abstract class RemoteFolderItem implements FolderItem { version = item.getVersion(); versionTime = item.getVersionTime(); - } - /** - * Returns the item type as defined by RepositoryItem which corresponds to specific - * implementation of this class. - * @return item type (Only {@link RepositoryItem#DATABASE} is supported). - * @see ghidra.framework.remote.RepositoryItem - */ - abstract int getItemType(); + textData = item.getTextData(); + } @Override public String getName() { @@ -74,11 +70,13 @@ public abstract class RemoteFolderItem implements FolderItem { @Override public RemoteFolderItem refresh() throws IOException { RepositoryItem item = repository.getItem(parentPath, itemName); - if (item == null) { + if (item == null || !Objects.equals(fileID, item.getFileID()) || + !contentType.equals(item.getContentType())) { return null; } version = item.getVersion(); versionTime = item.getVersionTime(); + textData = item.getTextData(); return this; } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteTextDataItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteTextDataItem.java new file mode 100644 index 0000000000..09652a749d --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteTextDataItem.java @@ -0,0 +1,72 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.store.remote; + +import java.io.File; +import java.io.IOException; + +import javax.help.UnsupportedOperationException; + +import ghidra.framework.client.RepositoryAdapter; +import ghidra.framework.remote.RepositoryItem; +import ghidra.framework.store.TextDataItem; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class RemoteTextDataItem extends RemoteFolderItem implements TextDataItem { + + RemoteTextDataItem(RepositoryAdapter repository, RepositoryItem item) { + super(repository, item); + } + + @Override + public long length() throws IOException { + return 0; + } + + @Override + public boolean hasCheckouts() throws IOException { + return false; + } + + @Override + public boolean canRecover() { + return false; + } + + @Override + public boolean isCheckinActive() throws IOException { + return false; + } + + @Override + public void updateCheckoutVersion(long checkoutId, int checkoutVersion, String user) + throws IOException { + throw new UnsupportedOperationException("Text data files do not support checkin"); + } + + @Override + public void output(File outputFile, int ver, TaskMonitor monitor) + throws IOException, CancelledException { + throw new UnsupportedOperationException("Text data files do not support serial output"); + } + + @Override + public String getTextData() { + return textData; + } + +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteUnknownFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteUnknownFolderItem.java new file mode 100644 index 0000000000..c95e7b4688 --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/remote/RemoteUnknownFolderItem.java @@ -0,0 +1,75 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.store.remote; + +import java.io.File; +import java.io.IOException; + +import javax.help.UnsupportedOperationException; + +import ghidra.framework.client.RepositoryAdapter; +import ghidra.framework.remote.RepositoryItem; +import ghidra.framework.store.UnknownFolderItem; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class RemoteUnknownFolderItem extends RemoteFolderItem implements UnknownFolderItem { + + private final int fileType; + + RemoteUnknownFolderItem(RepositoryAdapter repository, RepositoryItem item) { + super(repository, item); + fileType = item.getItemType(); + } + + @Override + public int getFileType() { + return fileType; + } + + @Override + public long length() throws IOException { + return 0; + } + + @Override + public boolean hasCheckouts() throws IOException { + return false; + } + + @Override + public boolean canRecover() { + return false; + } + + @Override + public boolean isCheckinActive() throws IOException { + return false; + } + + @Override + public void updateCheckoutVersion(long checkoutId, int checkoutVersion, String user) + throws IOException { + throw new UnsupportedOperationException("Text data files do not support checkin"); + } + + @Override + public void output(File outputFile, int ver, TaskMonitor monitor) + throws IOException, CancelledException { + throw new UnsupportedOperationException("Text data files do not support serial output"); + } + +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/PropertyFile.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/PropertyFile.java index 85079a383e..abb5b7646e 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/PropertyFile.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/util/PropertyFile.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,23 +15,22 @@ */ package ghidra.util; -import generic.stl.Pair; -import ghidra.framework.store.FileSystem; +import java.io.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.xml.sax.*; + +import ghidra.framework.store.local.ItemPropertyFile; import ghidra.util.exception.DuplicateFileException; import ghidra.util.xml.XmlUtilities; import ghidra.xml.NonThreadedXmlPullParserImpl; import ghidra.xml.XmlElement; -import java.io.*; -import java.util.HashMap; -import java.util.Map.Entry; - -import org.xml.sax.*; - /** - * Class that represents a file of property names and values. The file - * extension used is PROPERTY_EXT. - * + * {@link ItemPropertyFile} provides basic property storage. The file extension + * used is {@link #PROPERTY_EXT}. */ public class PropertyFile { @@ -40,15 +39,17 @@ public class PropertyFile { */ public final static String PROPERTY_EXT = ".prp"; - private static final String FILE_ID = "FILE_ID"; - protected File propertyFile; protected String storageName; - protected String parentPath; - protected String name; + //@formatter:off private static enum PropertyEntryType { - INT_TYPE("int"), LONG_TYPE("long"), BOOLEAN_TYPE("boolean"), STRING_TYPE("string"); + INT_TYPE("int"), + LONG_TYPE("long"), + BOOLEAN_TYPE("boolean"), + STRING_TYPE("string"); + //@formatter:on + PropertyEntryType(String rep) { this.rep = rep; } @@ -65,25 +66,25 @@ public class PropertyFile { } } - private HashMap> map = - new HashMap>(); + private record PropertyMapEntry(PropertyEntryType entityType, String value) { + // no behaviors + } + + private Map basicInfoMap = new HashMap(); /** * Construct a new or existing PropertyFile. - * This form ignores retained property values for NAME and PARENT path. - * @param dir parent directory + * This constructor ignores retained property values for NAME and PARENT path. + * This constructor will not throw an exception if the file does not exist. + * @param dir native directory where this file is stored * @param storageName stored property file name (without extension) - * @param parentPath path to parent - * @param name name of the property file - * @throws IOException + * @throws InvalidObjectException if a file parse error occurs + * @throws IOException if an IO error occurs reading an existing file */ - public PropertyFile(File dir, String storageName, String parentPath, String name) - throws IOException { + public PropertyFile(File dir, String storageName) throws IOException { if (!dir.isAbsolute()) { throw new IllegalArgumentException("dir must be specified by an absolute path"); } - this.name = name; - this.parentPath = parentPath; this.storageName = storageName; propertyFile = new File(dir, storageName + PROPERTY_EXT); if (propertyFile.exists()) { @@ -91,75 +92,33 @@ public class PropertyFile { } } - /** - * Return the name of this PropertyFile. A null value may be returned - * if this is an older property file and the name was not specified at - * time of construction. - */ - public String getName() { - return name; + protected boolean contains(String key) { + return basicInfoMap.containsKey(key); } /** - * Returns true if file is writable + * {@return true if file is read-only as reported by underlying native file-system} */ public boolean isReadOnly() { return !propertyFile.canWrite(); } /** - * Return the path to this PropertyFile. A null value may be returned - * if this is an older property file and the name and parentPath was not specified at - * time of construction. + * {@return the native parent storage directory containing this PropertyFile.} */ - public String getPath() { - if (parentPath == null || name == null) { - return null; - } - if (parentPath.length() == 1) { - return parentPath + name; - } - return parentPath + FileSystem.SEPARATOR_CHAR + name; - } - - /** - * Return the path to the parent of this PropertyFile. - */ - public String getParentPath() { - return parentPath; - } - - /** - * Return the parent file to this PropertyFile. - */ - public File getFolder() { + public File getParentStorageDirectory() { return propertyFile.getParentFile(); } /** - * Return the storage name of this PropertyFile. This name does not include the property + * Return the native storage name for this PropertyFile. This name does not include the property * file extension (.prp) + * @return native storage name */ public String getStorageName() { return storageName; } - /** - * Returns the FileID associated with this file. - * @return FileID associated with this file - */ - public String getFileID() { - return getString(FILE_ID, null); - } - - /** - * Set the FileID associated with this file. - * @param fileId - */ - public void setFileID(String fileId) { - putString(FILE_ID, fileId); - } - /** * Return the int value with the given propertyName. * @param propertyName name of property that is an int @@ -167,13 +126,12 @@ public class PropertyFile { * @return int value */ public int getInt(String propertyName, int defaultValue) { - Pair pair = map.get(propertyName); - if (pair == null || pair.first != PropertyEntryType.INT_TYPE) { + PropertyMapEntry entry = basicInfoMap.get(propertyName); + if (entry == null || entry.entityType != PropertyEntryType.INT_TYPE) { return defaultValue; } try { - String value = pair.second; - return Integer.parseInt(value); + return Integer.parseInt(entry.value); } catch (NumberFormatException e) { return defaultValue; @@ -186,8 +144,8 @@ public class PropertyFile { * @param value value to set */ public void putInt(String propertyName, int value) { - map.put(propertyName, new Pair(PropertyEntryType.INT_TYPE, - Integer.toString(value))); + basicInfoMap.put(propertyName, + new PropertyMapEntry(PropertyEntryType.INT_TYPE, Integer.toString(value))); } /** @@ -197,13 +155,12 @@ public class PropertyFile { * @return long value */ public long getLong(String propertyName, long defaultValue) { - Pair pair = map.get(propertyName); - if (pair == null || pair.first != PropertyEntryType.LONG_TYPE) { + PropertyMapEntry entry = basicInfoMap.get(propertyName); + if (entry == null || entry.entityType != PropertyEntryType.LONG_TYPE) { return defaultValue; } try { - String value = pair.second; - return Long.parseLong(value); + return Long.parseLong(entry.value); } catch (NumberFormatException e) { return defaultValue; @@ -216,8 +173,8 @@ public class PropertyFile { * @param value value to set */ public void putLong(String propertyName, long value) { - map.put(propertyName, - new Pair(PropertyEntryType.LONG_TYPE, Long.toString(value))); + basicInfoMap.put(propertyName, + new PropertyMapEntry(PropertyEntryType.LONG_TYPE, Long.toString(value))); } /** @@ -227,12 +184,11 @@ public class PropertyFile { * @return string value */ public String getString(String propertyName, String defaultValue) { - Pair pair = map.get(propertyName); - if (pair == null || pair.first != PropertyEntryType.STRING_TYPE) { + PropertyMapEntry entry = basicInfoMap.get(propertyName); + if (entry == null || entry.entityType != PropertyEntryType.STRING_TYPE) { return defaultValue; } - String value = pair.second; - return value; + return entry.value; } /** @@ -241,8 +197,13 @@ public class PropertyFile { * @param value value to set */ public void putString(String propertyName, String value) { - map.put(propertyName, new Pair(PropertyEntryType.STRING_TYPE, - value)); + if (value == null) { + basicInfoMap.remove(propertyName); + } + else { + basicInfoMap.put(propertyName, + new PropertyMapEntry(PropertyEntryType.STRING_TYPE, value)); + } } /** @@ -252,12 +213,11 @@ public class PropertyFile { * @return boolean value */ public boolean getBoolean(String propertyName, boolean defaultValue) { - Pair pair = map.get(propertyName); - if (pair == null || pair.first != PropertyEntryType.BOOLEAN_TYPE) { + PropertyMapEntry entry = basicInfoMap.get(propertyName); + if (entry == null || entry.entityType != PropertyEntryType.BOOLEAN_TYPE) { return defaultValue; } - String value = pair.second; - return Boolean.parseBoolean(value); + return Boolean.parseBoolean(entry.value); } /** @@ -266,20 +226,21 @@ public class PropertyFile { * @param value value to set */ public void putBoolean(String propertyName, boolean value) { - map.put(propertyName, new Pair(PropertyEntryType.BOOLEAN_TYPE, - Boolean.toString(value))); + basicInfoMap.put(propertyName, + new PropertyMapEntry(PropertyEntryType.BOOLEAN_TYPE, Boolean.toString(value))); } /** * Remove the specified property - * @param propertyName + * @param propertyName name of property to be removed */ public void remove(String propertyName) { - map.remove(propertyName); + basicInfoMap.remove(propertyName); } /** - * Return the time of last modification in number of milliseconds. + * Return the time of last modification in number of milliseconds + * @return time of last modification */ public long lastModified() { return propertyFile.lastModified(); @@ -290,15 +251,17 @@ public class PropertyFile { * @throws IOException thrown if there was a problem writing the file */ public void writeState() throws IOException { + // NOTE: To avoid severe incompatibility with older versions of Ghidra this XML + // schema should not be changed. PrintWriter writer = new PrintWriter(propertyFile); try { writer.println(""); writer.println(""); writer.println(" "); - for (Entry> entry : map.entrySet()) { + for (Entry entry : basicInfoMap.entrySet()) { String propertyName = entry.getKey(); - String propertyType = entry.getValue().first.rep; - String propertyValue = entry.getValue().second; + String propertyType = entry.getValue().entityType.rep; + String propertyValue = entry.getValue().value; writer.print(" (propertyType, - propertyValue)); + basicInfoMap.put(propertyName, new PropertyMapEntry(propertyType, propertyValue)); parser.end(state); } parser.end(basic_info); parser.end(file_info); } catch (Exception e) { - Msg.error(this, "Unexpected Exception: " + e.getMessage(), e); - throw new InvalidObjectException("XML parse error in properties file"); + String msg = "XML parse error in properties file"; + Msg.error(this, msg + ": " + propertyFile); + throw new InvalidObjectException(msg); } finally { if (parser != null) { @@ -368,20 +333,19 @@ public class PropertyFile { /** * Move this PropertyFile to the newParent file. - * @param newParent new parent of the file - * @param newStorageName new storage name - * @param newParentPath parent path of the new parent - * @param newName new name for this PropertyFile + * @param newStorageParent new storage parent of the native file + * @param newStorageName new storage name for this property file * @throws IOException thrown if there was a problem accessing the * @throws DuplicateFileException thrown if a file with the newName * already exists */ - public void moveTo(File newParent, String newStorageName, String newParentPath, String newName) + public void moveTo(File newStorageParent, String newStorageName) throws DuplicateFileException, IOException { - if (!newParent.equals(propertyFile.getParentFile()) || !newStorageName.equals(storageName)) { - File newPropertyFile = new File(newParent, newStorageName + PROPERTY_EXT); + if (!newStorageParent.equals(propertyFile.getParentFile()) || + !newStorageName.equals(storageName)) { + File newPropertyFile = new File(newStorageParent, newStorageName + PROPERTY_EXT); if (newPropertyFile.exists()) { - throw new DuplicateFileException(newName + " already exists"); + throw new DuplicateFileException(newPropertyFile + " already exists"); } if (!propertyFile.renameTo(newPropertyFile)) { throw new IOException("move failed"); @@ -389,12 +353,11 @@ public class PropertyFile { propertyFile = newPropertyFile; storageName = newStorageName; } - parentPath = newParentPath; - name = newName; } /** * Return whether the file for this PropertyFile exists. + * @return true if this file exists */ public boolean exists() { return propertyFile.exists(); @@ -409,10 +372,7 @@ public class PropertyFile { @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((propertyFile == null) ? 0 : propertyFile.hashCode()); - return result; + return propertyFile.hashCode(); } @Override @@ -426,15 +386,8 @@ public class PropertyFile { if (getClass() != obj.getClass()) { return false; } - PropertyFile other = (PropertyFile) obj; - if (propertyFile == null) { - if (other.propertyFile != null) { - return false; - } - } - else if (!propertyFile.equals(other.propertyFile)) { - return false; - } - return true; + ItemPropertyFile other = (ItemPropertyFile) obj; + return propertyFile.equals(other.propertyFile); } + } diff --git a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java index b65503a6ce..5ca2f5429e 100644 --- a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java +++ b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/AbstractLocalFileSystemTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -232,8 +232,9 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest { // Get storage name based upon data dir name ~.db String storageName = dataDir.getName(); - storageName = storageName.substring(0, - storageName.length() - LocalFolderItem.DATA_DIR_EXTENSION.length()).substring(1); + storageName = storageName + .substring(0, storageName.length() - LocalFolderItem.DATA_DIR_EXTENSION.length()) + .substring(1); File propertyFile = new File(dataDir.getParentFile(), storageName + PropertyFile.PROPERTY_EXT); assertTrue(propertyFile.isFile()); @@ -397,6 +398,12 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest { fs.createFolder("/", "aaa"); + // item's name and folder name may replicate each other + DataFileItem file = createItem(dataBytes, "/", "aaa"); + assertNotNull(file); + assertTrue(fs.folderExists("/aaa")); + assertTrue(fs.fileExists("/", "aaa")); + createItem(dataBytes, "/aaa", "~)(%$#@!JGJ"); for (char cstart = 20; cstart < 255; cstart += fs.getMaxNameLength()) { @@ -584,8 +591,9 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest { // Get storage name based upon data dir name ~.db String storageName = dataDir.getName(); - storageName = storageName.substring(0, - storageName.length() - LocalFolderItem.DATA_DIR_EXTENSION.length()).substring(1); + storageName = storageName + .substring(0, storageName.length() - LocalFolderItem.DATA_DIR_EXTENSION.length()) + .substring(1); File propertyFile = new File(dataDir.getParentFile(), storageName + PropertyFile.PROPERTY_EXT); assertTrue(propertyFile.isFile()); @@ -615,8 +623,8 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest { assertEquals("fred", items[1]); assertEquals("greg", items[2]); - assertEquals(LocalDataFile.class, fs.getItem("/abc", items[0]).getClass()); - assertEquals(LocalDataFile.class, fs.getItem("/abc", items[1]).getClass()); + assertEquals(LocalDataFileItem.class, fs.getItem("/abc", items[0]).getClass()); + assertEquals(LocalDataFileItem.class, fs.getItem("/abc", items[1]).getClass()); assertEquals(LocalDatabaseItem.class, fs.getItem("/abc", items[2]).getClass()); df = (DataFileItem) fs.getItem("/abc", items[0]); @@ -649,7 +657,7 @@ public abstract class AbstractLocalFileSystemTest extends AbstractGenericTest { fs.moveItem("/abc", "fred", "/xyz", "bob"); assertNull(fs.getItem("/abc", "fred")); - LocalDataFile df = (LocalDataFile) fs.getItem("/xyz", "bob"); + LocalDataFileItem df = (LocalDataFileItem) fs.getItem("/xyz", "bob"); assertNotNull(df); try (InputStream in = df.getInputStream()) { diff --git a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/IndexedPropertyFileTest.java b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/IndexedPropertyFileTest.java index 63d0217bf9..161bbe4180 100644 --- a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/IndexedPropertyFileTest.java +++ b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/IndexedPropertyFileTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,8 +15,7 @@ */ package ghidra.framework.store.local; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import java.io.File; import java.net.URLDecoder; @@ -39,7 +38,7 @@ public class IndexedPropertyFileTest extends AbstractGenericTest { String storageName = NamingUtilities.mangle(NAME); - PropertyFile pf = new IndexedPropertyFile(parent, storageName, "/", NAME); + ItemPropertyFile pf = new IndexedPropertyFile(parent, storageName, "/", NAME); assertEquals(storageName, pf.getStorageName()); assertEquals(NAME, pf.getName()); assertEquals("/", pf.getParentPath()); @@ -63,29 +62,44 @@ public class IndexedPropertyFileTest extends AbstractGenericTest { pf.writeState(); - PropertyFile pf2 = new IndexedPropertyFile(parent, storageName, "/", NAME); - pf2.readState(); + pf = new IndexedPropertyFile(parent, storageName, "/X", "XXX"); + // existing file ignores supplied name and parent path + assertEquals(storageName, pf.getStorageName()); + assertEquals(NAME, pf.getName()); + assertEquals("/", pf.getParentPath()); + assertEquals("/" + NAME, pf.getPath()); + assertTrue(pf.getBoolean("TestBooleanTrue", false)); + assertTrue(!pf.getBoolean("TestBooleanFalse", true)); + assertTrue(pf.getBoolean("TestBooleanBad", true)); + assertEquals(1234, pf.getInt("TestInt", -1)); + assertEquals(0x12345678, pf.getLong("TestLong", -1)); + assertEquals(str, URLDecoder.decode(pf.getString("TestString", null), "UTF-8")); - assertTrue(pf2.getBoolean("TestBooleanTrue", false)); - assertTrue(!pf2.getBoolean("TestBooleanFalse", true)); - assertTrue(pf2.getBoolean("TestBooleanBad", true)); - assertEquals(1234, pf2.getInt("TestInt", -1)); - assertEquals(0x12345678, pf2.getLong("TestLong", -1)); - assertEquals(str, URLDecoder.decode(pf2.getString("TestString", null), "UTF-8")); + pf = new IndexedPropertyFile(parent, storageName); + assertEquals(storageName, pf.getStorageName()); + assertEquals(NAME, pf.getName()); + assertEquals("/", pf.getParentPath()); + assertEquals("/" + NAME, pf.getPath()); - PropertyFile pf3 = - new IndexedPropertyFile(new File(parent, storageName + PropertyFile.PROPERTY_EXT)); - assertEquals(storageName, pf3.getStorageName()); - assertEquals(NAME, pf3.getName()); - assertEquals("/", pf3.getParentPath()); - assertEquals("/" + NAME, pf3.getPath()); + assertTrue(pf.getBoolean("TestBooleanTrue", false)); + assertTrue(!pf.getBoolean("TestBooleanFalse", true)); + assertTrue(pf.getBoolean("TestBooleanBad", true)); + assertEquals(1234, pf.getInt("TestInt", -1)); + assertEquals(0x12345678, pf.getLong("TestLong", -1)); + assertEquals(str, URLDecoder.decode(pf.getString("TestString", null), "UTF-8")); - assertTrue(pf3.getBoolean("TestBooleanTrue", false)); - assertTrue(!pf3.getBoolean("TestBooleanFalse", true)); - assertTrue(pf3.getBoolean("TestBooleanBad", true)); - assertEquals(1234, pf3.getInt("TestInt", -1)); - assertEquals(0x12345678, pf3.getLong("TestLong", -1)); - assertEquals(str, URLDecoder.decode(pf3.getString("TestString", null), "UTF-8")); + pf = new IndexedPropertyFile(new File(parent, storageName + PropertyFile.PROPERTY_EXT)); + assertEquals(storageName, pf.getStorageName()); + assertEquals(NAME, pf.getName()); + assertEquals("/", pf.getParentPath()); + assertEquals("/" + NAME, pf.getPath()); + + assertTrue(pf.getBoolean("TestBooleanTrue", false)); + assertTrue(!pf.getBoolean("TestBooleanFalse", true)); + assertTrue(pf.getBoolean("TestBooleanBad", true)); + assertEquals(1234, pf.getInt("TestInt", -1)); + assertEquals(0x12345678, pf.getLong("TestLong", -1)); + assertEquals(str, URLDecoder.decode(pf.getString("TestString", null), "UTF-8")); } diff --git a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/MangledLocalFileSystemTest.java b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/MangledLocalFileSystemTest.java index b1b75a6281..895734c686 100644 --- a/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/MangledLocalFileSystemTest.java +++ b/Ghidra/Framework/FileSystem/src/test.slow/java/ghidra/framework/store/local/MangledLocalFileSystemTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,6 +25,7 @@ import java.util.List; import org.junit.Test; import ghidra.framework.store.DataFileItem; +import utilities.util.FileUtilities; public class MangledLocalFileSystemTest extends AbstractLocalFileSystemTest { @@ -35,6 +36,10 @@ public class MangledLocalFileSystemTest extends AbstractLocalFileSystemTest { @Test public void testMigration() throws Exception { + File tmpProjectDir = new File(projectDir.getParentFile(), + LocalFileSystem.HIDDEN_DIR_PREFIX + '.' + projectDir.getName()); + FileUtilities.deleteDir(tmpProjectDir); + testFilePaths(); List names = new ArrayList(); diff --git a/Ghidra/Framework/FileSystem/src/test/java/ghidra/framework/store/local/ItemPropertyFileTest.java b/Ghidra/Framework/FileSystem/src/test/java/ghidra/framework/store/local/ItemPropertyFileTest.java new file mode 100644 index 0000000000..ce774ec4de --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/test/java/ghidra/framework/store/local/ItemPropertyFileTest.java @@ -0,0 +1,56 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.store.local; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import org.junit.Test; + +import ghidra.util.PropertyFileTest; + +public class ItemPropertyFileTest extends PropertyFileTest { + + public ItemPropertyFileTest() { + super(); + } + + @Override + protected ItemPropertyFile getPropertyFile() throws IOException { + return new ItemPropertyFile(storageDir, storageName, "/", NAME); + } + + @Test + public void testPropertyFileName() throws Exception { + + ItemPropertyFile pf = getPropertyFile(); + assertEquals(storageName, pf.getStorageName()); + assertEquals(NAME, pf.getName()); + assertEquals("/", pf.getParentPath()); + assertEquals("/" + NAME, pf.getPath()); + pf.writeState(); + + pf = getPropertyFile(); + assertTrue(pf.exists()); + assertEquals(storageName, pf.getStorageName()); + assertEquals(NAME, pf.getName()); + assertEquals("/", pf.getParentPath()); + assertEquals("/" + NAME, pf.getPath()); + + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/util/PropertyFileTest.java b/Ghidra/Framework/FileSystem/src/test/java/ghidra/util/PropertyFileTest.java similarity index 77% rename from Ghidra/Features/Base/src/test/java/ghidra/util/PropertyFileTest.java rename to Ghidra/Framework/FileSystem/src/test/java/ghidra/util/PropertyFileTest.java index 6597d69576..60d2eebbb6 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/util/PropertyFileTest.java +++ b/Ghidra/Framework/FileSystem/src/test/java/ghidra/util/PropertyFileTest.java @@ -18,37 +18,43 @@ package ghidra.util; import static org.junit.Assert.*; import java.io.File; +import java.io.IOException; import java.net.URLDecoder; import java.net.URLEncoder; +import org.junit.Before; import org.junit.Test; import generic.test.AbstractGenericTest; +import ghidra.util.NamingUtilities; +import ghidra.util.PropertyFile; public class PropertyFileTest extends AbstractGenericTest { - private static String NAME = "Test"; + protected static String NAME = "Test"; + + protected String storageName; + protected File storageDir; - /** - * Constructor for PropertyFileTest. - * @param arg0 - */ public PropertyFileTest() { super(); } + @Before + public void setUp() throws IOException { + storageDir = createTempDirectory(getName()); + storageName = NamingUtilities.mangle(NAME); + } + + protected PropertyFile getPropertyFile() throws IOException { + return new PropertyFile(storageDir, storageName); + } + @Test public void testPropertyFile() throws Exception { - String storageName = NamingUtilities.mangle(NAME); - - File parent = createTempDirectory(getName()); - - PropertyFile pf = new PropertyFile(parent, storageName, "/", NAME); + PropertyFile pf = getPropertyFile(); assertEquals(storageName, pf.getStorageName()); - assertEquals(NAME, pf.getName()); - assertEquals("/", pf.getParentPath()); - assertEquals("/" + NAME, pf.getPath()); pf.putBoolean("TestBooleanTrue", true); pf.putBoolean("TestBooleanFalse", false); @@ -73,8 +79,8 @@ public class PropertyFileTest extends AbstractGenericTest { pf.writeState(); - PropertyFile pf2 = new PropertyFile(parent, storageName, "/", NAME); - pf2.readState(); + PropertyFile pf2 = getPropertyFile(); + assertTrue(pf2.exists()); // state will be read at construction time assertTrue(pf2.getBoolean("TestBooleanTrue", false)); assertTrue(!pf2.getBoolean("TestBooleanFalse", true)); diff --git a/Ghidra/Framework/Gui/src/main/java/resources/MultiIcon.java b/Ghidra/Framework/Gui/src/main/java/resources/MultiIcon.java index e78c5d5138..fc81345147 100644 --- a/Ghidra/Framework/Gui/src/main/java/resources/MultiIcon.java +++ b/Ghidra/Framework/Gui/src/main/java/resources/MultiIcon.java @@ -148,7 +148,7 @@ public class MultiIcon implements Icon { if (disabled) { // Alpha blend to background - Color bgColor = c.getBackground(); + Color bgColor = c != null ? c.getBackground() : Color.gray; g.setColor(ColorUtils.withAlpha(bgColor, 128)); g.fillRect(x, y, width, height); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/core/help/AboutDomainObjectUtils.java b/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/core/help/AboutDomainObjectUtils.java index 164889a6c5..e4c342651b 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/core/help/AboutDomainObjectUtils.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/core/help/AboutDomainObjectUtils.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,7 +19,8 @@ import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.Transferable; import java.awt.event.*; -import java.util.*; +import java.util.Date; +import java.util.Map; import javax.swing.*; import javax.swing.text.JTextComponent; @@ -102,10 +103,11 @@ public class AboutDomainObjectUtils { addInfo(aboutPanel, "Last Modified:", (new Date(lastModified)).toString()); } addInfo(aboutPanel, "Readonly:", Boolean.toString(domainFile.isReadOnly())); + if (metadata.isEmpty() && domainFile.isLink()) { + addInfo(aboutPanel, "Link path/url:", domainFile.getLinkInfo().getLinkPath()); + } - Iterator it = metadata.keySet().iterator(); - while (it.hasNext()) { - String key = it.next(); + for (String key : metadata.keySet()) { String value = metadata.get(key); addInfo(aboutPanel, key + ":", value); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java index 39b0c04fef..dd21fb0ebf 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/ContentHandler.java @@ -21,9 +21,7 @@ import javax.help.UnsupportedOperationException; import javax.swing.Icon; import ghidra.framework.model.*; -import ghidra.framework.store.FileSystem; -import ghidra.framework.store.FolderItem; -import ghidra.framework.store.local.UnknownFolderItem; +import ghidra.framework.store.*; import ghidra.util.InvalidNameException; import ghidra.util.classfinder.ExtensionPoint; import ghidra.util.exception.CancelledException; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java index f9023969ed..283a78cd7c 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DefaultProjectData.java @@ -19,6 +19,8 @@ import java.io.*; import java.net.URL; import java.util.*; +import org.apache.commons.lang3.StringUtils; + import docking.widgets.OptionDialog; import generic.timer.GhidraSwinglessTimer; import ghidra.framework.client.*; @@ -253,8 +255,7 @@ public class DefaultProjectData implements ProjectData { */ public static Properties readProjectProperties(File projectDir) { try { - PropertyFile pf = - new PropertyFile(projectDir, PROPERTY_FILENAME, "/", PROPERTY_FILENAME); + PropertyFile pf = new PropertyFile(projectDir, PROPERTY_FILENAME); if (pf.exists()) { Properties properties = new Properties(); @@ -281,7 +282,7 @@ public class DefaultProjectData implements ProjectData { throws IOException, LockException { projectDir = localStorageLocator.getProjectDir(); - properties = new PropertyFile(projectDir, PROPERTY_FILENAME, "/", PROPERTY_FILENAME); + properties = new PropertyFile(projectDir, PROPERTY_FILENAME); if (create) { if (projectDir.exists()) { throw new DuplicateFileException( @@ -347,7 +348,7 @@ public class DefaultProjectData implements ProjectData { } String defaultMsg = "Unable to lock project! " + locator; - + // in headless mode, just spit out an error if (!allowInteractiveForce || SystemUtilities.isInHeadlessMode()) { throw new LockException(defaultMsg); @@ -358,8 +359,8 @@ public class DefaultProjectData implements ProjectData { String lockInformation = lock.getExistingLockFileInformation(); if (!lock.canForceLock()) { String msg = "Project is locked. You have another instance of Ghidra
    " + - "already running with this project open (locally or remotely).

    " + - projectStr + "

    " + "Lock information: " + lockInformation; + "already running with this project open (locally or remotely).

    " + + projectStr + "

    " + "Lock information: " + lockInformation; throw new LockException(msg); } @@ -586,35 +587,12 @@ public class DefaultProjectData implements ProjectData { @Override public DomainFolder getFolder(String path) { - int len = path.length(); - if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { - throw new IllegalArgumentException( - "Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'"); - } + return getFolder(path, DomainFolderFilter.ALL_INTERNAL_FOLDERS_FILTER); + } - DomainFolder folder = getRootFolder(); - String[] split = path.split(FileSystem.SEPARATOR); - if (split.length == 0) { - return folder; - } - - for (int i = 1; i < split.length; i++) { - DomainFolder subFolder = folder.getFolder(split[i]); - if (subFolder == null) { - // Check for folder link-file if folder not found - // NOTE: if real folder name matches link-file name it will block - // use of folder link-file. - DomainFile file = folder.getFile(split[i]); - if (file != null && file.isLinkFile()) { - subFolder = file.followLink(); - } - if (subFolder == null) { - return null; - } - } - folder = subFolder; - } - return folder; + @Override + public DomainFolder getFolder(String path, DomainFolderFilter filter) { + return ProjectDataUtils.getDomainFolder(getRootFolder(), path, filter); } @Override @@ -658,25 +636,32 @@ public class DefaultProjectData implements ProjectData { @Override public DomainFile getFile(String path) { - int len = path.length(); - if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { + return getFile(path, DomainFileFilter.ALL_INTERNAL_FILES_FILTER); + } + + @Override + public DomainFile getFile(String path, DomainFileFilter filter) { + if (StringUtils.isBlank(path) || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { throw new IllegalArgumentException( "Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'"); } - else if (path.charAt(len - 1) == FileSystem.SEPARATOR_CHAR) { + else if (path.charAt(path.length() - 1) == FileSystem.SEPARATOR_CHAR) { throw new IllegalArgumentException("Missing file name in path"); } int ix = path.lastIndexOf(FileSystem.SEPARATOR); DomainFolder folder; if (ix > 0) { - folder = getFolder(path.substring(0, ix)); + folder = getFolder(path.substring(0, ix), filter); } else { folder = getRootFolder(); } if (folder != null) { - return folder.getFile(path.substring(ix + 1)); + DomainFile file = folder.getFile(path.substring(ix + 1)); + if (file != null && filter.accept(file)) { + return file; + } } return null; } @@ -811,7 +796,9 @@ public class DefaultProjectData implements ProjectData { ItemCheckoutStatus otherCheckoutStatus = newRepository.getCheckout(df.getParent().getPathname(), df.getName(), checkoutId); - + if (otherCheckoutStatus == null) { + return true; + } if (!newRepository.getUser().getName().equals(otherCheckoutStatus.getUser())) { return true; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java index 3c558d0d20..d889af89be 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -247,24 +247,13 @@ public class DomainFileProxy implements DomainFile { } @Override - public boolean isLinkFile() { - DomainObjectAdapter dobj = getDomainObject(); - if (dobj != null) { - ContentHandler ch; - try { - ch = DomainObjectAdapter.getContentHandler(dobj); - return LinkHandler.class.isAssignableFrom(ch.getClass()); - } - catch (IOException e) { - // ignore - } - } + public boolean isLink() { return false; } @Override - public DomainFolder followLink() { - throw new UnsupportedOperationException(); + public LinkFileInfo getLinkInfo() { + return null; } @Override @@ -414,8 +403,8 @@ public class DomainFileProxy implements DomainFile { } @Override - public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { - return null; // not supported by proxy file + public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException { + throw new UnsupportedOperationException(); } @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java index 2e5e1c832c..19fa3dbf2e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java @@ -404,7 +404,7 @@ public abstract class DomainObjectAdapter implements DomainObject { checkContentHandlerMaps(); ContentHandler ch = contentHandlerTypeMap.get(contentType); if (ch == null) { - throw new IOException("Content handler not found for " + contentType); + throw new IOException("Content handler not found for content-type: " + contentType); } return ch; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java index a58294c239..901258ba5a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/FolderLinkContentHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,18 +15,15 @@ */ package ghidra.framework.data; +import java.io.FileNotFoundException; import java.io.IOException; -import java.net.URL; +import java.util.concurrent.atomic.AtomicReference; import javax.swing.Icon; import ghidra.framework.main.AppInfo; import ghidra.framework.model.*; -import ghidra.framework.store.FileSystem; -import ghidra.util.InvalidNameException; import ghidra.util.Msg; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; /** * {@code FolderLinkContentHandler} provide folder-link support. @@ -39,16 +36,6 @@ public class FolderLinkContentHandler extends LinkHandler + * IMPORTANT: The use of external GhidraURL-based links is only supported in the context + * of a an active project which is used to manage the associated project view. + *

    + * If the link refers to a folder within the active project (i.e., path based), the resulting + * linked folder will be treated as part of that project, otherwise content will be treated + * as read-only. + * * @param folderLinkFile folder-link file. * @return {@link LinkedGhidraFolder} referenced by specified folder-link file or null if * folderLinkFile content type is not {@value #FOLDER_LINK_CONTENT_TYPE}. - * @throws IOException if an IO or folder item access error occurs + * @throws IOException if an IO or folder item access error occurs or a linkage error + * exists. */ - public static LinkedGhidraFolder getReadOnlyLinkedFolder(DomainFile folderLinkFile) - throws IOException { + public static LinkedGhidraFolder getLinkedFolder(DomainFile folderLinkFile) throws IOException { - if (!FOLDER_LINK_CONTENT_TYPE.equals(folderLinkFile.getContentType())) { + LinkFileInfo linkInfo = folderLinkFile.getLinkInfo(); + if (linkInfo == null || !linkInfo.isFolderLink()) { return null; } - URL url = getURL(folderLinkFile); + AtomicReference linkStatus = new AtomicReference<>(); + AtomicReference errMsg = new AtomicReference<>(); - Project activeProject = AppInfo.getActiveProject(); - if (activeProject == null) { - Msg.error(FolderLinkContentHandler.class, - "Use of Linked Folders requires active project."); - return null; + // Following internal linkage will catch circular internal linkage + DomainFile folderLink = LinkHandler.followInternalLinkage(folderLinkFile, + s -> linkStatus.set(s), err -> errMsg.set(err)); + + LinkStatus s = linkStatus.get(); + if (s == LinkStatus.BROKEN) { + String msg = errMsg.get(); + if (msg == null) { + msg = "Unable to follow broken link"; + } + // TODO: Should we just log warning instead? + throw new IOException(msg + ": " + folderLink); } - GhidraFolder parent = ((GhidraFile) folderLinkFile).getParent(); - return new LinkedGhidraFolder(activeProject, parent, folderLinkFile.getName(), url); + + if (s == LinkStatus.EXTERNAL) { + Project activeProject = AppInfo.getActiveProject(); + if (activeProject == null) { + Msg.error(FolderLinkContentHandler.class, + "Use of Linked Folders requires an active project."); + return null; + } + return new LinkedGhidraFolder(folderLink, getLinkURL(folderLink)); + } + + if (folderLink != null) { + + ProjectData projectData; + DomainFolder parent = folderLink.getParent(); + if (parent instanceof LinkedDomainFolder lf) { + try { + projectData = lf.getLinkedProjectData(); + } + catch (IOException e) { + throw new RuntimeException("Unexpected", e); + } + } + else { + projectData = parent.getProjectData(); + } + + String linkPath = LinkHandler.getAbsoluteLinkPath(folderLink); + + DomainFolder linkedFolder = projectData.getFolder(linkPath); + if (linkedFolder != null) { + return new LinkedGhidraFolder(folderLinkFile, linkedFolder); + } + } + + // TODO: Not sure if this can ever occur + throw new FileNotFoundException("Invalid folder-link: " + folderLinkFile); } } - -/** - * Dummy domain object to satisfy {@link FolderLinkContentHandler#getDomainObjectClass()} - */ -final class NullFolderDomainObject extends DomainObjectAdapterDB { - private NullFolderDomainObject() { - // this object may not be instantiated - super(null, null, 0, NullFolderDomainObject.class); - throw new RuntimeException("Object may not be instantiated"); - } - - @Override - public boolean isChangeable() { - return false; - } - - @Override - public String getDescription() { - return "Dummy FolderLink Domain Object"; - } -} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java index 398c651354..6ac47181c5 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFile.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,13 +25,12 @@ import ghidra.framework.model.*; import ghidra.framework.store.ItemCheckoutStatus; import ghidra.framework.store.Version; import ghidra.framework.store.local.LocalFileSystem; -import ghidra.util.*; +import ghidra.util.InvalidNameException; +import ghidra.util.ReadOnlyException; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; -public class GhidraFile implements DomainFile { - - // FIXME: This implementation assumes a single implementation of the DomainFile and DomainFolder interfaces +public class GhidraFile implements DomainFile, LinkFileInfo { protected DefaultProjectData projectData; @@ -157,9 +156,9 @@ public class GhidraFile implements DomainFile { } @Override - public boolean isLinkFile() { + public boolean isLink() { try { - return getFileData().isLinkFile(); + return getFileData().isLink(); } catch (IOException e) { return false; @@ -167,12 +166,38 @@ public class GhidraFile implements DomainFile { } @Override - public DomainFolder followLink() { + public LinkFileInfo getLinkInfo() { + return isLink() ? this : null; + } + + @Override + public DomainFile getFile() { + return this; + } + + @Override + public String getLinkPath() { try { - return FolderLinkContentHandler.getReadOnlyLinkedFolder(this); + return getFileData().getLinkPath(false); } catch (IOException e) { - Msg.error(this, "Failed to following folder-link: " + getPathname()); + // ignore + } + return null; + } + + @Override + public String getAbsoluteLinkPath() throws IOException { + return LinkHandler.getAbsoluteLinkPath(this); + } + + @Override + public LinkedGhidraFolder getLinkedFolder() { + try { + return FolderLinkContentHandler.getLinkedFolder(this); + } + catch (IOException e) { + // Ignore } return null; } @@ -522,40 +547,40 @@ public class GhidraFile implements DomainFile { @Override public GhidraFile moveTo(DomainFolder newParent) throws IOException { - if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { - throw new UnsupportedOperationException("newParent does not support moveTo"); + + if (getParent().getProjectData() != newParent.getProjectData() || !isInWritableProject()) { + throw new IOException("Move only supported within the same writable project"); } - GhidraFolder newGhidraParent = (GhidraFolder) newParent; + + GhidraFolder newGhidraParent = GhidraFolder.getDestinationFolder(newParent); + return getFileData().moveTo(newGhidraParent.getFolderData()); } @Override public DomainFile copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, CancelledException { - if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { - throw new UnsupportedOperationException("newParent does not support copyTo"); - } - GhidraFolder newGhidraParent = (GhidraFolder) newParent; + + GhidraFolder newGhidraParent = GhidraFolder.getDestinationFolder(newParent); + return getFileData().copyTo(newGhidraParent.getFolderData(), monitor != null ? monitor : TaskMonitor.DUMMY); } @Override - public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { - if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { - throw new UnsupportedOperationException("newParent does not support copyToAsLink"); - } - GhidraFolder newGhidraParent = (GhidraFolder) newParent; - return getFileData().copyToAsLink(newGhidraParent.getFolderData()); + public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException { + + GhidraFolder newGhidraParent = GhidraFolder.getDestinationFolder(newParent); + + return getFileData().copyToAsLink(newGhidraParent.getFolderData(), relative); } @Override public DomainFile copyVersionTo(int version, DomainFolder destFolder, TaskMonitor monitor) throws IOException, CancelledException { - if (!GhidraFolder.class.isAssignableFrom(destFolder.getClass())) { - throw new UnsupportedOperationException("destFolder does not support copyVersionTo"); - } - GhidraFolder destGhidraFolder = (GhidraFolder) destFolder; + + GhidraFolder destGhidraFolder = GhidraFolder.getDestinationFolder(destFolder); + return getFileData().copyVersionTo(version, destGhidraFolder.getFolderData(), monitor != null ? monitor : TaskMonitor.DUMMY); } @@ -636,10 +661,15 @@ public class GhidraFile implements DomainFile { @Override public boolean equals(Object obj) { - if (!(obj instanceof GhidraFile)) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof GhidraFile other)) { return false; } - GhidraFile other = (GhidraFile) obj; if (projectData != other.projectData) { return false; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java index 072b9394a7..f718b1b098 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFileData.java @@ -81,6 +81,7 @@ public class GhidraFileData { private GhidraFolderData parent; private String name; private String fileID; + private String linkPath; private LocalFolderItem folderItem; private FolderItem versionedFolderItem; @@ -90,6 +91,7 @@ public class GhidraFileData { private AtomicBoolean busy = new AtomicBoolean(); private boolean mergeInProgress = false; + private boolean openInProgress = false; // TODO: Many of the old methods assumed that the state was up-to-date due to // refreshing ... we are relying on non-refreshed data to be dropped from cache map and no @@ -139,6 +141,7 @@ public class GhidraFileData { } void refresh(LocalFolderItem localFolderItem, FolderItem verFolderItem) { + linkPath = null; icon = null; disabledIcon = null; @@ -155,6 +158,7 @@ public class GhidraFileData { } private boolean refresh() throws IOException { + linkPath = null; String parentPath = parent.getPathname(); if (folderItem == null) { folderItem = fileSystem.getItem(parentPath, name); @@ -392,6 +396,11 @@ public class GhidraFileData { if (parent.containsFile(newName)) { throw new DuplicateFileException("File named " + newName + " already exists."); } + if (isFolderLink() && parent.getFolderData(newName, false) != null) { + // Folder-link file name not permitted to conflict with Folder + throw new DuplicateFileException("Name conflict. Folder named " + name + + " already exists in " + parent.getPathname()); + } String oldName = name; String folderPath = parent.getPathname(); @@ -425,6 +434,10 @@ public class GhidraFileData { } } + boolean isFolderLink() { + return FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(getContentType()); + } + /** * Returns content-type string for this file * @return the file content type or a reserved content type {@link ContentHandler#MISSING_CONTENT} @@ -432,7 +445,7 @@ public class GhidraFileData { */ String getContentType() { synchronized (fileSystem) { - FolderItem item = folderItem != null ? folderItem : versionedFolderItem; + FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION); // this can happen when we are trying to load a version file from // a server to which we are not connected if (item == null) { @@ -450,7 +463,7 @@ public class GhidraFileData { */ ContentHandler getContentHandler() throws IOException { synchronized (fileSystem) { - FolderItem item = folderItem != null ? folderItem : versionedFolderItem; + FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION); // this can happen when we are trying to load a version file from // a server to which we are not connected if (item == null) { @@ -539,40 +552,61 @@ public class GhidraFileData { FolderItem myFolderItem; DomainObjectAdapter domainObj = null; synchronized (fileSystem) { - if (fileSystem.isReadOnly() || isLinkFile()) { - return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor); + if (openInProgress) { + throw new IOException("Circular link reference detected: " + getPathname()); } - domainObj = getOpenedDomainObject(); - if (domainObj != null) { - if (!domainObj.addConsumer(consumer)) { - domainObj = null; - projectData.clearDomainObject(getPathname()); + openInProgress = true; + try { + if (fileSystem.isReadOnly()) { + openInProgress = false; + return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor); } - else { - return domainObj; + if (isLink()) { + String resolvedLinkPath = getLinkPath(true); + if (GhidraURL.isGhidraURL(resolvedLinkPath)) { + openInProgress = false; + return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, + monitor); + } + DomainFile file = projectData.getFile(resolvedLinkPath); + if (file == null) { + throw new FileNotFoundException( + "Linked file not found: " + resolvedLinkPath); + } + return file.getDomainObject(consumer, okToUpgrade, okToRecover, monitor); } - } - ContentHandler contentHandler = getContentHandler(); - if (folderItem == null) { - DomainObjectAdapter doa = contentHandler.getReadOnlyObject(versionedFolderItem, - DomainFile.DEFAULT_VERSION, true, consumer, monitor); - doa.setChanged(false); - DomainFileProxy proxy = new DomainFileProxy(name, parent.getPathname(), doa, - DomainFile.DEFAULT_VERSION, fileID, parent.getProjectLocator()); - proxy.setLastModified(getLastModifiedTime()); - return doa; - } - myFolderItem = folderItem; + domainObj = getOpenedDomainObject(); + if (domainObj != null) { + if (!domainObj.addConsumer(consumer)) { + domainObj = null; + projectData.clearDomainObject(getPathname()); + } + else { + return domainObj; + } + } + ContentHandler contentHandler = getContentHandler(); + if (folderItem == null) { + DomainObjectAdapter doa = contentHandler.getReadOnlyObject(versionedFolderItem, + DomainFile.DEFAULT_VERSION, true, consumer, monitor); + doa.setChanged(false); + DomainFileProxy proxy = new DomainFileProxy(name, parent.getPathname(), doa, + DomainFile.DEFAULT_VERSION, fileID, parent.getProjectLocator()); + proxy.setLastModified(getLastModifiedTime()); + return doa; + } + myFolderItem = folderItem; - domainObj = contentHandler.getDomainObject(myFolderItem, parent.getUserFileSystem(), - FolderItem.DEFAULT_CHECKOUT_ID, okToUpgrade, okToRecover, consumer, monitor); - projectData.setDomainObject(getPathname(), domainObj); + domainObj = contentHandler.getDomainObject(myFolderItem, parent.getUserFileSystem(), + FolderItem.DEFAULT_CHECKOUT_ID, okToUpgrade, okToRecover, consumer, monitor); + projectData.setDomainObject(getPathname(), domainObj); - // Notify file manager of in-use domain object. - // A link-file object is indirect with tracking intiated by the URL-referenced file. - if (!isLinkFile()) { + // Notify file manager of in-use domain object. projectData.trackDomainFileInUse(domainObj); } + finally { + openInProgress = false; + } } // Set domain file for newly opened domain object @@ -622,23 +656,51 @@ public class GhidraFileData { DomainObject getReadOnlyDomainObject(Object consumer, int version, TaskMonitor monitor) throws VersionException, IOException, CancelledException { synchronized (fileSystem) { - FolderItem item = - (folderItem != null && version == DomainFile.DEFAULT_VERSION) ? folderItem - : versionedFolderItem; - DomainObjectAdapter doa = - getContentHandler().getReadOnlyObject(item, version, true, consumer, monitor); - doa.setChanged(false); - - // Notify file manager of in-use domain object. - // A link-file object is indirect with tracking intiated by the URL-referenced file. - if (!isLinkFile()) { - projectData.trackDomainFileInUse(doa); + if (openInProgress) { + throw new IOException("Circular link reference detected: " + getPathname()); } + openInProgress = true; + try { + FolderItem item = getFolderItem(version); - DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), doa, - version, fileID, parent.getProjectLocator()); - proxy.setLastModified(getLastModifiedTime()); - return doa; + DomainObjectAdapter doa; + ContentHandler contentHandler = getContentHandler(); + if (contentHandler instanceof LinkHandler linkHandler) { + String resolvedLinkPath = getLinkPath(true); + + if (!GhidraURL.isGhidraURL(resolvedLinkPath)) { + DomainFile file = projectData.getFile(resolvedLinkPath); + if (file == null) { + throw new FileNotFoundException( + "Linked file not found: " + resolvedLinkPath); + } + return file.getReadOnlyDomainObject(consumer, version, monitor); + } + + // Handle link to Ghidra URL + URL ghidraUrl = new URL(resolvedLinkPath); + doa = linkHandler.getObject(ghidraUrl, version, consumer, monitor, false); + } + else { + doa = contentHandler.getReadOnlyObject(item, version, true, consumer, monitor); + } + + doa.setChanged(false); + + // Notify file manager of in-use domain object. + // A link-file object is indirect with tracking intiated by the URL-referenced file. + if (!isLink()) { + projectData.trackDomainFileInUse(doa); + } + + DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), doa, + version, fileID, parent.getProjectLocator()); + proxy.setLastModified(getLastModifiedTime()); + return doa; + } + finally { + openInProgress = false; + } } } @@ -663,27 +725,48 @@ public class GhidraFileData { DomainObject getImmutableDomainObject(Object consumer, int version, TaskMonitor monitor) throws VersionException, IOException, CancelledException { synchronized (fileSystem) { - DomainObjectAdapter obj = null; - ContentHandler contentHandler = getContentHandler(); - if (versionedFolderItem == null || - (version == DomainFile.DEFAULT_VERSION && folderItem != null) || isHijacked()) { - obj = contentHandler.getImmutableObject(folderItem, consumer, version, -1, monitor); - } - else { - obj = contentHandler.getImmutableObject(versionedFolderItem, consumer, version, -1, - monitor); + if (openInProgress) { + throw new IOException("Circular link reference detected: " + getPathname()); } + openInProgress = true; + try { + FolderItem item = getFolderItem(version); + DomainObjectAdapter doa; + ContentHandler contentHandler = getContentHandler(); + if (contentHandler instanceof LinkHandler linkHandler) { + String resolvedLinkPath = getLinkPath(true); - // Notify file manager of in-use domain object. - // A link-file object is indirect with tracking intiated by the URL-referenced file. - if (!isLinkFile()) { - projectData.trackDomainFileInUse(obj); - } + if (!GhidraURL.isGhidraURL(resolvedLinkPath)) { + DomainFile file = projectData.getFile(resolvedLinkPath); + if (file == null) { + throw new FileNotFoundException( + "Linked file not found: " + resolvedLinkPath); + } + return file.getImmutableDomainObject(consumer, version, monitor); + } - DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), obj, - version, fileID, parent.getProjectLocator()); - proxy.setLastModified(getLastModifiedTime()); - return obj; + // Handle link to Ghidra URL + URL ghidraUrl = new URL(resolvedLinkPath); + doa = linkHandler.getObject(ghidraUrl, version, consumer, monitor, true); + } + else { + doa = contentHandler.getImmutableObject(item, consumer, version, -1, monitor); + } + + // Notify file manager of in-use domain object. + // A link-file object is indirect with tracking intiated by the URL-referenced file. + if (!isLink()) { + projectData.trackDomainFileInUse(doa); + } + + DomainFileProxy proxy = new DomainFileProxy(name, getParent().getPathname(), doa, + version, fileID, parent.getProjectLocator()); + proxy.setLastModified(getLastModifiedTime()); + return doa; + } + finally { + openInProgress = false; + } } } @@ -783,9 +866,9 @@ public class GhidraFileData { } synchronized (fileSystem) { - boolean isLink = isLinkFile(); + boolean isLink = isLink(); - FolderItem item = folderItem != null ? folderItem : versionedFolderItem; + FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION); Icon baseIcon = new TranslateIcon(getBaseIcon(item), 1, 1); @@ -941,7 +1024,7 @@ public class GhidraFileData { versionedFileSystem.isReadOnly()) { return false; } - return !isLinkFile(); + return !isLink(); } catch (IOException e) { return false; @@ -1061,20 +1144,27 @@ public class GhidraFileData { if (!versionedFileSystem.isOnline()) { throw new NotConnectedException("Not connected to repository server"); } + if (folderItem == null) { + throw new FileNotFoundException("File not found"); + } + if (!versionedFileSystem.isSupportedItemType(folderItem)) { + throw new IOException(folderItem.getClass().getSimpleName() + + " not supported by versioned filesystem/repository"); + } if (fileSystem.isReadOnly() || versionedFileSystem.isReadOnly()) { throw new ReadOnlyException( "versioning permitted within writeable project and repository only"); } - if (folderItem == null) { - throw new FileNotFoundException("File not found"); - } + if (folderItem.isCheckedOut() || versionedFolderItem != null) { throw new IOException("File already versioned"); } ContentHandler contentHandler = getContentHandler(); - if (contentHandler instanceof LinkHandler linkHandler) { - // must check local vs remote URL - if (!GhidraURL.isServerRepositoryURL(LinkHandler.getURL(folderItem))) { + if (contentHandler == null) { + throw new IOException("Unsupported content-type: " + getContentType()); + } + if (contentHandler instanceof LinkHandler) { + if (!LinkHandler.canShareLink(folderItem)) { throw new IOException("Local project link-file may not be versioned"); } } @@ -1108,7 +1198,7 @@ public class GhidraFileData { try { inUseDomainObj = getAndLockInUseDomainObjectForMergeUpdate("checkin"); - if (isLinkFile()) { + if (isLink()) { keepCheckedOut = false; } else if (inUseDomainObj != null && !keepCheckedOut) { @@ -1121,8 +1211,7 @@ public class GhidraFileData { String parentPath = parent.getPathname(); String user = ClientUtil.getUserName(); try { - if (folderItem instanceof DatabaseItem) { - DatabaseItem databaseItem = (DatabaseItem) folderItem; + if (folderItem instanceof DatabaseItem databaseItem) { BufferFile bufferFile = databaseItem.open(); try { versionedFolderItem = versionedFileSystem.createDatabase(parentPath, @@ -1133,8 +1222,7 @@ public class GhidraFileData { bufferFile.dispose(); } } - else if (folderItem instanceof DataFileItem) { - DataFileItem dataFileItem = (DataFileItem) folderItem; + else if (folderItem instanceof DataFileItem dataFileItem) { InputStream istream = dataFileItem.getInputStream(); try { versionedFolderItem = versionedFileSystem.createDataFile(parentPath, @@ -1144,6 +1232,11 @@ public class GhidraFileData { istream.close(); } } + else if (folderItem instanceof TextDataItem textDataItem) { + versionedFileSystem.createTextDataItem(parentPath, name, + folderItem.getFileID(), folderItem.getContentType(), + textDataItem.getTextData(), comment); + } else { throw new IOException( "Unable to add unsupported content to version control"); @@ -1198,6 +1291,10 @@ public class GhidraFileData { inUseDomainObj.domainObjectRestored(); } } + catch (UnsupportedOperationException e) { + throw new IOException( + "The repository does not support file content. A newer server version may be required."); + } finally { unlockDomainObject(inUseDomainObj); busy.set(false); @@ -1235,7 +1332,7 @@ public class GhidraFileData { if (!versionedFileSystem.isOnline()) { throw new NotConnectedException("Not connected to repository server"); } - if (isLinkFile()) { + if (isLink()) { return false; } String user = ClientUtil.getUserName(); @@ -2013,8 +2110,8 @@ public class GhidraFileData { } // update checkout data within versioned repository - versionedFolderItem.updateCheckoutVersion(checkoutId, - folderItem.getCheckoutVersion(), ClientUtil.getUserName()); + versionedFolderItem.updateCheckoutVersion(checkoutId, folderItem.getCheckoutVersion(), + ClientUtil.getUserName()); Msg.info(this, "Updated checkout completed for " + name); @@ -2069,12 +2166,14 @@ public class GhidraFileData { throw new ReadOnlyException("moveTo permitted within writeable project only"); } if (getParent().getPathname().equals(newParent.getPathname())) { - throw new IllegalArgumentException("newParent must differ from current parent"); + throw new IllegalArgumentException( + "new parent must differ from current parent: " + newParent); } checkInUse(); + GhidraFolderData oldParent = parent; String oldName = name; - String newName = newParent.getTargetName(name); + String newName = newParent.getUniqueFileName(name, isFolderLink()); try { if (isHijacked()) { fileSystem.moveItem(parent.getPathname(), name, newParent.getPathname(), @@ -2113,6 +2212,18 @@ public class GhidraFileData { } } + /** + * Get the appropriate folder item (private or versioned) based upon the current state and + * targeted file version. + * @param version file version + * @return folder item to be used + */ + private FolderItem getFolderItem(int version) { + return (folderItem != null && (version == DomainFile.DEFAULT_VERSION || isHijacked())) + ? folderItem + : versionedFolderItem; + } + /** * Determine if this file is a link file which corresponds to either a file or folder link. * The {@link DomainObject} referenced by a link-file may be opened using @@ -2120,34 +2231,74 @@ public class GhidraFileData { * {@link #getDomainObject(Object, boolean, boolean, TaskMonitor)} method may also be used * to obtain a read-only instance. {@link #getImmutableDomainObject(Object, int, TaskMonitor)} * use is not supported. - * The URL stored within the link-file may be read using {@link #getLinkFileURL()}. + * The link path or URL stored within the link-file may be read using {@link #getLinkPath(boolean)}. * The content type (see {@link #getContentType()} of a link file will differ from that of the * linked object (e.g., "LinkedProgram" vs "Program"). * @return true if link file else false for a normal domain file */ - boolean isLinkFile() { - synchronized (fileSystem) { - try { - return LinkHandler.class.isAssignableFrom(getContentHandler().getClass()); - } - catch (IOException e) { - return false; - } + boolean isLink() { + try { + return LinkHandler.class.isAssignableFrom(getContentHandler().getClass()); + } + catch (IOException e) { + return false; } } /** - * Get URL associated with a link-file. The URL returned may reference either a folder - * or a file within another project/repository. - * @return link-file URL or null if not a link-file - * @throws IOException if an IO error occurs + * If this is a {@link #isLink() link file} this method will return the link-path which + * may be either an absolute or relative path within the the project or a Ghidra URL. + * + * @param resolve if true relative paths will always be converted to an absolute path + * @return associated link path or null if not a link file + * @throws IOException if an IO error occurs or resolving a relative link-path produced + * an invalid path. */ - URL getLinkFileURL() throws IOException { - if (!isLinkFile()) { + String getLinkPath(boolean resolve) throws IOException { + if (!isLink()) { return null; } - FolderItem item = folderItem != null ? folderItem : versionedFolderItem; - return LinkHandler.getURL(item); + + if (linkPath == null) { + FolderItem item = getFolderItem(DomainFile.DEFAULT_VERSION); + linkPath = LinkHandler.getLinkPath(item); + if (linkPath == null) { + linkPath = ""; // avoid repeated attempts + } + } + + if (StringUtils.isBlank(linkPath)) { + return null; + } + + if (!resolve) { + return linkPath; + } + + String path = linkPath; + if (!GhidraURL.isGhidraURL(linkPath)) { + path = getAbsolutePath(linkPath); + } + + return path; + } + + private String getAbsolutePath(String path) throws IOException { + String absPath = path; + if (!path.startsWith(FileSystem.SEPARATOR)) { + absPath = getParent().getPathname(); + if (!absPath.endsWith(FileSystem.SEPARATOR)) { + absPath += FileSystem.SEPARATOR; + } + absPath += path; + } + try { + absPath = FileSystem.normalizePath(absPath); + } + catch (IllegalArgumentException e) { + throw new IOException("Invalid link path: " + linkPath); + } + return absPath; } /** @@ -2177,16 +2328,19 @@ public class GhidraFileData { * managed project) the generated link will refer to the remote file with a remote * Ghidra URL, otherwise a local project storage path will be used. * @param newParent new parent folder + * @param relative if true, and this file is contained within the same active + * {@link ProjectData} instance as {@code newParent}, an internal-project relative path + * file-link will be created. * @return newly created domain file or null if content type does not support link use. * @throws IOException if an IO or access error occurs. */ - DomainFile copyToAsLink(GhidraFolderData newParent) throws IOException { + DomainFile copyToAsLink(GhidraFolderData newParent, boolean relative) throws IOException { synchronized (fileSystem) { LinkHandler lh = getContentHandler().getLinkHandler(); if (lh == null) { return null; } - return newParent.copyAsLink(projectData, getPathname(), name, lh); + return newParent.createLinkFile(projectData, getPathname(), relative, name, lh); } } @@ -2196,6 +2350,7 @@ public class GhidraFileData { * @param monitor task monitor * @return newly created domain file * @throws FileInUseException if this file is in-use / checked-out. + * @throws DuplicateFileException if this file's name already exists in newParent * @throws IOException if an IO or access error occurs. * @throws CancelledException if task monitor cancelled operation. */ @@ -2208,7 +2363,7 @@ public class GhidraFileData { FolderItem item = folderItem != null ? folderItem : versionedFolderItem; String pathname = newParent.getPathname(); String contentType = item.getContentType(); - String targetName = newParent.getTargetName(name); + String targetName = newParent.getUniqueFileName(name, isFolderLink()); String user = ClientUtil.getUserName(); try { if (item instanceof DatabaseItem) { @@ -2222,6 +2377,36 @@ public class GhidraFileData { bufferFile.dispose(); } } + else if (item instanceof TextDataItem) { + ContentHandler contentHandler = + DomainObjectAdapter.getContentHandler(contentType); + if (contentHandler instanceof LinkHandler) { + String lp = getLinkPath(true); + if (!GhidraURL.isGhidraURL(lp) && + !parent.getProjectLocator().equals(newParent.getProjectLocator())) { + // Force use of external URL for link copy from another project + URL url = LinkHandler.getLinkURL(getDomainFile()); + if (url != null) { + lp = url.toString(); + } + } + else { + lp = LinkHandler.getLinkPath(item); + } + if (!StringUtils.isBlank(lp)) { + newParent.getLocalFileSystem() + .createTextDataItem(pathname, targetName, + FileIDFactory.createFileID(), contentType, lp, null); + } + else { + throw new IOException( + "Invalid link-file item in copyTo: " + item.getPathName()); + } + } + else { + throw new IOException("Unsupported item in copyTo: " + item.getPathName()); + } + } else if (item instanceof DataFileItem) { InputStream istream = ((DataFileItem) item).getInputStream(); try { @@ -2234,7 +2419,7 @@ public class GhidraFileData { } } else { - throw new IOException("Unable to copy unsupported content"); + throw new IOException("Unsupported item in copyTo: " + item.getPathName()); } } catch (InvalidNameException e) { @@ -2260,6 +2445,9 @@ public class GhidraFileData { if (destFolder.getLocalFileSystem().isReadOnly()) { throw new ReadOnlyException("copyVersionTo permitted to writeable project"); } + if (isFolderLink()) { + throw new UnsupportedOperationException("Cannot copy folder-link version"); + } if (versionedFolderItem == null) { return null; // NOTE: versioned file system may be offline } @@ -2268,7 +2456,7 @@ public class GhidraFileData { } String pathname = destFolder.getPathname(); String contentType = versionedFolderItem.getContentType(); - String targetName = destFolder.getTargetName(name + "_v" + version); + String targetName = destFolder.getUniqueFileName(name + "_v" + version, false); String user = ClientUtil.getUserName(); try { BufferFile bufferFile = ((DatabaseItem) versionedFolderItem).open(version); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java index f547eca92b..4d1cbc3e22 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolder.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,8 +24,7 @@ import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.store.FileSystem; import ghidra.framework.store.local.LocalFileSystem; -import ghidra.util.InvalidNameException; -import ghidra.util.Msg; +import ghidra.util.*; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -94,7 +93,7 @@ public class GhidraFolder implements DomainFolder { /** * Create folder hierarchy in local filesystem if it does not already exist - * @param folderName + * @param folderName name of new folder * @return folder data * @throws IOException error while creating folder */ @@ -325,6 +324,19 @@ public class GhidraFolder implements DomainFolder { monitor != null ? monitor : TaskMonitor.DUMMY); } + @Override + public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname, + boolean makeRelative, String linkFilename, LinkHandler lh) throws IOException { + return createFolderData().createLinkFile(sourceProjectData, pathname, makeRelative, + linkFilename, lh); + } + + @Override + public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler lh) + throws IOException { + return createFolderData().createLinkFile(ghidraUrl, linkFilename, lh); + } + @Override public GhidraFolder createFolder(String folderName) throws InvalidNameException, IOException { return createFolderData().createFolder(folderName).getDomainFolder(); @@ -340,43 +352,64 @@ public class GhidraFolder implements DomainFolder { } } + static GhidraFolder getDestinationFolder(DomainFolder newParent) throws IOException { + + while (newParent instanceof LinkedDomainFolder linkedFolder) { + + if (!linkedFolder.isInWritableProject()) { + throw new IOException("Destination folder is not within writable project"); + } + + // Find real folder - we may have multiple levels of linking + // This should only be done within the same writable project + newParent = linkedFolder.getRealFolder(); + + } + + if (!newParent.isInWritableProject() || !(newParent instanceof GhidraFolder ghidraFolder)) { + throw new IOException("Destination folder is not within writable project"); + } + + return ghidraFolder; + } + @Override public GhidraFolder moveTo(DomainFolder newParent) throws IOException { if (parent == null) { throw new UnsupportedOperationException("root folder may not be moved"); } - if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { - throw new UnsupportedOperationException("newParent does not support moveTo"); + + if (getProjectData() != newParent.getProjectData() || !isInWritableProject()) { + throw new IOException("Move only supported within the same writable project"); } - GhidraFolderData folderData = getFolderData(); - GhidraFolder newGhidraParent = (GhidraFolder) newParent; - return folderData.moveTo(newGhidraParent.getFolderData()); + + GhidraFolder newGhidraParent = getDestinationFolder(newParent); + + return getFolderData().moveTo(newGhidraParent.getFolderData()); } @Override public GhidraFolder copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, CancelledException { - GhidraFolderData folderData = getFolderData(); - if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { - throw new UnsupportedOperationException("newParent does not support copyTo"); - } - GhidraFolder newGhidraParent = (GhidraFolder) newParent; - return folderData.copyTo(newGhidraParent.getFolderData(), + + GhidraFolder newGhidraParent = getDestinationFolder(newParent); + + return getFolderData().copyTo(newGhidraParent.getFolderData(), monitor != null ? monitor : TaskMonitor.DUMMY); } @Override - public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { - GhidraFolderData folderData = getFolderData(); - if (!GhidraFolder.class.isAssignableFrom(newParent.getClass())) { - throw new UnsupportedOperationException("newParent does not support copyToAsLink"); - } - GhidraFolder newGhidraParent = (GhidraFolder) newParent; - return folderData.copyToAsLink(newGhidraParent.getFolderData()); + public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException { + + GhidraFolder newGhidraParent = getDestinationFolder(newParent); + + return getFolderData().copyToAsLink(newGhidraParent.getFolderData(), relative); } /** - * used for testing + * ** Used for testing ** + * Check for existance of private folder + * @return true if private folder exists else false */ boolean privateExists() { try { @@ -388,7 +421,9 @@ public class GhidraFolder implements DomainFolder { } /** - * used for testing + * ** Used for testing ** + * Check for existance of versioned/shared folder + * @return true if versioned/shared folder exists else false */ boolean sharedExists() { try { @@ -406,16 +441,56 @@ public class GhidraFolder implements DomainFolder { @Override public boolean equals(Object obj) { - if (!(obj instanceof GhidraFolder)) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof GhidraFolder other)) { return false; } - GhidraFolder other = (GhidraFolder) obj; if (projectData != other.projectData) { return false; } return getPathname().equals(other.getPathname()); } + @Override + public boolean isSameOrAncestor(DomainFolder folder) { + + if (!getProjectLocator().equals(folder.getProjectLocator()) && + !SystemUtilities.isEqual(projectData.getSharedProjectURL(), + folder.getProjectData().getSharedProjectURL())) { + // Containing project/repository appears to be unrelated + return false; + } + + String pathname = getPathname(); + + DomainFolder f = folder; + while (f != null) { + if (f == this || pathname.equals(f.getPathname())) { + return true; + } + f = f.getParent(); + } + return false; + } + + @Override + public boolean isSame(DomainFolder folder) { + + if (!getProjectLocator().equals(folder.getProjectLocator()) && + !SystemUtilities.isEqual(projectData.getSharedProjectURL(), + folder.getProjectData().getSharedProjectURL())) { + // Containing project/repository appears to be unrelated + return false; + } + + return getPathname().equals(folder.getPathname()); + } + @Override public int hashCode() { return getPathname().hashCode(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java index 6a5c786ea4..03c7736d37 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/GhidraFolderData.java @@ -17,16 +17,18 @@ package ghidra.framework.data; import java.io.*; import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.protocol.ghidra.TransientProjectData; +import ghidra.framework.store.*; import ghidra.framework.store.FileSystem; -import ghidra.framework.store.FolderItem; -import ghidra.framework.store.FolderNotEmptyException; -import ghidra.framework.store.local.*; +import ghidra.framework.store.local.LocalFileSystem; +import ghidra.framework.store.local.LocalFolderItem; import ghidra.util.*; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; @@ -211,11 +213,21 @@ class GhidraFolderData { } /** - * Return this folder's name. - * @return the name + * {@return this folder's name. The root folder will return the project or repository name} */ String getName() { - return name; + // Allow root folder to use project/repository name + return parent != null ? name : projectData.getProjectLocator().getName(); + } + + private void checkFolderLinkConflict(String folderName) throws DuplicateFileException { + GhidraFile file = getDomainFile(folderName); + if (file != null && file.isFolderLink()) { + // Folder-link file name not permitted to conflict with Folder + // NOTE: There is still lthe possibility of conflicting with offline version filesystem + throw new DuplicateFileException("Name conflict. Folder-link named " + folderName + + " already exists in " + getPathname()); + } } /** @@ -224,7 +236,7 @@ class GhidraFolderData { * @return renamed domain file (the original DomainFolder object becomes invalid since it is * immutable) * @throws InvalidNameException if newName contains illegal characters - * @throws DuplicateFileException if a folder named newName + * @throws DuplicateFileException if a folder of folder-link named newName * already exists in this files domain folder. * @throws FileInUseException if any file within this folder or its descendants is * in-use / checked-out. @@ -233,11 +245,14 @@ class GhidraFolderData { GhidraFolder setName(String newName) throws InvalidNameException, IOException { synchronized (fileSystem) { if (parent == null || fileSystem.isReadOnly()) { - throw new UnsupportedOperationException("setName not permitted on this folder"); + throw new IOException("setName not permitted on this folder"); } + updateExistenceState(); checkInUse(); + parent.checkFolderLinkConflict(newName); + String oldName = name; String parentPath = parent.getPathname(); @@ -650,7 +665,7 @@ class GhidraFolderData { Msg.error(this, "Project folder contains " + nullNameCount + " null items: " + getPathname()); } - if (badItemCount != 0) { + if (unknownItemCount != 0) { Msg.error(this, "Project folder contains " + unknownItemCount + " unsupported items: " + getPathname()); } @@ -1072,7 +1087,7 @@ class GhidraFolderData { * Create a subfolder within this folder. * @param folderName sub-folder name * @return the new folder - * @throws DuplicateFileException if a folder by this name already exists + * @throws DuplicateFileException if a folder or folder-link with this name already exists * @throws InvalidNameException if name is an empty string of if it contains characters other * than alphanumerics. * @throws IOException if IO or access error occurs @@ -1082,6 +1097,9 @@ class GhidraFolderData { if (fileSystem.isReadOnly()) { throw new AssertException("createFile permitted within writeable project only"); } + + checkFolderLinkConflict(folderName); + fileSystem.createFolder(getPathname(), folderName); folderChanged(folderName); @@ -1102,6 +1120,9 @@ class GhidraFolderData { if (fileSystem.isReadOnly()) { throw new AssertException("delete permitted within writeable project only"); } + if (parent == null) { + throw new IOException("root folder may not be deleted"); + } checkInUse(); try { fileSystem.deleteFolder(getPathname()); @@ -1150,19 +1171,26 @@ class GhidraFolderData { GhidraFolder moveTo(GhidraFolderData newParent) throws IOException { synchronized (fileSystem) { if (newParent.getLocalFileSystem() != fileSystem || fileSystem.isReadOnly()) { - throw new AssertException("moveTo permitted within writeable project only"); + throw new IOException("moveTo permitted within writeable project only"); + } + if (parent == null) { + throw new IOException("root folder may not be moved"); } if (getPathname().equals(newParent.getPathname())) { - throw new IllegalArgumentException("newParent must differ from current parent"); + throw new IllegalArgumentException( + "new parent must differ from current parent: " + newParent); } checkInUse(); + if (newParent.containsFolder(name)) { + throw new DuplicateFileException( + "Folder named " + name + " already exists in " + newParent); + } + + newParent.checkFolderLinkConflict(name); + updateExistenceState(); try { - if (newParent.containsFolder(name)) { - throw new DuplicateFileException( - "Folder named " + getName() + " already exists in " + newParent); - } if (folderExists) { fileSystem.moveFolder(parent.getPathname(), name, newParent.getPathname()); @@ -1210,21 +1238,13 @@ class GhidraFolderData { } /** - * Determine if the specified folder if an ancestor of this folder - * (i.e., parent, grand-parent, etc.). + * {@return true if the specified folder is an ancestor of this folder + * (i.e., parent, grand-parent, etc.).} * @param folderData folder to be checked - * @return true if the specified folder if an ancestor of this folder */ boolean isAncestor(GhidraFolderData folderData) { - if (!folderData.projectData.getProjectLocator().equals(projectData.getProjectLocator())) { - // check if projects share a common repository - RepositoryAdapter myRepository = projectData.getRepository(); - RepositoryAdapter otherRepository = folderData.projectData.getRepository(); - if (myRepository == null || otherRepository == null || - !myRepository.getServerInfo().equals(otherRepository.getServerInfo()) || - !myRepository.getName().equals(otherRepository.getName())) { - return false; - } + if (!hasSameProjectOrRepository(folderData)) { + return false; } GhidraFolderData checkParent = folderData; while (checkParent != null) { @@ -1236,6 +1256,37 @@ class GhidraFolderData { return false; } + /** + * {@return true if the specified folder is associated with the same project or repository as + * this folder.} + * @param folderData folder to be checked + */ + boolean hasSameProjectOrRepository(GhidraFolderData folderData) { + if (folderData.projectData.getProjectLocator().equals(projectData.getProjectLocator())) { + return true; + } + // check if projects share a common repository + RepositoryAdapter myRepository = projectData.getRepository(); + RepositoryAdapter otherRepository = folderData.projectData.getRepository(); + if (myRepository == null || otherRepository == null || + !myRepository.getServerInfo().equals(otherRepository.getServerInfo()) || + !myRepository.getName().equals(otherRepository.getName())) { + return false; + } + return true; + } + + /** + * {@return true if the specified folder is considered the same as this folder.} + * @param folderData folder to be checked + */ + boolean isSame(GhidraFolderData folderData) { + if (!hasSameProjectOrRepository(folderData)) { + return false; + } + return getPathname().equals(folderData.getPathname()); + } + /** * Copy this folder into the newParent folder. * @param newParent new parent folder @@ -1255,16 +1306,19 @@ class GhidraFolderData { if (isAncestor(newParent)) { throw new IOException("self-referencing copy not permitted"); } - GhidraFolderData newFolderData = newParent.getFolderData(name, false); - + String folderName = getName(); + GhidraFolderData newFolderData = newParent.getFolderData(folderName, false); if (newFolderData == null) { try { - newFolderData = newParent.createFolder(name); + newFolderData = newParent.createFolder(folderName); } catch (InvalidNameException e) { throw new AssertException("Unexpected error", e); } } + else if (isSame(newFolderData)) { + throw new IOException("self-referencing copy not permitted"); + } List files = getFileNames(); for (String file : files) { monitor.checkCancelled(); @@ -1296,13 +1350,16 @@ class GhidraFolderData { * managed project) the generated link will refer to the remote folder with a remote * Ghidra URL, otherwise a local project storage path will be used. * @param newParent new parent folder where link-file is to be created - * @return newly created domain file (i.e., link-file) or null if link use not supported. + * @param relative if true, and this file is contained within the same active + * {@link ProjectData} instance as {@code newParent}, an internal-project relative path + * file-link will be created. + * @return newly created domain file which is a folder-link (i.e., link-file). * @throws IOException if an IO or access error occurs. */ - DomainFile copyToAsLink(GhidraFolderData newParent) throws IOException { + DomainFile copyToAsLink(GhidraFolderData newParent, boolean relative) throws IOException { synchronized (fileSystem) { String linkFilename = name; - if (linkFilename == null) { + if (linkFilename == null) { // create name for link to root folder if (projectData instanceof TransientProjectData) { linkFilename = projectData.getRepository().getName(); } @@ -1310,66 +1367,74 @@ class GhidraFolderData { linkFilename = projectData.getProjectLocator().getName(); } } - return newParent.copyAsLink(projectData, getPathname(), linkFilename, + + return newParent.createLinkFile(projectData, getPathname(), relative, linkFilename, FolderLinkContentHandler.INSTANCE); } } /** - * Create a link-file within this folder. The link-file may correspond to various types of - * content (e.g., Program, Trace, Folder, etc.) based upon specified link handler. + * Create a link-file within this folder which references the specified file or folder + * {@code pathname} within the project specified by {@code sourceProjectData}. The link-file + * may correspond to various types of content (e.g., Program, Trace, Folder, etc.) based upon + * the specified {@link LinkHandler} instance. + * * @param sourceProjectData referenced content project data within which specified path exists. - * @param pathname path of referenced content with source project data - * @param linkFilename name of link-file to be created within this folder. - * @param lh link file handler used to create specific link file. - * @return link-file + * If this differ's from this folder's project a Ghidra URL will be used, otherwise and internal + * link path reference will be used. + * @param pathname an absolute path of project folder or file within the specified source + * project data (a Ghidra URL is not permitted) + * @param makeRelative if true, and this file is contained within the same active + * {@link ProjectData} instance as {@code newParent}, an internal-project relative path + * link-file will be created. + * @param linkFilename name of link-file to be created within this folder. NOTE: This name may + * be modified to ensure uniqueness within this folder. + * @param lh link-file handler used to create specific link-file (see derived implementations + * of {@link LinkHandler} and their public static INSTANCE. + * @return newly created link-file * @throws IOException if IO error occurs during link creation */ - DomainFile copyAsLink(ProjectData sourceProjectData, String pathname, String linkFilename, - LinkHandler lh) throws IOException { + DomainFile createLinkFile(ProjectData sourceProjectData, String pathname, boolean makeRelative, + String linkFilename, LinkHandler lh) throws IOException { synchronized (fileSystem) { if (fileSystem.isReadOnly()) { throw new ReadOnlyException("copyAsLink permitted to writeable project only"); } - if (sourceProjectData == projectData) { - // internal linking not yet supported - Msg.error(this, "Internal file/folder links not yet supported"); - return null; + if (!pathname.startsWith(FileSystem.SEPARATOR)) { + throw new IllegalArgumentException("invalid pathname specified"); } - URL ghidraUrl = null; - if (sourceProjectData instanceof TransientProjectData) { + String linkPath; + if (sourceProjectData == projectData) { + if (makeRelative) { + linkPath = getRelativePath(pathname, getPathname()); + } + else { + linkPath = pathname; + } + } + else if (sourceProjectData instanceof TransientProjectData) { RepositoryAdapter repository = sourceProjectData.getRepository(); ServerInfo serverInfo = repository.getServerInfo(); - ghidraUrl = GhidraURL.makeURL(serverInfo.getServerName(), + URL ghidraUrl = GhidraURL.makeURL(serverInfo.getServerName(), serverInfo.getPortNumber(), repository.getName(), pathname); + linkPath = ghidraUrl.toExternalForm(); } else { ProjectLocator projectLocator = sourceProjectData.getProjectLocator(); if (projectLocator.equals(projectData.getProjectLocator())) { return null; // local internal linking not supported } - ghidraUrl = GhidraURL.makeURL(projectLocator, pathname, null); + URL ghidraUrl = GhidraURL.makeURL(projectLocator, pathname, null); + linkPath = ghidraUrl.toExternalForm(); } - String newName = linkFilename; - int i = 1; - while (true) { - GhidraFileData fileData = getFileData(newName, false); - if (fileData != null) { - // return existing file if link URL matches - if (ghidraUrl.equals(fileData.getLinkFileURL())) { - return getDomainFile(newName); - } - newName = linkFilename + "." + i; - ++i; - } - break; - } + // Force use of unique link-file name + String newName = getUniqueName(linkFilename); try { - lh.createLink(ghidraUrl, fileSystem, getPathname(), newName); + lh.createLink(linkPath, fileSystem, getPathname(), newName); } catch (InvalidNameException e) { throw new IOException(e); // unexpected @@ -1381,17 +1446,84 @@ class GhidraFolderData { } /** - * Generate a non-conflicting file name for this folder based upon the specified preferred name. + * Create an external link-file within this folder which references the specified + * {@code ghidraUrl} and whose content is defined by the specified {@link LinkHandler lh} + * instance. + * + * @param ghidraUrl a Ghidra URL which corresponds to a file or a folder based on the designated + * {@link LinkHandler lh} instance. Only rudimentary URL checks are performed. + * @param linkFilename name of link-file to be created within this folder. NOTE: This name may + * be modified to ensure uniqueness within this folder. + * @param lh link-file handler used to create specific link-file (see derived implementations + * of {@link LinkHandler} and their public static INSTANCE. + * @return newly created link-file + * @throws IOException if IO error occurs during link creation + */ + DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler lh) + throws IOException { + + URL url = new URL(ghidraUrl); + if (!GhidraURL.isLocalGhidraURL(ghidraUrl) && !GhidraURL.isServerRepositoryURL(url)) { + throw new IllegalArgumentException("Invalid Ghidra URL specified"); + } + + // Force use of unique link-file name + String newName = getUniqueName(linkFilename); + + try { + lh.createLink(ghidraUrl, fileSystem, getPathname(), newName); + } + catch (InvalidNameException e) { + throw new IOException(e); // unexpected + } + + fileChanged(newName); + return getDomainFile(newName); + } + + private String getUniqueName(String name) throws IOException { + String newName = name; + int i = 1; + while (true) { + // Check for unique name considering both files and folders + // NOTE: If disconnected from repository, remote content is not considered + if (getFileData(newName, false) == null && getFolderData(newName, false) == null) { + return newName; + } + newName = name + "." + i; + ++i; + } + } + + private static String getRelativePath(String referencedPathname, String linkParentPathname) { + Path referencedPath = Paths.get(referencedPathname); + Path linkParentPath = Paths.get(linkParentPathname); + Path relativePath = linkParentPath.relativize(referencedPath); + String path = relativePath.toString(); + if (referencedPathname.endsWith(FileSystem.SEPARATOR) && + !path.endsWith(FileSystem.SEPARATOR)) { + path += FileSystem.SEPARATOR; + } + return path; + } + + /** + * Generate a non-conflicting file name for this destination folder based upon the specified + * preferred name. * NOTE: This method is subject to race conditions where returned name could conflict by the - * time it is actually used. + * time it is actually used or names already present in repository while disconnected, etc. * @param preferredName preferred file name + * @param checkFilesAndFolders if true ensure name is unique relative to both files and folders, + * else unqiue among files only. * @return non-conflicting file name * @throws IOException if an IO error occurs during file checks */ - String getTargetName(String preferredName) throws IOException { + String getUniqueFileName(String preferredName, boolean checkFilesAndFolders) + throws IOException { String newName = preferredName; int i = 1; - while (getFileData(newName, false) != null) { + while (getFileData(newName, false) != null || + (checkFilesAndFolders && containsFolder(newName))) { newName = preferredName + "." + i; i++; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java index 0951a7319c..5c321bfc0a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,20 +18,24 @@ package ghidra.framework.data; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import java.util.Map; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import javax.swing.Icon; +import org.apache.commons.lang3.StringUtils; + import generic.theme.GIcon; import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.*; -import ghidra.framework.store.FileSystem; -import ghidra.framework.store.FolderItem; +import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl; +import ghidra.framework.store.*; import ghidra.framework.store.local.LocalFileSystem; import ghidra.util.InvalidNameException; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; +import utility.function.Dummy; /** * NOTE: ALL ContentHandler implementations MUST END IN "ContentHandler". If not, @@ -42,59 +46,110 @@ import ghidra.util.task.TaskMonitor; * * @param {@link URLLinkObject} implementation class */ -public abstract class LinkHandler extends DBContentHandler { +public abstract class LinkHandler implements ContentHandler { + /** + * Legacy linkPath metadata key for database storage + */ public static final String URL_METADATA_KEY = "link.url"; - // 16x16 link icon where link is placed in lower-left corner + /** + * 16x16 link icon where link is placed in lower-left corner + */ public static final Icon LINK_ICON = new GIcon("icon.content.handler.link.overlay"); + /** + * {@link LinkStatus} provides a link evaluation for its ulimate type or if it is + * considered broken. See {@link LinkHandler#getLinkFileStatus(DomainFile, Consumer)}. + */ + public enum LinkStatus { + + /** + * The link-file specified does not refer to a valid file or content-type. + */ + BROKEN, + + /** + * The link-file ultimately refers to a file or folder path within the same project. + */ + INTERNAL, + + /** + * The link-file ultimately refers to an external project/repository path with a Ghidra URL. + */ + EXTERNAL, + + /** + * The specified file is not a link-file + */ + NON_LINK; + } + + @Override + public LinkHandler getLinkHandler() { + return this; // allow links to the same type of link + } + /** * Create a link file using the specified URL - * @param ghidraUrl link URL (must be a Ghidra URL - see {@link GhidraURL}). + * @param linkPath link path or Ghidra URL. * @param fs filesystem where link file should be created * @param folderPath folder path which should contain link file * @param linkFilename link filename * @throws IOException if an IO error occurs * @throws InvalidNameException if invalid folderPath or linkFilename specified */ - protected final void createLink(URL ghidraUrl, LocalFileSystem fs, String folderPath, + protected final void createLink(String linkPath, LocalFileSystem fs, String folderPath, String linkFilename) throws IOException, InvalidNameException { - URLLinkObject link = new URLLinkObject(linkFilename, ghidraUrl, this); - try { - createFile(fs, null, folderPath, linkFilename, link, TaskMonitor.DUMMY); - } - catch (CancelledException e) { - throw new AssertException(e); // won't happen - } - finally { - link.release(this); - } + + fs.createTextDataItem(folderPath, linkFilename, FileIDFactory.createFileID(), + getContentType(), linkPath, null); + } + + @Override + public final long createFile(FileSystem fs, FileSystem userfs, String path, String name, + DomainObject domainObject, TaskMonitor monitor) + throws IOException, InvalidNameException, CancelledException { + throw new UnsupportedOperationException("createLink must be used for link-file"); + } + + @Override + public final T getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, + boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor) + throws IOException, CancelledException, VersionException { + throw new UnsupportedOperationException("getObject must be used for link-file"); } @Override public final T getReadOnlyObject(FolderItem item, int version, boolean okToUpgrade, Object consumer, TaskMonitor monitor) throws IOException, VersionException, CancelledException { - if (!okToUpgrade) { - throw new UnsupportedOperationException("okToUpgrade must be true for link-file"); - } - return getObject(item, version, consumer, monitor, false); + throw new UnsupportedOperationException("getObject must be used for link-file"); } @Override public T getImmutableObject(FolderItem item, Object consumer, int version, int minChangeVersion, TaskMonitor monitor) throws IOException, CancelledException, VersionException { - if (minChangeVersion != -1) { - throw new UnsupportedOperationException("minChangeVersion must be -1 for link-file"); - } - return getObject(item, version, consumer, monitor, true); + throw new UnsupportedOperationException("getObject must be used for link-file"); } - private T getObject(FolderItem item, int version, Object consumer, TaskMonitor monitor, - boolean immutable) throws IOException, VersionException, CancelledException { + /** + * Get immutable or read-only domain object based upon an initial external GhidraURL. + * @param ghidraUrl external URL + * @param version {@link DomainFile} version (ignored if URL end-point is a DomainFile since + * the {@link GhidraURLConnection} has no way to convey the version. + * @param consumer domain object consumer + * @param monitor task monitor + * @param immutable true if object is open immutable (no upgrade support), else read-only + * @return domain object + * @throws IOException if an IO error occurs + * @throws VersionException if a version exception prevents opening the file + * @throws CancelledException if task is cancelled + */ + T getObject(URL ghidraUrl, int version, Object consumer, TaskMonitor monitor, boolean immutable) + throws IOException, VersionException, CancelledException { - URL ghidraUrl = getURL(item); + // TODO: may not have insight into version associated with a link-file Class domainObjectClass = getDomainObjectClass(); if (domainObjectClass == null) { @@ -103,32 +158,36 @@ public abstract class LinkHandler extends DBCon AtomicReference verExcRef = new AtomicReference<>(); AtomicReference domainObjectRef = new AtomicReference<>(); - GhidraURLQuery.queryUrl(ghidraUrl, new GhidraURLResultHandlerAdapter(true) { + GhidraURLQuery.queryUrl(ghidraUrl, getDomainObjectClass(), + new GhidraURLResultHandlerAdapter(true) { + // GhidraURLQuery will perform the link-following + @Override + public void processResult(DomainFile domainFile, URL url, TaskMonitor m) + throws IOException, CancelledException { + if (!getDomainObjectClass() + .isAssignableFrom(domainFile.getDomainObjectClass())) { + throw new BadLinkException("Expected " + getDomainObjectClass() + + " but linked to " + domainFile.getDomainObjectClass()); + } + try { + @SuppressWarnings("unchecked") + T linkedObject = immutable + ? (T) domainFile.getImmutableDomainObject(consumer, version, + monitor) + : (T) domainFile.getReadOnlyDomainObject(consumer, version, + monitor); + domainObjectRef.set(linkedObject); + } + catch (VersionException e) { + verExcRef.set(e); + } + } - @Override - public void processResult(DomainFile domainFile, URL url, TaskMonitor m) - throws IOException, CancelledException { - if (!getDomainObjectClass().isAssignableFrom(domainFile.getDomainObjectClass())) { - throw new BadLinkException("Expected " + getDomainObjectClass() + - " but linked to " + domainFile.getDomainObjectClass()); + @Override + public void handleUnauthorizedAccess(URL url) throws IOException { + throw new IOException("Authorization failure"); } - try { - @SuppressWarnings("unchecked") - T linkedObject = immutable - ? (T) domainFile.getImmutableDomainObject(consumer, version, monitor) - : (T) domainFile.getReadOnlyDomainObject(consumer, version, monitor); - domainObjectRef.set(linkedObject); - } - catch (VersionException e) { - verExcRef.set(e); - } - } - - @Override - public void handleUnauthorizedAccess(URL url) throws IOException { - throw new IOException("Authorization failure"); - } - }, monitor); + }, LinkFileControl.FOLLOW_EXTERNAL, monitor); VersionException versionException = verExcRef.get(); if (versionException != null) { @@ -138,19 +197,11 @@ public abstract class LinkHandler extends DBCon T domainObj = domainObjectRef.get(); if (domainObj == null) { throw new IOException( - "Failed to obtain linked object for unknown reason: " + item.getPathName()); + "Failed to obtain linked object for unknown reason: " + ghidraUrl); } return domainObj; } - @Override - public final T getDomainObject(FolderItem item, FileSystem userfs, long checkoutId, - boolean okToUpgrade, boolean okToRecover, Object consumer, TaskMonitor monitor) - throws IOException, CancelledException, VersionException { - // getReadOnlyObject or getImmutableObject should be used - throw new UnsupportedOperationException("link-file does not support getDomainObject"); - } - @Override public final ChangeSet getChangeSet(FolderItem versionedFolderItem, int olderVersion, int newerVersion) throws VersionException, IOException { @@ -168,49 +219,6 @@ public abstract class LinkHandler extends DBCon throw new UnsupportedOperationException("Link file requires checking server vs local URL"); } - /** - * Get the link URL which corresponds to the specified link file. - * See {@link DomainFile#isLinkFile()}. - * @param linkFile link-file domain file - * @return link URL - * @throws MalformedURLException if link is bad or unsupported. - * @throws IOException if IO error or supported link file not specified - */ - public static URL getURL(DomainFile linkFile) throws IOException { - String contentType = linkFile.getContentType(); - ContentHandler ch = DomainObjectAdapter.getContentHandler(contentType); - if (ch instanceof LinkHandler) { - Map metadata = linkFile.getMetadata(); - String urlStr = metadata.get(URL_METADATA_KEY); - if (urlStr != null) { - return new URL(urlStr); - } - } - throw new IOException("Invalid link file: " + contentType); - } - - /** - * Get the link URL which corresponds to the specified link file. - * See {@link DomainFile#isLinkFile()}. - * @param linkFile link-file folder item - * @return link URL - * @throws MalformedURLException if link is bad or unsupported. - * @throws IOException if IO error or supported link file not specified - */ - static URL getURL(FolderItem linkFile) throws IOException { - - String contentType = linkFile.getContentType(); - ContentHandler ch = DomainObjectAdapter.getContentHandler(contentType); - if (ch instanceof LinkHandler) { - Map metadata = GhidraFileData.getMetadata(linkFile); - String urlStr = metadata.get(URL_METADATA_KEY); - if (urlStr != null) { - return new URL(urlStr); - } - } - throw new IOException("Invalid link file: " + contentType); - } - /** * Get the base icon for this link-file which does not include the * link overlay icon. @@ -218,4 +226,322 @@ public abstract class LinkHandler extends DBCon @Override abstract public Icon getIcon(); + ////////////////////////// + // Static package methods + ////////////////////////// + + /** + * Determine if the contents of a link file can be shared (i.e., added to repository). + * Local project Ghidra-URL paths may not be shared. + * + * @param linkFile link file + * @return true if link may be shared + */ + static boolean canShareLink(FolderItem linkFile) { + try { + String linkPath = getLinkPath(linkFile); + return !GhidraURL.isLocalGhidraURL(linkPath); + } + catch (IOException e) { + // ignore + } + return false; + } + + /** + * Get the stored link-path or Ghidra-URL + * + * @param linkFile link file (see {@link DomainFile#isLink()}). + * @return stored link-path or Ghidra-URL + * @throws IOException if an IO error occurs or a valid link-file was not specified + */ + static String getLinkPath(FolderItem linkFile) throws IOException { + String contentType = linkFile.getContentType(); + ContentHandler ch = DomainObjectAdapter.getContentHandler(contentType); + if (ch instanceof LinkHandler) { + String linkPath = null; + if (linkFile instanceof TextDataItem textItem) { + linkPath = textItem.getTextData(); + } + if (linkPath == null) { + // Fallback to reading old database storage form as metadata + Map metadata = GhidraFileData.getMetadata(linkFile); + linkPath = metadata.get(URL_METADATA_KEY); + } + if (StringUtils.isBlank(linkPath)) { + throw new IOException("Invalid link-file: " + linkFile.getPathName()); + } + return linkPath; + } + throw new IOException("Invalid link-file content: " + linkFile.getPathName()); + } + + ////////////////////////// + // Static public methods + ////////////////////////// + + /** + * Get the link URL which corresponds to the specified link file's link-path. + * If link-path was originally specified as an internal path it will be transformed + * into a URL. See {@link DomainFile#isLink()}. + * + * @param linkFile link-file domain file which may correspond to a linked-folder or file. + * @return link URL or null if invalid link-URL or a non-link-file is specified + * @throws IOException if linkFile has an invalid relative link-path that failed to normalize + */ + public static URL getLinkURL(DomainFile linkFile) throws IOException { + + // TODO: link traversal not handled (e.g., path element is a linked folder) + // May have to follow incrementally + + String linkPath = getAbsoluteLinkPath(linkFile); + if (linkPath == null) { + return null; + } + + try { + if (!GhidraURL.isGhidraURL(linkPath)) { + ProjectData projectData = linkFile.getParent().getProjectData(); + return GhidraURL.makeURL(projectData.getProjectLocator(), linkPath, null); + } + return new URL(linkPath); + } + catch (MalformedURLException | IllegalArgumentException e) { + // Bad URL from link path + throw new IOException("Failed to form URL from linkPath: " + linkPath, e); + } + } + + /** + * Get the Ghidra URL or absolute normalized link-path from a link file. + * Path normalization eliminates any path element of "./" or "../". + * A local folder-link path will always end with a "/" path separator. + * Path normalization is not performed on Ghidra URLs. + * + * @param linkFile link file + * @return Ghidra URL or absolute normalized link-path from a link file + * @throws IOException if linkFile has an invalid relative link-path that failed to normalize + */ + public static String getAbsoluteLinkPath(DomainFile linkFile) throws IOException { + + LinkFileInfo linkInfo = linkFile.getLinkInfo(); + if (linkInfo == null) { + return null; + } + String linkPath = linkInfo.getLinkPath(); + if (StringUtils.isBlank(linkPath)) { + return null; + } + + String path = linkPath; + if (!GhidraURL.isGhidraURL(path)) { + if (!linkPath.startsWith(FileSystem.SEPARATOR)) { + path = linkFile.getParent().getPathname(); + if (!path.endsWith(FileSystem.SEPARATOR)) { + path += FileSystem.SEPARATOR; + } + path += linkPath; + } + try { + return FileSystem.normalizePath(path); + } + catch (IllegalArgumentException e) { + throw new IOException("Invalid link path: " + linkPath); + } + } + return path; + } + + /** + * Determine the link status for the specified {@link DomainFile#isLink() link-file}. + * If a status is {@link LinkStatus#BROKEN} and an {@code errorConsumer} has been specified + * the error details will be reported. + * + * @param file domain file + * @param errorConsumer broken link error consumer (may be null) + * @return link status + */ + public static LinkStatus getLinkFileStatus(DomainFile file, Consumer errorConsumer) { + AtomicReference status = new AtomicReference<>(); + followInternalLinkage(file, s -> status.set(s), errorConsumer); + return status.get(); + } + + /** + * Add real internal folder path for specified folder or folder-link and check for + * circular conflict. + * @param pathSet real path accumulator + * @param linkPath internal linkPath + * @return true if no path conflict detected, false if path conflict is detected + */ + private static boolean addLinkPathPath(Set pathSet, String linkPath) { + // Must ensure that all paths end with '/' separator - even if path is endpoint + if (!linkPath.endsWith(FileSystem.SEPARATOR)) { + linkPath += FileSystem.SEPARATOR; + } + for (String path : pathSet) { + if (path.startsWith(linkPath)) { + return false; + } + } + pathSet.add(linkPath); + return true; + } + + /** + * Follow the internal linkage, if any, for the specified file. Any broken linkage details will + * be reported to the specified {@code errorConsumer}. + * + * @param file domain file to be checked + * @param statusConsumer link status consumer (required) + * @param errorConsumer broken link error consumer (may be null) + * @return the final {@link DomainFile} within the same project or null if file specified was + * not a link-file. A broken link will return the last valid link-file in chain. + */ + public static DomainFile followInternalLinkage(DomainFile file, + Consumer statusConsumer, Consumer errorConsumer) { + + Objects.requireNonNull(statusConsumer, "Status consumer is required"); + + errorConsumer = Dummy.ifNull(errorConsumer); + + LinkFileInfo linkInfo = file.getLinkInfo(); + if (linkInfo == null) { + statusConsumer.accept(LinkStatus.NON_LINK); + return null; + } + + Set linkPathsVisited = new HashSet<>(); + + ProjectData projectData; + DomainFolder parent = file.getParent(); + if (parent instanceof LinkedDomainFolder lf) { + try { + projectData = lf.getLinkedProjectData(); + addLinkPathPath(linkPathsVisited, lf.getLinkedPathname()); + } + catch (IOException e) { + throw new RuntimeException("Unexpected", e); + } + } + else { + projectData = parent.getProjectData(); + addLinkPathPath(linkPathsVisited, file.getPathname()); + } + + String contentType = file.getContentType(); + Class domainObjectClass = file.getDomainObjectClass(); + boolean isFolderLink = + FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(contentType); + + // Loop recurses through link-chain to arrive at final internal link-file + DomainFile nextLinkFile = file; + + while (true) { + + String linkPath = null; + try { + linkPath = LinkHandler.getAbsoluteLinkPath(nextLinkFile); + } + catch (IOException e) { + errorConsumer.accept(e.getMessage()); + break; + } + if (linkPath == null) { + errorConsumer.accept("Invalid link-path storage"); + break; + } + + if (isFolderLink) { + String name = nextLinkFile.getName(); + if (nextLinkFile.getParent().getFolder(name) != null) { + errorConsumer.accept( + "Folder name conflicts with this folder-link in same folder: " + name); + break; + } + } + + if (GhidraURL.isGhidraURL(linkPath)) { + statusConsumer.accept(LinkStatus.EXTERNAL); + return nextLinkFile; + } + + if (!addLinkPathPath(linkPathsVisited, linkPath)) { + errorConsumer.accept("Link has a circular reference"); + break; // broken and can't continue + } + + DomainFile linkedFile = null; + if (!linkPath.endsWith(FileSystem.SEPARATOR)) { + linkedFile = projectData.getFile(linkPath); + } + + if (isFolderLink) { + // Check for folder existence at linkPath + if (getNonLinkedFolder(projectData, linkPath) != null) { + // Check for folder-link that conflicts with folder found + if (linkedFile != null) { + LinkFileInfo linkedFileLinkInfo = linkedFile.getLinkInfo(); + if (linkedFileLinkInfo != null && linkedFileLinkInfo.isFolderLink()) { + errorConsumer.accept( + "Referenced folder name conflicts with folder-link in the same folder: " + + linkPath); + break; + } + } + statusConsumer.accept(LinkStatus.INTERNAL); + return nextLinkFile; + } + } + + if (linkedFile == null) { + String acceptableType = isFolderLink ? "folder" : "file"; + errorConsumer.accept( + "Broken " + contentType + " - " + acceptableType + " not found: " + linkPath); + break; + } + + if (!domainObjectClass.isAssignableFrom(linkedFile.getDomainObjectClass())) { + // NOTE: folder-links use NullFolderDomainObject + errorConsumer.accept( + "Broken " + contentType + " - incompatible content-type: " + linkPath); + break; + } + + if (!linkedFile.isLink()) { + statusConsumer.accept(LinkStatus.INTERNAL); + return linkedFile; + } + + nextLinkFile = linkedFile; + } + + // Must be broken to end up here + statusConsumer.accept(LinkStatus.BROKEN); + return nextLinkFile; + } + + private static DomainFolder getNonLinkedFolder(ProjectData projectData, String path) { + int len = path.length(); + if (len == 0 || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { + throw new IllegalArgumentException( + "Absolute path must begin with '" + FileSystem.SEPARATOR_CHAR + "'"); + } + + DomainFolder folder = projectData.getRootFolder(); + String[] split = path.split(FileSystem.SEPARATOR); + if (split.length == 0) { + return folder; + } + + for (int i = 1; i < split.length; i++) { + DomainFolder subFolder = folder.getFolder(split[i]); + if (subFolder == null) { + return null; + } + folder = subFolder; + } + return folder; + } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java index a298700753..aa17e5d2f1 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFile.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -30,14 +30,14 @@ import org.apache.commons.lang3.StringUtils; import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.store.*; -import ghidra.util.*; +import ghidra.util.InvalidNameException; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; /** * {@code LinkedGhidraFile} corresponds to a {@link DomainFile} contained within a - * {@link LinkedGhidraFolder}. + * {@link LinkedGhidraSubFolder}. */ class LinkedGhidraFile implements LinkedDomainFile { @@ -68,6 +68,25 @@ class LinkedGhidraFile implements LinkedDomainFile { return fileName; } + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof LinkedGhidraFile other)) { + return false; + } + return fileName.equals(other.fileName) && parent.equals(other.parent); + } + + @Override + public int hashCode() { + return getPathname().hashCode(); + } + @Override public int compareTo(DomainFile df) { return fileName.compareToIgnoreCase(df.getName()); @@ -86,7 +105,8 @@ class LinkedGhidraFile implements LinkedDomainFile { @Override public DomainFile setName(String newName) throws InvalidNameException, IOException { - throw new ReadOnlyException("linked file is read only"); + String name = getLinkedFile().setName(newName).getName(); + return parent.getFile(name); } @Override @@ -130,6 +150,7 @@ class LinkedGhidraFile implements LinkedDomainFile { @Override public ProjectLocator getProjectLocator() { + // TODO: Should this reflect real project? return parent.getProjectLocator(); } @@ -147,17 +168,21 @@ class LinkedGhidraFile implements LinkedDomainFile { @Override public ChangeSet getChangesByOthersSinceCheckout() throws VersionException, IOException { - return null; + return getLinkedFile().getChangesByOthersSinceCheckout(); } @Override public DomainObject getDomainObject(Object consumer, boolean okToUpgrade, boolean okToRecover, TaskMonitor monitor) throws VersionException, IOException, CancelledException { - return getReadOnlyDomainObject(consumer, DomainFile.DEFAULT_VERSION, monitor); + return getLinkedFile().getDomainObject(consumer, okToUpgrade, okToRecover, monitor); } @Override public DomainObject getOpenedDomainObject(Object consumer) { + DomainFile df = getLinkedFileNoError(); + if (df != null) { + return df.getOpenedDomainObject(consumer); + } return null; } @@ -195,7 +220,8 @@ class LinkedGhidraFile implements LinkedDomainFile { @Override public boolean isInWritableProject() { - return false; // While project may be writeable this folder/file is not + // TODO: Is this correct? + return parent.isInWritableProject(); } @Override @@ -212,47 +238,56 @@ class LinkedGhidraFile implements LinkedDomainFile { @Override public boolean isCheckedOut() { - return false; + DomainFile df = getLinkedFileNoError(); + return df != null ? df.isCheckedOut() : false; } @Override public boolean isCheckedOutExclusive() { - return false; + DomainFile df = getLinkedFileNoError(); + return df != null ? df.isCheckedOutExclusive() : false; } @Override public boolean modifiedSinceCheckout() { - return false; + DomainFile df = getLinkedFileNoError(); + return df != null ? df.modifiedSinceCheckout() : false; } @Override public boolean canCheckout() { - return false; + DomainFile df = getLinkedFileNoError(); + return df != null ? df.canCheckout() : false; } @Override public boolean canCheckin() { - return false; + DomainFile df = getLinkedFileNoError(); + return df != null ? df.canCheckin() : false; } @Override public boolean canMerge() { - return false; + DomainFile df = getLinkedFileNoError(); + return df != null ? df.canMerge() : false; } @Override public boolean canAddToRepository() { - return false; + DomainFile df = getLinkedFileNoError(); + return df != null ? df.canAddToRepository() : false; } @Override public void setReadOnly(boolean state) throws IOException { - // ignore + getLinkedFile().setReadOnly(state); } @Override public boolean isReadOnly() { - return true; // not reflected by icon + DomainFile df = getLinkedFileNoError(); + // read-only state not reflected by icon + return df != null ? df.isReadOnly() : true; } @Override @@ -263,7 +298,8 @@ class LinkedGhidraFile implements LinkedDomainFile { @Override public boolean isHijacked() { - return false; + DomainFile df = getLinkedFileNoError(); + return df != null ? df.isHijacked() : false; } @Override @@ -274,85 +310,84 @@ class LinkedGhidraFile implements LinkedDomainFile { @Override public boolean isLatestVersion() { - return true; + DomainFile df = getLinkedFileNoError(); + return df != null ? df.isLatestVersion() : true; } @Override public int getVersion() { - // TODO: Do we want to reveal linked-local-project checkout details? - return getLatestVersion(); + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getVersion() : DomainFile.DEFAULT_VERSION; } @Override public Version[] getVersionHistory() throws IOException { - DomainFile df = getLinkedFileNoError(); + DomainFile df = getLinkedFile(); return df != null ? df.getVersionHistory() : new Version[0]; } @Override public void addToVersionControl(String comment, boolean keepCheckedOut, TaskMonitor monitor) throws IOException, CancelledException { - throw new UnsupportedOperationException(); + getLinkedFile().addToVersionControl(comment, keepCheckedOut, monitor); } @Override public boolean checkout(boolean exclusive, TaskMonitor monitor) throws IOException, CancelledException { - throw new UnsupportedOperationException(); + return getLinkedFile().checkout(exclusive, monitor); } @Override public void checkin(CheckinHandler checkinHandler, TaskMonitor monitor) throws IOException, VersionException, CancelledException { - throw new UnsupportedOperationException(); + getLinkedFile().checkin(checkinHandler, monitor); } @Override public void merge(boolean okToUpgrade, TaskMonitor monitor) throws IOException, VersionException, CancelledException { - throw new UnsupportedOperationException(); + getLinkedFile().merge(okToUpgrade, monitor); } @Override public void undoCheckout(boolean keep) throws IOException { - throw new UnsupportedOperationException(); + getLinkedFile().undoCheckout(keep); } @Override public void undoCheckout(boolean keep, boolean force) throws IOException { - throw new UnsupportedOperationException(); + getLinkedFile().undoCheckout(keep, force); } @Override public void terminateCheckout(long checkoutId) throws IOException { - throw new UnsupportedOperationException(); + getLinkedFile().terminateCheckout(checkoutId); } @Override public ItemCheckoutStatus[] getCheckouts() throws IOException { - DomainFile df = getLinkedFileNoError(); - return df != null ? df.getCheckouts() : new ItemCheckoutStatus[0]; + return getLinkedFile().getCheckouts(); } @Override public ItemCheckoutStatus getCheckoutStatus() throws IOException { - // TODO: Do we want to reveal linked-local-project checkout details? - return null; + return getLinkedFile().getCheckoutStatus(); } @Override public void delete() throws IOException { - throw new ReadOnlyException("linked file is read only"); + getLinkedFile().delete(); } @Override public void delete(int version) throws IOException { - throw new ReadOnlyException("linked file is read only"); + getLinkedFile().delete(version); } @Override public DomainFile moveTo(DomainFolder newParent) throws IOException { - throw new ReadOnlyException("linked file is read only"); + return getLinkedFile().moveTo(newParent); } @Override @@ -368,8 +403,8 @@ class LinkedGhidraFile implements LinkedDomainFile { } @Override - public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { - return getLinkedFile().copyToAsLink(newParent); + public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException { + return getLinkedFile().copyToAsLink(newParent, relative); } @Override @@ -385,17 +420,18 @@ class LinkedGhidraFile implements LinkedDomainFile { @Override public boolean isChanged() { - return false; + DomainFile df = getLinkedFileNoError(); + return df != null ? df.isChanged() : false; } @Override public boolean isOpen() { - return false; // domain file proxy always used + return false; // real file may be but this is not } @Override public boolean isBusy() { - return false; // domain file proxy always used + return false; // real file may be but this is not } @Override @@ -416,24 +452,30 @@ class LinkedGhidraFile implements LinkedDomainFile { } @Override - public boolean isLinkFile() { + public boolean isLink() { DomainFile df = getLinkedFileNoError(); - return df != null ? df.isLinkFile() : false; + return df != null ? df.isLink() : false; } @Override - public DomainFolder followLink() { - try { - return FolderLinkContentHandler.getReadOnlyLinkedFolder(this); - } - catch (IOException e) { - Msg.error(this, "Failed to following folder-link: " + getPathname()); - } - return null; + public LinkFileInfo getLinkInfo() { + DomainFile df = getLinkedFileNoError(); + return df != null ? df.getLinkInfo() : null; + } + + @Override + public String getLinkedPathname() { + return parent.getLinkedPathname(fileName); } @Override public String toString() { - return "LinkedGhidraFile: " + getPathname(); + String str = parent.toString(); + if (!str.endsWith("/")) { + str += "/"; + } + str += getName(); + return str; } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java index 865038d3b6..d99d53830d 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraFolder.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,9 +22,12 @@ import java.net.URL; import javax.swing.Icon; import generic.theme.GIcon; +import ghidra.framework.client.RepositoryAdapter; +import ghidra.framework.main.AppInfo; import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.store.FileSystem; +import ghidra.util.InvalidNameException; /** * {@code LinkedGhidraFolder} provides the base {@link LinkedDomainFolder} implementation which @@ -36,39 +39,88 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder { new GIcon("icon.content.handler.linked.folder.closed"); public static Icon FOLDER_LINK_OPEN_ICON = new GIcon("icon.content.handler.linked.folder.open"); - private final Project activeProject; - private final DomainFolder localParent; - private final URL folderUrl; + private final DomainFile folderLinkFile; - private String linkedPathname; + // Linked folder established using either a URL or a folder + private final URL linkedFolderUrl; + private final DomainFolder linkedFolder; + private final String linkedPathname; + private final URL projectUrl; - private URL projectUrl; + private boolean offline = false; // allow single failure /** - * Construct a linked-folder. - * @param activeProject active project responsible for linked project life-cycle management. - * @param localParent local domain folder which contains folder-link or corresponds directly to - * folder-link (name=null). - * @param linkFilename folder-link filename - * @param folderUrl linked folder URL + * Construct a linked-folder which is linked via a Ghidra URL. + *

    + * NOTE: An active project is required as conveyed by {@link AppInfo#getActiveProject()} + * which will take ownership of any project view which is required. This should be pre-checked + * since an error will occur if there is no active project at the time the link is followed. + * + * @param folderLinkFile link-file which corresponds to a linked-folder + * (see {@link LinkFileInfo#isFolderLink()}). + * @param linkedFolderUrl linked folder URL */ - LinkedGhidraFolder(Project activeProject, DomainFolder localParent, String linkFilename, - URL folderUrl) { - super(linkFilename); + LinkedGhidraFolder(DomainFile folderLinkFile, URL linkedFolderUrl) { + super(folderLinkFile.getName()); - if (!GhidraURL.isServerRepositoryURL(folderUrl) && - !GhidraURL.isLocalProjectURL(folderUrl)) { - throw new IllegalArgumentException("Invalid Ghidra URL: " + folderUrl); + if (!GhidraURL.isServerRepositoryURL(linkedFolderUrl) && + !GhidraURL.isLocalProjectURL(linkedFolderUrl)) { + throw new IllegalArgumentException("Invalid Ghidra URL: " + linkedFolderUrl); } - this.activeProject = activeProject; - this.localParent = localParent; - this.folderUrl = folderUrl; + this.folderLinkFile = folderLinkFile; - linkedPathname = GhidraURL.getProjectPathname(folderUrl); - if (linkedPathname.length() > 0 && linkedPathname.endsWith(FileSystem.SEPARATOR)) { - linkedPathname = linkedPathname.substring(0, linkedPathname.length() - 1); + this.linkedFolderUrl = linkedFolderUrl; + this.linkedFolder = null; + + String pathname = GhidraURL.getProjectPathname(linkedFolderUrl); + if (!FileSystem.SEPARATOR.equals(pathname) && pathname.endsWith(FileSystem.SEPARATOR)) { + // avoid trailing path separator except on root pathname + pathname = pathname.substring(0, pathname.length() - 1); } + linkedPathname = pathname; + projectUrl = GhidraURL.getProjectURL(linkedFolderUrl); + } + + /** + * Construct a linked-folder which is linked to another folder within the associated + * {@link #getProjectData() project data} instance. + * + * @param folderLinkFile link-file which corresponds to a linked-folder + * (see {@link LinkFileInfo#isFolderLink()}). + * @param linkedFolder locally-linked folder within same project + */ + LinkedGhidraFolder(DomainFile folderLinkFile, DomainFolder linkedFolder) { + super(folderLinkFile.getName()); + + this.folderLinkFile = folderLinkFile; + + this.linkedFolder = linkedFolder; + this.linkedFolderUrl = null; + + linkedPathname = linkedFolder.getPathname(); + + projectUrl = linkedFolder.getProjectLocator().getURL(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof LinkedGhidraFolder other)) { + return false; + } + return linkedPathname.equals(other.linkedPathname) && + folderLinkFile.equals(other.folderLinkFile); + } + + @Override + public boolean isExternal() { + return linkedFolderUrl != null; } /** @@ -76,9 +128,6 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder { * @return Ghidra URL of the project/repository folder referenced by this object */ public URL getProjectURL() { - if (projectUrl == null) { - projectUrl = GhidraURL.getProjectURL(folderUrl); - } return projectUrl; } @@ -87,16 +136,67 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder { return this; } - DomainFolder getLinkedFolder(String linkedPath) throws IOException { + @Override + public boolean isInWritableProject() { + return linkedFolder != null && linkedFolder.isInWritableProject(); + } - ProjectData projectData = activeProject.addProjectView(getProjectURL(), false); - if (projectData == null) { - throw new FileNotFoundException(); + @Override + public ProjectData getLinkedProjectData() throws IOException { + // NOTE: The offline tracking is done to avoid repeatedly prompting for a connection + // password. Only one connect attempt per instance will be performed. + ProjectData projectData; + if (linkedFolder != null) { + projectData = linkedFolder.getProjectData(); } + else { + // Handle GhidraURL linkages + Project activeProject = AppInfo.getActiveProject(); + if (activeProject == null) { + offline = true; + throw new IOException("active project not found"); + } + URL url = getProjectURL(); + projectData = activeProject.getProjectData(url); + if (projectData == null && !offline) { + offline = true; + projectData = activeProject.addProjectView(url, false); + if (projectData != null) { + offline = false; + RepositoryAdapter repository = projectData.getRepository(); + if (repository != null && !repository.isConnected()) { + // User chose not to connect - don't force them + offline = true; + } + } + } + if (projectData == null) { + throw new FileNotFoundException("failed to add project view: " + url); + } + } + return projectData; + } + + synchronized DomainFolder getRealFolder(String linkedPath) throws IOException { + ProjectData projectData = getLinkedProjectData(); DomainFolder folder = projectData.getFolder(linkedPath); if (folder == null) { - throw new FileNotFoundException(folderUrl.toExternalForm()); + RepositoryAdapter repository = projectData.getRepository(); + if (repository != null) { + if (!offline && !repository.isConnected()) { + repository.connect(); + if (!repository.isConnected()) { + offline = true; + throw new FileNotFoundException("linked project/repository not connected"); + } + folder = projectData.getFolder(linkedPath); + } + } + if (folder == null) { + String notConnectedMsg = offline ? " (not connected)" : ""; + throw new FileNotFoundException("folder not found" + notConnectedMsg); + } } return folder; } @@ -106,24 +206,41 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder { return linkedPathname; } + @Override + public DomainFolder getRealFolder() throws IOException { + return getRealFolder(linkedPathname); + } + @Override public ProjectLocator getProjectLocator() { - return localParent.getProjectLocator(); + return folderLinkFile.getProjectLocator(); } @Override public ProjectData getProjectData() { - return localParent.getProjectData(); + return folderLinkFile.getParent().getProjectData(); } @Override public DomainFolder getParent() { - return localParent; + return folderLinkFile.getParent(); + } + + @Override + public DomainFolder setName(String newName) throws InvalidNameException, IOException { + DomainFile linkFile = folderLinkFile.setName(newName); + if (linkedFolder != null) { + return new LinkedGhidraFolder(linkFile, linkedFolder); + } + return new LinkedGhidraFolder(linkFile, linkedFolderUrl); } @Override public String toString() { - return "LinkedGhidraFolder: " + getPathname(); + if (linkedFolder != null) { + return "->" + getLinkedPathname(); + } + return "->" + linkedFolderUrl.toString(); } @Override @@ -135,4 +252,19 @@ public class LinkedGhidraFolder extends LinkedGhidraSubFolder { public boolean isLinked() { return true; } + + /** + * Determine if this linked-folder corresponds to an external URL linkage and not an internal + * project linkage. + * @return true if linked based on external URL + */ + public boolean isUrlLinked() { + if (linkedFolderUrl != null) { + return true; + } + if (linkedFolder instanceof LinkedGhidraFolder lf) { + return lf.isUrlLinked(); + } + return false; + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java index 05c07f1aad..24d5813916 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LinkedGhidraSubFolder.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -38,12 +38,21 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { private final LinkedGhidraSubFolder parent; private final String folderName; - LinkedGhidraSubFolder(String folderName) { + /** + * Construct root-linked-folder based on the name of a folder-link link-file. + * @param linkFileName name of link-file which represents a folder-link + */ + LinkedGhidraSubFolder(String linkFileName) { this.linkedRootFolder = getLinkedRootFolder(); this.parent = null; // must override getParent() - this.folderName = folderName; + this.folderName = linkFileName; } + /** + * Construct a linked-folder child + * @param parent parent folder within a linked-folder hierarchy + * @param folderName folder name + */ LinkedGhidraSubFolder(LinkedGhidraSubFolder parent, String folderName) { this.linkedRootFolder = parent.getLinkedRootFolder(); this.parent = parent; @@ -59,9 +68,14 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { return linkedRootFolder; } + @Override + public boolean isExternal() { + return linkedRootFolder.isExternal(); + } + @Override public boolean isInWritableProject() { - return false; // While project may be writeable this folder is not + return linkedRootFolder.isInWritableProject(); } @Override @@ -75,8 +89,27 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { } @Override - public DomainFolder getLinkedFolder() throws IOException { - return linkedRootFolder.getLinkedFolder(getLinkedPathname()); + public DomainFolder getRealFolder() throws IOException { + return linkedRootFolder.getRealFolder(getLinkedPathname()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof LinkedGhidraSubFolder other)) { + return false; + } + return folderName.equals(other.folderName) && parent.equals(other.parent); + } + + @Override + public int hashCode() { + return getPathname().hashCode(); } @Override @@ -84,9 +117,50 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { return getName().compareToIgnoreCase(df.getName()); } + @Override + public boolean isSame(DomainFolder folder) { + + // NOTE: This project check relates to the outermost containing project + // and not the project that may be referenenced by a link. + if (!getProjectLocator().equals(folder.getProjectLocator()) && + !SystemUtilities.isEqual(getProjectData().getSharedProjectURL(), + folder.getProjectData().getSharedProjectURL())) { + // Containing project/repository appears to be unrelated + return false; + } + + return getPathname().equals(folder.getPathname()); + } + + @Override + public boolean isSameOrAncestor(DomainFolder folder) { + + // NOTE: This project check relates to the outermost containing project + // and not the project that may be referenenced by a link. + if (!getProjectLocator().equals(folder.getProjectLocator()) && + !SystemUtilities.isEqual(getProjectData().getSharedProjectURL(), + folder.getProjectData().getSharedProjectURL())) { + // Containing project/repository appears to be unrelated + return false; + } + + String pathname = getPathname(); + + DomainFolder f = folder; + while (f != null) { + if (f == this || pathname.equals(f.getPathname())) { + return true; + } + f = f.getParent(); + } + return false; + } + @Override public DomainFolder setName(String newName) throws InvalidNameException, IOException { - throw new ReadOnlyException("linked folder is read only"); + DomainFolder linkedFolder = getRealFolder(); + String name = linkedFolder.setName(newName).getName(); + return parent.getFolder(name); } @Override @@ -125,6 +199,11 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { return parent.getProjectLocator(); } + @Override + public ProjectData getLinkedProjectData() throws IOException { + return linkedRootFolder.getLinkedProjectData(); + } + @Override public ProjectData getProjectData() { return parent.getProjectData(); @@ -142,23 +221,24 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { return path; } - /** - * Get the pathname of this folder within the linked-project/repository - * @return absolute linked folder path within the linked-project/repository - */ + @Override public String getLinkedPathname() { - String path = parent.getLinkedPathname(); + return parent.getLinkedPathname(folderName); + } + + final String getLinkedPathname(String childName) { + String path = getLinkedPathname(); if (!path.endsWith(FileSystem.SEPARATOR)) { path += FileSystem.SEPARATOR; } - path += folderName; + path += childName; return path; } @Override public LinkedGhidraSubFolder[] getFolders() { try { - DomainFolder linkedFolder = getLinkedFolder(); + DomainFolder linkedFolder = getRealFolder(); DomainFolder[] folders = linkedFolder.getFolders(); LinkedGhidraSubFolder[] linkedSubFolders = new LinkedGhidraSubFolder[folders.length]; for (int i = 0; i < folders.length; i++) { @@ -167,7 +247,7 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { return linkedSubFolders; } catch (IOException e) { - Msg.error(this, "Linked folder failure: " + e.getMessage()); + Msg.error(this, "Linked folder failure '" + this + "': " + e.getMessage()); return new LinkedGhidraSubFolder[0]; } } @@ -175,14 +255,14 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { @Override public LinkedGhidraSubFolder getFolder(String name) { try { - DomainFolder linkedFolder = getLinkedFolder(); + DomainFolder linkedFolder = getRealFolder(); DomainFolder f = linkedFolder.getFolder(name); if (f != null) { return new LinkedGhidraSubFolder(this, name); } } catch (IOException e) { - Msg.error(this, "Linked folder failure: " + e.getMessage()); + Msg.error(this, "Linked folder failure '" + this + "': " + e.getMessage()); } return null; } @@ -190,7 +270,7 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { @Override public DomainFile[] getFiles() { try { - DomainFolder linkedFolder = getLinkedFolder(); + DomainFolder linkedFolder = getRealFolder(); DomainFile[] files = linkedFolder.getFiles(); LinkedGhidraFile[] linkedSubFolders = new LinkedGhidraFile[files.length]; for (int i = 0; i < files.length; i++) { @@ -199,7 +279,7 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { return linkedSubFolders; } catch (IOException e) { - Msg.error(this, "Linked folder failure: " + e.getMessage()); + Msg.error(this, "Linked folder failure '" + this + "': " + e.getMessage()); return new LinkedGhidraFile[0]; } } @@ -211,17 +291,17 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { */ public DomainFile getLinkedFileNoError(String name) { try { - DomainFolder linkedFolder = getLinkedFolder(); + DomainFolder linkedFolder = getRealFolder(); return linkedFolder.getFile(name); } catch (IOException e) { - Msg.error(this, "Linked folder failure: " + e.getMessage()); + // Ignore } return null; } DomainFile getLinkedFile(String name) throws IOException { - DomainFolder linkedFolder = getLinkedFolder(); + DomainFolder linkedFolder = getRealFolder(); DomainFile df = linkedFolder.getFile(name); if (df == null) { throw new FileNotFoundException("linked-file '" + name + "' not found"); @@ -238,11 +318,11 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { @Override public boolean isEmpty() { try { - DomainFolder linkedFolder = getLinkedFolder(); + DomainFolder linkedFolder = getRealFolder(); return linkedFolder.isEmpty(); } catch (IOException e) { - Msg.error(this, "Linked folder failure: " + e.getMessage()); + Msg.error(this, "Linked folder failure '" + this + "': " + e.getMessage()); // TODO: what should we return if folder not found or error occurs? // True is returned to allow this method to be used to avoid continued access. return true; @@ -252,51 +332,83 @@ class LinkedGhidraSubFolder implements LinkedDomainFolder { @Override public DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor) throws InvalidNameException, IOException, CancelledException { - throw new ReadOnlyException("linked folder is read only"); + DomainFolder linkedFolder = getRealFolder(); + return linkedFolder.createFile(name, obj, monitor); } @Override public DomainFile createFile(String name, File packFile, TaskMonitor monitor) throws InvalidNameException, IOException, CancelledException { - throw new ReadOnlyException("linked folder is read only"); + DomainFolder linkedFolder = getRealFolder(); + return linkedFolder.createFile(name, packFile, monitor); + } + + @Override + public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname, + boolean makeRelative, String linkFilename, LinkHandler lh) throws IOException { + DomainFolder linkedFolder = getRealFolder(); + return linkedFolder.createLinkFile(sourceProjectData, pathname, makeRelative, linkFilename, + lh); + } + + @Override + public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler lh) + throws IOException { + DomainFolder linkedFolder = getRealFolder(); + return linkedFolder.createLinkFile(ghidraUrl, linkFilename, lh); } @Override public DomainFolder createFolder(String name) throws InvalidNameException, IOException { - throw new ReadOnlyException("linked folder is read only"); + DomainFolder linkedFolder = getRealFolder(); + DomainFolder child = linkedFolder.createFolder(name); + return new LinkedGhidraSubFolder(parent, child.getName()); } @Override public void delete() throws IOException { - throw new ReadOnlyException("linked folder is read only"); + DomainFolder linkedFolder = getRealFolder(); + linkedFolder.delete(); } @Override public DomainFolder moveTo(DomainFolder newParent) throws IOException { - throw new ReadOnlyException("linked folder is read only"); + DomainFolder linkedFolder = getRealFolder(); + return linkedFolder.moveTo(newParent); } @Override public DomainFolder copyTo(DomainFolder newParent, TaskMonitor monitor) throws IOException, CancelledException { - DomainFolder linkedFolder = getLinkedFolder(); + DomainFolder linkedFolder = getRealFolder(); return linkedFolder.copyTo(newParent, monitor); } @Override - public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { - DomainFolder linkedFolder = getLinkedFolder(); - return linkedFolder.copyToAsLink(newParent); + public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException { + DomainFolder linkedFolder = getRealFolder(); + return linkedFolder.copyToAsLink(newParent, relative); } @Override public void setActive() { - // do nothing + try { + DomainFolder linkedFolder = getRealFolder(); + linkedFolder.setActive(); + } + catch (IOException e) { + // ignore + } } @Override public String toString() { - return "LinkedGhidraSubFolder: " + getPathname(); + String str = parent.toString(); + if (!str.endsWith("/")) { + str += "/"; + } + str += getName(); + return str; } @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/NullFolderDomainObject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/NullFolderDomainObject.java new file mode 100644 index 0000000000..620f736309 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/NullFolderDomainObject.java @@ -0,0 +1,37 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.data; + +/** + * Dummy domain object to satisfy {@link FolderLinkContentHandler#getDomainObjectClass()} + */ +public final class NullFolderDomainObject extends DomainObjectAdapterDB { + private NullFolderDomainObject() { + // this object may not be instantiated + super(null, null, 0, NullFolderDomainObject.class); + throw new RuntimeException("Object may not be instantiated"); + } + + @Override + public boolean isChangeable() { + return false; + } + + @Override + public String getDescription() { + return "Dummy FolderLink Domain Object"; + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java index c04cd40978..dc268c1712 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/URLLinkObject.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,61 +17,43 @@ package ghidra.framework.data; import java.io.File; import java.io.IOException; -import java.net.URL; import javax.help.UnsupportedOperationException; import db.DBHandle; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainObject; -import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; /** - * {@code DomainObjectAdapterLink} object provides a Ghidra URL (see {@link GhidraURL}) wrapper - * where the URL is intended to refer to a {@link DomainFile} within another local or remote + * {@link URLLinkObject} provides a link-file path/URL wrapper + * where the path/URL is intended to refer to a {@link DomainFile} within a local or remote * project/repository. Link files which correspond to this type of {@link DomainObject} are * not intended to be modified and should be created or deleted. A checkout may be used when * an offline copy is required but otherwise serves no purpose since a modification and checkin * is not supported. + *

    + * NOTE: This exists for backward compatibility and is no longer used for storing newly created + * link-files. */ public class URLLinkObject extends DomainObjectAdapterDB { - // Use a reduced DB buffer size to reduce file size for minimal content. - // This will allow a 4-KByte DB buffer file to hold a URL upto ~470 bytes long. - // Longer URLs will rely on 1-KByte chained buffers which will increase file length. - private static final int DB_BUFFER_SIZE = 1024; - - private URL url; + private String linkPath; /** - * Constructs a new link file object - * @param name link name - * @param ghidraUrl link URL - * @param consumer the object that is using this program. - * @throws IOException if there is an error accessing the database or invalid URL specified. - */ - public URLLinkObject(String name, URL ghidraUrl, Object consumer) throws IOException { - super(new DBHandle(DB_BUFFER_SIZE), name, 500, consumer); - metadata.put(LinkHandler.URL_METADATA_KEY, ghidraUrl.toString()); - updateMetadata(); - } - - /** - * Constructs a link file object from a DBHandle (read-only) + * Constructs an existing link file object from a DBHandle (read-only) * @param dbh a handle to an open program database. * @param consumer the object that keeping the program open. * @throws IOException if an error accessing the database occurs. */ - public URLLinkObject(DBHandle dbh, Object consumer) throws IOException { + URLLinkObject(DBHandle dbh, Object consumer) throws IOException { super(dbh, "Untitled", 500, consumer); loadMetadata(); - String urlText = metadata.get(LinkHandler.URL_METADATA_KEY); - if (urlText == null) { - throw new IOException("Null link object"); + linkPath = metadata.get(LinkHandler.URL_METADATA_KEY); + if (linkPath == null) { + throw new IOException("Null link path/URL"); } - url = new URL(urlText); } @Override @@ -80,11 +62,11 @@ public class URLLinkObject extends DomainObjectAdapterDB { } /** - * Get link URL - * @return link URL + * Get the stored link path/URL + * @return link path/URL */ - public URL getLink() { - return url; + public String getLinkPath() { + return linkPath; } @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AcceptUrlContentTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AcceptUrlContentTask.java index 355332770b..a45ab7b3c8 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AcceptUrlContentTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AcceptUrlContentTask.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,8 +20,8 @@ import java.io.IOException; import java.net.URL; import java.util.Set; -import ghidra.framework.data.FolderLinkContentHandler; import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl; import ghidra.framework.protocol.ghidra.GhidraURLQueryTask; import ghidra.util.Msg; import ghidra.util.Swing; @@ -31,8 +31,9 @@ public class AcceptUrlContentTask extends GhidraURLQueryTask { private FrontEndPlugin plugin; - public AcceptUrlContentTask(URL url, FrontEndPlugin plugin) { - super("Accepting URL", url); + public AcceptUrlContentTask(URL url, boolean followExternalLinks, FrontEndPlugin plugin) { + super("Accepting URL", url, null, followExternalLinks ? LinkFileControl.FOLLOW_EXTERNAL + : LinkFileControl.FOLLOW_INTERNAL); this.plugin = plugin; } @@ -65,8 +66,8 @@ public class AcceptUrlContentTask extends GhidraURLQueryTask { } Swing.runNow(() -> { - if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE - .equals(domainFile.getContentType())) { + LinkFileInfo linkInfo = domainFile.getLinkInfo(); + if (linkInfo != null && linkInfo.isFolderLink()) { // Simply select folder link-file within project - do not follow - let user do that. if (isSameLocalProject(activeProject.getProjectLocator(), domainFile.getProjectLocator())) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/BrokenLinkIcon.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/BrokenLinkIcon.java new file mode 100644 index 0000000000..e797098513 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/BrokenLinkIcon.java @@ -0,0 +1,67 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.main; + +import java.awt.*; + +import javax.swing.Icon; + +/** + * Icon class for for altering a baseIcon to render as a "broken" link-file icon. + */ +public class BrokenLinkIcon implements Icon { + + private Icon baseIcon; + + /** + * Constructs a "broken" link-file icon. + * @param baseIcon the base icon that will always be drawn first. + */ + public BrokenLinkIcon(Icon baseIcon) { + this.baseIcon = baseIcon; + } + + @Override + public int getIconHeight() { + return baseIcon.getIconHeight(); + } + + @Override + public int getIconWidth() { + return baseIcon.getIconWidth(); + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + baseIcon.paintIcon(c, g, x, y); + + Graphics2D g2d = (Graphics2D) g; + + // Enable anti-aliasing + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g.setColor(Color.red); + + int h = getIconHeight(); + int halfh = h / 2; + int w = getIconWidth(); + int halfw = w / 2; + // + g.drawLine(x, y + halfh - 1, x + halfw + 1, y + halfh - 3); + g.drawLine(x + halfw + 1, y + halfh - 3, x + halfw - 1, y + halfh + 1); + g.drawLine(x + halfw - 1, y + halfh + 1, x + w - 1, y + halfh - 1); + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java index 8dbf28a051..0048dd7c8a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndPlugin.java @@ -22,6 +22,7 @@ import java.io.File; import java.io.IOException; import java.net.URL; import java.util.*; +import java.util.concurrent.atomic.AtomicReference; import javax.swing.*; import javax.swing.border.BevelBorder; @@ -30,8 +31,7 @@ import docking.*; import docking.action.DockingAction; import docking.action.MenuData; import docking.tool.ToolConstants; -import docking.widgets.OkDialog; -import docking.widgets.OptionDialog; +import docking.widgets.*; import docking.widgets.button.GButton; import docking.widgets.dialogs.InputDialog; import docking.widgets.filechooser.GhidraFileChooser; @@ -41,7 +41,9 @@ import generic.theme.GIcon; import ghidra.app.plugin.PluginCategoryNames; import ghidra.framework.GenericRunInfo; import ghidra.framework.client.*; -import ghidra.framework.data.*; +import ghidra.framework.data.ContentHandler; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.LinkHandler.LinkStatus; import ghidra.framework.main.datatable.ProjectDataTablePanel; import ghidra.framework.main.datatree.*; import ghidra.framework.main.projectdata.actions.*; @@ -131,8 +133,10 @@ public class FrontEndPlugin extends Plugin private ProjectDataCopyAction copyAction; private ProjectDataPasteAction pasteAction; private ProjectDataPasteLinkAction pasteLinkAction; + private ProjectDataPasteLinkAction pasteRelativeLinkAction; private ProjectDataRenameAction renameAction; private ProjectDataOpenDefaultToolAction openAction; + private ProjectDataFollowLinkAction followLinkAction; private ProjectDataExpandAction expandAction; private ProjectDataCollapseAction collapseAction; private ProjectDataSelectAction selectAction; @@ -153,6 +157,8 @@ public class FrontEndPlugin extends Plugin private FindCheckoutsAction findCheckoutsAction; private ToolChestChangeListener toolChestChangeListener; + private OptionDialogBuilder filterWarningBuilder; + /** * Construct a new FrontEndPlugin. This plugin is constructed once when * the Front end tool (Ghidra Project Window) is created. When a @@ -212,6 +218,10 @@ public class FrontEndPlugin extends Plugin private void createActions() { String owner = getName(); + // Top of popup menu actions - no group + openAction = new ProjectDataOpenDefaultToolAction(owner, null); + followLinkAction = new ProjectDataFollowLinkAction(this, null); + String groupName = "Cut/copy/paste/new1"; newFolderAction = new FrontEndProjectDataNewFolderAction(owner, groupName); @@ -220,12 +230,12 @@ public class FrontEndPlugin extends Plugin clearCutAction = new ClearCutAction(owner); copyAction = new ProjectDataCopyAction(owner, groupName); pasteAction = new ProjectDataPasteAction(owner, groupName); - pasteLinkAction = new ProjectDataPasteLinkAction(owner, groupName); + pasteLinkAction = new ProjectDataPasteLinkAction(owner, groupName, false); + pasteRelativeLinkAction = new ProjectDataPasteLinkAction(owner, groupName, true); groupName = "Delete/Rename"; renameAction = new ProjectDataRenameAction(owner, groupName); deleteAction = new ProjectDataDeleteAction(owner, groupName); - openAction = new ProjectDataOpenDefaultToolAction(owner, "Open"); groupName = "Expand/Collapse"; expandAction = new FrontEndProjectDataExpandAction(owner, groupName); @@ -244,8 +254,10 @@ public class FrontEndPlugin extends Plugin tool.addAction(copyAction); tool.addAction(pasteAction); tool.addAction(pasteLinkAction); + tool.addAction(pasteRelativeLinkAction); tool.addAction(deleteAction); tool.addAction(openAction); + tool.addAction(followLinkAction); tool.addAction(renameAction); tool.addAction(expandAction); tool.addAction(collapseAction); @@ -804,6 +816,7 @@ public class FrontEndPlugin extends Plugin @Override protected void dispose() { + projectDataPanel.setActiveProject(null); // force all project views to be disposed dataTablePanel.dispose(); dataTreePanel.dispose(); projectActionManager.dispose(); @@ -1081,7 +1094,7 @@ public class FrontEndPlugin extends Plugin } public void openDomainFile(DomainFile domainFile) { - + String contentType = domainFile.getContentType(); if (ContentHandler.UNKNOWN_CONTENT.equals(contentType)) { Msg.showInfo(this, tool.getToolFrame(), "Cannot Find Tool", @@ -1091,8 +1104,27 @@ public class FrontEndPlugin extends Plugin return; } - if (FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(contentType)) { - showLinkedFolderInViewedProject(domainFile); + if (domainFile.isLink() && domainFile.getLinkInfo().isFolderLink()) { + + // Follow and check internal linkage + AtomicReference status = new AtomicReference<>(); + DomainFile lastLink = + LinkHandler.followInternalLinkage(domainFile, s -> status.set(s), null); + + try { + // Tree already handles opening folder-link while table does nothing + if (lastLink != null && status.get() == LinkStatus.EXTERNAL) { + showInViewedProject(LinkHandler.getLinkURL(lastLink), true); + } + else if (!dataTreePanel.isShowing()) { + // Filter table on absolute link path + String linkPath = LinkHandler.getAbsoluteLinkPath(domainFile); + dataTablePanel.setFilter(linkPath); + } + } + catch (IOException e) { + Msg.showError(this, tool.getActiveWindow(), "Link Error", e.getMessage()); + } return; } @@ -1120,64 +1152,32 @@ public class FrontEndPlugin extends Plugin "opens this type of file"); } - private void showLinkedFolderInViewedProject(DomainFile domainFile) { + void showInViewedProject(URL ghidraUrl, boolean isFolder) { - try { - LinkedGhidraFolder linkedFolder = - FolderLinkContentHandler.getReadOnlyLinkedFolder(domainFile); - if (linkedFolder == null) { - return; // unsupported use - } - - ProjectDataTreePanel dtp = projectDataPanel.openView(linkedFolder.getProjectURL()); - if (dtp == null) { - return; - } - - // Do not hang onto domainFile, linkedFolder or their underlying project data - - ProjectData viewedProjectData = dtp.getProjectData(); - DomainFolder domainFolder = - viewedProjectData.getFolder(linkedFolder.getLinkedPathname()); - - if (domainFolder != null) { - // delayed to ensure tree is displayed - Swing.runLater(() -> dtp.selectDomainFolder(domainFolder)); - } + // Check if active project can be used + URL activeProjectURL = activeProject.getProjectLocator().getURL(); + URL viewProjectURL = GhidraURL.getProjectURL(ghidraUrl); + String path = GhidraURL.getProjectPathname(ghidraUrl); + boolean useActiveProject = activeProjectURL.equals(viewProjectURL); + if (!useActiveProject) { + // Check for shared repository match + useActiveProject = + viewProjectURL.equals(activeProject.getProjectData().getSharedProjectURL()); } - catch (IOException e) { - Msg.showError(this, projectDataPanel, "Linked-folder failure: " + domainFile.getName(), - e); + if (useActiveProject) { + selectTreeNode(dataTreePanel, path, isFolder); + return; } - } - - void showInViewedProject(URL ghidraURL, boolean isFolder) { - - ProjectDataTreePanel dtp = projectDataPanel.openView(GhidraURL.getProjectURL(ghidraURL)); + // Show in viewed project tree + ProjectDataTreePanel dtp = projectDataPanel.openView(GhidraURL.getProjectURL(ghidraUrl)); if (dtp == null) { return; } Swing.runLater(() -> { // delayed to ensure tree is displayed - - ProjectData viewedProjectData = dtp.getProjectData(); - - String path = GhidraURL.getProjectPathname(ghidraURL); - - if (isFolder) { - DomainFolder viewedProjectFolder = getViewProjectFolder(viewedProjectData, path); - if (viewedProjectFolder != null) { - dtp.selectDomainFolder(viewedProjectFolder); - } - } - else { - DomainFile viewedProjectFile = getViewProjectFile(viewedProjectData, path); - if (viewedProjectFile != null) { - dtp.selectDomainFile(viewedProjectFile); - } - } + selectTreeNode(dtp, path, isFolder); }); } @@ -1198,6 +1198,71 @@ public class FrontEndPlugin extends Plugin return viewedProjectData.getFolder(path); } + public void showInProjectTree(ProjectData projectData, String path, boolean isFolder) { + if (activeProject.getProjectData() == projectData) { + // Active project tree + selectTreeNode(dataTreePanel, path, isFolder); + return; + } + + ProjectLocator projectLocator = projectData.getProjectLocator(); + URL viewURL = projectLocator.getURL(); + if (viewURL != null) { + ProjectDataTreePanel dtp = projectDataPanel.openView(viewURL); + // Found matching tree panel + selectTreeNode(dtp, path, isFolder); + return; + } + + Msg.error(this, "Failed to open project tree: " + projectLocator.getName()); + } + + private void selectTreeNode(ProjectDataTreePanel dtp, String path, boolean isFolder) { + + // NOTE: Would be nice to draw attention to the tree panel where the selection + // occurred since the selection may not change. + + ProjectData viewedProjectData = dtp.getProjectData(); + boolean foundIt = false; + if (isFolder) { + DomainFolder viewedProjectFolder = getViewProjectFolder(viewedProjectData, path); + if (viewedProjectFolder != null) { + if (viewedProjectFolder.isLinked()) { + isFolder = false; // linked-folder: must select as link-file node + } + else { + foundIt = true; + dtp.selectDomainFolder(viewedProjectFolder); + } + } + } + + if (!isFolder) { + DomainFile viewedProjectFile = getViewProjectFile(viewedProjectData, path); + if (viewedProjectFile != null) { + foundIt = true; + dtp.selectDomainFile(viewedProjectFile); + } + } + + DataTree dataTree = dtp.getDataTree(); + if (!foundIt) { + Msg.showError(this, dataTree, "Invalid ", + "Referenced path not found or it conflicts with a link-file: " + + dataTree.getModelRoot().getName() + ":" + path); + } + else if (dataTree.isFiltered()) { + if (filterWarningBuilder == null) { + filterWarningBuilder = + new OptionDialogBuilder("Active Tree Filter: " + dtp.getName(), + "A project tree filter is currently active and may block the selection"); + filterWarningBuilder.setMessageType(OptionDialog.WARNING_MESSAGE); + filterWarningBuilder.addDontShowAgainOption(); + } + filterWarningBuilder.show(tool.getToolFrame()); + } + } + private class MyToolChestChangeListener implements ToolChestChangeListener { @Override @@ -1216,4 +1281,5 @@ public class FrontEndPlugin extends Plugin } } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java index 90a733aaff..c2d89af22a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/FrontEndTool.java @@ -184,7 +184,7 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { if (!GhidraURL.isLocalProjectURL(url) && !GhidraURL.isServerRepositoryURL(url)) { return false; } - Swing.runLater(() -> execute(new AcceptUrlContentTask(url, plugin))); + Swing.runLater(() -> execute(new AcceptUrlContentTask(url, true, plugin))); return true; } @@ -355,8 +355,8 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener { "When enabled data buffers sent to Ghidra Server are compressed (see server " + "configuration for other direction)"); - options.registerOption(BLINKING_CURSORS_OPTION_NAME, true, help, "This controls whether" + - " text cursors blink when focused"); + options.registerOption(BLINKING_CURSORS_OPTION_NAME, true, help, + "This controls whether" + " text cursors blink when focused"); options.registerOption(RESTORE_PREVIOUS_PROJECT_NAME, true, help, "Restore the previous project when Ghidra starts."); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/GetDomainObjectTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/GetDomainObjectTask.java index c1ad49b0ec..8cf93f035d 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/GetDomainObjectTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/GetDomainObjectTask.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -37,7 +37,7 @@ import ghidra.util.task.TaskMonitor; * A file open for read-only use will be upgraded if needed and is possible. Once open it is * important that the specified consumer be released from the domain object when done using * the open object (see {@link DomainObject#release(Object)}). - */ + */ public class GetDomainObjectTask extends Task { private Object consumer; @@ -46,7 +46,7 @@ public class GetDomainObjectTask extends Task { private boolean immutable; private DomainObject versionedObj; - + /** * Construct task open specified domainFile read only. * An upgrade is performed if needed and is possible. @@ -76,9 +76,9 @@ public class GetDomainObjectTask extends Task { this.versionNumber = versionNumber; this.immutable = immutable; } - + @Override - public void run(TaskMonitor monitor) { + public void run(TaskMonitor monitor) { String contentType = domainFile.getContentType(); try { monitor.setMessage("Getting Version " + versionNumber + " for " + domainFile.getName()); @@ -97,7 +97,8 @@ public class GetDomainObjectTask extends Task { catch (IOException e) { ClientUtil.handleException(AppInfo.getActiveProject().getRepository(), e, contentType + " Open", null); - } catch (VersionException e) { + } + catch (VersionException e) { if (immutable && e.isUpgradable()) { String detailMessage = e.getDetailMessage() == null ? "" : "\n" + e.getDetailMessage(); @@ -115,10 +116,10 @@ public class GetDomainObjectTask extends Task { return; } VersionExceptionHandler.showVersionError(null, domainFile.getName(), - domainFile.getContentType(), contentType + " Open", e); + domainFile.getContentType(), contentType + " Open", false, e); } } - + /** * Return the domain object instance. * @return domain object which was opened or null if task cancelled or failed diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataFollowLinkAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataFollowLinkAction.java new file mode 100644 index 0000000000..0690854008 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataFollowLinkAction.java @@ -0,0 +1,111 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.main; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import docking.action.MenuData; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.main.datatable.FrontendProjectTreeAction; +import ghidra.framework.main.datatable.ProjectDataContext; +import ghidra.framework.main.datatree.DataTree; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.ProjectData; +import ghidra.framework.protocol.ghidra.GhidraURL; +import ghidra.util.HelpLocation; +import ghidra.util.Msg; + +public class ProjectDataFollowLinkAction extends FrontendProjectTreeAction { + + private FrontEndPlugin plugin; + + public ProjectDataFollowLinkAction(FrontEndPlugin plugin, String group) { + super("Follow Link", plugin.getName()); + this.plugin = plugin; + setPopupMenuData(new MenuData(new String[] { "Follow Link" }, group)); + setHelpLocation(new HelpLocation("FrontEndPlugin", "Follow_Link")); + } + + @Override + protected void actionPerformed(ProjectDataContext context) { + + List selectedFiles = context.getSelectedFiles(); + if (selectedFiles.size() != 1) { + return; + } + DomainFile file = selectedFiles.get(0); + if (!file.isLink()) { + return; + } + + // Folder link may refer to another folder link + String linkPath; + try { + linkPath = LinkHandler.getAbsoluteLinkPath(file); + if (linkPath == null) { + Msg.showError(this, context.getComponent(), "Invalid Link", + "Link-file failed to provide link path: " + file); + return; + } + } + catch (IOException e) { + Msg.showError(this, context.getComponent(), "Invalid Link", e.getMessage()); + return; + } + + boolean isFolderLink = file.getLinkInfo().isFolderLink(); + if (GhidraURL.isGhidraURL(linkPath)) { + // Follow URL using a project view + try { + plugin.showInViewedProject(new URL(linkPath), isFolderLink); + return; + } + catch (MalformedURLException e) { + Msg.error(this, "Invalid link URL: " + e.getMessage()); + return; + } + } + + // Check internal link + ProjectData projectData = context.getProjectData(); + boolean isFolder = isFolderLink && projectData.getFolder(linkPath) != null; + if (!isFolder) { + DomainFile referencedFile = projectData.getFile(linkPath); + if (referencedFile == null) { + // referenced folder or file not found + return; + } + } + + // Path is local to its project data tree + plugin.showInProjectTree(context.getProjectData(), linkPath, isFolder); + } + + @Override + protected boolean isEnabledForContext(ProjectDataContext context) { + if (!(context.getComponent() instanceof DataTree)) { + return false; + } + if (context.getFolderCount() != 0 || context.getFileCount() != 1) { + return false; + } + DomainFile file = context.getSelectedFiles().get(0); + return file.isLink(); + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java index 2ebc410ce4..5eb3f06970 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ProjectDataPanel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -172,6 +172,7 @@ class ProjectDataPanel extends JSplitPane implements ProjectViewListener { private void clearReadOnlyViews() { readOnlyTab.removeAll(); + readOnlyViews.values().forEach(ProjectDataTreePanel::dispose); readOnlyViews.clear(); setViewsVisible(false); } @@ -214,6 +215,10 @@ class ProjectDataPanel extends JSplitPane implements ProjectViewListener { if (projectData == null) { return null; // repository connection may have been cancelled } + + // Force refresh to purge any stale data + projectData.refresh(true); + projectManager.rememberViewedProject(projectView); String viewName = projectData.getProjectLocator().getName(); final ProjectDataTreePanel newPanel = diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java index 186034e61c..c3e7c2f6df 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/RepositoryChooser.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,13 +17,13 @@ package ghidra.framework.main; import java.awt.BorderLayout; import java.awt.CardLayout; +import java.awt.event.ItemListener; import java.awt.event.MouseEvent; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import javax.swing.*; -import javax.swing.event.ChangeListener; import javax.swing.event.MouseInputAdapter; import docking.ReusableDialogComponentProvider; @@ -173,26 +173,22 @@ class RepositoryChooser extends ReusableDialogComponentProvider { radioButtonPanel.getAccessibleContext().setAccessibleName("Radio Buttons"); radioButtonPanel.setBorder(BorderFactory.createTitledBorder("Repository Specification")); - ChangeListener choiceListener = e -> { - Object src = e.getSource(); - if (src instanceof JRadioButton) { - JRadioButton choiceButton = (JRadioButton) src; - choiceButton.getAccessibleContext().setAccessibleName("Choice"); - if (choiceButton.isSelected()) { - choiceActivated(choiceButton); - } + ItemListener choiceListener = e -> { + JRadioButton choiceButton = (JRadioButton) e.getSource(); + if (choiceButton.isSelected()) { + choiceActivated(choiceButton); } }; serverInfoChoice = new GRadioButton("Ghidra Server"); serverInfoChoice.getAccessibleContext().setAccessibleName("Ghidra Server"); serverInfoChoice.setSelected(true); - serverInfoChoice.addChangeListener(choiceListener); + serverInfoChoice.addItemListener(choiceListener); radioButtonPanel.add(serverInfoChoice); urlChoice = new GRadioButton("Ghidra URL"); - urlChoice.addChangeListener(choiceListener); urlChoice.getAccessibleContext().setAccessibleName("Ghidra URL"); + urlChoice.addItemListener(choiceListener); radioButtonPanel.add(urlChoice); ButtonGroup panelChoices = new ButtonGroup(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java index fceacc7439..e6fbdc1684 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/ToolButton.java @@ -21,7 +21,9 @@ import java.awt.dnd.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import javax.swing.*; @@ -33,6 +35,8 @@ import docking.dnd.*; import docking.tool.ToolConstants; import docking.util.image.ToolIconURL; import docking.widgets.EmptyBorderButton; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.LinkHandler.LinkStatus; import ghidra.framework.main.datatree.*; import ghidra.framework.model.*; import ghidra.framework.plugintool.PluginTool; @@ -507,7 +511,33 @@ class ToolButton extends EmptyBorderButton implements Draggable, Droppable { plugin.getActiveWorkspace().runTool(template); } else { - PluginTool tool = toolServices.launchTool(template.getName(), domainFiles); + List files = new ArrayList<>(); + domainFiles.forEach(file -> { + if (file.isLink()) { + if (file.getLinkInfo().isFolderLink()) { + return; // ignore folder links + } + AtomicReference errorMsg = new AtomicReference<>(); + LinkStatus status = + LinkHandler.getLinkFileStatus(file, error -> errorMsg.set(error)); + if (status == LinkStatus.BROKEN) { + String msg = errorMsg.get(); + String pathname = file.getPathname(); + if (!msg.contains(pathname)) { + msg += ": " + pathname; + } + Msg.showError(this, getParent(), "Failed to Open File", + msg + ": " + file.getPathname()); + return; + } + } + files.add(file); + }); + if (files.isEmpty()) { + return; + } + + PluginTool tool = toolServices.launchTool(template.getName(), files); if (tool == null) { Msg.showError(this, getParent(), "Failed to Launch Tool", "Failed to launch " + template.getName() + " tool.\nSee log for details."); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java index 726b7b56ed..16bc739fa5 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/DomainFileInfo.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,19 +19,22 @@ import java.util.*; import javax.swing.Icon; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.LinkHandler.LinkStatus; +import ghidra.framework.main.BrokenLinkIcon; +import ghidra.framework.main.datatree.DomainFileNode; import ghidra.framework.model.DomainFile; public class DomainFileInfo { - // TODO: should not hang onto DomainFile since it may not track changes anymore - // Think of DomainFile like a File object - private DomainFile domainFile; private String name; private String path; private Map metadata; private Date modificationDate; private DomainFileType domainFileType; + private Boolean isBrokenLink; + private String toolTipText; public DomainFileInfo(DomainFile domainFile) { this.domainFile = domainFile; @@ -84,14 +87,14 @@ public class DomainFileInfo { return path; } - public Icon getIcon() { - return domainFile.getIcon(false); - } - public synchronized DomainFileType getDomainFileType() { if (domainFileType == null) { + checkStatus(); String contentType = domainFile.getContentType(); Icon icon = domainFile.getIcon(false); + if (isBrokenLink) { + icon = new BrokenLinkIcon(icon); + } boolean isVersioned = domainFile.isVersioned(); domainFileType = new DomainFileType(contentType, icon, isVersioned); } @@ -131,14 +134,15 @@ public class DomainFileInfo { public synchronized void clearMetaCache() { metadata = null; modificationDate = null; - domainFileType = null; refresh(); } public synchronized void refresh() { - this.name = null; - this.path = null; - + domainFileType = null; + isBrokenLink = null; + toolTipText = null; + name = null; + path = null; } public String getMetaDataValue(String key) { @@ -150,4 +154,26 @@ public class DomainFileInfo { return domainFile.getName(); } + private void checkStatus() { + if (isBrokenLink == null) { + isBrokenLink = false; + List linkErrors = null; + if (domainFile.isLink()) { + List errors = new ArrayList<>(); + LinkStatus linkStatus = + LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg)); + isBrokenLink = (linkStatus == LinkStatus.BROKEN); + if (isBrokenLink) { + linkErrors = errors; + } + } + toolTipText = DomainFileNode.getToolTipText(domainFile, linkErrors); + } + } + + public String getToolTip() { + checkStatus(); + return toolTipText; + } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataContext.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataContext.java index 2d3778524e..97cd908a46 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataContext.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataContext.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -66,6 +66,10 @@ public class ProjectDataContext extends DefaultActionContext implements DomainFi return (getFolderCount() + getFileCount()) == 1; } + public boolean hasOneOrMoreFilesAndFolders() { + return getFolderCount() + getFileCount() > 0; + } + public int getFolderCount() { if (selectedFolders == null) { return 0; @@ -101,10 +105,6 @@ public class ProjectDataContext extends DefaultActionContext implements DomainFi return !projectData.getRootFolder().isInWritableProject(); } - public boolean hasOneOrMoreFilesAndFolders() { - return getFolderCount() + getFileCount() > 0; - } - public boolean containsRootFolder() { if (getFolderCount() == 0) { return false; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java index d09c92b596..f3ca6b5efe 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectDataTablePanel.java @@ -103,6 +103,7 @@ public class ProjectDataTablePanel extends JPanel { .addListSelectionListener(e -> plugin.getTool().contextChanged(null)); gTable.setDefaultRenderer(Date.class, new DateCellRenderer()); gTable.setDefaultRenderer(DomainFileType.class, new TypeCellRenderer()); + gTable.getColumn("Name").setCellRenderer(new NameCellRenderer()); // self-registering drag provider new ProjectDataTableDragProvider(); @@ -125,6 +126,10 @@ public class ProjectDataTablePanel extends JPanel { help.registerHelp(table, helpLocation); } + public void setFilter(String filterText) { + table.setFiterText(filterText); + } + public void setSelectedDomainFiles(Set files) { if (model.isBusy()) { // we don't want to attempt to find the items to select while we the threaded @@ -208,8 +213,7 @@ public class ProjectDataTablePanel extends JPanel { public ActionContext getActionContext(ComponentProvider provider, MouseEvent e) { int[] selectedRows = gTable.getSelectedRows(); if (selectedRows.length == 0) { - return new ProjectDataContext(provider, projectData, gTable, null, null, gTable, - true); + return new ProjectDataContext(provider, projectData, gTable, null, null, gTable, true); } List list = new ArrayList<>(); @@ -535,15 +539,41 @@ public class ProjectDataTablePanel extends JPanel { JLabel renderer = (JLabel) super.getTableCellRendererComponent(data); - Object value = data.getValue(); - - renderer.setText(""); - if (value != null) { - DomainFileType type = (DomainFileType) value; - setToolTipText(type.getContentType()); - setText(""); - setIcon(type.getIcon()); + DomainFileInfo info = (DomainFileInfo) data.getRowObject(); + if (info != null) { + DomainFileType type = (DomainFileType) data.getValue(); + renderer.setText(type.toString()); + renderer.setIcon(type.getIcon()); + String toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(info.getToolTip()); + renderer.setToolTipText(toolTipText); } + else { + renderer.setText(""); + renderer.setToolTipText(null); + } + + return renderer; + } + } + + private class NameCellRenderer extends GTableCellRenderer { + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + + JLabel renderer = (JLabel) super.getTableCellRendererComponent(data); + + DomainFileInfo info = (DomainFileInfo) data.getRowObject(); + if (info != null) { + renderer.setText((String) data.getValue()); + String toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(info.getToolTip()); + renderer.setToolTipText(toolTipText); + } + else { + renderer.setText(""); + renderer.setToolTipText(null); + } + return renderer; } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeContext.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeContext.java index 3d9aca35cb..06739260e8 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeContext.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatable/ProjectTreeContext.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -44,6 +44,16 @@ public interface ProjectTreeContext { */ public int getFileCount(); + /** + * {@return true of only one file or folder has been selected, else false.} + */ + public boolean hasExactlyOneFileOrFolder(); + + /** + * {@return true if one or more file and/or folders have been selected, else false.} + */ + public boolean hasOneOrMoreFilesAndFolders(); + /** * Returns a list of {@link DomainFolder}s selected in the tree. * @return a list of {@link DomainFolder}s selected in the tree. diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java index 8d62de971b..f3b5199ecd 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ChangeManager.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,69 +15,169 @@ */ package ghidra.framework.main.datatree; +import java.io.IOException; import java.util.*; +import java.util.function.Consumer; + +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreePath; import docking.widgets.tree.GTreeNode; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.main.datatree.DataTreeNode.NodeType; import ghidra.framework.model.*; /** * Class to handle changes when a domain folder changes; updates the * tree model to reflect added/removed/renamed nodes. */ -class ChangeManager implements DomainFolderChangeListener { +class ChangeManager implements DomainFolderChangeListener, TreeModelListener { private DomainFolderRootNode root; + private ProjectData projectData; // may be null private ProjectDataTreePanel treePanel; private DataTree tree; + // + // Link back-reference tree + // Associates file/folder-links with their referenced linked-files and folders. + // This tracking allows for rapid identification of link-related tree nodes which + // may be impacted by changes made to other files and folders. + // + private LinkedTreeNode linkTreeRoot = new LinkedTreeNode(null, null); + + private boolean skipLinkUpdate = false; // updates within Swing event dispatch thread only + ChangeManager(ProjectDataTreePanel treePanel) { this.treePanel = treePanel; + projectData = treePanel.getProjectData(); tree = treePanel.getDataTree(); root = (DomainFolderRootNode) tree.getModelRoot(); + if (projectData != null) { + // Without a project this change manager does nothing (e.g., empty tree) + projectData.addDomainFolderChangeListener(this); + tree.addGTModelListener(this); + } + } + + void dispose() { + if (projectData != null) { + projectData.removeDomainFolderChangeListener(this); + tree.removeGTModelListener(this); + projectData = null; + } + } + + // + // File Changes + // + + @Override + public void domainFileAdded(DomainFile file) { + boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink(); + String fileName = file.getName(); + DomainFolder parentFolder = file.getParent(); + updateLinkedContent(parentFolder, p -> addFileNode(p, fileName, isFolderLink), + ltn -> ltn.refreshLinks(fileName)); + DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true); + if (folderNode != null && folderNode.isLoaded()) { + addFileNode(folderNode, fileName, isFolderLink); + } } @Override public void domainFileRemoved(DomainFolder parent, String name, String fileID) { - updateFolderNode(parent); + updateLinkedContent(parent, null, ltn -> ltn.refreshLinks(name)); DomainFolderNode folderNode = findDomainFolderNode(parent, true); - if (folderNode == null) { - return; + if (folderNode != null) { + updateChildren(folderNode); } + } - List children = folderNode.getChildren(); - for (GTreeNode child : children) { - if (child instanceof DomainFileNode) { - if (child.getName().equals(name)) { - folderNode.removeNode(child); - } - } + @Override + public void domainFileRenamed(DomainFile file, String oldName) { + boolean isFolderLink = file.isLink() && file.getLinkInfo().isFolderLink(); + updateLinkedContent(file.getParent(), p -> { + updateChildren(p); + addFileNode(p, file.getName(), isFolderLink); + }, ltn -> { + ltn.refreshLinks(oldName); + ltn.refreshLinks(file.getName()); + }); + DomainFolder parent = file.getParent(); + skipLinkUpdate = true; + try { + domainFileRemoved(parent, oldName, file.getFileID()); + domainFileAdded(file); + } + finally { + skipLinkUpdate = false; + } + } + + @Override + public void domainFileMoved(DomainFile file, DomainFolder oldParent, String oldName) { + domainFileRemoved(oldParent, oldName, null); + domainFileAdded(file); + } + + @Override + public void domainFileStatusChanged(DomainFile file, boolean fileIDset) { + DomainFolder parentFolder = file.getParent(); + updateLinkedContent(parentFolder, fn -> { + /* No folder update required */ + }, ltn -> ltn.refreshLinks(file.getName())); + DomainFileNode fileNode = findDomainFileNode(file, true); + if (fileNode != null) { + fileNode.refresh(); + } + treePanel.contextChanged(); + } + + // + // Folder Changes + // + + @Override + public void domainFolderAdded(DomainFolder folder) { + String folderName = folder.getName(); + DomainFolder parentFolder = folder.getParent(); + updateLinkedContent(parentFolder, p -> addFolderNode(p, folderName), + ltn -> ltn.refreshLinks(folderName)); + DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true); + if (folderNode != null && folderNode.isLoaded()) { + addFolderNode(folderNode, folderName); } } @Override public void domainFolderRemoved(DomainFolder parent, String name) { - updateFolderNode(parent); - - ArrayList folderPath = new ArrayList(); - getFolderPath(parent, folderPath); - folderPath.add(name); - - DomainFolderNode folderNode = findDomainFolderNode(folderPath, true); + updateLinkedContent(parent, null, ltn -> ltn.refreshLinks(name)); + DomainFolderNode folderNode = findDomainFolderNode(parent, true); if (folderNode != null) { - folderNode.getParent().removeNode(folderNode); + updateChildren(folderNode); } } @Override public void domainFolderRenamed(DomainFolder folder, String oldName) { - domainFolderRemoved(folder.getParent(), oldName); - domainFolderAdded(folder); - } - - @Override - public void domainFileRenamed(DomainFile file, String oldName) { - domainFileRemoved(file.getParent(), oldName, file.getFileID()); - domainFileAdded(file); + updateLinkedContent(folder.getParent(), p -> { + updateChildren(p); + addFolderNode(p, folder.getName()); + }, ltn -> { + ltn.refreshLinks(oldName); + ltn.refreshLinks(folder.getName()); + }); + DomainFolder parent = folder.getParent(); + skipLinkUpdate = true; + try { + domainFolderRemoved(parent, oldName); + domainFolderAdded(folder); + } + finally { + skipLinkUpdate = false; + } } @Override @@ -86,52 +186,6 @@ class ChangeManager implements DomainFolderChangeListener { domainFolderAdded(folder); } - @Override - public void domainFileMoved(DomainFile file, DomainFolder oldParent, String oldName) { - updateFolderNode(oldParent); - domainFileAdded(file); - } - - @Override - public void domainFileAdded(DomainFile file) { - DomainFileNode domainFileNode = findDomainFileNode(file, true); - if (domainFileNode != null) { - return; - } - DomainFolder parent = file.getParent(); - DomainFolderNode folderNode = findDomainFolderNode(parent, true); - if (folderNode != null) { - if (folderNode.isLoaded()) { - DomainFileNode newNode = new DomainFileNode(file); - addNode(folderNode, newNode); - } - } - } - - static void addNode(GTreeNode parentNode, GTreeNode newNode) { - List allChildren = parentNode.getChildren(); - int index = Collections.binarySearch(allChildren, newNode); - if (index < 0) { - index = -index - 1; - } - parentNode.addNode(index, newNode); - } - - @Override - public void domainFolderAdded(DomainFolder folder) { - DomainFolderNode domainFolderNode = findDomainFolderNode(folder, true); - if (domainFolderNode != null) { - return; - } - DomainFolder parentFolder = folder.getParent(); - DomainFolderNode folderNode = findDomainFolderNode(parentFolder, true); - if (folderNode != null && folderNode.isLoaded()) { - DomainFolderNode newNode = - new DomainFolderNode(folder, folderNode.getDomainFileFilter()); - addNode(folderNode, newNode); - } - } - @Override public void domainFolderSetActive(DomainFolder folder) { DomainFolderNode folderNode = findDomainFolderNode(folder, false); @@ -140,13 +194,58 @@ class ChangeManager implements DomainFolderChangeListener { } } - @Override - public void domainFileStatusChanged(DomainFile file, boolean fileIDset) { - DomainFileNode fileNode = findDomainFileNode(file, true); - if (fileNode != null) { - fileNode.refresh(); + // + // Helper methods + // + + private DomainFolder getDomainFolder(DataTreeNode node) { + DomainFolder folder = null; + if (node instanceof DomainFileNode fileNode) { + folder = fileNode.getLinkedFolder(); // may return null + } + else if (node instanceof DomainFolderNode folderNode) { + folder = folderNode.getDomainFolder(); + } + return folder; + } + + private void addFileNode(DataTreeNode node, String fileName, boolean isFolderLink) { + if (node.isLeaf() || !node.isLoaded()) { + return; + } + // Check for existance of file by that name + DomainFileNode fileNode = (DomainFileNode) node.getChild(fileName, + isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE); + if (fileNode != null) { + domainFileStatusChanged(fileNode.getDomainFile(), false); + return; + } + DomainFolder folder = getDomainFolder(node); + if (folder != null) { + DomainFile file = folder.getFile(fileName); + if (file != null) { + DomainFileNode newNode = new DomainFileNode(file, root.getDomainFileFilter()); + node.addNode(newNode); + } + } + } + + private void addFolderNode(DataTreeNode node, String folderName) { + if (node.isLeaf() || !node.isLoaded()) { + return; + } + // Check for existance of folder by that name + if (node.getChild(folderName, NodeType.FOLDER) != null) { + return; + } + DomainFolder folder = getDomainFolder(node); + if (folder != null) { + DomainFolder f = folder.getFolder(folderName); + if (f != null) { + DomainFolderNode newNode = new DomainFolderNode(f, root.getDomainFileFilter()); + node.addNode(newNode); + } } - treePanel.domainChange(); } private void getFolderPath(DomainFolder df, List list) { @@ -164,14 +263,12 @@ class ChangeManager implements DomainFolderChangeListener { } private DomainFolderNode findDomainFolderNode(List folderPath, boolean lazy) { - DomainFolderNode folderNode = root; for (String name : folderPath) { if (lazy && !folderNode.isLoaded()) { return null; // not visited } - folderNode = - (DomainFolderNode) folderNode.getChild(name, n -> (n instanceof DomainFolderNode)); + folderNode = (DomainFolderNode) folderNode.getChild(name, NodeType.FOLDER); if (folderNode == null) { return null; } @@ -187,32 +284,333 @@ class ChangeManager implements DomainFolderChangeListener { if (lazy && !folderNode.isLoaded()) { return null; // not visited } - + boolean isFolderLink = domainFile.isLink() && domainFile.getLinkInfo().isFolderLink(); return (DomainFileNode) folderNode.getChild(domainFile.getName(), - n -> (n instanceof DomainFileNode)); + isFolderLink ? NodeType.FOLDER_LINK : NodeType.FILE); } - private void updateFolderNode(DomainFolder parent) { - DomainFolderNode folderNode = findDomainFolderNode(parent, true); - if (folderNode == null) { + /** + * Removes all children within the specified {@code parentNode} which no longer exist. + * @param parentNode parent node within tree + */ + private void updateChildren(DataTreeNode parentNode) { + + if (!parentNode.isLoaded()) { return; } - DomainFolder folder = folderNode.getDomainFolder(); + + DomainFolder folder = null; + if (parentNode instanceof DomainFileNode fileNode) { + folder = fileNode.getLinkedFolder(); + } + else if (parentNode instanceof DomainFolderNode folderNode) { + folder = folderNode.getDomainFolder(); + } + if (folder == null) { + return; + } + // loop through children looking for nodes whose underlying model object // does not have this folder as its parent; - List children = folderNode.getChildren(); + List children = parentNode.getChildren(); for (GTreeNode child : children) { if (child instanceof DomainFileNode) { if (folder.getFile(child.getName()) == null) { - folderNode.removeNode(child); + parentNode.removeNode(child); } } else if (child instanceof DomainFolderNode) { if (folder.getFolder(child.getName()) == null) { - folderNode.removeNode(child); + parentNode.removeNode(child); } } } } + // + // DataTree listener + // + + @Override + public void treeStructureChanged(TreeModelEvent e) { + + // This is used when an existing node is loaded to register all of its link-file children + // since the occurance of treeNodesChanged cannot be relied upon for notification of + // these existing children. + + TreePath treePath = e.getTreePath(); + if (treePath == null) { + return; + } + Object treeNode = treePath.getLastPathComponent(); + if (!(treeNode instanceof DataTreeNode dataTreeNode)) { + return; + } + if (!dataTreeNode.isLoaded()) { + return; + } + // Register all visible link-file nodes + for (GTreeNode child : dataTreeNode.getChildren()) { + if (child instanceof DomainFileNode fileNode) { + if (fileNode.getDomainFile().isLink()) { + addLinkFile(fileNode); + } + } + } + } + + @Override + public void treeNodesChanged(TreeModelEvent e) { + + // This is used to register link-file nodes which may be added to the tree as a result + // of changes to the associated project data. + + Object treeNode = e.getTreePath().getLastPathComponent(); + if (treeNode instanceof DomainFileNode fileNode) { + addLinkFile(fileNode); + } + } + + @Override + public void treeNodesInserted(TreeModelEvent e) { + // Do nothing + } + + @Override + public void treeNodesRemoved(TreeModelEvent e) { + // Do nothing + } + + // + // Link tracking tree update support + // + + /** + * Update link tree if the specified {@code domainFileNode} corresponds to an link-file + * which has an internal link-path which links to either a file or folder within the same + * project. Removal of obsolete link details within the link tree is done is a lazy + * fashion when refresh methods are invoked on a {@link LinkedTreeNode}. + * + * @param domainFileNode domain file tree node + */ + void addLinkFile(DomainFileNode domainFileNode) { + + DomainFile file = domainFileNode.getDomainFile(); + + LinkFileInfo linkInfo = file.getLinkInfo(); + if (linkInfo == null || linkInfo.isExternalLink()) { + return; + } + + try { + String linkPath = LinkHandler.getAbsoluteLinkPath(file); + if (linkPath == null) { + return; + } + boolean isFolderLink = linkInfo.isFolderLink(); + String[] pathElements = linkPath.split("/"); + int lastFolderIndex = pathElements.length - 1; + if (!isFolderLink) { + --lastFolderIndex; + } + LinkedTreeNode folderLinkNode = linkTreeRoot; + for (int i = 1; i <= lastFolderIndex; i++) { + folderLinkNode = folderLinkNode.addFolder(pathElements[i]); + } + + if (isFolderLink) { + folderLinkNode.addLinkedFolder(domainFileNode); + } + else { + folderLinkNode.addLinkedFile(pathElements[lastFolderIndex + 1], domainFileNode); + } + } + catch (IOException e) { + // ignore + } + } + + /** + * Perform updates of linked tree content which relate to content within the specified + * {@code parentFolder}. All loaded folder linkages which include the specified + * {@code parentFolder} will be checked and the specified {@code folderNodeConsumer} will + * be invoked for each parent tree node which is a linked-reflection of it to facilitate + * specific updates. In addition, the specified {@code linkNodeConsumer} will be invoked + * once if a {@code LinkedTreeNode} is found which corresponds to the specified + * {@code parentFolder}. This allows targeted refresh of link-files. + * + * @param parentFolder a parent folder which relates to a change + * @param folderNodeConsumer optional consumer which will be invoked for each loaded parent + * tree node which is a linked-reflection of the specified {@code parentFolder}. If null is + * specified for this consumer a general update will be performed to remove any missing nodes. + * @param linkNodeConsumer optional consumer which will be invoked once if a {@code LinkedTreeNode} + * is found which corresponds to the specified {@code parentFolder}. + */ + void updateLinkedContent(DomainFolder parentFolder, Consumer folderNodeConsumer, + Consumer linkNodeConsumer) { + if (skipLinkUpdate) { + return; + } + String pathname = parentFolder.getPathname(); + String[] pathElements = pathname.split("/"); + LinkedTreeNode folderLinkNode = linkTreeRoot; + folderLinkNode.updateLinkedContent(pathElements, 1, folderNodeConsumer); + for (int i = 1; i < pathElements.length; i++) { + folderLinkNode = folderLinkNode.folderMap.get(pathElements[i]); + if (folderLinkNode == null) { + return; // requested folder not contained within link-tree + } + folderLinkNode.updateLinkedContent(pathElements, i + 1, folderNodeConsumer); + } + + // Requested folder was found in link-tree - invoke consumer to perform + // selective refresh + if (linkNodeConsumer != null) { + linkNodeConsumer.accept(folderLinkNode); + } + } + + private class LinkedTreeNode { + + private final LinkedTreeNode parent; + private final String name; + + private Map folderMap = new HashMap<>(); + private Set folderLinks = new HashSet<>(); + private Map> linkedFilesMap = new HashMap<>(); + + LinkedTreeNode(LinkedTreeNode parent, String name) { + this.parent = parent; + this.name = name; + } + + private void updateLinkedContent(String[] pathElements, int subFolderPathIndex, + Consumer folderNodeConsumer) { + + // NOTE: This logic will not handle recursively linked-folders which is not supported. + + boolean updateThisNode = subFolderPathIndex >= pathElements.length; + + for (DomainFileNode folderLink : folderLinks) { + + if (!folderLink.isLoaded()) { + continue; + } + + if (updateThisNode) { + if (folderNodeConsumer != null) { + folderNodeConsumer.accept(folderLink); + } + else { + updateChildren(folderLink); + } + continue; + } + + DomainFolderNode folderNode = null; + for (int ix = subFolderPathIndex; ix < pathElements.length; ++ix) { + folderNode = + (DomainFolderNode) folderLink.getChild(pathElements[ix], NodeType.FOLDER); + if (folderNode == null || !folderNode.isLoaded()) { + folderNode = null; + break; + } + } + if (folderNode != null) { + if (folderNodeConsumer != null) { + folderNodeConsumer.accept(folderNode); + } + else { + updateChildren(folderNode); + } + } + } + + } + + private void refreshLinks(String childName) { + // We are forced to refresh file-links and folder-links since a folder-link may be + // referencing another folder-link file and not the final referenced folder. + if (refreshFileLinks(childName) || refreshFolderLinks(childName)) { + purgeFolderWithoutLinks(); + } + } + + private boolean refreshFolderLinks(String folderName) { + LinkedTreeNode linkedTreeNode = folderMap.get(folderName); + if (linkedTreeNode != null) { + refresh(linkedTreeNode.folderLinks); + return linkedTreeNode.folderLinks.isEmpty(); + } + return false; + } + + private boolean refreshFileLinks(String fileName) { + Set linkFiles = linkedFilesMap.get(fileName); + if (linkFiles != null) { + refresh(linkFiles); + if (linkFiles.isEmpty()) { + linkedFilesMap.remove(fileName); + return true; + } + } + return false; + } + + private LinkedTreeNode addFolder(String folderName) { + return folderMap.computeIfAbsent(folderName, n -> new LinkedTreeNode(this, n)); + } + + private void addLinkedFolder(DomainFileNode folderLink) { + folderLinks.add(folderLink); + } + + private void addLinkedFile(String fileName, DomainFileNode fileLink) { + Set fileLinks = + linkedFilesMap.computeIfAbsent(fileName, n -> new HashSet<>()); + fileLinks.add(fileLink); + } + + private void purgeFolderWithoutLinks() { + if (parent != null && folderMap.isEmpty() && folderLinks.isEmpty() && + linkedFilesMap.isEmpty()) { + parent.folderMap.remove(name); + parent.purgeFolderWithoutLinks(); + } + } + + private static void refresh(Set linkFiles) { + List purgeList = null; + for (DomainFileNode fileLink : linkFiles) { + DomainFile file = fileLink.getDomainFile(); + // Perform lazy purge of missing link files + if (!file.isLink()) { + if (purgeList == null) { + purgeList = new ArrayList<>(); + } + purgeList.add(fileLink); + } + else { + fileLink.refresh(); + } + } + if (purgeList != null) { + linkFiles.removeAll(purgeList); + } + } + + private String getPathname() { + if (parent == null) { + return "/"; + } + return parent.getPathname() + name + "/"; + } + + @Override + public String toString() { + return getPathname(); + } + + } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java index 15a1fd4c98..35c48efadc 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/CheckInTask.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -95,7 +95,7 @@ public class CheckInTask extends VersionControlTask implements CheckinHandler { } catch (VersionException e) { VersionExceptionHandler.showVersionError(parent, df.getName(), - df.getContentType(), "Checkin", e); + df.getContentType(), "Check In", false, e); } if (myMonitor.isCancelled()) { break; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/Cuttable.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/Cuttable.java index d16b898679..3cf07922cd 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/Cuttable.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/Cuttable.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,8 +15,20 @@ */ package ghidra.framework.main.datatree; +/** + * {@link Cuttable} associated with an element which supports cut/paste operation + */ public interface Cuttable { - public void setIsCut(boolean b); + + /** + * Set this node to be deleted so that it can be rendered as such. + * @param isCut true if node will be cut and moved + */ + public void setIsCut(boolean isCut); + + /** + * {@return true if node will be cut and moved} + */ public boolean isCut(); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java index 1bfe373361..4aebd2fd41 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTree.java @@ -16,6 +16,7 @@ package ghidra.framework.main.datatree; import java.awt.event.KeyEvent; +import java.io.IOException; import javax.swing.KeyStroke; import javax.swing.ToolTipManager; @@ -26,7 +27,9 @@ import docking.action.DockingAction; import docking.actions.KeyBindingUtils; import docking.widgets.tree.GTree; import docking.widgets.tree.GTreeNode; +import ghidra.framework.data.LinkHandler.LinkStatus; import ghidra.framework.main.FrontEndTool; +import ghidra.framework.model.*; /** * Tree that shows the folders and domain files in a Project @@ -105,4 +108,72 @@ public class DataTree extends GTree { public void stopEditing() { getJTree().stopEditing(); } + + /** + * Method returns either a {@link DomainFolder} within the node's project or null. + * The following cases indicate how the return value is established + * based on the specified {@link GTreeNode node}: + *

      + *
    1. {@link DomainFolderNode} - the node's domain folder will be returned
    2. + *
    3. {@link DomainFileNode} (folder-link content type) - the referenced folder within the node's + * project will be returned under the following conditions, otherwise null will be returned:

    4. + *
        + *
      • The file corresponds to a folder-link, and
      • + *
      • the folder-link ultimately refers to a domain folder within the same project + * (i.e., a URL-based link path is not used and link status is {@link LinkStatus#INTERNAL}).
      • + *
      + *
    5. {@link DomainFileNode} (normal file or file-link) - the node's parent folder will be + * returned.
    6. + *
    + *

    + * Folder-links which reference other internal folder-links will be followed until a + * folder can be identified or the link-chain is considered is {@link LinkStatus#BROKEN} + * or {@link LinkStatus#EXTERNAL} in which case null will be returned. + *

    + * A {@link LinkedDomainFolder} will always be resolved to its real folder which it corresponds to. + *

    + * + * @param node Data Tree Node to be evaluated for its real internal folder + * @return internal project folder which corresponds to the specified node. + */ + public static DomainFolder getRealInternalFolderForNode(GTreeNode node) { + DomainFolder folder = null; + if (node instanceof DomainFolderNode folderNode) { + folder = folderNode.getDomainFolder(); + } + else if (node instanceof DomainFileNode fileNode) { + if (fileNode.isFolderLink()) { + // Handle case where file node corresponds to a folder-link. + // Folder-Link status needs to be checked to ensure it corresponds to a folder + // internal to the same project. + LinkFileInfo linkInfo = fileNode.getDomainFile().getLinkInfo(); + if (linkInfo == null) { + return null; // unexpected + } + LinkStatus linkStatus = linkInfo.getLinkStatus(null); + if (linkStatus != LinkStatus.INTERNAL) { + return null; + } + // Get linked folder - status check ensures null will not be returned + folder = linkInfo.getLinkedFolder(); + } + else { + // Handle normal file cases where we return node's parent folder + GTreeNode parent = node.getParent(); + if (parent instanceof DomainFolderNode folderNode) { + folder = folderNode.getDomainFolder(); + } + } + } + if (folder instanceof LinkedDomainFolder linkedFolder) { + // Resolve linked internal folder to its real folder + try { + folder = linkedFolder.getRealFolder(); + } + catch (IOException e) { + folder = null; + } + } + return folder; + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java index e21a238206..6e3798f48c 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeDragNDropHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -118,8 +118,7 @@ public class DataTreeDragNDropHandler implements GTreeDragNDropHandler { if (ToolConstants.NO_ACTIVE_PROJECT.equals(destUserData.getName())) { return false; } - - return true; + return DataTree.getRealInternalFolderForNode(destUserData) != null; } @Override @@ -164,30 +163,28 @@ public class DataTreeDragNDropHandler implements GTreeDragNDropHandler { private List removeDuplicates(List allNodes) { - List folderNodes = getDomainFolderNodes(allNodes); + List parentNodes = getDomainParentNodes(allNodes); // if a file has a parent in the list, then it is not needed as a separate entry return allNodes.stream() - .filter(node -> !isChildOfFolders(folderNodes, node)) + .filter(node -> !isChildOfParents(parentNodes, node)) .collect(Collectors.toList()); } - private List getDomainFolderNodes(List nodeList) { - List folderList = new ArrayList<>(); - + private List getDomainParentNodes(List nodeList) { + List parentList = new ArrayList<>(); for (GTreeNode node : nodeList) { - if (node instanceof DomainFolderNode) { - folderList.add(node); + if (!node.isLeaf()) { + parentList.add(node); } } - - return folderList; + return parentList; } - private boolean isChildOfFolders(List folderNodes, GTreeNode fileNode) { + private boolean isChildOfParents(List parentNodes, GTreeNode fileNode) { GTreeNode node = fileNode.getParent(); while (node != null) { - if (folderNodes.contains(node)) { + if (parentNodes.contains(node)) { return true; } node = node.getParent(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java new file mode 100644 index 0000000000..a1537eae5f --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DataTreeNode.java @@ -0,0 +1,313 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.main.datatree; + +import java.util.*; + +import docking.widgets.tree.GTreeNode; +import docking.widgets.tree.GTreeSlowLoadingNode; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.LinkHandler.LinkStatus; +import ghidra.framework.model.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * {@link DataTreeNode} provides the base implementation for all node types contained within + * a {@link DataTree}. + */ +public abstract class DataTreeNode extends GTreeSlowLoadingNode implements Cuttable { + + /** + * {@link NodeType} is used to aid the sorting/comparison of data tree node. The + * sort order is based upon the following comparisons in order of significance: + *

      + *
    1. Node type weighting. Folder and Folder-Links have equal weighting.
    2. + *
    3. Node comparison by name (see {@link DataTreeNode#compareNodeNames(String, String)}).
    4. + *
    5. Node type ordinal (e.g., ensures that a Folder-Link with the same name as a Folder + * will be placed after the Folder.
    6. + *
    + */ + enum NodeType { + + FOLDER(1), FOLDER_LINK(1), FILE(2), OTHER(3); + + int weight; + + NodeType(int weight) { + this.weight = weight; + } + + static NodeType getNodeType(GTreeNode node) { + if (node instanceof DomainFolderNode) { + return FOLDER; + } + if (node instanceof DomainFileNode fileNode) { + return fileNode.isFolderLink() ? FOLDER_LINK : FILE; + } + return OTHER; + } + } + + /** + * Sort {@link Comparator} for use with sorting children and node comparison + */ + static final Comparator DATA_NODE_SORT_COMPARATOR = new DataNodeSortComparator(); + + /** + * Search {@link Comparator} for use by {@link #getChild(String, NodeType)} only + */ + private static final DataNodeSearchComparator DATA_NODE_SEARCH_COMPARATOR = + new DataNodeSearchComparator(); + + private volatile boolean isCut; // true if this node is marked as cut + + @Override + public final void setIsCut(boolean isCut) { + if (isCut != this.isCut) { + this.isCut = isCut; + fireNodeChanged(); + } + } + + @Override + public final boolean isCut() { + return isCut; + } + + /** + * Get the project data instance to which this file or folder belongs. + * @return project data instance + */ + public abstract ProjectData getProjectData(); + + @Override + public abstract int compareTo(GTreeNode node); + + @Override + public abstract boolean equals(Object obj); + + @Override + public abstract int hashCode(); + + @Override + public void addNode(GTreeNode newNode) { + if (!isLoaded()) { + return; + } + List allChildren = getChildren(); + int index = Collections.binarySearch(allChildren, newNode, DATA_NODE_SORT_COMPARATOR); + if (index < 0) { + index = -index - 1; + } + addNode(index, newNode); + + if (newNode instanceof DomainFolderNode) { + // Refresh possible conflicting folder-link + DomainFileNode folderLink = + (DomainFileNode) getChild(newNode.getName(), NodeType.FOLDER_LINK); + if (folderLink != null) { + folderLink.refresh(); + } + } + } + + @Override + public void removeNode(GTreeNode node) { + if (!isLoaded()) { + return; + } + // NOTE: Remove node is not implemented in a manner where we can remove by index + // using a binary search. + super.removeNode(node); + + if (node instanceof DomainFolderNode) { + // Refresh possible conflicting folder-link resolved + DomainFileNode folderLink = + (DomainFileNode) getChild(node.getName(), NodeType.FOLDER_LINK); + if (folderLink != null) { + folderLink.refresh(); + } + } + } + +// NOTE: The use of this method should be blocked since it does not properly handle duplicate child +// names within the same folder. +// /** +// * Domain folders and files may have the same name within a parent. This method should +// * not be used. +// */ +// @Override +// public final GTreeNode getChild(String name) { +// throw new UnsupportedOperationException("DataTree node names may not be unique"); +// } + + /** + * Find a child using a binary-search approach. + * + * @param name name of child to find + * @param type node type + * @return matching tree node or null if not found + */ + public abstract GTreeNode getChild(String name, NodeType type); + + /** + * Find a child using a binary-search approach vs. the default brute-force search. + * Note that two supported node types may have the same name, one being a {@link DomainFolderNode} + * and the other being a {@link DomainFileNode}. Folders are always placed before Files, + * although such different node types with the same name are not adjacent. For this reason + * a binary search cannot be used with a arbitrary predicate. + * + * @param children children to be searched + * @param name name of child to find + * @param type node type + * @return matching tree node or null if not found + */ + @SuppressWarnings("unchecked") + static GTreeNode getChild(List children, String name, NodeType type) { + ChildSearchRecord childSearchRecord = new ChildSearchRecord(name, type); + int index = + Collections.binarySearch(children, childSearchRecord, DATA_NODE_SEARCH_COMPARATOR); + return index >= 0 ? children.get(index) : null; + } + + private record ChildSearchRecord(String name, NodeType type) { + } + + @SuppressWarnings("rawtypes") + private static class DataNodeSearchComparator implements Comparator { + @Override + public int compare(Object o1, Object o2) { + + GTreeNode node = (GTreeNode) o1; + ChildSearchRecord childSearchRecord = (ChildSearchRecord) o2; + + NodeType type1 = NodeType.getNodeType(node); + NodeType type2 = childSearchRecord.type; + + int comp = type1.weight - type2.weight; + if (comp != 0) { + return comp; + } + + // NOTE: This name comparison is consistent with the sort order and + // will provide a case-senstive name-match + comp = compareNodeNames(node.getName(), childSearchRecord.name); + if (comp == 0) { + return type1.ordinal() - type2.ordinal(); + } + return comp; + } + } + + private static class DataNodeSortComparator implements Comparator { + @Override + public int compare(GTreeNode o1, GTreeNode o2) { + + // + // Goal is to have folders appear before files except for folder-links + // which should be grouped with folders but come after a folder with + // the same name + + NodeType type1 = NodeType.getNodeType(o1); + NodeType type2 = NodeType.getNodeType(o2); + + int comp = type1.weight - type2.weight; + if (comp != 0) { + return comp; + } + + // NOTE: This name comparison is consistent with compareTo implementaions + comp = compareNodeNames(o1.getName(), o2.getName()); + if (comp == 0) { + return type1.ordinal() - type2.ordinal(); + } + return comp; + } + } + + /** + * Name comparison to be used for DataTreeNode comparators and node comparison. + * @param n1 first name + * @param n2 second name + * @return comparison result consistent with {@link String#compareTo(String) n1.compareTo(n2)} + */ + static int compareNodeNames(String n1, String n2) { + int c = n1.compareToIgnoreCase(n2); + if (c == 0) { + // disambiguate for deterministic sort + c = n1.compareTo(n2); + } + return c; + } + + /** + * Generate filtered child nodes for a DomainFolder + * @param domainFolder folder + * @param filter filter + * @param monitor load task monitor + * @return list of filtered chidren + * @throws CancelledException if load task is cancelled + */ + static List generateChildren(DomainFolder domainFolder, DomainFileFilter filter, + TaskMonitor monitor) throws CancelledException { + + boolean hideFolderLinks = false; + boolean hideBroken = false; + boolean hideExternal = false; + if (filter != null) { + hideFolderLinks = filter.ignoreFolderLinks(); + hideBroken = filter.ignoreBrokenLinks(); + hideExternal = filter.ignoreExternalLinks(); + } + + List children = new ArrayList<>(); + if (domainFolder != null) { + + DomainFolder[] folders = domainFolder.getFolders(); + for (DomainFolder folder : folders) { + monitor.checkCancelled(); + children.add(new DomainFolderNode(folder, filter)); + } + + DomainFile[] files = domainFolder.getFiles(); + for (DomainFile df : files) { + monitor.checkCancelled(); + if (filter != null) { + boolean isFolderLink = df.isLink() && df.getLinkInfo().isFolderLink(); + if (hideFolderLinks && isFolderLink) { + continue; + } + if ((hideBroken || hideExternal) && df.isLink()) { + LinkStatus linkStatus = LinkHandler.getLinkFileStatus(df, null); + if (hideBroken && linkStatus == LinkStatus.BROKEN) { + continue; + } + if (hideExternal && linkStatus == LinkStatus.EXTERNAL) { + continue; + } + } + if (!isFolderLink && !filter.accept(df)) { + continue; + } + } + children.add(new DomainFileNode(df, filter)); + } + } + Collections.sort(children, DATA_NODE_SORT_COMPARATOR); + return children; + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DialogProjectTreeContext.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DialogProjectTreeContext.java index 49166d8e93..fca389e279 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DialogProjectTreeContext.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DialogProjectTreeContext.java @@ -39,9 +39,8 @@ public class DialogProjectTreeContext extends DialogActionContext implements Pro private List selectedFolders; private List selectedFiles; - public DialogProjectTreeContext(ProjectData projectData, - TreePath[] selectionPaths, List folderList, List fileList, - DataTree tree) { + public DialogProjectTreeContext(ProjectData projectData, TreePath[] selectionPaths, + List folderList, List fileList, DataTree tree) { super(getContextObject(selectionPaths), tree); this.selectionPaths = selectionPaths; this.selectedFolders = folderList; @@ -98,6 +97,16 @@ public class DialogProjectTreeContext extends DialogActionContext implements Pro return selectedFiles.size(); } + @Override + public boolean hasExactlyOneFileOrFolder() { + return (getFolderCount() + getFileCount()) == 1; + } + + @Override + public boolean hasOneOrMoreFilesAndFolders() { + return getFolderCount() + getFileCount() > 0; + } + @Override public GTreeNode getContextNode() { return (GTreeNode) super.getContextObject(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java index 785a8c93d2..cfad87ce7f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFileNode.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,47 +16,64 @@ package ghidra.framework.main.datatree; import java.io.IOException; +import java.net.MalformedURLException; import java.net.URL; import java.text.SimpleDateFormat; -import java.util.Date; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import javax.swing.Icon; import javax.swing.SwingWorker; import docking.widgets.tree.GTreeNode; import generic.theme.GIcon; -import ghidra.framework.data.FolderLinkContentHandler; import ghidra.framework.data.LinkHandler; -import ghidra.framework.model.DomainFile; +import ghidra.framework.data.LinkHandler.LinkStatus; +import ghidra.framework.main.BrokenLinkIcon; +import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.GhidraURL; import ghidra.framework.store.ItemCheckoutStatus; import ghidra.util.*; +import ghidra.util.exception.CancelledException; import ghidra.util.exception.DuplicateFileException; -import resources.ResourceManager; +import ghidra.util.task.TaskMonitor; /** * Class to represent a node in the Data tree. */ -public class DomainFileNode extends GTreeNode implements Cuttable { +public class DomainFileNode extends DataTreeNode { private static final Icon UNKNOWN_FILE_ICON = new GIcon("icon.datatree.node.domain.file"); + private static final String RIGHT_ARROW = "\u2b95"; private final DomainFile domainFile; private volatile String displayName; // name displayed in the tree private volatile Icon icon = UNKNOWN_FILE_ICON; - private volatile Icon disabledIcon; - protected volatile String toolTipText; + private volatile Icon cutIcon; + private volatile String toolTipText; + private AtomicInteger refreshCount = new AtomicInteger(); - private volatile boolean isCut; // true if this node is marked as cut + private boolean isLeaf = true; + private LinkFileInfo linkInfo; + private DomainFileFilter filter; // relavent when expand folder-link which is a file - private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa"); + private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy MMM dd hh:mm aaa"); - DomainFileNode(DomainFile domainFile) { + DomainFileNode(DomainFile domainFile, DomainFileFilter filter) { this.domainFile = domainFile; + this.linkInfo = domainFile.getLinkInfo(); + this.filter = filter != null ? filter : DomainFileFilter.ALL_FILES_FILTER; displayName = domainFile.getName(); refresh(); } + @Override + public boolean isAutoExpandPermitted() { + // Prevent auto-expansion through linked-folders + return false; + } + /** * Get the domain file if this node represents a file object versus a folder; interface method * for DomainDataTransfer. @@ -69,7 +86,39 @@ public class DomainFileNode extends GTreeNode implements Cuttable { @Override public boolean isLeaf() { - return true; + return isLeaf; + } + + @Override + public int getChildCount() { + if (isLeaf) { + // Optimization to avoid repeated attempts at following a bad link + return 0; + } + return super.getChildCount(); + } + + /** + * Determine if this file node corresponds to a folder-link + * @return true if file is a folder-link + */ + public boolean isFolderLink() { + if (linkInfo != null) { + return linkInfo.isFolderLink(); + } + return false; + } + + /** + * Get linked folder which corresponds to this folder-link + * (see {@link #isFolderLink()}). + * @return linked folder or null if this is not a folder-link + */ + LinkedDomainFolder getLinkedFolder() { + if (!isLeaf() && linkInfo != null) { // verifies that we are allowed to follow based upon filter + return linkInfo.getLinkedFolder(); + } + return null; } @Override @@ -93,24 +142,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable { @Override public int hashCode() { - return System.identityHashCode(domainFile); - } - - /** - * Set this node to be deleted so that it can be rendered as such. - */ - @Override - public void setIsCut(boolean isCut) { - this.isCut = isCut; - fireNodeChanged(); - } - - /** - * Returns whether this node is marked as deleted. - */ - @Override - public boolean isCut() { - return isCut; + return domainFile.hashCode(); } @Override @@ -120,8 +152,8 @@ public class DomainFileNode extends GTreeNode implements Cuttable { @Override public Icon getIcon(boolean expanded) { - if (isCut) { - return disabledIcon; + if (isCut()) { + return cutIcon; } return icon; } @@ -152,32 +184,80 @@ public class DomainFileNode extends GTreeNode implements Cuttable { @Override protected DomainFileNode doInBackground() throws Exception { - doRefresh(); + try { + doRefresh(); + } + finally { + refreshCount.decrementAndGet(); + } return DomainFileNode.this; } } + /** + * This method intended for test use only. + * {@return true if a pending refresh exists for this node} + */ + public boolean hasPendingRefresh() { + return refreshCount.get() != 0; + } + /** * Update the display name. */ void refresh() { + refreshCount.incrementAndGet(); DomainFileNodeSwingWorker worker = new DomainFileNodeSwingWorker(); worker.execute(); } private void doRefresh() { - //DomainFolderNode parent = (DomainFolderNode) getParent(); + isLeaf = true; + linkInfo = null; - String name = domainFile.getName(); - //domainFile = parent.getDomainFolder().getFile(name); + boolean brokenLink = false; + List linkErrors = null; + if (domainFile.isLink()) { + linkInfo = domainFile.getLinkInfo(); + List errors = new ArrayList<>(); + LinkStatus linkStatus = + LinkHandler.getLinkFileStatus(domainFile, msg -> errors.add(msg)); + brokenLink = (linkStatus == LinkStatus.BROKEN); + if (brokenLink) { + linkErrors = errors; + } + else if (isFolderLink()) { + if (linkStatus == LinkStatus.INTERNAL) { + isLeaf = false; + } + else if (linkStatus == LinkStatus.EXTERNAL && + filter.followExternallyLinkedFolders()) { + isLeaf = false; + } + } + } - String newDisplayName = name; + if (isLeaf) { + unloadChildren(); + } + displayName = getFormattedDisplayName(); + + toolTipText = HTMLUtilities.toLiteralHTMLForTooltip(getToolTipText(domainFile, linkErrors)); + + refreshIcons(brokenLink); + + fireNodeChanged(); + } + + private String getFormattedDisplayName() { + + String newDisplayName = domainFile.getName(); if (domainFile.isHijacked()) { newDisplayName += " (hijacked)"; } - else if (domainFile.isVersioned() && !domainFile.isLinkFile()) { + else if (domainFile.isVersioned() && !domainFile.isLink()) { int versionNumber = domainFile.getVersion(); String versionStr = "" + versionNumber; @@ -200,58 +280,89 @@ public class DomainFileNode extends GTreeNode implements Cuttable { newDisplayName += " (" + versionStr + ")"; } } - displayName = newDisplayName; - setToolTipText(); - - icon = domainFile.getIcon(false); - disabledIcon = ResourceManager.getDisabledIcon(icon); - - fireNodeChanged(); + if (domainFile.isLink()) { + newDisplayName += " " + RIGHT_ARROW + " " + getFormattedLinkPath(); + } + return newDisplayName; } - private void setToolTipText() { - String newToolTipText = null; - if (domainFile.isInWritableProject() && domainFile.isHijacked()) { - newToolTipText = "Hijacked file should be deleted or renamed"; - } - else { - StringBuilder buf = new StringBuilder(); + private String getFormattedLinkPath() { + + String linkPath = linkInfo != null ? linkInfo.getLinkPath() : null; + if (GhidraURL.isGhidraURL(linkPath)) { try { - if (domainFile.isLinkFile()) { - URL url = LinkHandler.getURL(domainFile); - buf.append("URL: "); - buf.append(StringUtilities.trimMiddle(url.toString(), 120)); - newToolTipText = buf.toString(); - } - } - catch (IOException e1) { - // ignore - } - if (newToolTipText == null) { - long lastModified = domainFile.getLastModifiedTime(); - newToolTipText = "Last Modified " + formatter.format(new Date(lastModified)); - } - if (domainFile.isCheckedOut()) { - try { - ItemCheckoutStatus status = domainFile.getCheckoutStatus(); - if (status != null) { - newToolTipText = "Checked out " + - formatter.format(new Date(status.getCheckoutTime())) + - "\n" + newToolTipText; + URL url = new URL(linkPath); + if (GhidraURL.isLocalGhidraURL(linkPath)) { + ProjectLocator loc = GhidraURL.getProjectStorageLocator(url); + if (loc != null) { + String projectPath = GhidraURL.getProjectPathname(url); + linkPath = loc.getName() + ":" + projectPath; } } - catch (IOException e) { - // just ignore and use the previously set tooltip + else if (GhidraURL.isServerURL(linkPath)) { + String host = url.getHost(); + String repo = GhidraURL.getRepositoryName(url); + if (repo != null) { + String projectPath = GhidraURL.getProjectPathname(url); + linkPath = host + "[" + repo + "]:" + projectPath; + } } } - - if (domainFile.isReadOnly()) { - newToolTipText += " (read only)"; + catch (MalformedURLException e) { + // ignore - use original linkPath } - newToolTipText = HTMLUtilities.toLiteralHTML(newToolTipText, 0); } - toolTipText = newToolTipText; + return linkPath; + } + + private void refreshIcons(boolean isBrokenLink) { + + icon = domainFile.getIcon(false); + cutIcon = domainFile.getIcon(true); + if (isBrokenLink) { + icon = new BrokenLinkIcon(icon); + cutIcon = new BrokenLinkIcon(cutIcon); + } + } + + public static String getToolTipText(DomainFile domainFile, List linkErrors) { + StringBuilder buf = new StringBuilder(); + if (domainFile.isInWritableProject() && domainFile.isHijacked()) { + buf.append("Hijacked file should be deleted or renamed"); + } + + if (linkErrors != null) { + linkErrors.forEach(linkError -> appendLine(buf, linkError)); + } + + if (domainFile.isCheckedOut()) { + try { + ItemCheckoutStatus status = domainFile.getCheckoutStatus(); + if (status != null) { + appendLine(buf, + "Checked out " + formatter.format(new Date(status.getCheckoutTime()))); + } + } + catch (IOException e) { + // just ignore and use the previously set tooltip + } + } + + long lastModified = domainFile.getLastModifiedTime(); + appendLine(buf, "Last Modified " + formatter.format(new Date(lastModified))); + + if (domainFile.isReadOnly()) { + appendLine(buf, "(read only)"); + } + return buf.toString(); + } + + private static void appendLine(StringBuilder buf, String line) { + if (!buf.isEmpty()) { + buf.append('\n'); + } + buf.append(line); } @Override @@ -261,36 +372,7 @@ public class DomainFileNode extends GTreeNode implements Cuttable { @Override public int compareTo(GTreeNode node) { - // Goal is to sort folder link-files similar to a folder - if (node instanceof DomainFolderNode) { - if (isFolderLink()) { - int c = super.compareTo(node); - if (c != 0) { - // A link-file name is permitted to match another folder node but - // should not be considered equal - return c; - } - } - return 1; - } - if (node instanceof DomainFileNode) { - DomainFileNode otherFileNode = (DomainFileNode) node; - if (isFolderLink()) { - if (otherFileNode.isFolderLink()) { - return super.compareTo(node); - } - return -1; - } - else if (otherFileNode.isFolderLink()) { - return 1; - } - } - return super.compareTo(node); - } - - boolean isFolderLink() { - return FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE - .equals(domainFile.getContentType()); + return DATA_NODE_SORT_COMPARATOR.compare(this, node); } @Override @@ -318,4 +400,22 @@ public class DomainFileNode extends GTreeNode implements Cuttable { return getName(); } + @Override + public List generateChildren(TaskMonitor monitor) throws CancelledException { + if (isLeaf || linkInfo == null) { + return List.of(); + } + return generateChildren(linkInfo.getLinkedFolder(), filter, monitor); + } + + @Override + public GTreeNode getChild(String name, NodeType type) { + return getChild(children(), name, type); + } + + @Override + public ProjectData getProjectData() { + return domainFile.getParent().getProjectData(); + } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFilesPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFilesPanel.java index 763d68de17..a4e3af383f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFilesPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFilesPanel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -79,6 +79,7 @@ class DomainFilesPanel extends JPanel { /** * Get the selected domain files. + * @return selected domain files */ DomainFile[] getSelectedDomainFiles() { List list = new ArrayList<>(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java index eb3f2606b0..c7f48fa5c5 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/DomainFolderNode.java @@ -16,12 +16,11 @@ package ghidra.framework.main.datatree; import java.io.IOException; -import java.util.*; +import java.util.List; import javax.swing.Icon; import docking.widgets.tree.GTreeNode; -import docking.widgets.tree.GTreeSlowLoadingNode; import ghidra.framework.model.*; import ghidra.util.*; import ghidra.util.exception.CancelledException; @@ -31,7 +30,7 @@ import resources.ResourceManager; /** * Class to represent a node in the Data tree. */ -public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable { +public class DomainFolderNode extends DataTreeNode { private static final Icon ENABLED_OPEN_FOLDER = DomainFolder.OPEN_FOLDER_ICON; private static final Icon ENABLED_CLOSED_FOLDER = DomainFolder.CLOSED_FOLDER_ICON; @@ -42,7 +41,6 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable { ResourceManager.getDisabledIcon(ENABLED_CLOSED_FOLDER); private DomainFolder domainFolder; - private boolean isCut; private DomainFileFilter filter; // variables that are accessed in with a lock on the filesystem in the underlying folder @@ -55,8 +53,7 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable { // TODO: how can the folder be null?...doesn't really make sense...I don't think it ever is if (domainFolder != null) { - toolTipText = StringUtilities.trimMiddle(domainFolder.getPathname(), 120); - toolTipText = HTMLUtilities.toLiteralHTML(toolTipText, 0); + setToolTipText(); isEditable = domainFolder.isInWritableProject(); } } @@ -84,33 +81,12 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable { return false; } - /** - * Set this node to be deleted so that it can be rendered as such. - */ - @Override - public void setIsCut(boolean isCut) { - this.isCut = isCut; - fireNodeChanged(); - } - - /** - * Returns whether this node is marked as deleted. - */ - @Override - public boolean isCut() { - return isCut; - } - @Override public Icon getIcon(boolean expanded) { - if (domainFolder instanceof LinkedDomainFolder) { - // NOTE: cut operation not supported - return ((LinkedDomainFolder) domainFolder).getIcon(expanded); + if (isCut()) { + return expanded ? DISABLED_OPEN_FOLDER : DISABLED_CLOSED_FOLDER; } - if (expanded) { - return isCut ? DISABLED_OPEN_FOLDER : ENABLED_OPEN_FOLDER; - } - return isCut ? DISABLED_CLOSED_FOLDER : ENABLED_CLOSED_FOLDER; + return expanded ? ENABLED_OPEN_FOLDER : ENABLED_CLOSED_FOLDER; } @Override @@ -128,38 +104,20 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable { return toolTipText; } + private void setToolTipText() { + String newToolTipText; + if (domainFolder instanceof LinkedDomainFolder) { + newToolTipText = domainFolder.toString(); + } + else { + newToolTipText = domainFolder.getPathname(); + } + toolTipText = HTMLUtilities.toLiteralHTML(newToolTipText, 0); + } + @Override public List generateChildren(TaskMonitor monitor) throws CancelledException { - - List children = new ArrayList<>(); - if (domainFolder == null || domainFolder.isEmpty()) { - return children; - } - - // NOTE: isEmpty() is used to avoid multiple failed connection attempts on this folder - - DomainFolder[] folders = domainFolder.getFolders(); - for (DomainFolder folder : folders) { - monitor.checkCancelled(); - children.add(new DomainFolderNode(folder, filter)); - } - - DomainFile[] files = domainFolder.getFiles(); - for (DomainFile domainFile : files) { - monitor.checkCancelled(); - if (domainFile.isLinkFile() && filter != null && filter.followLinkedFolders()) { - DomainFolder folder = domainFile.followLink(); - if (folder != null) { - children.add(new DomainFolderNode(folder, filter)); - continue; - } - } - if (filter == null || filter.accept(domainFile)) { - children.add(new DomainFileNode(domainFile)); - } - } - Collections.sort(children); - return children; + return generateChildren(domainFolder, filter, monitor); } @Override @@ -188,7 +146,7 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable { @Override public int hashCode() { - return System.identityHashCode(domainFolder); + return domainFolder.hashCode(); } public DomainFileFilter getDomainFileFilter() { @@ -197,11 +155,7 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable { @Override public int compareTo(GTreeNode node) { - if (node instanceof DomainFileNode) { - // defer to DomainFileNode for comparison - return -((DomainFileNode) node).compareTo(this); - } - return super.compareTo(node); + return DATA_NODE_SORT_COMPARATOR.compare(this, node); } @Override @@ -222,4 +176,14 @@ public class DomainFolderNode extends GTreeSlowLoadingNode implements Cuttable { } } } + + @Override + public GTreeNode getChild(String name, NodeType type) { + return getChild(children(), name, type); + } + + @Override + public ProjectData getProjectData() { + return domainFolder.getProjectData(); + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/FindCheckoutsTableModel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/FindCheckoutsTableModel.java index 67a6207b2b..2f5ac81fab 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/FindCheckoutsTableModel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/FindCheckoutsTableModel.java @@ -98,6 +98,12 @@ class FindCheckoutsTableModel extends ThreadedTableModelStub { if (monitor.isCancelled()) { throw new CancelledException(); } + if (file.isLink()) { + // NOTE: We do not currently consider link-files whose referenced file + // is checked-out. + continue; + } + if (file.isCheckedOut()) { try { CheckoutInfo info = new CheckoutInfo(file); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java index 388a8a5e18..37458a911c 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalTreeNodeHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,8 +24,7 @@ import java.util.List; import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeState; import ghidra.app.util.FileOpenDataFlavorHandler; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainFolder; +import ghidra.framework.model.*; import ghidra.framework.plugintool.PluginTool; import ghidra.util.Msg; import ghidra.util.SystemUtilities; @@ -74,7 +73,12 @@ public final class LocalTreeNodeHandler return false; } - CopyAllTask task = new CopyAllTask(list, destinationNode, dropAction); + DomainFolder destFolder = DataTree.getRealInternalFolderForNode(destinationNode); + if (destFolder == null || !destFolder.isInWritableProject()) { + return false; + } + + CopyAllTask task = new CopyAllTask(list, destFolder, dropAction); new TaskLauncher(task, dataTree, 1000); if (treeState != null) { // is set to null if drag results in a task @@ -87,38 +91,46 @@ public final class LocalTreeNodeHandler return true; } - private void add(GTreeNode destNode, GTreeNode draggedNode, int dropAction, + private void add(DomainFolder destFolder, GTreeNode draggedNode, int dropAction, TaskMonitor monitor) { - DomainFolderNode folderNode = getDestinationFolderNode(destNode); - if (!isValidDrag(folderNode, draggedNode)) { + if (destFolder instanceof LinkedDomainFolder linkedDomainFolder) { + try { + destFolder = linkedDomainFolder.getRealFolder(); + } + catch (IOException e) { + Msg.error(this, "Unable to resolve linked-folder: " + destFolder.getName()); + return; + } + } + + if (!isValidDrag(destFolder, draggedNode)) { return; } - DomainFolder destFolder = folderNode.getDomainFolder(); addDraggedTreeNode(destFolder, draggedNode, dropAction, monitor); } - private boolean isValidDrag(DomainFolderNode folderNode, GTreeNode draggedNode) { - if (folderNode == draggedNode) { - return false; + private boolean isValidDrag(DomainFolder destFolder, GTreeNode draggedNode) { + // NOTE: We may have issues since checks are not based on canonical paths + if (draggedNode instanceof DomainFolderNode folderNode) { + // This also checks cases where src/dest projects are using the same repository. + // Unfortunately, it will also prevent cases where shared-project folder + // does not contain versioned content and could actually be allowed. + DomainFolder folder = folderNode.getDomainFolder(); + return !folder.isSameOrAncestor(destFolder); } - if (draggedNode.getParent() == folderNode) { - return false; // dragging a node onto its parent has no effect + if (draggedNode instanceof DomainFileNode fileNode) { + DomainFolder folder = fileNode.getDomainFile().getParent(); + DomainFile file = fileNode.getDomainFile(); + if (file.isVersioned()) { + // This also checks cases where src/dest projects are using the same repository. + return !folder.isSame(destFolder); + } + DomainFile destFile = destFolder.getFile(file.getName()); + return destFile == null || !destFile.equals(file); } - - if (draggedNode instanceof DomainFolderNode) { - return !draggedNode.isAncestor(folderNode); - } - - return true; - } - - private DomainFolderNode getDestinationFolderNode(GTreeNode destNode) { - if (destNode instanceof DomainFolderNode) { - return (DomainFolderNode) destNode; - } - return (DomainFolderNode) destNode.getParent(); + return false; } private void addDraggedTreeNode(DomainFolder destFolder, GTreeNode data, int dropAction, @@ -178,8 +190,8 @@ public final class LocalTreeNodeHandler } catch (DuplicateFileException dfe) { Msg.showError(this, dataTree, "Error Moving Folder", - "Destination folder already contains a folder named \"" + sourceFolder.getName() + - "\""); + "Destination folder already contains a folder or folder-link named \"" + + sourceFolder.getName() + "\""); } catch (FileInUseException fiue) { String message = fiue.getMessage(); @@ -201,13 +213,13 @@ public final class LocalTreeNodeHandler private class CopyAllTask extends Task { private List toCopy; - private GTreeNode destination; + private DomainFolder destFolder; private int dropAction; - CopyAllTask(List toCopy, GTreeNode destination, int dropAction) { + CopyAllTask(List toCopy, DomainFolder destFolder, int dropAction) { super("Copy Files", true, true, true); this.toCopy = toCopy; - this.destination = destination; + this.destFolder = destFolder; this.dropAction = dropAction; } @@ -226,7 +238,7 @@ public final class LocalTreeNodeHandler monitor.setMessage( "Processing file " + (i + 1) + " of " + size + ": " + copyNode.getName()); - add(destination, copyNode, dropAction, subMonitors[i]); + add(destFolder, copyNode, dropAction, subMonitors[i]); monitor.setProgress(i); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalVersionInfoHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalVersionInfoHandler.java index d86f249973..fcfebee170 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalVersionInfoHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/LocalVersionInfoHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -35,8 +35,7 @@ public final class LocalVersionInfoHandler VersionInfo info = (VersionInfo) obj; DomainFile file = tool.getProject().getProjectData().getFile(info.getDomainFilePath()); - GetDomainObjectTask task = - new GetDomainObjectTask(this, file, info.getVersionNumber()); + GetDomainObjectTask task = new GetDomainObjectTask(this, file, info.getVersionNumber()); tool.execute(task, 250); DomainObject versionedObj = task.getDomainObject(); if (versionedObj != null) { @@ -53,7 +52,7 @@ public final class LocalVersionInfoHandler @Override public boolean handle(PluginTool tool, DataTree dataTree, GTreeNode destinationNode, Object transferData, int dropAction) { - DomainFolder folder = getDomainFolder(destinationNode); + DomainFolder folder = DataTree.getRealInternalFolderForNode(destinationNode); VersionInfo info = (VersionInfo) transferData; RepositoryAdapter rep = tool.getProject().getProjectData().getRepository(); @@ -77,14 +76,4 @@ public final class LocalVersionInfoHandler return false; } - private DomainFolder getDomainFolder(GTreeNode destinationNode) { - if (destinationNode instanceof DomainFolderNode) { - return ((DomainFolderNode) destinationNode).getDomainFolder(); - } - else if (destinationNode instanceof DomainFileNode) { - DomainFolderNode parent = (DomainFolderNode) destinationNode.getParent(); - return parent.getDomainFolder(); - } - return null; - } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/PasteFileTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/PasteFileTask.java index 2fa3a358f3..0c90b77682 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/PasteFileTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/PasteFileTask.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -36,7 +36,7 @@ import ghidra.util.task.*; */ public class PasteFileTask extends Task { - private DomainFolderNode destNode; + private DomainFolder destFolder; private List list; private boolean isCut; private RepositoryAdapter repository; // null if project is not shared @@ -46,13 +46,13 @@ public class PasteFileTask extends Task { /** * Constructor for PasteFileTask. * - * @param destNode destination folder + * @param destFolder destination folder * @param list list of GTreeNodes being pasted * @param isCut boolean flag, true means source nodes were cut instead of copied. */ - public PasteFileTask(DomainFolderNode destNode, List list, boolean isCut) { + public PasteFileTask(DomainFolder destFolder, List list, boolean isCut) { super(list.size() > 1 ? "Paste Files" : "Paste File", true, true, true); - this.destNode = destNode; + this.destFolder = destFolder; this.list = list; this.isCut = isCut; repository = AppInfo.getActiveProject().getRepository(); @@ -70,13 +70,14 @@ public class PasteFileTask extends Task { for (GTreeNode node : list) { monitor.checkCancelled(); - if (node instanceof DomainFolderNode) { + if (node instanceof DomainFolderNode folderNode) { monitor.setMessage("Pasting folder"); - pasteFolder(((DomainFolderNode) node).getDomainFolder(), subMonitor); + pasteFolder(folderNode.getDomainFolder(), subMonitor); } - else if (node instanceof DomainFileNode) { + else if (node instanceof DomainFileNode fileNode) { monitor.setMessage("Pasting file"); - pasteFile(((DomainFileNode) node).getDomainFile(), subMonitor); + // NOTE: This may be a link-file + pasteFile(fileNode.getDomainFile(), subMonitor); } monitor.incrementProgress(1); @@ -96,10 +97,10 @@ public class PasteFileTask extends Task { */ private void pasteFile(DomainFile file, TaskMonitor monitor) { if (isCut) { - moveFile(file, destNode.getDomainFolder()); + moveFile(file, destFolder); } else { - copyFile(file, destNode.getDomainFolder(), monitor); + copyFile(file, destFolder, monitor); } } @@ -108,10 +109,10 @@ public class PasteFileTask extends Task { */ private void pasteFolder(DomainFolder folder, TaskMonitor monitor) { if (isCut) { - moveFolder(folder, destNode.getDomainFolder()); + moveFolder(folder, destFolder); } else { - copyFolder(folder, destNode.getDomainFolder(), monitor); + copyFolder(folder, destFolder, monitor); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java index cc34358872..a40283cbc8 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/ProjectDataTreePanel.java @@ -48,6 +48,33 @@ public class ProjectDataTreePanel extends JPanel { private static final String EXPANDED_PATHS_SEPARATOR = ":"; private static final int MAX_PROJECT_SIZE_TO_SEARCH = 1000; + private static final DomainFileFilter ALL_FILES_NO_EXTERNAL_FOLLOW = new DomainFileFilter() { + @Override + public boolean accept(DomainFile df) { + // Show all files + return true; + } + + @Override + public boolean ignoreBrokenLinks() { + // Always show broken links in the main data tree + // A link file's status can change based on other changes to project data + return false; + } + + @Override + public boolean ignoreExternalLinks() { + // Always show external links, but we do not allow expanding them. + return false; + } + + @Override + public boolean followExternallyLinkedFolders() { + // Do not allow expanding external linked-folders. + return false; + } + }; + private DataTree tree; private ProjectData projectData; private GTreeNode root; @@ -60,21 +87,24 @@ public class ProjectDataTreePanel extends JPanel { private FrontEndPlugin plugin; /** - * Construct an empty panel that is going to be used as the active panel + * Construct an empty data tree panel that is going to be used for the active project tree + * within the frontend tool. + * * @param plugin front end plugin */ public ProjectDataTreePanel(FrontEndPlugin plugin) { - this(null, true, plugin, null); + this(null, true, plugin, ALL_FILES_NO_EXTERNAL_FOLLOW); } /** - * Constructor + * Constructor * * @param projectName name of project * @param isActiveProject true if the project is active, and the * data tree may be modified * @param plugin front end plugin; will be null if the panel is used in a dialog - * @param filter optional filter that is used to hide programs from view + * @param filter optional filter that is used to hide programs from view. If null is specified + * a default filter is employed which shows all domain files and link-files. */ public ProjectDataTreePanel(String projectName, boolean isActiveProject, FrontEndPlugin plugin, DomainFileFilter filter) { @@ -84,7 +114,8 @@ public class ProjectDataTreePanel extends JPanel { this.tool = (FrontEndTool) plugin.getTool(); this.plugin = plugin; } - this.filter = filter; + this.filter = + filter != null ? filter : DomainFileFilter.ALL_FILES_NO_EXTERNAL_FOLDERS_FILTER; create(projectName); @@ -105,9 +136,8 @@ public class ProjectDataTreePanel extends JPanel { if (this.projectData == projectData) { return; // this can happen during setup if listeners get activated } - - if (this.projectData != null) { - this.projectData.removeDomainFolderChangeListener(changeMgr); + if (changeMgr != null) { + changeMgr.dispose(); } this.projectData = projectData; @@ -117,7 +147,7 @@ public class ProjectDataTreePanel extends JPanel { oldRoot.dispose(); changeMgr = new ChangeManager(this); - projectData.addDomainFolderChangeListener(changeMgr); + isActiveProject = projectData.getRootFolder().isInWritableProject(); tree.setProjectActive(isActiveProject); } @@ -144,6 +174,16 @@ public class ProjectDataTreePanel extends JPanel { oldRoot.removeAll(); } + /** + * Generate a list of TreePaths which correspond to a set of {@link DomainFile domain files}. + *

    + * NOTE: The {@link DomainFileNode} included in the paths as the last component is not the same + * instance as may, or may not, be contained within the tree. This path is intended for + * generating a selection only and is not a reflection of the actual tree state. + * + * @param files set of domain files + * @return generated list of file tree paths + */ private List getTreePaths(Set files) { List results = new ArrayList<>(); for (DomainFile file : files) { @@ -152,8 +192,18 @@ public class ProjectDataTreePanel extends JPanel { return results; } + /** + * Generate a TreePath which corresponds to the specified {@link DomainFile}. + *
    + * NOTE: The {@link DomainFileNode} included in the path as the last component is not the same + * instance as may, or may not, be contained within the tree. This path is intended for + * generating a selection only and is not a reflection of the actual tree state. + * + * @param domainFile domain file + * @return generated file tree path + */ private TreePath getTreePath(DomainFile domainFile) { - DomainFileNode node = new DomainFileNode(domainFile); + DomainFileNode node = new DomainFileNode(domainFile, filter); DomainFolder parent = domainFile.getParent(); if (parent != null) { return getTreePath(parent).pathByAddingChild(node); @@ -161,13 +211,37 @@ public class ProjectDataTreePanel extends JPanel { return new TreePath(node); } + /** + * Generate a TreePath which corresponds to the specified {@link DomainFolder}. + *

    + * NOTE: The node included in the path as the last component is not the same instance as + * may, or may not, be contained within the tree. This path is intended for generating a + * selection only and is not a reflection of the actual tree state. + *

    + * NOTE: If the specified folder is a linked-folder which corresponds to a link-file + * (see {@link DomainFolder#isLinked()}) the returned path will correspond to a + * {@link DomainFileNode}, otherwise it will be a {@link DomainFolderNode}. + * + * @param domainFolder domain folder (may be a linked-folder) + * @return generated tree path + */ private TreePath getTreePath(DomainFolder domainFolder) { DomainFolder parent = domainFolder.getParent(); if (parent != null) { - return getTreePath(parent).pathByAddingChild(new DomainFolderNode(domainFolder, null)); + if (domainFolder.isLinked()) { + // linked-folder: must handle as link-file node + DomainFile linkFile = parent.getFile(domainFolder.getName()); + if (linkFile != null) { + return getTreePath(parent) + .pathByAddingChild(new DomainFileNode(linkFile, filter)); + } + } + else { + return getTreePath(parent) + .pathByAddingChild(new DomainFolderNode(domainFolder, filter)); + } } return new TreePath(root); - } /** @@ -187,6 +261,13 @@ public class ProjectDataTreePanel extends JPanel { tree.expandAndSelectPaths(treePaths); } + /** + * Select the specified domainFile if it exists in the tree. + *

    + * NOTE: The selection is performed in a delayed non-blocking fashion. + * + * @param domainFile domain file + */ public void selectDomainFile(DomainFile domainFile) { if (domainFile != null) { selectDomainFiles(Set.of(domainFile)); @@ -221,10 +302,7 @@ public class ProjectDataTreePanel extends JPanel { */ public DomainFolder getSelectedDomainFolder() { GTreeNode node = tree.getLastSelectedPathComponent(); - if (node instanceof DomainFolderNode) { - return ((DomainFolderNode) node).getDomainFolder(); - } - return null; + return DataTree.getRealInternalFolderForNode(node); } /** @@ -285,8 +363,9 @@ public class ProjectDataTreePanel extends JPanel { } public void dispose() { - if (projectData != null) { - projectData.removeDomainFolderChangeListener(changeMgr); + if (changeMgr != null) { + changeMgr.dispose(); + changeMgr = null; } tree.dispose(); } @@ -318,11 +397,12 @@ public class ProjectDataTreePanel extends JPanel { for (TreePath treePath : selectionPaths) { GTreeNode node = (GTreeNode) treePath.getLastPathComponent(); - if (node instanceof DomainFolderNode) { - domainFolderList.add(((DomainFolderNode) node).getDomainFolder()); + if (node instanceof DomainFolderNode folderNode) { + domainFolderList.add(folderNode.getDomainFolder()); } - else if (node instanceof DomainFileNode) { - domainFileList.add(((DomainFileNode) node).getDomainFile()); + else if (node instanceof DomainFileNode fileNode) { + // NOTE: File may be a linked-folder. Treatment as folder or file depends on action + domainFileList.add(fileNode.getDomainFile()); } } @@ -406,6 +486,7 @@ public class ProjectDataTreePanel extends JPanel { private GTreeNode findFolderNodeChild(GTreeNode node, String text) { List children = node.getChildren(); + // NOTE: Does not traverse link-files which may have children for (GTreeNode child : children) { if ((child instanceof DomainFolderNode) && child.getName().equals(text)) { return child; @@ -435,7 +516,7 @@ public class ProjectDataTreePanel extends JPanel { tree.setProjectActive(isActiveProject); } - void domainChange() { + void contextChanged() { if (plugin == null) { return; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java index 964519907b..26b05de100 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/datatree/VersionControlTask.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -64,7 +64,7 @@ public abstract class VersionControlTask extends Task { VersionControlDialog vcDialog = new VersionControlDialog(addToVersionControl); vcDialog.setCurrentFileName(file.getName()); vcDialog.setMultiFiles(list.size() > 1); - if (file.isLinkFile()) { + if (file.isLink()) { vcDialog.setKeepCheckboxEnabled(false, false, "Link file may not be Checked Out"); } else { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java index 5e2ac411c8..61ba49e411 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/DeleteProjectFilesTask.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,8 +22,7 @@ import java.util.Set; import docking.widgets.OptionDialog; import docking.widgets.OptionDialogBuilder; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainFolder; +import ghidra.framework.model.*; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.task.Task; @@ -105,6 +104,22 @@ public class DeleteProjectFilesTask extends Task { private void deleteFolder(DomainFolder folder, TaskMonitor monitor) throws CancelledException { + while (folder instanceof LinkedDomainFolder linkedFolder) { + if (linkedFolder.isLinked()) { + throw new IllegalArgumentException( + "Linked-folder's originating file-link should have been removed instead: " + + linkedFolder.getPathname()); + } + try { + folder = linkedFolder.getRealFolder(); + } + catch (IOException e) { + Msg.error(this, "Error following linked-folder: " + e.getMessage() + "\n" + + folder.getPathname()); + return; + } + } + for (DomainFolder subFolder : folder.getFolders()) { monitor.checkCancelled(); if (!selectedFolders.contains(subFolder)) { @@ -204,10 +219,10 @@ public class DeleteProjectFilesTask extends Task { } String msg = - "The file \"" + file.getName() + "\" is a versioned file and if you continue, \n" + + "The file \"" + file.getName() + "\" is a versioned file and if you continue\n" + "it (and all its versions) will be PERMANENTLY deleted!\n" + - "If this is a shared project, it will be deleted on the server (if permitted)\n" + - "for ALL users (if permitted)!" + "\nAre you sure you want to delete it?"; + "If this is a shared project, it will be deleted on the server\n" + + "for ALL users (if permitted)!" + "\n\nAre you sure you want to delete it?"; versionedDialogBuilder.setMessage(msg); return versionedDialogBuilder.show(parent); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FindCheckoutsAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FindCheckoutsAction.java index 467aeec546..4d31f451d7 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FindCheckoutsAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/FindCheckoutsAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -26,13 +26,17 @@ import docking.widgets.OptionDialog; import generic.theme.GIcon; import ghidra.framework.client.*; import ghidra.framework.main.datatable.ProjectTreeAction; -import ghidra.framework.main.datatree.FindCheckoutsDialog; -import ghidra.framework.main.datatree.FrontEndProjectTreeContext; -import ghidra.framework.model.DomainFolder; -import ghidra.framework.model.ProjectData; +import ghidra.framework.main.datatree.*; +import ghidra.framework.model.*; import ghidra.framework.plugintool.Plugin; import ghidra.util.HelpLocation; +/** + * {@link FindCheckoutsAction} provide the ability to initiate the show checkout status for + * files selected within the {@link ProjectDataTreePanel}. Since link-files cannot be checked-out + * these files will never show checkouts and do not currently attempt to show checkout information + * for a referenced file. + */ public class FindCheckoutsAction extends ProjectTreeAction { private static final Icon FIND_ICON = new GIcon("icon.projectdata.find.checkouts.search"); @@ -54,7 +58,20 @@ public class FindCheckoutsAction extends ProjectTreeAction { @Override protected void actionPerformed(FrontEndProjectTreeContext context) { - DomainFolder domainFolder = context.getSelectedFolders().get(0); + DomainFolder domainFolder = null; + if (context.getFolderCount() == 1) { + domainFolder = context.getSelectedFolders().get(0); + } + else if (context.getFileCount() == 1) { + DomainFile domainFile = context.getSelectedFiles().get(0); + LinkFileInfo linkInfo = domainFile.getLinkInfo(); + if (linkInfo != null && linkInfo.isFolderLink() && !linkInfo.isExternalLink()) { + domainFolder = linkInfo.getLinkedFolder(); + } + } + if (domainFolder == null) { + return; + } ProjectData projectData = domainFolder.getProjectData(); RepositoryAdapter repository = projectData.getRepository(); if (repository != null && !repository.isConnected()) { @@ -81,14 +98,19 @@ public class FindCheckoutsAction extends ProjectTreeAction { @Override protected boolean isEnabledForContext(FrontEndProjectTreeContext context) { - if (context.isReadOnlyProject()) { + if (context.isReadOnlyProject() || !context.hasExactlyOneFileOrFolder()) { return false; } - return context.getFolderCount() == 1; + if (context.getFolderCount() == 1) { + return true; + } + // Only allow a local folder-link to be treated as a folder + DomainFile file = context.getSelectedFiles().get(0); + LinkFileInfo linkInfo = file.getLinkInfo(); + return linkInfo != null && linkInfo.isFolderLink() && !linkInfo.isExternalLink(); } private void findCheckouts(DomainFolder folder, Component comp) { - FindCheckoutsDialog dialog = new FindCheckoutsDialog(plugin, folder); plugin.getTool().showDialog(dialog, comp); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCollapseAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCollapseAction.java index 1e287cf7b1..6b2d0866bb 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCollapseAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCollapseAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,7 +21,7 @@ import docking.action.ContextSpecificAction; import docking.action.MenuData; import docking.widgets.tree.GTreeNode; import ghidra.framework.main.datatable.ProjectTreeContext; -import ghidra.framework.main.datatree.DataTree; +import ghidra.framework.main.datatree.*; public class ProjectDataCollapseAction extends ContextSpecificAction { @@ -39,14 +39,20 @@ public class ProjectDataCollapseAction collapse(tree, paths[0]); } - @Override - public boolean isAddToPopup(T context) { - return context.getFolderCount() == 1 && context.getFileCount() == 0; - } - @Override protected boolean isEnabledForContext(T context) { - return context.getFolderCount() == 1 && context.getFileCount() == 0; + if (!context.hasExactlyOneFileOrFolder()) { + return false; + } + TreePath[] paths = context.getSelectionPaths(); + GTreeNode node = (GTreeNode) paths[0].getLastPathComponent(); + if (node instanceof DomainFolderNode folderNode) { + return folderNode.isLoaded(); + } + if (node instanceof DomainFileNode fileNode) { + return fileNode.isFolderLink() && !fileNode.isLeaf() && fileNode.isLoaded(); + } + return false; } /** diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java index 092b89fe45..e705ef56f8 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCopyAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,8 +23,9 @@ import javax.swing.tree.TreePath; import docking.action.KeyBindingData; import docking.action.MenuData; import generic.theme.GIcon; -import ghidra.framework.main.datatree.DataTreeClipboardUtils; -import ghidra.framework.main.datatree.FrontEndProjectTreeContext; +import ghidra.framework.main.AppInfo; +import ghidra.framework.main.datatree.*; +import ghidra.framework.model.Project; import ghidra.util.HelpLocation; public class ProjectDataCopyAction extends ProjectDataCopyCutBaseAction { @@ -50,7 +51,10 @@ public class ProjectDataCopyAction extends ProjectDataCopyCutBaseAction { if (!context.hasOneOrMoreFilesAndFolders()) { return false; } - + Project activeProject = AppInfo.getActiveProject(); + if (activeProject == null || !context.isInActiveProject()) { + return true; + } return !context.containsRootFolder(); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCutAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCutAction.java index 29f1a5c36e..61106dc85f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCutAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataCutAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -25,6 +25,7 @@ import docking.action.MenuData; import docking.widgets.tree.GTreeNode; import generic.theme.GIcon; import ghidra.framework.main.datatree.*; +import ghidra.framework.model.DomainFolder; import ghidra.util.HelpLocation; public class ProjectDataCutAction extends ProjectDataCopyCutBaseAction { @@ -48,15 +49,31 @@ public class ProjectDataCutAction extends ProjectDataCopyCutBaseAction { @Override protected boolean isEnabledForContext(FrontEndProjectTreeContext context) { - if (!context.hasOneOrMoreFilesAndFolders()) { + if (!context.isInActiveProject() || !context.hasOneOrMoreFilesAndFolders()) { return false; } + return !context.containsRootFolder() && canMarkNodesCut(context.getSelectionPaths()); + } - if (!context.isInActiveProject()) { - return false; + private boolean canMarkNodesCut(TreePath[] paths) { + for (TreePath treePath : paths) { + GTreeNode node = (GTreeNode) treePath.getLastPathComponent(); + DomainFolder folder; + if (node instanceof DomainFileNode fileNode) { + folder = fileNode.getDomainFile().getParent(); + } + else if (node instanceof DomainFolderNode folderNode) { + folder = folderNode.getDomainFolder(); + } + else { + return false; + } + if (!folder.isInWritableProject()) { + // linked content may reside within a read-only project view + return false; + } } - - return !context.containsRootFolder(); + return true; } private void markNodesCut(TreePath[] paths) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java index d29431c3c8..4aa8698fbd 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataDeleteAction.java @@ -92,8 +92,9 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction { if (fileCount == 1) { if (!selectedFiles.isEmpty()) { DomainFile file = CollectionUtils.any(selectedFiles); - return "Are you sure you want to permanently delete \"" + - HTMLUtilities.escapeHTML(file.getName()) + "\"?"; + String type = file.isLink() ? "link" : "file"; + return "Are you sure you want to permanently delete " + type + + " \"" + HTMLUtilities.escapeHTML(file.getName()) + "\"?"; } // only folders are selected, but they contain files @@ -108,6 +109,7 @@ public class ProjectDataDeleteAction extends FrontendProjectTreeAction { @Override protected boolean isEnabledForContext(ProjectDataContext context) { + // NOTE: Folder-links are treated as files if (!context.hasOneOrMoreFilesAndFolders()) { return false; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataExpandAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataExpandAction.java index b824356c9f..4d42d9bc11 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataExpandAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataExpandAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,7 +21,7 @@ import docking.action.ContextSpecificAction; import docking.action.MenuData; import docking.widgets.tree.GTreeNode; import ghidra.framework.main.datatable.ProjectTreeContext; -import ghidra.framework.main.datatree.DataTree; +import ghidra.framework.main.datatree.*; public class ProjectDataExpandAction extends ContextSpecificAction { @@ -39,14 +39,20 @@ public class ProjectDataExpandAction expand(tree, paths[0]); } - @Override - public boolean isAddToPopup(T context) { - return context.getFolderCount() == 1 && context.getFileCount() == 0; - } - @Override protected boolean isEnabledForContext(T context) { - return context.getFolderCount() == 1 && context.getFileCount() == 0; + if (!context.hasExactlyOneFileOrFolder()) { + return false; + } + TreePath[] paths = context.getSelectionPaths(); + GTreeNode node = (GTreeNode) paths[0].getLastPathComponent(); + if (node instanceof DomainFolderNode) { + return true; + } + if (node instanceof DomainFileNode fileNode) { + return fileNode.isFolderLink() && !fileNode.isLeaf(); + } + return false; } /** diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java index 12cd31839a..a56873c35f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataNewFolderAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,8 +24,7 @@ import docking.action.MenuData; import docking.widgets.tree.GTreeNode; import generic.theme.GIcon; import ghidra.framework.main.datatable.ProjectTreeContext; -import ghidra.framework.main.datatree.DataTree; -import ghidra.framework.main.datatree.DomainFileNode; +import ghidra.framework.main.datatree.*; import ghidra.framework.model.DomainFile; import ghidra.framework.model.DomainFolder; import ghidra.util.InvalidNameException; @@ -47,11 +46,6 @@ public class ProjectDataNewFolderAction createNewFolder(context); } - @Override - public boolean isAddToPopup(T context) { - return (context.getFolderCount() + context.getFileCount()) == 1; - } - @Override protected boolean isEnabledForContext(T context) { return getFolder(context).isInWritableProject(); @@ -91,11 +85,15 @@ public class ProjectDataNewFolderAction private DomainFolder getFolder(T context) { // the following code relies on the isAddToPopup to ensure that there is exactly one // file or folder selected - if (context.getFolderCount() > 0) { + if (context.getFolderCount() == 1 && context.getFileCount() == 0) { return context.getSelectedFolders().get(0); } - DomainFile file = context.getSelectedFiles().get(0); - return file.getParent(); + if (context.getFileCount() == 1 && context.getFolderCount() == 0) { + DomainFile file = context.getSelectedFiles().get(0); + return file.getParent(); + } + DomainFolderNode rootNode = (DomainFolderNode) context.getTree().getModelRoot(); + return rootNode.getDomainFolder(); } private GTreeNode getParentNode(T context) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataOpenDefaultToolAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataOpenDefaultToolAction.java index e1404cb494..355f57f0a2 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataOpenDefaultToolAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataOpenDefaultToolAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,7 +29,7 @@ public class ProjectDataOpenDefaultToolAction extends FrontendProjectTreeAction public ProjectDataOpenDefaultToolAction(String owner, String group) { super("Open File", owner); - setPopupMenuData(new MenuData(new String[] { "Open in Default Tool" })); + setPopupMenuData(new MenuData(new String[] { "Open in Default Tool" }, group)); setKeyBindingData(new KeyBindingData(KeyEvent.VK_ENTER, 0)); markHelpUnnecessary(); } @@ -37,11 +37,24 @@ public class ProjectDataOpenDefaultToolAction extends FrontendProjectTreeAction @Override protected void actionPerformed(ProjectDataContext context) { List selectedFiles = context.getSelectedFiles(); + // NOTE: Seems like we should confirm opening more than one file AppInfo.getActiveProject().getToolServices().launchDefaultTool(selectedFiles); } @Override protected boolean isEnabledForContext(ProjectDataContext context) { - return context.getSelectedFiles().size() > 0 && context.getSelectedFolders().size() == 0; + if (!context.getSelectedFolders().isEmpty()) { + return false; + } + List selectedFiles = context.getSelectedFiles(); + if (selectedFiles.isEmpty()) { + return false; + } + for (DomainFile file : context.getSelectedFiles()) { + if (file.isLink() && file.getLinkInfo().isFolderLink()) { + return false; + } + } + return true; } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteAction.java index e45b44c9a1..4fbd1b720b 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteAction.java @@ -16,6 +16,7 @@ package ghidra.framework.main.projectdata.actions; import java.awt.event.InputEvent; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -26,6 +27,8 @@ import docking.action.MenuData; import docking.widgets.tree.GTreeNode; import generic.theme.GIcon; import ghidra.framework.main.datatree.*; +import ghidra.framework.model.DomainFolder; +import ghidra.framework.model.LinkedDomainFolder; import ghidra.util.HelpLocation; import ghidra.util.Msg; import ghidra.util.task.TaskLauncher; @@ -43,23 +46,20 @@ public class ProjectDataPasteAction extends ProjectDataCopyCutBaseAction { @Override protected void actionPerformed(FrontEndProjectTreeContext context) { GTreeNode node = (GTreeNode) context.getContextObject(); - DomainFolderNode destNode = getFolderForNode(node); - - paste(context.getTree(), destNode); + DomainFolder destFolder = DataTree.getRealInternalFolderForNode(node); + if (destFolder != null) { + paste(context.getTree(), destFolder); + } } @Override protected boolean isEnabledForContext(FrontEndProjectTreeContext context) { - if (!context.hasExactlyOneFileOrFolder()) { - return false; - } - if (!context.isInActiveProject()) { + if (!context.isInActiveProject() || !context.hasExactlyOneFileOrFolder()) { return false; } GTreeNode node = (GTreeNode) context.getContextObject(); - GTreeNode destNode = getFolderForNode(node); - return checkNodeForPaste(destNode); - + DomainFolder destFolder = DataTree.getRealInternalFolderForNode(node); + return checkNodeForPaste(destFolder); } @Override @@ -70,27 +70,30 @@ public class ProjectDataPasteAction extends ProjectDataCopyCutBaseAction { return context.isInActiveProject(); } - private DomainFolderNode getFolderForNode(GTreeNode node) { - if (node instanceof DomainFolderNode) { - return (DomainFolderNode) node; - } - return (DomainFolderNode) node.getParent(); - } - /** * Check the destination node for whether clipboard data can be pasted there. + * Ancestry checks are performed for the node(s) in the clipboard against the + * specified destination folder. * - * @param destNode destination for paste operation + * @param destFolder destination for paste operation * @return true if least one node can be pasted at destNode */ - private boolean checkNodeForPaste(GTreeNode destNode) { + static boolean checkNodeForPaste(DomainFolder destFolder) { + + if (destFolder == null || !destFolder.isInWritableProject()) { + return false; + } List list = DataTreeClipboardUtils.getDataTreeNodesFromClipboard(); for (GTreeNode node : list) { - if (!node.isAncestor(destNode)) { - // at least one node can be pasted from system clipboard - return true; + if (node instanceof DomainFileNode fileNode && !fileNode.isFolderLink()) { + return true; // at least one good paste from clipboard + } + // Check folder-link or folder + DomainFolder folder = DataTree.getRealInternalFolderForNode(node); + if (folder != null && !folder.isSameOrAncestor(destFolder)) { + return true; // at least one good paste from clipboard } } return false; @@ -99,14 +102,14 @@ public class ProjectDataPasteAction extends ProjectDataCopyCutBaseAction { /** * Process a "paste" request from a menu action. */ - private void paste(DataTree tree, DomainFolderNode folderNode) { + private void paste(DataTree tree, DomainFolder destFolder) { List list = DataTreeClipboardUtils.getDataTreeNodesFromClipboard(); boolean isCutOperation = isCutOperation(list); - checkPasteList(tree, folderNode, list, isCutOperation); + checkPasteList(tree, destFolder, list, isCutOperation); if (!list.isEmpty()) { - PasteFileTask task = new PasteFileTask(folderNode, list, isCutOperation); + PasteFileTask task = new PasteFileTask(destFolder, list, isCutOperation); new TaskLauncher(task, tree, 1000); } else { @@ -121,74 +124,136 @@ public class ProjectDataPasteAction extends ProjectDataCopyCutBaseAction { * Update the given list of nodes to paste if the corresponding file or * folder cannot be pasted; remove it from the list and update the * clipboard with the new list. - * @param destNode destination node + * @param destFolder destination folder * @param list list of nodes to paste * @param isCutOperation true if this is a cut vs copy; for cut, files * cannot be in use */ - private void checkPasteList(DataTree tree, GTreeNode destNode, List list, + private void checkPasteList(DataTree tree, DomainFolder destFolder, List list, boolean isCutOperation) { - if (list == null) { + if (list == null || list.isEmpty()) { return; } - boolean listChanged = removeDescendantsFromList(list); + removeDescendantsFromList(list); - boolean resetClipboard = false; - StringBuffer sb = new StringBuffer(); + StringBuilder msgBuffer = new StringBuilder(); for (int i = 0; i < list.size(); i++) { GTreeNode tnode = list.get(i); - boolean removeNodeFromList = false; - - if (tnode.getParent() != null && isCutOperation && !destNode.equals(tnode)) { - if (destNode == tnode.getParent()) { - removeNodeFromList = true; - sb.append( - "File " + tnode.getName() + " already exists at " + tnode.getParent()); - } - else if (tnode instanceof DomainFolderNode) { - if (destNode.isAncestor(tnode)) { - removeNodeFromList = true; - } - } - } - else if (tnode.getParent() == null || destNode == tnode) { - removeNodeFromList = true; - if (destNode == tnode) { - sb.append("Cannot paste file to itself: " + destNode.getName()); - } + boolean removeNodeFromList = true; + if (tnode instanceof DataTreeNode dataTreeNode) { + removeNodeFromList = + !canCopyNode(dataTreeNode, destFolder, isCutOperation, msgBuffer); } if (removeNodeFromList) { - list.remove(i); - if (i > 0) { - --i; - } - resetClipboard = true; - if (tnode.getParent() != null) { - if (tnode instanceof Cuttable) { - ((Cuttable) tnode).setIsCut(false); - } + // After removing the current 'tnode' from the list, decrement list index 'i' + // to compensate for the loop's index 'i' increment since the next node will + // reside at the same index position within the list. + list.remove(i--); + if (tnode instanceof Cuttable cuttable) { + cuttable.setIsCut(false); } } } - if (resetClipboard || listChanged) { - if (sb.length() > 0) { - String title = isCutOperation ? "Cannot Move File(s)" : "Cannot Copy File(s)"; - String action = isCutOperation ? "moved" : "copied"; + if (msgBuffer.length() > 0) { + String title = isCutOperation ? "Cannot Move File(s)" : "Cannot Copy File(s)"; + String action = isCutOperation ? "moved" : "copied"; + Msg.showWarn(getClass(), tree, title, + "The following content could not be " + action + ":\n" + msgBuffer.toString()); + } + } - Msg.showWarn(getClass(), tree, title, - "The following file(s) could not be " + action + ":\n" + sb.toString()); + private void appendMsg(String msg, StringBuilder msgBuffer) { + if (!msg.isEmpty()) { + msgBuffer.append("\n"); + } + msgBuffer.append(msg); + } + + /** + * Determine if the specified node can be copied or moved to the specified destination folder. + * @param dataTreeNode copy/cut node + * @param destFolder destination folder + * @param isCutOperation true if node is being moved to {@code destFolder} + * @param msgBuffer error message buffer + * @return true if node copy/move is permitted, else false in which case {@code msgBuffer} + * may have messages. + */ + private boolean canCopyNode(DataTreeNode dataTreeNode, DomainFolder destFolder, + boolean isCutOperation, StringBuilder msgBuffer) { + try { + String nodeType = (dataTreeNode instanceof DomainFolderNode) ? "Folder" : "File"; + DomainFolder folder = getRealFolder(dataTreeNode); + if (isCutOperation) { + if (!folder.isInWritableProject()) { + appendMsg("Read-only project. " + nodeType + " '" + dataTreeNode.getName() + + "' cannot be moved", msgBuffer); + return false; + } + if (dataTreeNode.getParent() == null) { + return false; // ignore root node cut selection + } + DomainFolder checkFolder = + (dataTreeNode instanceof DomainFolderNode) ? folder.getParent() : folder; + if (destFolder.equals(checkFolder)) { + return false; // ignore move to same location + } + } + + if (dataTreeNode instanceof DomainFolderNode) { + if (folder.isSameOrAncestor(destFolder)) { + appendMsg( + nodeType + " '" + dataTreeNode.getName() + + "' contains destination folder '" + destFolder.getName() + "'", + msgBuffer); + return false; + } + if (destFolder.getFolder(folder.getName()) != null) { + appendMsg("Folder '" + destFolder.getName() + + "' already contains a folder named '" + dataTreeNode.getName() + "'", + msgBuffer); + return false; + } } } + catch (IOException e) { + Msg.warn(this, + "Failed to resolve linked item: " + dataTreeNode.getName() + ": " + e.getMessage()); + appendMsg("Failed to resolve linked item: " + dataTreeNode.getName(), msgBuffer); + return false; + } + return true; + } + + /** + * {@return the real folder which corresponds to a folder node or the parent of a file node} + * @param dataTreeNode file or folder data tree node + * @throws IOException if a linked-folder IO error occurs + */ + private DomainFolder getRealFolder(DataTreeNode dataTreeNode) throws IOException { + DomainFolder folder = null; + if (dataTreeNode instanceof DomainFileNode fileNode) { + folder = fileNode.getDomainFile().getParent(); + } + else if (dataTreeNode instanceof DomainFolderNode folderNode) { + folder = folderNode.getDomainFolder(); + } + if (folder instanceof LinkedDomainFolder linkedFolder) { + // need real folder to simplify relationship checks + folder = linkedFolder.getRealFolder(); + } + return folder; } /** * Remove descendant nodes from the list; having the parent node * is enough when folders are getting pasted. */ - private boolean removeDescendantsFromList(List list) { + private void removeDescendantsFromList(List list) { + // NOTE: This needs to be optimized and is not well suited + // for a large number of nodes List newList = new ArrayList<>(); for (int i = 0; i < list.size(); i++) { GTreeNode destNode = list.get(i); @@ -205,7 +270,6 @@ public class ProjectDataPasteAction extends ProjectDataCopyCutBaseAction { for (int i = 0; i < newList.size(); i++) { list.remove(newList.get(i)); } - return newList.size() > 0; } private boolean isCutOperation(List list) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteLinkAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteLinkAction.java index 9fd17d4014..271e4af8e2 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteLinkAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataPasteLinkAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,11 +22,11 @@ import javax.swing.Icon; import docking.action.MenuData; import docking.widgets.tree.GTreeNode; -import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.*; +import ghidra.framework.main.AppInfo; import ghidra.framework.main.datatable.ProjectTreeAction; import ghidra.framework.main.datatree.*; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainFolder; +import ghidra.framework.model.*; import ghidra.util.HelpLocation; import ghidra.util.Msg; import resources.Icons; @@ -35,10 +35,18 @@ import resources.MultiIcon; public class ProjectDataPasteLinkAction extends ProjectTreeAction { private static Icon baseIcon = Icons.PASTE_ICON; - public ProjectDataPasteLinkAction(String owner, String group) { - super("Paste Link", owner); - setPopupMenuData(new MenuData(new String[] { "Paste as Link" }, getIcon(), group)); - setHelpLocation(new HelpLocation("FrontEndPlugin", "Create_File_Links")); + private boolean relative; + + public ProjectDataPasteLinkAction(String owner, String group, boolean relative) { + super("Paste " + getLinkType(relative), owner); + this.relative = relative; + setPopupMenuData( + new MenuData(new String[] { "Paste as " + getLinkType(relative) }, getIcon(), group)); + setHelpLocation(new HelpLocation("FrontEndPlugin", "Paste_Link")); + } + + private static String getLinkType(boolean relative) { + return relative ? "Relative-Link" : "Link"; } private static Icon getIcon() { @@ -49,18 +57,22 @@ public class ProjectDataPasteLinkAction extends ProjectTreeAction { @Override protected void actionPerformed(FrontEndProjectTreeContext context) { - GTreeNode node = (GTreeNode) context.getContextObject(); - DomainFolderNode destNode = getFolderForNode(node); if (!isEnabledForContext(context)) { + return; + } + + GTreeNode node = (GTreeNode) context.getContextObject(); + DomainFolder destFolder = DataTree.getRealInternalFolderForNode(node); + if (destFolder == null) { Msg.showWarn(getClass(), context.getTree(), "Unsupported Operation", "Unsupported paste link condition"); } GTreeNode copyNode = getFolderOrFileCopyNode(); - if (copyNode instanceof DomainFileNode) { + if (copyNode instanceof DomainFileNode fileNode) { try { - DomainFile domainFile = ((DomainFileNode) copyNode).getDomainFile(); - domainFile.copyToAsLink(destNode.getDomainFolder()); + DomainFile domainFile = fileNode.getDomainFile(); + domainFile.copyToAsLink(destFolder, relative); } catch (IOException e) { Msg.showError(getClass(), context.getTree(), "Cannot Create Link", @@ -70,7 +82,7 @@ public class ProjectDataPasteLinkAction extends ProjectTreeAction { else { try { DomainFolder domainFolder = ((DomainFolderNode) copyNode).getDomainFolder(); - domainFolder.copyToAsLink(destNode.getDomainFolder()); + domainFolder.copyToAsLink(destFolder, relative); } catch (IOException e) { Msg.showError(getClass(), context.getTree(), "Cannot Create Link", @@ -82,65 +94,52 @@ public class ProjectDataPasteLinkAction extends ProjectTreeAction { @Override protected boolean isEnabledForContext(FrontEndProjectTreeContext context) { - if (!context.hasExactlyOneFileOrFolder()) { - return false; - } - if (!context.isInActiveProject()) { + if (!context.isInActiveProject() || !context.hasExactlyOneFileOrFolder()) { return false; } GTreeNode node = (GTreeNode) context.getContextObject(); - DomainFolderNode destNode = getFolderForNode(node); - - GTreeNode copyNode = getFolderOrFileCopyNode(); - if (copyNode == null || copyNode.getParent() == null) { + DomainFolder destFolder = DataTree.getRealInternalFolderForNode(node); + if (!ProjectDataPasteAction.checkNodeForPaste(destFolder)) { return false; } - - // local internal linking not supported - if (destNode.getRoot() == copyNode.getRoot()) { - return false; + Project activeProject = AppInfo.getActiveProject(); + DataTreeNode copyNode = getFolderOrFileCopyNode(); + if (copyNode != null) { + if (relative && copyNode.getProjectData() != activeProject.getProjectData()) { + return false; + } + if (copyNode instanceof DomainFileNode fileNode) { + // Only enable action if a LinkHandler exists for the file + DomainFile domainFile = fileNode.getDomainFile(); + try { + ContentHandler contentHandler = + DomainObjectAdapter.getContentHandler(domainFile.getContentType()); + return contentHandler.getLinkHandler() != null; + } + catch (IOException e) { + return false; + } + } + return true; } - - if (copyNode instanceof DomainFileNode) { - DomainFile df = ((DomainFileNode) copyNode).getDomainFile(); - return df.isLinkingSupported(); - } - return true; + return false; } - @Override - protected boolean isAddToPopup(FrontEndProjectTreeContext context) { - if (!context.hasOneOrMoreFilesAndFolders()) { - return false; - } - if (!context.isInActiveProject()) { - return false; - } - GTreeNode copyNode = getFolderOrFileCopyNode(); - return copyNode != null && copyNode.getParent() != null; - } - - private DomainFolderNode getFolderForNode(GTreeNode node) { - if (node instanceof DomainFolderNode) { - return (DomainFolderNode) node; - } - return (DomainFolderNode) node.getParent(); - } - - private GTreeNode getFolderOrFileCopyNode() { + private DataTreeNode getFolderOrFileCopyNode() { + // Null will be returned if single node is marked for cut operation List list = DataTreeClipboardUtils.getDataTreeNodesFromClipboard(); if (list.size() != 1) { return null; } GTreeNode copyNode = list.get(0); - if (copyNode instanceof DomainFileNode) { - if (!((DomainFileNode) copyNode).isCut()) { - return copyNode; + if (copyNode instanceof DomainFileNode fileNode) { + if (!fileNode.isCut()) { + return fileNode; } } - if (copyNode instanceof DomainFolderNode) { - if (!((DomainFolderNode) copyNode).isCut()) { - return copyNode; + if (copyNode instanceof DomainFolderNode folderNode) { + if (!folderNode.isCut()) { + return folderNode; } } return null; diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataRefreshAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataRefreshAction.java index 9c5a0ecc53..7364eedefb 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataRefreshAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataRefreshAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -42,6 +42,11 @@ public class ProjectDataRefreshAction extends FrontendProjectTreeAction { markHelpUnnecessary(); } + @Override + protected boolean isEnabledForContext(ProjectDataContext context) { + return context.hasOneOrMoreFilesAndFolders(); + } + @Override protected void actionPerformed(ProjectDataContext context) { refresh(context.getProjectData(), context.getComponent()); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataSelectAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataSelectAction.java index 5190105dbd..54941d9576 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataSelectAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/ProjectDataSelectAction.java @@ -15,6 +15,7 @@ */ package ghidra.framework.main.projectdata.actions; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -23,9 +24,11 @@ import javax.swing.tree.TreePath; import docking.action.MenuData; import docking.widgets.tree.GTreeNode; import docking.widgets.tree.tasks.GTreeExpandAllTask; +import ghidra.framework.data.LinkHandler; import ghidra.framework.main.datatable.ProjectTreeAction; -import ghidra.framework.main.datatree.DataTree; -import ghidra.framework.main.datatree.FrontEndProjectTreeContext; +import ghidra.framework.main.datatree.*; +import ghidra.framework.model.DomainFile; +import ghidra.framework.store.FileSystem; public class ProjectDataSelectAction extends ProjectTreeAction { @@ -40,12 +43,42 @@ public class ProjectDataSelectAction extends ProjectTreeAction { DataTree tree = context.getTree(); TreePath[] paths = context.getSelectionPaths(); GTreeNode node = (GTreeNode) paths[0].getLastPathComponent(); + selectAllChildren(tree, node); } @Override public boolean isAddToPopup(FrontEndProjectTreeContext context) { - return context.getFolderCount() == 1 && context.getFileCount() == 0; + if (!context.hasExactlyOneFileOrFolder()) { + return false; + } + if (context.getFolderCount() == 1) { + return true; + } + DomainFile folderLinkFile = context.getSelectedFiles().get(0); + return canTraverseFolderLinkFile(folderLinkFile); + } + + private static boolean canTraverseFolderLinkFile(DomainFile file) { + if (file.isLink() && file.getLinkInfo().isFolderLink()) { + // Prevent selection of folder-link which is contained within referenced link-path. + // Cycle prevention in tree should prevent this from being an issue + String filePath = file.getPathname() + FileSystem.SEPARATOR; + String linkPath; + try { + linkPath = LinkHandler.getAbsoluteLinkPath(file); + if (!linkPath.endsWith(FileSystem.SEPARATOR)) { + linkPath += FileSystem.SEPARATOR; + } + if (!filePath.startsWith(linkPath)) { + return true; + } + } + catch (IOException e) { + // ignore + } + } + return false; } /** @@ -70,9 +103,30 @@ public class ProjectDataSelectAction extends ProjectTreeAction { * Select all descendants starting at node. */ private void getAllTreePaths(GTreeNode node, List paths) { - paths.add(node.getTreePath()); + + // Origin node is intentionally not included in selection since the origin node + // is not a child of itself. Including the root node can present problems as well. + List children = node.getChildren(); for (GTreeNode child : children) { + // Limit recursion through folder-links which may be self-referencing + if (child instanceof DomainFileNode fileNode) { + + if (fileNode.isLeaf()) { + // add individual child + paths.add(child.getTreePath()); + continue; + } + + // We should only get here is file is a internal folder link + // which needs to be checked for possible circular ancestry issue + if (!canTraverseFolderLinkFile(fileNode.getDomainFile())) { + continue; + } + } + + // recurse and add child with its children + paths.add(child.getTreePath()); getAllTreePaths(child, paths); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlViewCheckOutAction.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlViewCheckOutAction.java index 38549f0d66..603c47429e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlViewCheckOutAction.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/projectdata/actions/VersionControlViewCheckOutAction.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -61,7 +61,8 @@ public class VersionControlViewCheckOutAction extends VersionControlAction { } DomainFile domainFile = domainFiles.get(0); - return domainFile.isVersioned(); + // Link files do not support checkout + return !domainFile.isLink() && domainFile.isVersioned(); } /** diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DefaultDomainFileFilter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DefaultDomainFileFilter.java new file mode 100644 index 0000000000..f00ce34b97 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DefaultDomainFileFilter.java @@ -0,0 +1,58 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.model; + +/** + * {@link DefaultDomainFileFilter} provides a simple default domain file filter which accepts + * files for a specified domain object interface class. + */ +public class DefaultDomainFileFilter implements DomainFileFilter { + + private final Class domainObjectClass; + private final boolean ignoreExternalLinks; + + /** + * Construct a {@link DomainFileFilter} which accepts a specific domain object interface + * class and either shows or hides external links. If external links are not ignored + * the filter will allow following external folder-links into other projects or server + * repositories. Note that this should be enabled carefully since it may required + * proper repository authentication support to facilitate access. + * Broken links are always ignored and all internal linked-folders and linked-files will be + * followed/processed. + * + * @param domainObjectClass domain object interface class. May be null to disallow all files + * (i.e., only folders and folder-links are shown). + * @param ignoreExternalLinks true to ignore/skip external links, else they will be + * shown/processed and opening/following such links will be supported. + */ + public DefaultDomainFileFilter(Class domainObjectClass, + boolean ignoreExternalLinks) { + this.domainObjectClass = domainObjectClass; + this.ignoreExternalLinks = ignoreExternalLinks; + } + + @Override + public boolean accept(DomainFile file) { + return domainObjectClass != null && + domainObjectClass.isAssignableFrom(file.getDomainObjectClass()); + } + + @Override + public boolean ignoreExternalLinks() { + return ignoreExternalLinks; + } + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java index b9fdae05f8..4b14f99c83 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFile.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,10 +31,57 @@ import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; /** - * DomainFile provides a storage interface for project files. A - * DomainFile is an immutable reference to file contained within a project. The state - * of a DomainFile object does not track name/parent changes made to the referenced - * project file. + * {@link DomainFile} provides a storage interface for a project file. A domain file + * provides an immutable reference to a stored file contained within a project. The state + * of a file object does not track name/parent changes made to the referenced project file. + * An up-to-date object may be obtained from {@link ProjectData#getFile(String)}, + * {@link ProjectData#getFileByID(String)}, or may be returned by any method used to move or rename + * it. The project data object for the active + * {@link Project} may be obtained via {@link Project#getProjectData()}. + *

    + * Link Files + *

    + * Link files may exist or be created within a project where the methods {@link #isLink()} and + * {@link #getLinkInfo()} may be used to obtain more details about a link and in the case of a + * linked-folder can facilitate obtainining the referenced {@link LinkedGhidraFolder}. This + * information object can also be used to determine if the referenced file or folder is external + * to this file's project. + *

    + * A link-file can become "broken" if its reference has one of the following conditions + * occur: + *

      + *
    1. A referenced internal file or folder does not exist, or
    2. + *
    3. the nature/content-type of the referenced file does not match the designated type when the + * link was created, or
    4. + *
    5. the link has a circular reference path within this file's project.
    6. + *
    + *

    + * The method {@link LinkFileInfo#getLinkStatus(java.util.function.Consumer)} may be used to + * determine if a link is "broken". Use of a broken link may result in an IOException or other + * failure. The domain object for a file-link (e.g., ProgramLink) may be obtained in the same + * manner as a normal file (e.g., {@link #getDomainObject(Object, boolean, boolean, TaskMonitor)}. + * However, as with any file it is recommended that {@link #getDomainObjectClass()} first be used + * to ensure the file corresponds to the expected/supported content type. + *

    + * NOTE: Using external links to shared projects or + * repositories may result in required authentication; which in headless situations may be + * limited by the active authentication handler (see {@link LinkFileInfo#isExternalLink()} and + * {@link LinkFileInfo#getLinkStatus(java.util.function.Consumer)} for more details). + *

    + * Link files can facilitate a link to either a folder or another file of a specific content type + * within a Ghidra project. Here's why someone might choose to use them: + *

      + *
    • File Organization: links allow users to organize files and folders in a way that makes + * sense for their workflow without duplicating data. A single file can appear to exist in multiple + * locations without taking up additional space.
    • + *
    • Dynamic Updates: If the original file or folder is modified, the changes are automatically + * reflected wherever the link is used, ensuring consistency without manual updates.
    • + *
    • Shared Resources: links can be used to establish shortcuts to files stored in different + * repositories, projects or directories, enabling easy access without navigating deeply nested folder + * structures or replicating stored data.
    • + *
    • System Configuration: links can be used to link different versions of programs or libraries + * without changing paths.
    • + *
    */ public interface DomainFile extends Comparable { @@ -575,20 +622,21 @@ public interface DomainFile extends Comparable { throws IOException, CancelledException; /** - * Copy this file into the newParent folder as a link file. Restrictions: - *
      - *
    • Specified newParent must reside within a different project since internal linking is - * not currently supported.
    • - *
    • Content type must support linking (see {@link #isLinkingSupported()}).
    • - *
    - * If this file is associated with a temporary transient project (i.e., not a locally - * managed project) the generated link will refer to the remote file with a remote - * Ghidra URL, otherwise a local project storage path will be used. + * Copy this file into the newParent folder as a file-link. A file-link references another + * file without actually copying all of its content. If this file is associated with a + * temporary transient project (i.e., not a locally managed project) the generated link will + * refer to the this file with a Ghidra URL. If this file is contained within the + * same active {@link ProjectData} instance as {@code newParent} an internal link reference + * will be made. + * * @param newParent new parent folder + * @param relative if true, and this file is contained within the same active + * {@link ProjectData} instance as {@code newParent}, an internal-project relative path + * file-link will be created. * @return newly created domain file or null if content type does not support link use. * @throws IOException if an IO or access error occurs. */ - public DomainFile copyToAsLink(DomainFolder newParent) throws IOException; + public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException; /** * Determine if this file's content type supports linking. @@ -650,25 +698,35 @@ public interface DomainFile extends Comparable { public long length() throws IOException; /** - * Determine if this file is a link file which corresponds to either a file or folder link. + * Determine if this file is a link-file which corresponds to either a file or folder link. + * See {@link #getLinkInfo()} to obtain link information. + *

    + * If the link-file is a {@link LinkFileInfo#isFolderLink() folder-link} the method + * {@link LinkFileInfo#getLinkedFolder()} can be used to get the linked domain folder where the + * resulting folder's {@link DomainFolder#isLinked()} indicates that it was the result of + * following a folder-link. + *

    + * The associated link path/URL may be obtained with {@link LinkFileInfo#getLinkPath()}. + *

    + * The content type (see {@link #getContentType()} of a link-file will differ from that of the + * linked object (e.g., "LinkedProgram" vs "Program"). It is highly recommended that the + * {@link #getDomainObjectClass()} method be used instead since it will return the same value + * for a normal file or link-file that corresponds to the same {@link DomainObject} implementation. + *

    * The {@link DomainObject} referenced by a link-file may be opened using * {@link #getReadOnlyDomainObject(Object, int, TaskMonitor)}. The * {@link #getDomainObject(Object, boolean, boolean, TaskMonitor)} method may also be used - * to obtain a read-only instance. {@link #getImmutableDomainObject(Object, int, TaskMonitor)} - * use is not supported. - * If the link-file content type equals {@value FolderLinkContentHandler#FOLDER_LINK_CONTENT_TYPE} - * the method {@link #followLink()} can be used to get the linked domain folder. - * The associated link URL may be obtained with {@link LinkHandler#getURL(DomainFile)}. - * The content type (see {@link #getContentType()} of a link file will differ from that of the - * linked object (e.g., "LinkedProgram" vs "Program"). + * to obtain a read-only instance. These methods should not be used on a folder-link since + * an {@link UnsupportedOperationException} will be thrown. + * * @return true if link file else false for a normal domain file */ - public boolean isLinkFile(); + public boolean isLink(); /** - * If this is a folder-link file get the corresponding linked folder. - * @return a linked domain folder or null if not a folder-link. + * If this file is a {@link #isLink() link-file} the link information will be returned. + * @return link information or null if this is not a link-file */ - public DomainFolder followLink(); + public LinkFileInfo getLinkInfo(); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java index 9ceb871e8f..303bef66f7 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFileFilter.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,28 +15,131 @@ */ package ghidra.framework.model; -/** - * Interface to indicate whether a domain file should be included in a list or - * set of domain files. - */ -public interface DomainFileFilter { +import ghidra.framework.data.LinkHandler; - /** - * Tests whether or not the specified domain file should be - * included in a domain file list. - * - * @param df The domain file to be tested - * @return true if and only if df - * - */ - public boolean accept(DomainFile df); +/** + * {@link DomainFileFilter} interface to indicate whether a domain file should be included in a + * list or set of domain files. This interface extends {@link DomainFolderFilter} which also + * controls the following of linked-folders. + *

    + * Without specific overrides the default behavior: + *

      + *
    • {@link #ignoreBrokenLinks()} (true) Ignores all broken links
    • + *
    • {@link #ignoreExternalLinks()} (true) Ignores all external links
    • + *
    • {@link #ignoreFolderLinks()} (false) Will follow folder-links
    • + *
    • {@link #followExternallyLinkedFolders()} Based on + * NOT-{@link #ignoreExternalLinks()} AND NOT-{@link #ignoreFolderLinks()}
    • + *
    + *

    + * The specific handling of link-files is determined by the consumer of this filter. + */ +public interface DomainFileFilter extends DomainFolderFilter { /** - * Determine if linked folders represented by a link-file should be followed. - * If this method is not implemented the default will return {@code true}. - * @return true if linked-folders should be followed or false to ignore. + * File filter which accepts all files, including all external file-links, + * and allows opening/expanding of external folder-links. All broken links are ignored. */ - public default boolean followLinkedFolders() { - return true; + DomainFileFilter ALL_FILES_FILTER = new DomainFileFilter() { + @Override + public boolean accept(DomainFile df) { + return true; + } + + @Override + public boolean ignoreExternalLinks() { + return false; + } + }; + + /** + * File filter which accepts all files, including all external file-links, + * but does not allow opening/expanding of external folder-links. All broken links are ignored. + */ + DomainFileFilter ALL_FILES_NO_EXTERNAL_FOLDERS_FILTER = new DomainFileFilter() { + @Override + public boolean accept(DomainFile df) { + return true; + } + + @Override + public boolean ignoreExternalLinks() { + return false; + } + + @Override + public boolean followExternallyLinkedFolders() { + return false; + } + }; + + /** + * File filter which allows all internal folders and files. + * All external and broken links are ignored. This filter is useful when + * selecting a file with an arbitrary content type. If targeting a specific file content + * type the use of {@link DefaultDomainFileFilter} may be preferred. + */ + DomainFileFilter ALL_INTERNAL_FILES_FILTER = new DomainFileFilter() { + @Override + public boolean accept(DomainFile df) { + return true; + } + }; + + /** + * File filter which allows all non-linked internal folders and files. + * All links are ignored. This filter is useful if code does not handle some of the + * implications of following links such as: + *

      + *
    • External repository authentication
    • + *
    • Processing the same project content more than once or lack of support for link-files
    • + *
    + * If targeting a specific file content type the use of {@link DefaultDomainFileFilter} may + * be preferred. + */ + public static DomainFileFilter NON_LINKED_FILE_FILTER = new DomainFileFilter() { + + @Override + public boolean accept(DomainFile df) { + // Accept all domain files which are not a link-file. + // Processing of link-files may result in the same file being returned by the + // iterator more than once. + return !df.isLink(); + } + + @Override + public boolean ignoreFolderLinks() { + return true; + } + }; + + /** + * Tests whether or not the specified domain file should be included in a domain file list. + * Since link-files will also be subject to this constraint the ability to handle or follow + * such links must be considered. + *

    + * NOTE: File-links have the same {@link DomainFile#getDomainObjectClass()} as the file they + * refer to, while their {@link DomainFile#getContentType()} is specific to their + * {@link LinkHandler} implementation. + * + * @param df The domain file to be tested + * @return true if and only if df + */ + public boolean accept(DomainFile df); + + /** + * Check if the children of an externally-linked folder should be loaded/processed. + *

    + * If this method is not implemented the value returned is + * NOT-{@link #ignoreExternalLinks()} AND NOT-{@link #ignoreFolderLinks()}. + *

    + * NOTE: Following an external link utilizes the application's active project to retain + * and external project as one of it's viewed-projects. In the process of accessing a + * viewed-project the user may be required to authenticate to a remote server. + * + * @return true if children of an externally-linked folder should be traversed or displayed + * (subject to a successful connection to the referenced project or server-based repository). + */ + public default boolean followExternallyLinkedFolders() { + return !ignoreExternalLinks() && !ignoreFolderLinks(); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java index c68de7fe3e..1196cabe2b 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolder.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,19 +22,70 @@ import java.net.URL; import javax.swing.Icon; import generic.theme.GIcon; +import ghidra.framework.data.LinkHandler; import ghidra.framework.store.FolderNotEmptyException; import ghidra.util.InvalidNameException; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; /** - * DomainFolder provides a storage interface for project folders. A - * DomainFolder is an immutable reference to a folder contained within a project. The - * state of a DomainFolder object does not track name/parent changes made to the - * referenced project folder. + * {@link DomainFolder} provides a storage interface for a project folder. A + * domain folder is an immutable reference to a folder contained within a project. Provided the + * corresponding path exists within the project it may continue to be used to create and access + * its files and sub-folders. The state of a folder object does not track name/parent changes made + * to the referenced project file. An up-to-date instance may be obtained from + * {@link ProjectData#getFolder(String)} or may be returned by any method used to move or rename it. + * The project data object for the active {@link Project} may be obtained via + * {@link Project#getProjectData()}. + *

    + * Link Files + *

    + * Link files may exist or be created within a folder. See {@link DomainFile} for more information. + *

    + * Obtaining the folder which corresponds to a linked-folder is done indirectly via a link file. + * There are different ways to arrive on a linked-folder: + *

      + *
    1. Obtained directly via a folder request by path, or
    2. + *
    3. discovered by the presence of a link file which corresponds to a linked-folder.
    4. + *
    + *

    + * Example #1, where folder path is known (external links will be followed): + *

    + *    DomainFolder folder = projectData.getFolder("/A/B/linkedFolder");
    + *    if (folder != null) {
    + *       if (folder.isLinked())
    + *          LinkedDomainFolder linkedFolder = (LinkedDomainFolder) folder;
    + *          // linkedFolder behaves the same as a normal folder
    + *       }
    + *       DomainFile[] files = folder.getFiles();
    + *    }
    + * 
    + *

    + * Example #2, where we locate via a link file: + *

    + *    DomainFile file = ...
    + *    LinkFileInfo linkInfo = file.getLinkInfo();
    + *    if (linkInfo != null && linkInfo.isFolderLink()) {
    + *       LinkStatus status = linkInfo.getLinkStatus(null);
    + *       if (status != LinkStatus.INTERNAL) {
    + *          return; // Only consider internally linked-folder, skip broken or external link
    + *       }
    + *       LinkedDomainFolder linkedFolder = linkInfo.getLinkedFolder();
    + *       if (linkedFolder != null) {    
    + *          DomainFile[] files = linkedFolder.getFiles();
    + *       }
    + *    }
    + * 
    + *

    + * The utility method {@link ProjectDataUtils#descendantFiles(DomainFolder, DomainFileFilter)} + * may also come in handy to iterate over folder contents while restricting treatment of + * linked content. */ public interface DomainFolder extends Comparable { + // TODO: Need more robust folder icon support to allow repository connection state + // for root node to be reflected in icon (GP-5333) + public static final Icon OPEN_FOLDER_ICON = new GIcon("icon.datatree.node.domain.folder.open"); public static final Icon CLOSED_FOLDER_ICON = @@ -88,6 +139,31 @@ public interface DomainFolder extends Comparable { */ public String getPathname(); + /** + * Returns true if the given folder is the same as this folder based on path + * and underlying project/repository. Unlike the {@link #equals(Object)} check, this method + * handles cases where the folder provided may correspond to another project instance + * which is considered the same as the project that this folder is contained within. + * + * @param folder the potential same or descendant folder to check + * @return true if the given folder is the same or a child of this folder or + * one of its decendents. + */ + public boolean isSame(DomainFolder folder); + + /** + * Returns true if the given folder is the same or a child of this folder or + * one of its decendents based on path and underlying project/repository. Unlike the + * {@link #equals(Object)} check, this method + * handles cases where the folder provided may correspond to another project instance + * which is considered the same as the project that this folder is contained within. + * + * @param folder the potential same or descendant folder to check + * @return true if the given folder is the same or a child of this folder or + * one of its decendents. + */ + public boolean isSameOrAncestor(DomainFolder folder); + /** * Get a remote Ghidra URL for this domain folder if available within an associated shared * project repository. URL path will end with "/". A null value will be returned if shared @@ -125,6 +201,7 @@ public interface DomainFolder extends Comparable { /** * Return the folder for the given name. + * Folder link-files are ignored. * @param name of folder to retrieve * @return folder or null if there is no folder by the given name. */ @@ -182,6 +259,47 @@ public interface DomainFolder extends Comparable { public DomainFile createFile(String name, File packFile, TaskMonitor monitor) throws InvalidNameException, IOException, CancelledException; + /** + * Create a link-file within this folder which references the specified file or folder + * {@code pathname} within the project specified by {@code sourceProjectData}. The link-file + * may correspond to various types of content (e.g., Program, Trace, Folder, etc.) based upon + * the specified {@link LinkHandler} instance. + * + * @param sourceProjectData referenced content project data within which specified path exists. + * If this differ's from this folder's project a Ghidra URL will be used, otherwise and internal + * link path reference will be used. + * @param pathname an absolute path of project folder or file within the specified source + * project data (a Ghidra URL is not permitted) + * @param makeRelative if true, and this file is contained within the same active + * {@link ProjectData} instance as {@code newParent}, an internal-project relative path + * link-file will be created. + * @param linkFilename name of link-file to be created within this folder. NOTE: This name may + * be modified to ensure uniqueness within this folder. + * @param lh link-file handler used to create specific link-file (see derived implementations + * of {@link LinkHandler} and their public static INSTANCE. + * @return newly created link-file + * @throws IOException if IO error occurs during link creation + */ + public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname, + boolean makeRelative, String linkFilename, LinkHandler lh) throws IOException; + + /** + * Create an external link-file within this folder which references the specified + * {@code ghidraUrl} and whose content is defined by the specified {@link LinkHandler lh} + * instance. + * + * @param ghidraUrl a Ghidra URL which corresponds to a file or a folder based on the designated + * {@link LinkHandler lh} instance. Only rudimentary URL checks are performed. + * @param linkFilename name of link-file to be created within this folder. NOTE: This name may + * be modified to ensure uniqueness within this folder. + * @param lh link-file handler used to create specific link-file (see derived implementations + * of {@link LinkHandler} and their public static INSTANCE. + * @return newly created link-file + * @throws IOException if IO error occurs during link creation + */ + public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler lh) + throws IOException; + /** * Create a subfolder within this folder. * @param folderName sub-folder name @@ -229,20 +347,21 @@ public interface DomainFolder extends Comparable { throws IOException, CancelledException; /** - * Create a new link-file in the specified newParent which will reference this folder - * (i.e., linked-folder). Restrictions: - *

      - *
    • Specified newParent must reside within a different project since internal linking is - * not currently supported.
    • - *
    - * If this folder is associated with a temporary transient project (i.e., not a locally - * managed project) the generated link will refer to the remote folder with a remote - * Ghidra URL, otherwise a local project storage path will be used. + * Copy this folder into the newParent folder as a folder-link. A folder-link references another + * folder without actually copying all of its children. If this folder is associated with a + * temporary transient project (i.e., not a locally managed project) the generated link will + * refer to the this folder with a Ghidra URL. If this folder is contained within the + * same active {@link ProjectData} instance as {@code newParent} an internal link reference + * will be made. + * * @param newParent new parent folder where link-file is to be created - * @return newly created domain file (i.e., link-file) or null if link use not supported. + * @param relative if true, and this folder is contained within the same active + * {@link ProjectData} instance as {@code newParent}, an internal-project relative path + * folder-link will be created. + * @return newly created domain file which is a folder-link (i.e., link-file). * @throws IOException if an IO or access error occurs. */ - public DomainFile copyToAsLink(DomainFolder newParent) throws IOException; + public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException; /** * Allows the framework to react to a request to make this folder the "active" one. @@ -250,7 +369,11 @@ public interface DomainFolder extends Comparable { public void setActive(); /** - * Determine if this folder corresponds to a linked-folder. + * Determine if this folder corresponds to a linked-folder which directly corresponds to a + * folder-link file. While this method is useful for identify a linked-folder root, in some + * cases it may be preferrable to simply check for instanceof {@link LinkedDomainFolder} which + * applies to the linked-folder root as well as its child sub-folders. + * * @return true if folder corresponds to a linked-folder, else false. */ public default boolean isLinked() { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolderFilter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolderFilter.java new file mode 100644 index 0000000000..987d308f42 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainFolderFilter.java @@ -0,0 +1,101 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.model; + +/** + * {@link DomainFolderFilter} interface to controls the following of linked-folders. + *

    + * Without specific overrides the default behavior: + *

      + *
    • {@link #ignoreBrokenLinks()} (true) Ignores all broken links
    • + *
    • {@link #ignoreExternalLinks()} (true) Ignore external folder-links
    • + *
    • {@link #ignoreFolderLinks()} (false) Will follow internal folder-links
    • + *
    + */ +public interface DomainFolderFilter { + + /** + * Folder filter which accepts all folders and will follow all linked folders. + * All broken links are ignored. + */ + DomainFolderFilter ALL_FOLDERS_FILTER = new DomainFolderFilter() { + + @Override + public boolean ignoreExternalLinks() { + return false; + } + }; + + /** + * File filter which allows only folders and internal folder-links. + * All external and broken links are ignored. This filter is useful when + * selecting a folder when creating/saving a file to the active project. + * If targeting a specific file content type for creation or saving use of + * {@link DefaultDomainFileFilter} may be preferred. + *

    + * It is the consumer of this filter who is responsible for following folder-links. + */ + DomainFolderFilter ALL_INTERNAL_FOLDERS_FILTER = new DomainFolderFilter() { + // Default bahaviors + }; + + /** + * Folder filter which accepts only real folders and ignores all folder-links. + * All broken links are ignored. + */ + DomainFolderFilter NON_LINKED_FOLDER_FILTER = new DomainFolderFilter() { + + @Override + public boolean ignoreFolderLinks() { + return true; + } + }; + + /** + * Check if folder-links should be ignored (includes internal and external). + * + * @return true if all folder-links should be ignored (i.e., not followed/displayed) + */ + public default boolean ignoreFolderLinks() { + return false; + } + + /** + * Check if link-files should be ignored if the link is external (i.e., Ghidra-URL). + * Multi-level internal links are followed within the same project before a determination is made. + *

    + * If this method is not implemented the default behavior will ignore external links. + * This method should be ignored for folder-links if {@link #ignoreFolderLinks()} returns true. + * + * @return true if external links should be ignored (i.e., not displayed) + */ + public default boolean ignoreExternalLinks() { + return true; + } + + /** + * Check if link-files should be ignored if the link is broken. Multi-level internal links + * are followed within the same project before a determination is made. + *

    + * If this method is not implemented the default behavior will ignore broken links. + * + * @return true if broken links should be ignored (i.e., not followed/displayed) + */ + public default boolean ignoreBrokenLinks() { + return true; + } + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkFileInfo.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkFileInfo.java new file mode 100644 index 0000000000..22c401096a --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkFileInfo.java @@ -0,0 +1,102 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.model; + +import java.io.IOException; +import java.util.function.Consumer; + +import ghidra.framework.data.*; +import ghidra.framework.data.LinkHandler.LinkStatus; +import ghidra.framework.protocol.ghidra.GhidraURL; + +/** + * {@link LinkFileInfo} provides access to link details for a {@link DomainFile} which is + * a link-file. + */ +public interface LinkFileInfo { + + /** + * {@return the file that is associated with this link information.} + */ + public DomainFile getFile(); + + /** + * Determine if the link "directly" refers to an external resource + * (i.e., URL-based {@link #getLinkPath() link path}). + *

    + * NOTE: It is important to understand that if this method returns {@code false} it + * may link to another link that is external. If the a file external status is required + * an {@link LinkStatus#EXTERNAL} status should be checked using {@link #getLinkStatus(Consumer)}. + * + * @return true if link-path is URL-based, else false + */ + public default boolean isExternalLink() { + return GhidraURL.isGhidraURL(getLinkPath()); + } + + /** + * {@return true if this file is a folder-link, else false.} + */ + public default boolean isFolderLink() { + return FolderLinkContentHandler.FOLDER_LINK_CONTENT_TYPE.equals(getFile().getContentType()); + } + + /** + * If this is a folder-link file get the corresponding linked folder. Invoking this + * method on an {@link #isExternalLink() external-link} will cause the associated + * project or repository to be opened and associated with the active project as a + * a viewed-project. The resulting folder instance will return true to the method + * {@link DomainFolder#isLinked()}. This method will recurse all internal folder-links + * which may be chained together. + * + * @return a linked domain folder or null if not a valid folder-link. + */ + public LinkedGhidraFolder getLinkedFolder(); + + /** + * Get the stored link-path. This may be either be an absolute or relative path within the + * link-file's project or a Ghidra URL. + *

    + * If you want to ensure that a project path returned is absolute and normalized, then + * {@link #getAbsoluteLinkPath()} may be used. + * + * @return associated link path + */ + public String getLinkPath(); + + /** + * Get the stored link-path as a Ghidra URL or absolute normalized link-path from a link file. + * Path normalization eliminates any path element of "./" or "../". + * A local folder-link path will always end with a "/" path separator. + * Path normalization is not performed on Ghidra URLs. + * + * @return Ghidra URL or absolute normalized link-path from a link file + * @throws IOException if linkFile has an invalid relative link-path that failed to normalize + */ + public String getAbsoluteLinkPath() throws IOException; + + /** + * Determine the link status. If a status is {@link LinkStatus#BROKEN} and an + * {@code errorConsumer} has been specified the error details will be reported. + * + * @param errorConsumer broken link error consumer (may be null) + * @return link status + */ + public default LinkStatus getLinkStatus(Consumer errorConsumer) { + return LinkHandler.getLinkFileStatus(getFile(), errorConsumer); + } + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java index dab005fc34..bfc8df833c 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFile.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,6 +23,13 @@ import java.io.IOException; */ public interface LinkedDomainFile extends DomainFile { + /** + * Get the project file pathname relative to the linked-folder root. + * NOTE: It may be a link-file path. + * @return project pathname + */ + public String getLinkedPathname(); + /** * Get the real domain file which corresponds to this file contained within a linked-folder. * @return domain file diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java index 5ea918735e..f7158040ce 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/LinkedDomainFolder.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,6 +15,7 @@ */ package ghidra.framework.model; +import java.io.FileNotFoundException; import java.io.IOException; import javax.swing.Icon; @@ -28,17 +29,51 @@ import ghidra.framework.data.FolderLinkContentHandler; public interface LinkedDomainFolder extends DomainFolder { /** - * Get the real domain folder which corresponds to this linked-folder. - * @return domain folder + * Get the project data that corresponds to the linked-project and contains the + * {@link #getLinkedPathname()} which corresponds to this folder. + * + * @return linked project data * @throws IOException if an IO error occurs */ - public DomainFolder getLinkedFolder() throws IOException; + public ProjectData getLinkedProjectData() throws IOException; + + /** + * Get the project folder/file pathname for this this linked-folder relative to the + * linked-folder root. + * + * @return project pathname + */ + public String getLinkedPathname(); + + /** + * Get the real domain folder which corresponds to this linked-folder. + * In the process of resolving the real folder a remote project or repository may be + * required. + * + * @return domain folder + * @throws FileNotFoundException if folder does not exist (could occur due to connection issue) + * @throws IOException if an IO error occurs while connecting/accessing the associated + * project or repository. + */ + public DomainFolder getRealFolder() throws IOException; /** * Get the appropriate icon for this folder + * * @param isOpen true if open icon, false for closed * @return folder icon */ public Icon getIcon(boolean isOpen); + /** + * Determine if this folder resides within an external project or repository. The + * term "external" means the actual folder does not reside within the same project + * as the folder-link that referenced it and which was used to produce this + * linked folder instance. + * + * @return true if linked-folder is external to the link file which was used to access, + * else false if internal to the same project. + */ + public boolean isExternal(); + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java index f25900cf61..c065e25bde 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/Project.java @@ -33,43 +33,42 @@ import ghidra.framework.options.SaveState; public interface Project extends AutoCloseable, Iterable { /** - * Convenience method to get the name of this project. + * {@return the name of this project} */ public String getName(); /** - * Get the project locator for this project. + * {@return the project locator for this project} */ public ProjectLocator getProjectLocator(); /** - * Returns the project manager of this project. - * @return the project manager of this project. + * {@return the project manager of this project} */ public ProjectManager getProjectManager(); /** - * Return the tool manager for this project. + * {@return the tool manager for this project} */ public ToolManager getToolManager(); /** - * Return the tool services for this project. + * {@return the tool services for this project} */ public ToolServices getToolServices(); /** - * Return whether the project configuration has changed. + * {@return whether the project configuration has changed} */ public boolean hasChanged(); /** - * Returns whether this project instance has been closed + * {@return whether this project instance has been closed} */ public boolean isClosed(); /** - * Return the local tool chest for the user logged in. + * {@return the local tool chest for the user logged in} */ public ToolChest getLocalToolChest(); @@ -83,13 +82,14 @@ public interface Project extends AutoCloseable, Iterable { /** * Add the given project URL to this project's list project views. * The project view allows users to look at data files from another - * project. + * project. If the URL corresponds to this project its ProjectData will be returned. * @param projectURL identifier for the project view (ghidra protocol only). * @param visible true if project may be made visible or false if hidden. Hidden viewed * projects are used when only life-cycle management is required (e.g., close view project * when this project is closed). * @return project data for this view - * @throws IOException if I/O error occurs or if project/repository not found + * @throws IOException if this project is closed, an invalid URL is specified, or failed to + * open/connect to project/repository. */ public ProjectData addProjectView(URL projectURL, boolean visible) throws IOException; @@ -100,7 +100,7 @@ public interface Project extends AutoCloseable, Iterable { public void removeProjectView(URL projectURL); /** - * Return the list of visible project views in this project. + * {@return the list of visible project views in this project} */ public ProjectLocator[] getProjectViews(); @@ -143,13 +143,16 @@ public interface Project extends AutoCloseable, Iterable { /** * Allows the user to store data related to the project. - * @param key A value used to store and lookup saved data + * See {@link #getSaveableData(String)} for future retieval of data. + * @param key a value used to store and lookup saved data * @param saveState a container of data that will be written out when persisted */ public void setSaveableData(String key, SaveState saveState); /** - * The analog for {@link #setSaveableData(String, SaveState)}. + * {@return the user data previously stored to the project} + * See {@link #setSaveableData(String, SaveState)}. + * @param key a value used to store and lookup saved data */ public SaveState getSaveableData(String key); @@ -160,16 +163,28 @@ public interface Project extends AutoCloseable, Iterable { public List getOpenData(); /** - * Get the root domain data folder in the project. + * {@return the root domain data folder in the project} */ public ProjectData getProjectData(); /** * Returns the Project Data for the given Project locator. The Project locator must * be either the current active project or an currently open project view. + * The returned view may not be visible. + * @param projectLocator project locator object used to open project + * @return requested project data */ public ProjectData getProjectData(ProjectLocator projectLocator); + /** + * Returns the Project Data for the given Project URL. The Project URL must + * be either the current active project or a currently open project view. + * The returned view may not be visible. + * @param projectURL identifier for the project view (ghidra protocol only). + * @return project data for this view or null + */ + public ProjectData getProjectData(URL projectURL); + /** * Get the project data for visible viewed projects that are * managed by this project. @@ -195,9 +210,14 @@ public interface Project extends AutoCloseable, Iterable { */ public void removeProjectViewListener(ProjectViewListener listener); + /** + * Return a {@link DomainFile} iterator over all non-link files within this project's data store. + * If links should be followed use an appropropriate static method from {@link ProjectDataUtils}. + * @return domain file iterator + */ @Override public default Iterator iterator() { - return new ProjectDataUtils.DomainFileIterator(this); + return getProjectData().iterator(); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java index 767820a6a3..d3fe7fc423 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectData.java @@ -30,6 +30,10 @@ import ghidra.util.task.TaskMonitor; /** * The ProjectData interface provides access to all the data files and folders * in a project. + *

    + * NOTE: Iterating over this project data instance will ignore all link-files. If links should + * be handled please instantiate {@link ProjectDataUtils#descendantFiles(DomainFolder, DomainFileFilter)} + * with a suitable {@link DomainFileFilter}. */ public interface ProjectData extends Iterable { @@ -45,12 +49,36 @@ public interface ProjectData extends Iterable { public DomainFolder getRootFolder(); /** - * Get domain folder specified by an absolute data path. - * @param path the absolute path of domain folder relative to the data folder. + * Get domain folder specified by an absolute data path. All internal folder-links will be followed. + * All path elements must refer to a valid internal non-conflicting folder or folder-link. + * Internal folder-links will be resolved to their corresponding linked-folder. + *

    + * External links are not followed. If external links should be followed the + * {@link #getFolder(String, DomainFolderFilter)} method should be used with an appropriate filter. + *

    + * NOTE: Absolute paths do not include the project name which may be shown in the project + * data tree in place of the root folder node {@code "/"}. + * + * @param path the absolute path of a domain folder within the project. * @return domain folder or null if folder not found */ public DomainFolder getFolder(String path); + /** + * Get domain folder specified by an absolute data path. If path refers to a + * non-conflicting folder-link the specified filter will determine if it should be + * followed to the linked-folder. All folder path elements must satisfy the filter restrictions. + *

    + * NOTE: Absolute paths do not include the project name which may be shown in the project + * data tree in place of the root folder node {@code "/"}. + * + * @param path the absolute path of a domain folder within the project. + * @param filter domain folder filter which constrains returned folder and following of + * folder-links. + * @return domain folder or null if folder not found + */ + public DomainFolder getFolder(String path, DomainFolderFilter filter); + /** * Get the approximate number of files contained within the project. The number * may be reduced if not connected to the shared repository. Only the newer @@ -68,15 +96,41 @@ public interface ProjectData extends Iterable { public int getFileCount(); /** - * Get domain file specified by an absolute data path. - * @param path the absolute path of domain file relative to the root folder. + * Get domain file specified by an absolute data path. All internal folder-links will be followed. + * The returned file may be a link-file and {@link DomainFile#getLinkInfo()} result and/or + * {@link DomainFile#getDomainObjectClass()} / {@link DomainFile#getContentType()} should be + * checked if needed. + *

    + * External links are not followed. If external links should be followed the + * {@link #getFile(String, DomainFileFilter)} method should be used with an appropriate filter. + *

    + * NOTE: Absolute path does not include the project name which may be shown in the project + * data tree in place of the root folder node {@code "/"}. + * + * @param path the absolute path of domain file within the project. * @return domain file or null if file not found */ public DomainFile getFile(String path); /** - * Finds all open domain files and appends - * them to the specified list. + * Get domain file specified by an absolute data path which satisfies the specified filter. + * If permitted by the filter the returned file may be a link-file. This may occur if filter + * constrains based upon {@link DomainFile#getDomainObjectClass()} instead of + * {@link DomainFile#getContentType()}. {@link DomainFile#getLinkInfo()} result can be checked + * if needed. + *

    + * NOTE: Absolute path does not include the project name which may be shown in the project + * data tree in place of the root folder node {@code "/"}. + * + * @param path the absolute path of domain file within the project. + * @param filter domain file filter which constrains returned file and following of folder-links + * and file-links. + * @return domain file or null if file not found + */ + public DomainFile getFile(String path, DomainFileFilter filter); + + /** + * Finds all open domain files and appends them to the specified list. * @param list the list to receive the open domain files */ public void findOpenFiles(List list); @@ -106,7 +160,7 @@ public interface ProjectData extends Iterable { throws IOException, CancelledException; /** - * Get domain file specified by its unique fileID. + * Get domain file specified by its unique fileID. Link following is not performed. * @param fileID domain file ID * @return domain file or null if file not found */ @@ -116,7 +170,7 @@ public interface ProjectData extends Iterable { * Transform the specified name into an acceptable folder or file item name. Only an individual folder * or file name should be specified, since any separators will be stripped-out. * NOTE: Uniqueness of name within the intended target folder is not considered. - * @param name + * @param name original name to be sanitized * @return valid name or "unknown" if no valid characters exist within name provided */ public String makeValidName(String name); @@ -225,8 +279,13 @@ public interface ProjectData extends Iterable { */ public URL getLocalProjectURL(); + /** + * Return a {@link DomainFile} iterator over all non-link files within this project data store. + * If links should be followed use an appropropriate static method from {@link ProjectDataUtils}. + * @return domain file iterator + */ @Override public default Iterator iterator() { - return new ProjectDataUtils.DomainFileIterator(getRootFolder()); + return ProjectDataUtils.descendantFiles(getRootFolder()).iterator(); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectDataUtils.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectDataUtils.java index c6d01bd97d..d6d3843140 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectDataUtils.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectDataUtils.java @@ -17,141 +17,92 @@ package ghidra.framework.model; import java.io.IOException; import java.util.*; +import java.util.concurrent.atomic.AtomicReference; -import ghidra.util.InvalidNameException; +import org.apache.commons.lang3.StringUtils; + +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.LinkHandler.LinkStatus; +import ghidra.framework.data.LinkedGhidraFolder; +import ghidra.framework.store.FileSystem; +import ghidra.util.*; public class ProjectDataUtils { - /** - * A not-thread-safe {@link DomainFile} iterator that recursively walks a - * {@link ProjectData project's data} and returns each {@code DomainFile} that is - * found. - */ - public static class DomainFileIterator implements Iterator { - - private Deque fileQueue = new LinkedList<>(); - private Deque folderQueue = new LinkedList<>(); - - /** - * Recursively traverse a {@link Project} starting in its root folder. - * - * @param project - */ - public DomainFileIterator(Project project) { - this(project.getProjectData().getRootFolder()); - } - - /** - * Recursively traverse the {@link DomainFile}s under a specific {@link DomainFolder}. - * - * @param startFolder - */ - public DomainFileIterator(DomainFolder startFolder) { - folderQueue.add(startFolder); - } - - private void queueNextFiles() { - DomainFolder folder; - while (fileQueue.isEmpty() && (folder = folderQueue.poll()) != null) { - DomainFolder[] folders = folder.getFolders(); - for (int i = folders.length - 1; i >= 0; i--) { - DomainFolder subfolder = folders[i]; - folderQueue.addFirst(subfolder); - } - for (DomainFile subfile : folder.getFiles()) { - fileQueue.addLast(subfile); - } - } - } - - @Override - public boolean hasNext() { - queueNextFiles(); - return !fileQueue.isEmpty(); - } - - @Override - public DomainFile next() { - return fileQueue.poll(); - } - } /** - * A not-thread-safe {@link DomainFolder} iterator that recursively walks a - * {@link ProjectData project's data} and returns each {@code DomainFolder} that is - * found. - */ - public static class DomainFolderIterator implements Iterator { - - private Deque folderQueue = new LinkedList<>(); - private DomainFolder nextFolder; - - /** - * Recursively traverse a {@link Project} starting in its root folder. - * - * @param project - */ - public DomainFolderIterator(Project project) { - this(project.getProjectData().getRootFolder()); - } - - /** - * Recursively traverse the {@link DomainFolder}s under a specific {@link DomainFolder}. - * - * @param startFolder - */ - public DomainFolderIterator(DomainFolder startFolder) { - folderQueue.add(startFolder); - } - - private void queueNextFiles() { - if (nextFolder == null && !folderQueue.isEmpty()) { - nextFolder = folderQueue.poll(); - DomainFolder[] folders = nextFolder.getFolders(); - for (int i = folders.length - 1; i >= 0; i--) { - DomainFolder subfolder = folders[i]; - folderQueue.addFirst(subfolder); - } - } - } - - @Override - public boolean hasNext() { - queueNextFiles(); - return nextFolder != null; - } - - @Override - public DomainFolder next() { - DomainFolder tmp = nextFolder; - nextFolder = null; - return tmp; - } - } - - /** - * Returns a {@link Iterable} sequence of all the {@link DomainFile}s that exist under - * the specified {@link DomainFolder folder}. + * Returns a {@link Iterable} of {@link DomainFile}s that exist under + * the specified {@link DomainFolder folder} including all sub-folder content. + * All folder-links and file-links will be ignored and files of all content-types will + * be returned by the iterator. + *

    + * Use {@link ProjectDataUtils#descendantFiles(DomainFolder, DomainFileFilter)} for + * finer-grained control over returned files. * - * @param folder - * @return + * @param folder domain folder + * @return domain file iterator */ public static Iterable descendantFiles(DomainFolder folder) { - return () -> new DomainFileIterator(folder); + return new DomainFileIterator(folder, DomainFileFilter.NON_LINKED_FILE_FILTER); } /** - * Returns a {@link Iterable} sequence of all the {@link DomainFolder}s that exist under - * the specified {@link DomainFolder folder}. - * @param folder - * @return + * Returns a {@link Iterable} of {@link DomainFile}s that exist under + * the specified {@link DomainFolder folder}, including all sub-folder content, + * which satisfy the specified filter restrictions. + *

    + * NOTE: Care must be taken in the presence of folder-links and file-links since such links can + * result in the same files being returned by the iterator multiple times. In + * general it is recommended that all links (see {@link DomainFile#isLink()}) be ignored + * when iterating over an entire project. When restricting content-type it is highly recommended + * that the method {@link DomainFile#getDomainObjectClass()} since both linked and non-linked + * files for the same content will specify the same {@link DomainObject} class + * (e.g., {@code Program.class}). + * + * @param folder domain folder + * @param filter the filter which determines which files should be returned by the + * iterator and what links should be followed. + * @return domain file iterator + */ + public static Iterable descendantFiles(DomainFolder folder, + DomainFileFilter filter) { + return new DomainFileIterator(folder, filter); + } + + /** + * Returns a {@link Iterable} of {@link DomainFolder}s that exist under + * the specified {@link DomainFolder folder} including all sub-folders. + * All folder-links will be ignored. + *

    + * Use {@link ProjectDataUtils#descendantFolders(DomainFolder, boolean, boolean)} if + * folder-links should be followed. + * + * @param folder domain folder + * @return domain folder iterator */ public static Iterable descendantFolders(DomainFolder folder) { - return () -> new DomainFolderIterator(folder); + return descendantFolders(folder, true, true); + } + + /** + * Returns a {@link Iterable} of {@link DomainFolder}s that exist under + * the specified {@link DomainFolder folder} including all sub-folders. + * subject to the specified folder-link restrictions. All broken folder-links encountered + * will be logged and skipped. + * + * @param folder domain folder + * @param ignoreFolderLinks true if all folder-links should be ignored + * @param ignoreExternalLinks true if all external-links should be ignored + * (ignored if ignoreFolderLinks is true) + * @return domain folder iterator + */ + public static Iterable descendantFolders(DomainFolder folder, + boolean ignoreFolderLinks, boolean ignoreExternalLinks) { + return new DomainFolderIterator(folder, ignoreFolderLinks, ignoreExternalLinks); } /** * Returns a Ghidra {@link DomainFolder} with the matching path, creating - * any missing parent folders as needed. + * any missing parent folders as needed. Broken folder-links will always be ignored. * * @param currentFolder starting {@link DomainFolder}. * @param path relative path to the desired DomainFolder, using forward slashes @@ -159,54 +110,147 @@ public class ProjectDataUtils { * trailing slashes ignored. * @return {@link DomainFolder} that the path points to. * @throws InvalidNameException if bad name - * @throws IOException if problem when creating folder + * @throws ReadOnlyException if unable to create a folder within a read-only project + * @throws IOException if problem when creating folder or a conflicting/broken folder/folder-link + * encountered. */ public static DomainFolder createDomainFolderPath(DomainFolder currentFolder, String path) throws InvalidNameException, IOException { - String[] pathElements = path.split("/"); + if (!currentFolder.isInWritableProject()) { + throw new ReadOnlyException("Folder is read-only: " + currentFolder); + } + + if (StringUtils.isBlank(path)) { + return currentFolder; + } + + DomainFolder folder = currentFolder; + + String[] pathElements = path.split(FileSystem.SEPARATOR); for (String pathElement : pathElements) { - pathElement = pathElement.trim(); + + // pathElement = pathElement.trim(); // NOTE: Seems too forgiving if (pathElement.isEmpty()) { continue; } - DomainFolder nextFolder = currentFolder.getFolder(pathElement); - if (nextFolder == null) { - // TODO: race condition between getFolder() and createFolder() - nextFolder = currentFolder.createFolder(pathElement); + + DomainFolder subFolder = folder.getFolder(pathElement); + + // Check for folder link-file + DomainFile file = folder.getFile(pathElement); + if (file != null && file.isLink()) { + LinkFileInfo linkInfo = file.getLinkInfo(); + if (linkInfo.isFolderLink()) { + if (subFolder != null) { + throw new IOException( + "Folder and folder-link name conflict encountered: " + file); + } + // May only follow non-external and non-broken folder-links + if (linkInfo.isExternalLink()) { + throw new IOException("May not follow external folder-link: " + file); + } + if (LinkHandler.getLinkFileStatus(file, null) == LinkStatus.BROKEN) { + throw new IOException("May not follow broken folder-link: " + file); + } + subFolder = linkInfo.getLinkedFolder(); + } } - currentFolder = nextFolder; + if (subFolder == null) { + subFolder = folder.createFolder(pathElement); + } + folder = subFolder; } - return currentFolder; + + return folder; + } + + /** + * Returns a Ghidra {@link DomainFolder} with the matching path within the baseFolder's + * project, or null if not found. Broken and external folder-links will be ignored. + * + * @param baseFolder Base {@link DomainFolder} for relativePath + * @param relativePath path relative to the specified DomainFolder, using forward slashes + * as separators. Empty string ok, multiple slashes in a row treated as single slash, + * leading and trailing slashes ignored. + * @return {@link DomainFolder} that the path points to or null if not found. + */ + public static DomainFolder getDomainFolder(DomainFolder baseFolder, String relativePath) { + return getDomainFolder(baseFolder, relativePath, + DomainFolderFilter.ALL_INTERNAL_FOLDERS_FILTER); } /** * Returns a Ghidra {@link DomainFolder} with the matching path, or null if not found. * - * @param currentFolder starting {@link DomainFolder}. - * @param path relative path to the desired DomainFolder, using forward slashes + * @param baseFolder Base {@link DomainFolder} for relativePath + * @param relativePath path relative to the specified DomainFolder, using forward slashes * as separators. Empty string ok, multiple slashes in a row treated as single slash, - * trailing slashes ignored. - * @return {@link DomainFolder} that the path points to or null if not found. + * leading and trailing slashes ignored. + * @param filter domain folder filter which constrains returned folder and following of + * folder-links. Broken links will always be ignored. + * @return {@link DomainFolder} that the path points to or null if not found or path contains + * a broken folder-link. */ - public static DomainFolder lookupDomainPath(DomainFolder currentFolder, String path) { + public static DomainFolder getDomainFolder(DomainFolder baseFolder, String relativePath, + DomainFolderFilter filter) { - String[] pathElements = path.split("/"); + if (StringUtils.isBlank(relativePath)) { + return baseFolder; + } + + DomainFolder folder = baseFolder; + + String[] pathElements = relativePath.split(FileSystem.SEPARATOR); for (String pathElement : pathElements) { - pathElement = pathElement.trim(); + + // pathElement = pathElement.trim(); // NOTE: Seems too forgiving if (pathElement.isEmpty()) { continue; } - currentFolder = currentFolder.getFolder(pathElement); - if (currentFolder == null) { - break; + + DomainFolder subFolder = folder.getFolder(pathElement); + + // Check for folder link-file + // NOTE: if real folder name matches folder-link-file name it will fail + // to resolve folder - either folder or link should be renamed. + DomainFile file = folder.getFile(pathElement); + if (file != null && file.isLink()) { + LinkFileInfo linkInfo = file.getLinkInfo(); + if (linkInfo.isFolderLink()) { + if (filter.ignoreFolderLinks()) { + return null; + } + if (subFolder != null) { + Msg.error(ProjectDataUtils.class, + "Folder and folder-link name conflict encountered: " + file); + return null; // conflicting folder and folder-link + } + if (linkInfo.isExternalLink() && filter.ignoreExternalLinks()) { + return null; + } + if (LinkHandler.getLinkFileStatus(file, null) == LinkStatus.BROKEN) { + Msg.warn(ProjectDataUtils.class, + "Skipping broken folder-link: " + file.getPathname()); + return null; + } + subFolder = linkInfo.getLinkedFolder(); + } } + + if (subFolder == null) { + return null; // folder path element not found + } + folder = subFolder; } - return currentFolder; + + return folder; } /** - * Returns a unique name in a Ghidra {@link DomainFolder}. + * Returns a unique folder/file name within the specified {@link DomainFolder folder}. + * The specified {@code baseName} will be used as the basis for the name returned with an + * appended number. * * @param folder {@link DomainFolder} to check for child name collisions. * @param baseName String base name of the file or folder @@ -226,4 +270,219 @@ public class ProjectDataUtils { } return null; } + + /** + * A non-thread-safe {@link DomainFile} iterator that recursively walks a + * {@link ProjectData project's data} and returns each {@code DomainFile} that is + * found. + *

    + * This iterator will never return a folder-link as a file. If a folder-link is not ignored + * its children will be processed. + */ + private static class DomainFileIterator implements Iterator, Iterable { + + private Deque fileQueue = new LinkedList<>(); + private Deque folderQueue = new LinkedList<>(); + + private DomainFileFilter filter; + + /** + * Recursively traverse the {@link DomainFile}s under a specific {@link DomainFolder}. + *

    + * NOTE: Care must be taken in the presence of folder-links and file-links since such links + * can result in the same files being returned by the iterator multiple times. In + * general it is recommended that all links (see {@link DomainFile#isLink()}) be ignored + * when iterating over an entire project. When restricting content-type it is highly recommended + * that the method {@link DomainFile#getDomainObjectClass()} since both linked and non-linked + * files for the same content will specify the same {@link DomainObject} class + * (e.g., {@code Program.class}). + * + * @param startFolder folder to start from + * @param filter the filter which determines which files should be returned by the + * iterator and what links should be followed. Following of external links is blocked + * and takes precendence over specified filter. + */ + DomainFileIterator(DomainFolder startFolder, DomainFileFilter filter) { + Objects.requireNonNull(startFolder, "folder not specified"); + Objects.requireNonNull(filter, "domain file filter not specified"); + folderQueue.add(startFolder); + this.filter = filter; + } + + private void queueNextFiles() { + DomainFolder folder; + while (fileQueue.isEmpty() && (folder = folderQueue.poll()) != null) { + DomainFolder[] folders = folder.getFolders(); + for (int i = folders.length - 1; i >= 0; i--) { + DomainFolder subfolder = folders[i]; + folderQueue.addFirst(subfolder); + } + for (DomainFile df : folder.getFiles()) { + if (df.isLink()) { + AtomicReference linkStatus = new AtomicReference<>(); + if (skipLinkFile(df, linkStatus)) { + continue; + } + if (df.getLinkInfo().isFolderLink()) { + LinkedGhidraFolder linkedFolder = + resolveFolderLink(df, linkStatus.get()); + if (linkedFolder != null) { + // queue folder for subsequent processing + folderQueue.addFirst(linkedFolder); + } + continue; + } + // A file-link may drop-through (e.g., ProgramLink) but will be + // subject to filter.accept method below. + } + if (filter.accept(df)) { + fileQueue.addLast(df); + } + } + } + } + + private LinkedGhidraFolder resolveFolderLink(DomainFile folderLinkFile, LinkStatus status) { + if (status == LinkStatus.BROKEN) { + Msg.warn(this, "Skipping broken folder-link: " + folderLinkFile.getPathname()); + return null; + } + if (status == LinkStatus.EXTERNAL && !filter.followExternallyLinkedFolders()) { + return null; + } + return folderLinkFile.getLinkInfo().getLinkedFolder(); + } + + /** + * Check linkFile against filter to see if it should be skipped. + * @param linkFile link file to be checked + * @param returnedLinkStatus if method returns false this will be updated with status + * @return true if linkFile should be skipped, else false + */ + private boolean skipLinkFile(DomainFile linkFile, + AtomicReference returnedLinkStatus) { + LinkFileInfo linkInfo = linkFile.getLinkInfo(); + boolean isFolderLink = linkInfo.isFolderLink(); + if (isFolderLink && filter.ignoreFolderLinks()) { + return true; + } + LinkStatus linkStatus = LinkHandler.getLinkFileStatus(linkFile, null); + if (linkStatus == LinkStatus.BROKEN && filter.ignoreBrokenLinks()) { + return true; + } + if (linkStatus == LinkStatus.EXTERNAL) { + return true; + } + if (linkStatus == LinkStatus.BROKEN) { + // Filter did not ignore broken link so we will simply report it and continue + Msg.warn(this, "Skipping broken link-file: " + linkFile.getPathname()); + return true; + } + returnedLinkStatus.set(linkStatus); + return false; + } + + @Override + public boolean hasNext() { + queueNextFiles(); + return !fileQueue.isEmpty(); + } + + @Override + public DomainFile next() { + return fileQueue.poll(); + } + + @Override + public Iterator iterator() { + return this; + } + } + + /** + * A non-thread-safe {@link DomainFolder} iterator that recursively walks a + * {@link ProjectData project's data} and returns each {@code DomainFolder} that is + * found. Non-broken folder-links will be followed based upon specified constraints. + */ + private static class DomainFolderIterator + implements Iterator, Iterable { + + private Deque folderQueue = new LinkedList<>(); + private DomainFolder nextFolder; + + private boolean ignoreFolderLinks; + private boolean ignoreExternalLinks; + + /** + * Recursively traverse the {@link DomainFolder}s under a specific {@link DomainFolder} + * subject to the specified folder-link restrictions. All broken folder-links encountered + * will be logged and skipped. + * + * @param startFolder domain folder + * @param ignoreFolderLinks true if all folder-links should be ignored + * @param ignoreExternalLinks true if all external-links should be ignored + * (ignored if ignoreFolderLinks is true) + */ + DomainFolderIterator(DomainFolder startFolder, boolean ignoreFolderLinks, + boolean ignoreExternalLinks) { + folderQueue.add(startFolder); + this.ignoreFolderLinks = ignoreFolderLinks; + this.ignoreExternalLinks = ignoreExternalLinks; + } + + private void queueNextFiles() { + if (nextFolder == null && !folderQueue.isEmpty()) { + nextFolder = folderQueue.poll(); + DomainFolder[] folders = nextFolder.getFolders(); + for (int i = folders.length - 1; i >= 0; i--) { + DomainFolder subfolder = folders[i]; + folderQueue.addFirst(subfolder); + } + if (!ignoreFolderLinks) { + for (DomainFile df : nextFolder.getFiles()) { + LinkedGhidraFolder linkedFolder = resolveFolderLink(df); + if (linkedFolder != null) { + // queue folder for subsequent processing + folderQueue.addFirst(linkedFolder); + } + } + } + } + + } + + private LinkedGhidraFolder resolveFolderLink(DomainFile file) { + LinkFileInfo linkInfo = file.getLinkInfo(); + if (linkInfo == null || !linkInfo.isFolderLink()) { + return null; + } + LinkStatus linkStatus = LinkHandler.getLinkFileStatus(file, null); + if (linkStatus == LinkStatus.BROKEN) { + Msg.warn(this, "Skipping broken folder-link: " + file.getPathname()); + return null; + } + if (linkStatus == LinkStatus.EXTERNAL && ignoreExternalLinks) { + return null; + } + return linkInfo.getLinkedFolder(); + } + + @Override + public boolean hasNext() { + queueNextFiles(); + return nextFolder != null; + } + + @Override + public DomainFolder next() { + DomainFolder tmp = nextFolder; + nextFolder = null; + return tmp; + } + + @Override + public Iterator iterator() { + return this; + } + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java index 227a4071f1..26668731c1 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectLocator.java @@ -59,6 +59,10 @@ public class ProjectLocator { * @throws IllegalArgumentException if an absolute path is not specified or invalid project name */ public ProjectLocator(String path, String name) { + this(path, name, null); + } + + protected ProjectLocator(String path, String name, URL url) { if (name.contains("/") || name.contains("\\")) { throw new IllegalArgumentException("name contains path separator character: " + name); } @@ -71,7 +75,7 @@ public class ProjectLocator { path = Application.getUserTempDirectory().getAbsolutePath(); } this.location = checkAbsolutePath(path); - url = GhidraURL.makeURL(location, name); + this.url = url != null ? url : GhidraURL.makeURL(location, name); } /** @@ -216,12 +220,12 @@ public class ProjectLocator { return false; } ProjectLocator projectLocator = (ProjectLocator) obj; - return name.equals(projectLocator.name) && location.equals(projectLocator.location); + return url.equals(projectLocator.getURL()); } @Override public int hashCode() { - return name.hashCode() + location.hashCode(); + return url.hashCode(); } @Override diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java index 9a65ec9ef8..584a27a1db 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ProjectViewListener.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,7 +20,7 @@ import java.net.URL; /** * {@code ProjectViewListener} provides a listener interface for tracking project views added * and removed from the associated project. - *
    + *

    * NOTE: notification callbacks are not guarenteed to occur within the swing thread. */ public interface ProjectViewListener { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolChest.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolChest.java index 25e7596ee8..859832d463 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolChest.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/ToolChest.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,71 +19,70 @@ package ghidra.framework.model; * Interface to define methods to manage tools in a central location. */ public interface ToolChest { - - - /** - * Get the tool template for the given tool name. - * @param toolName name of tool - * @return null if there is no tool template for the given - * toolName. - */ - public ToolTemplate getToolTemplate(String toolName); - /** - * Get the tool templates from the tool chest. - * @return list of tool template - */ - public ToolTemplate[] getToolTemplates(); + /** + * Get the tool template for the given tool name. + * @param toolName name of tool + * @return null if there is no tool template for the given + * toolName. + */ + public ToolTemplate getToolTemplate(String toolName); - /** - * Add a listener to be notified when the tool chest is changed. - * @param l listener to add - */ - public void addToolChestChangeListener(ToolChestChangeListener l); + /** + * Get the tool templates from the tool chest. + * @return list of tool template + */ + public ToolTemplate[] getToolTemplates(); - /** - * - * Remove a listener that is listening to when the tool chest is changed. - * @param l to remove - */ - public void removeToolChestChangeListener(ToolChestChangeListener l); + /** + * Add a listener to be notified when the tool chest is changed. + * @param l listener to add + */ + public void addToolChestChangeListener(ToolChestChangeListener l); - /** - * Add tool template to the tool chest. - *
    - * Note: If the given tool template name already exists in the project, then the name will - * be altered by appending an underscore and a one-up value. The template - * parameter's name is also updated with then new name. - *

    - * To simply replace a tool with without changing its name, call - * {@link #replaceToolTemplate(ToolTemplate)} - * - * @param template tool template to add - */ - public boolean addToolTemplate(ToolTemplate template); + /** + * + * Remove a listener that is listening to when the tool chest is changed. + * @param l to remove + */ + public void removeToolChestChangeListener(ToolChestChangeListener l); - /** - * Remove entry (toolTemplate or toolSet) from the tool chest. - * - * @param toolName name of toolConfig or toolSet to remove - * @return true if the toolConfig or toolset was - * successfully removed from the tool chest. - */ - public boolean remove(String toolName); - - /** - * Get the number of tools in this tool chest. - * @return tool count. - */ - public int getToolCount(); + /** + * Add tool template to the tool chest. + *

    + * Note: If the given tool template name already exists in the project, then the name will + * be altered by appending an underscore and a one-up value. The template + * parameter's name is also updated with then new name. + *

    + * To simply replace a tool with without changing its name, call + * {@link #replaceToolTemplate(ToolTemplate)} + * + * @param template tool template to add + */ + public boolean addToolTemplate(ToolTemplate template); - /** - * Performs the same action as calling {@link #remove(String)} and then - * {@link #addToolTemplate(ToolTemplate)}. However, calling this method prevents state from - * being lost in the transition, such as position in the tool chest and default tool status. - * - * @param template The template to add to the tool chest, replacing any tools with the same name. - * @return True if the template was added. - */ - public boolean replaceToolTemplate(ToolTemplate template); + /** + * Remove entry (toolTemplate or toolSet) from the tool chest. + * + * @param toolName name of toolConfig or toolSet to remove + * @return true if the toolConfig or toolset was + * successfully removed from the tool chest. + */ + public boolean remove(String toolName); + + /** + * Get the number of tools in this tool chest. + * @return tool count. + */ + public int getToolCount(); + + /** + * Performs the same action as calling {@link #remove(String)} and then + * {@link #addToolTemplate(ToolTemplate)}. However, calling this method prevents state from + * being lost in the transition, such as position in the tool chest and default tool status. + * + * @param template The template to add to the tool chest, replacing any tools with the same name. + * @return True if the template was added. + */ + public boolean replaceToolTemplate(ToolTemplate template); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java index 626c274921..7c3b2e36bb 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProject.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -285,6 +285,10 @@ public class DefaultProject implements Project { throw new IOException("Invalid Ghidra URL specified: " + url); } + if (url.equals(projectLocator.getURL())) { + return projectData; + } + ProjectData viewedProjectData = otherViewsMap.get(url); if (viewedProjectData == null) { viewedProjectData = openProjectView(url); @@ -298,6 +302,18 @@ public class DefaultProject implements Project { } } + @Override + public ProjectData getProjectData(URL url) { + + if (url.equals(projectLocator.getURL())) { + return projectData; + } + + synchronized (otherViewsMap) { + return otherViewsMap.get(url); + } + } + /** * Remove the view from this project. */ diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java index bb9afd9b80..655463045f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/DefaultProjectManager.java @@ -141,6 +141,7 @@ public class DefaultProjectManager implements ProjectManager { try { currentProject = new DefaultProject(this, projectLocator, resetOwner); + AppInfo.setActiveProject(currentProject); if (doRestore) { currentProject.restore(); } @@ -164,7 +165,6 @@ public class DefaultProjectManager implements ProjectManager { throw e; } finally { - AppInfo.setActiveProject(currentProject); if (currentProject == null) { File dirFile = projectLocator.getProjectDir(); if (!dirFile.exists() || !dirFile.isDirectory()) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java index d56d7db157..01e757d58c 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolServicesImpl.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -243,8 +243,8 @@ class ToolServicesImpl implements ToolServices { @Override public PluginTool launchDefaultToolWithURL(URL ghidraUrl) throws IllegalArgumentException { String contentType = getContentType(ghidraUrl); - if (contentType == null) { - return null; + if (contentType == null || ContentHandler.UNKNOWN_CONTENT.equals(contentType)) { + return null; // assume folder, non-existent, or unsupported content } ToolTemplate template = getDefaultToolTemplate(contentType); return defaultLaunch(template, t -> { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java index 11714b6f50..5a266c6bc7 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/ContentTypeQueryTask.java @@ -19,6 +19,7 @@ import java.net.URL; import ghidra.framework.data.ContentHandler; import ghidra.framework.model.DomainFile; +import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl; import ghidra.util.task.TaskMonitor; /** @@ -35,7 +36,7 @@ public class ContentTypeQueryTask extends GhidraURLQueryTask { * (see {@link GhidraURL}). */ public ContentTypeQueryTask(URL ghidraUrl) { - super("Query URL Content Type", ghidraUrl); + super("Query URL Content Type", ghidraUrl, null, LinkFileControl.NO_FOLLOW); } /** @@ -54,4 +55,5 @@ public class ContentTypeQueryTask extends GhidraURLQueryTask { public void processResult(DomainFile domainFile, URL url, TaskMonitor monitor) { contentType = domainFile.getContentType(); } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java index 53cb117531..c90c8be0f1 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURL.java @@ -250,14 +250,14 @@ public class GhidraURL { path = "/" + path; } else { - throw new IllegalArgumentException("absolute path required"); + throw new IllegalArgumentException("Absolute project path required"); } scanIndex = 3; } else if (len >= 3 && hasDriveLetter(path, 1)) { if (len < 4 || path.charAt(3) != '/') { // path such as "/c:" not permitted - throw new IllegalArgumentException("absolute path required"); + throw new IllegalArgumentException("Absolute project path required"); } scanIndex = 4; } @@ -439,7 +439,7 @@ public class GhidraURL { return "/"; } - throw new IllegalArgumentException("not project/repository URL"); + throw new IllegalArgumentException("Not a project/repository URL"); } /** @@ -569,11 +569,13 @@ public class GhidraURL { } /** - * Create a URL which refers to a local Ghidra project with optional project file and ref + * Create a URL which refers to a local Ghidra project with optional project folder/file path + * and optional reference * @param projectLocation absolute path of project location directory * @param projectName name of project - * @param projectFilePath file path (e.g., /a/b/c, may be null) - * @param ref location reference (may be null) + * @param projectFilePath an absolute folder or file path within the project (e.g., /a/b/c, may be null) + * @param ref optional location reference (may be null) which is appended to URL with a '#' + * delimiter. * @return local Ghidra project URL * @throws IllegalArgumentException if an absolute projectLocation path is not specified */ @@ -593,7 +595,7 @@ public class GhidraURL { if (!StringUtils.isBlank(projectFilePath)) { if (!projectFilePath.startsWith("/") || projectFilePath.contains("\\")) { - throw new IllegalArgumentException("Invalid project file path"); + throw new IllegalArgumentException("Absolute path required using '/' delimiter"); } buf.append("?"); buf.append(projectFilePath); @@ -611,8 +613,9 @@ public class GhidraURL { } /** - * Create a URL which refers to a local Ghidra project with optional project file and ref - * @param projectLocator local project locator + * Create a URL which refers to a Ghidra project with optional project file and ref. + * If project locator corresponds to a transient project a server URL form will be returned. + * @param projectLocator project locator (local or transient) * @param projectFilePath file path (e.g., /a/b/c, may be null) * @param ref location reference (may be null) * @return local Ghidra project URL @@ -620,6 +623,31 @@ public class GhidraURL { * instantion fails. */ public static URL makeURL(ProjectLocator projectLocator, String projectFilePath, String ref) { + + if (projectLocator.isTransient()) { + + // Transient project corresponds to server-based repository + String serverUrl = projectLocator.getURL().toExternalForm(); + if (projectFilePath != null) { + if (!projectFilePath.startsWith("/")) { + throw new IllegalArgumentException( + "Absolute path required using '/' delimiter"); + } + serverUrl += projectFilePath; + } + if (ref != null) { + serverUrl += "#"; + serverUrl += ref; + } + try { + return new URL(serverUrl); + } + catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + // Handle local project case return makeURL(projectLocator.getLocation(), projectLocator.getName(), projectFilePath, ref); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java index cd034c1063..44d5538694 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQuery.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,9 +17,14 @@ package ghidra.framework.protocol.ghidra; import java.io.IOException; import java.net.URL; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainFolder; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.LinkHandler.LinkStatus; +import ghidra.framework.data.NullFolderDomainObject; +import ghidra.framework.model.*; import ghidra.framework.protocol.ghidra.GhidraURLConnection.StatusCode; import ghidra.util.exception.CancelledException; import ghidra.util.task.Task; @@ -30,46 +35,127 @@ import ghidra.util.task.TaskMonitor; * queries for processing either a {@link DomainFile} or {@link DomainFolder} that a * Ghidra URL may reference. */ -public abstract class GhidraURLQuery { +public class GhidraURLQuery { + + /** + * {@link LinkFileControl} setting control how link-files will be followed. + */ + public enum LinkFileControl { + + /** + * No links are followed and only a single file/folder which corresponds to the URL + * will be queried. + */ + NO_FOLLOW, + + /** + * All links will be followed to arrive at an end-point + */ + + FOLLOW_EXTERNAL, + /** + * Beyond the initial URL only internal links local to the corresponding project or + * repository will be followed. + */ + FOLLOW_INTERNAL; + } + + /** + * When recuring through link-files we must keep track of URLs considered and ensure + * we don't encounter a link cycle. + */ + private static final ThreadLocal> linkedUrlSet = ThreadLocal.withInitial(() -> null); + + private final URL ghidraUrl; + private final boolean readOnly; + private final GhidraURLResultHandler resultHandler; + private final LinkFileControl linkFileControl; + + private Class contentClass; + + private boolean cleanupUrlSetUponReturn = false; + + private GhidraURLQuery(URL ghidraUrl, Class contentClass, + boolean readOnly, LinkFileControl linkFileControl, + GhidraURLResultHandler resultHandler) { + this.ghidraUrl = ghidraUrl; + this.contentClass = contentClass; + this.readOnly = readOnly; + this.resultHandler = resultHandler; + this.linkFileControl = linkFileControl; + } /** * Perform read-only query using specified GhidraURL and process result. * Both local project and remote repository URLs are supported. * This method is intended to be invoked from within a {@link Task} or for headless operations. * @param ghidraUrl local or remote Ghidra URL + * @param contentClass expected content class or null. If a folder is expected + * {@link NullFolderDomainObject} class should be specified. * @param resultHandler query result handler + * @param linkFileControl controls how or if link files will be followed * @param monitor task monitor * @throws IOException if an IO error occurs which was re-thrown by {@code resultHandler} * @throws CancelledException if task is cancelled */ - public static void queryUrl(URL ghidraUrl, GhidraURLResultHandler resultHandler, + public static void queryUrl(URL ghidraUrl, Class contentClass, + GhidraURLResultHandler resultHandler, LinkFileControl linkFileControl, TaskMonitor monitor) throws IOException, CancelledException { - doQueryUrl(ghidraUrl, true, resultHandler, monitor); + GhidraURLQuery ghidraUrlQuery = + new GhidraURLQuery(ghidraUrl, contentClass, true, linkFileControl, resultHandler); + ghidraUrlQuery.query(monitor); } /** * Perform query using specified GhidraURL and process result. * Both local project and remote repository URLs are supported. - * This method is intended to be invoked from within a {@link Task} or for headless operations. - * @param ghidraUrl local or remote Ghidra URL + * This method is intended to be invoked from within a {@link Task} or for headless operations. + * @param ghidraUrl local or remote folder-level Ghidra URL * @param readOnly allows update/commit (false) or read-only (true) access. * @param resultHandler query result handler + * @param linkFileControl controls how or if link files will be followed * @param monitor task monitor * @throws IOException if an IO error occurs which was re-thrown by {@code resultHandler} * @throws CancelledException if task is cancelled */ public static void queryRepositoryUrl(URL ghidraUrl, boolean readOnly, - GhidraURLResultHandler resultHandler, TaskMonitor monitor) - throws IOException, CancelledException { + GhidraURLResultHandler resultHandler, LinkFileControl linkFileControl, + TaskMonitor monitor) throws IOException, CancelledException { if (!GhidraURL.isServerRepositoryURL(ghidraUrl)) { throw new IllegalArgumentException("Unsupported repository URL: " + ghidraUrl); } - doQueryUrl(ghidraUrl, readOnly, resultHandler, monitor); + GhidraURLQuery ghidraUrlQuery = new GhidraURLQuery(ghidraUrl, NullFolderDomainObject.class, + readOnly, linkFileControl, resultHandler); + ghidraUrlQuery.query(monitor); } - private static void doQueryUrl(URL ghidraUrl, boolean readOnly, - GhidraURLResultHandler resultHandler, TaskMonitor monitor) - throws IOException, CancelledException { + private void query(TaskMonitor monitor) throws IOException, CancelledException { + + try { + doQuery(monitor); + } + finally { + if (cleanupUrlSetUponReturn) { + // cleanup thread local URL set + linkedUrlSet.set(null); + } + cleanupUrlSetUponReturn = false; + } + } + + private void doQuery(TaskMonitor monitor) throws IOException, CancelledException { + + URL normalizedUrl = GhidraURL.getNormalizedURL(ghidraUrl); + + Set urls = linkedUrlSet.get(); + if (urls == null) { + urls = new HashSet<>(); + linkedUrlSet.set(urls); + cleanupUrlSetUponReturn = true; + } + if (!urls.add(normalizedUrl)) { + throw new IOException("Circular link reference detected: " + ghidraUrl); + } GhidraURLConnection c; Object obj = null; @@ -133,18 +219,12 @@ public abstract class GhidraURLQuery { return; } + // NOTE: We cannot handle ambiguous folder vs folder URL. A folder-link + // may refer to another folder-link or a folder. If duplicate name exists + // a failure may occur. + monitor.checkCancelled(); - if (content instanceof DomainFile file) { - resultHandler.processResult(file, ghidraUrl, monitor); - } - else if (content instanceof DomainFolder folder) { - resultHandler.processResult(folder, ghidraUrl, monitor); - } - else { - // unexpected condition - resultHandler.handleError("Unsupported Content", - "Content class: " + content.getClass().getName(), ghidraUrl, null); - } + processContent(content, monitor); } finally { if (content != null) { @@ -154,4 +234,114 @@ public abstract class GhidraURLQuery { } } + private void processContent(Object content, TaskMonitor monitor) + throws IOException, CancelledException { + if (content instanceof DomainFile file) { + + if (!checkContentClass(file)) { + return; + } + + if (linkFileControl != LinkFileControl.NO_FOLLOW && file.isLink()) { + + // Establish content class if not specified to pickup on link inconsistencies + if (contentClass == null) { + contentClass = file.getDomainObjectClass(); + } + + // Following link may return null on error or if external link already handled + file = followLink(file, monitor); + if (file == null) { + return; + } + + LinkFileInfo linkInfo = file.getLinkInfo(); + if (linkInfo != null && linkInfo.isFolderLink()) { + // Handle folder link as folder + DomainFolder folder = linkInfo.getLinkedFolder(); + if (folder == null) { + resultHandler.handleError("Link Resolution Failed", + "Unable to follow invalid folder-link", ghidraUrl, null); + } + else { + resultHandler.processResult(folder, ghidraUrl, monitor); + } + return; + } + } + + // process file result + resultHandler.processResult(file, ghidraUrl, monitor); + } + else if (content instanceof DomainFolder folder) { + if (contentClass != null && contentClass != NullFolderDomainObject.class) { + URL url = folder.getLocalProjectURL(); + if (url == null) { + url = folder.getSharedProjectURL(); + } + resultHandler.handleError("Unexpected Content", "Unexpected folder", url, null); + } + else { + // process folder result + resultHandler.processResult(folder, ghidraUrl, monitor); + } + } + else { + // unexpected condition + resultHandler.handleError("Unsupported Content", + "Content class: " + content.getClass().getName(), ghidraUrl, null); + } + } + + private boolean checkContentClass(DomainFile file) throws IOException { + Class domainObjectClass = file.getDomainObjectClass(); + if (contentClass != null && !contentClass.isAssignableFrom(file.getDomainObjectClass())) { + URL url = file.getLocalProjectURL(null); + if (url == null) { + url = file.getSharedProjectURL(null); + } + resultHandler.handleError("Unexpected Content", + "File content is " + domainObjectClass.getSimpleName(), url, null); + return false; + } + return true; + } + + private DomainFile followLink(DomainFile file, TaskMonitor monitor) + throws CancelledException, IOException { + + AtomicReference linkStatus = new AtomicReference<>(); + AtomicReference errMsg = new AtomicReference<>(); + + // Following internal linkage will catch circular internal linkage + file = + LinkHandler.followInternalLinkage(file, s -> linkStatus.set(s), err -> errMsg.set(err)); + + LinkStatus s = linkStatus.get(); + if (s == LinkStatus.BROKEN) { + String msg = errMsg.get(); + if (msg == null) { + msg = "Unable to follow broken link"; + } + resultHandler.handleError("Link Resolution Failed", msg, ghidraUrl, null); + return null; + } + + if (s == LinkStatus.EXTERNAL) { + // file is expected to be an external link-file + if (linkFileControl == LinkFileControl.FOLLOW_EXTERNAL) { + URL linkURL = LinkHandler.getLinkURL(file); + // continue recursion with external link + queryUrl(linkURL, contentClass, resultHandler, linkFileControl, monitor); + return null; + } + + // cannot follow external link + resultHandler.externalLinkIgnored(ghidraUrl); + return null; + } + + return file; + } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java index 9bcb535399..7ad6a250e3 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLQueryTask.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,8 +19,9 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.net.URL; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainFolder; +import ghidra.framework.data.NullFolderDomainObject; +import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.task.*; @@ -42,6 +43,8 @@ import ghidra.util.task.*; public abstract class GhidraURLQueryTask extends Task implements GhidraURLResultHandler { private final URL ghidraUrl; + private final Class contentClass; + private final LinkFileControl linkFileControl; private boolean done = false; @@ -49,16 +52,22 @@ public abstract class GhidraURLQueryTask extends Task implements GhidraURLResult * Construct a Ghidra URL read-only query task. * @param title task dialog title * @param ghidraUrl Ghidra URL (local or remote) + * @param contentClass expected content class or null. If a folder is expected + * {@link NullFolderDomainObject} class should be specified. + * @param linkFileControl controls how or if link files will be followed * @throws IllegalArgumentException if specified URL is not a Ghidra URL * (see {@link GhidraURL}). */ - protected GhidraURLQueryTask(String title, URL ghidraUrl) { + protected GhidraURLQueryTask(String title, URL ghidraUrl, + Class contentClass, LinkFileControl linkFileControl) { super(title, true, false, true); if (!GhidraURL.isLocalProjectURL(ghidraUrl) && !GhidraURL.isServerRepositoryURL(ghidraUrl)) { throw new IllegalArgumentException("Unsupported URL: " + ghidraUrl); } this.ghidraUrl = ghidraUrl; + this.contentClass = contentClass; + this.linkFileControl = linkFileControl; } /** @@ -77,7 +86,7 @@ public abstract class GhidraURLQueryTask extends Task implements GhidraURLResult monitor.addCancelledListener(cancelledListener); try { - GhidraURLQuery.queryUrl(ghidraUrl, this, monitor); + GhidraURLQuery.queryUrl(ghidraUrl, contentClass, this, linkFileControl, monitor); } catch (InterruptedIOException e) { // ignore - assume cancelled diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java index 61e579318e..ebfd2abcd7 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/GhidraURLResultHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -76,4 +76,13 @@ public interface GhidraURLResultHandler { default void handleUnauthorizedAccess(URL url) throws IOException { // do nothing - assume user has already been notified or issue has been logged } + + /** + * Handle an external link URL which is not followed. + * @param url connection URL + * @throws IOException may be thrown if handler decides to propogate error + */ + default void externalLinkIgnored(URL url) throws IOException { + // do nothing + } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java index 8a435f06cb..4ddc9a5dfc 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/protocol/ghidra/TransientProjectManager.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,7 +17,6 @@ package ghidra.framework.protocol.ghidra; import java.io.File; import java.io.IOException; -import java.net.URL; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -192,15 +191,10 @@ public class TransientProjectManager { private final RepositoryInfo repositoryInfo; TransientProjectStorageLocator(String path, String name, RepositoryInfo repositoryInfo) { - super(path, name); + super(path, name, repositoryInfo.repositoryURL); this.repositoryInfo = repositoryInfo; } - @Override - public URL getURL() { - return repositoryInfo.repositoryURL; - } - @Override public String getName() { return repositoryInfo.repositoryURL.toExternalForm(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/util/VersionExceptionHandler.java b/Ghidra/Framework/Project/src/main/java/ghidra/util/VersionExceptionHandler.java index 0e58cf9abc..6b6d82064c 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/util/VersionExceptionHandler.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/util/VersionExceptionHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,6 +18,7 @@ package ghidra.util; import java.awt.Component; import docking.widgets.OptionDialog; +import ghidra.framework.data.LinkHandler; import ghidra.framework.model.DomainFile; import ghidra.util.exception.VersionException; @@ -25,10 +26,28 @@ public class VersionExceptionHandler { public static boolean isUpgradeOK(Component parent, DomainFile domainFile, String actionName, VersionException ve) { + String contentType = domainFile.getContentType(); - if (domainFile.isReadOnly() || ve.getVersionIndicator() != VersionException.OLDER_VERSION || - !ve.isUpgradable()) { - showVersionError(parent, domainFile.getName(), contentType, actionName, ve); + if (ve.getVersionIndicator() != VersionException.OLDER_VERSION || !ve.isUpgradable()) { + showVersionError(parent, domainFile.getName(), contentType, actionName, false, ve); + return false; + } + + boolean linkUsed = domainFile.isLink(); + if (linkUsed) { + DomainFile file = LinkHandler.followInternalLinkage(domainFile, s -> { + /* ignore */ }, null); + if (file.isLink() && file.getLinkInfo().isExternalLink()) { + VersionExceptionHandler.showVersionError(null, domainFile.getName(), + domainFile.getContentType(), actionName, true, ve); + return false; + } + // redirect error handling to linked file + domainFile = file; + } + + if (domainFile.isReadOnly() || !domainFile.isInWritableProject()) { + showVersionError(parent, domainFile.getName(), contentType, actionName, true, ve); return false; } String filename = domainFile.getName(); @@ -90,8 +109,20 @@ public class VersionExceptionHandler { OptionDialog.WARNING_MESSAGE); } + /** + * Show a version error in response to a content {@link VersionException}. + * @param parent popup message parent + * @param filename name of file + * @param contentType file content type + * @param actionName action name (e.g., "Open") + * @param readOnly true if read-only, else false. Specify false if not a factor to presenting + * the error. + * @param ve version exception + */ + public static void showVersionError(final Component parent, final String filename, - final String contentType, final String actionName, VersionException ve) { + final String contentType, final String actionName, boolean readOnly, + VersionException ve) { int versionIndicator = ve.getVersionIndicator(); final String versionType; @@ -111,7 +142,8 @@ public class VersionExceptionHandler { } Msg.showError(VersionExceptionHandler.class, parent, actionName + " Failed!", - "Unable to " + actionName + " " + contentType + ": " + filename + "\n \n" + - "File was created with a" + versionType + " version of Ghidra" + upgradeMsg); + "Unable to " + actionName + " " + (readOnly ? " read-only " : "") + contentType + ": " + + filename + "\n \n" + "File was created with a" + versionType + + " version of Ghidra" + upgradeMsg); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java b/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java index 6722c55c15..9f468b724f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/util/exception/BadLinkException.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -27,4 +27,8 @@ public class BadLinkException extends IOException { super(msg); } + public BadLinkException(String msg, Throwable cause) { + super(msg, cause); + } + } diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java index 9a676ed1bf..94dd9180c2 100644 --- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFile.java @@ -25,12 +25,13 @@ import javax.swing.Icon; import org.apache.commons.lang3.StringUtils; -import ghidra.framework.data.CheckinHandler; +import ghidra.framework.Application; +import ghidra.framework.data.*; import ghidra.framework.store.ItemCheckoutStatus; import ghidra.framework.store.Version; import ghidra.util.InvalidNameException; -import ghidra.util.exception.CancelledException; -import ghidra.util.exception.VersionException; +import ghidra.util.classfinder.ClassSearcher; +import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; /** @@ -38,7 +39,7 @@ import ghidra.util.task.TaskMonitor; * * @see TestDummyDomainFolder */ -public class TestDummyDomainFile implements DomainFile { +public class TestDummyDomainFile implements DomainFile, LinkFileInfo { private String name; private TestDummyDomainFolder parent; @@ -47,9 +48,47 @@ public class TestDummyDomainFile implements DomainFile { private boolean isVersioned; private boolean isInUse; + private String contentType; + private Class domainObjectClass; + + /** + * Construct test file with unknown content-type. + * + * @param parent parent folder + * @param name file name + */ public TestDummyDomainFile(TestDummyDomainFolder parent, String name) { + this(parent, name, null); + } + + /** + * Construct test file with a specified content-type. When a content-type other than + * {@link ContentHandler#UNKNOWN_CONTENT} is specified the corresponding {@link ContentHandler} + * must be available which will require the {@link ClassSearcher} to be active with + * appropriate {@link Application} initialization. + *

    + * NOTE: Support for a link-file will require a derived implementation. + * + * @param parent parent folder + * @param name file name + * @param fileContentType {@link DomainObject} content-type as specified by corresponding + * {@link ContentHandler} implementation class. + */ + public TestDummyDomainFile(TestDummyDomainFolder parent, String name, String fileContentType) { this.parent = parent; this.name = name; + contentType = fileContentType != null ? fileContentType : ContentHandler.UNKNOWN_CONTENT; + domainObjectClass = DomainObject.class; + if (!ContentHandler.UNKNOWN_CONTENT.equals(contentType)) { + try { + ContentHandler ch = DomainObjectAdapter.getContentHandler(contentType); + domainObjectClass = ch.getDomainObjectClass(); + } + catch (IOException e) { + // Ensure corresponding content-handler has been found by ClassSearcher. + throw new AssertException("Unsupported content type: " + contentType); + } + } } public void setInUse() { @@ -111,24 +150,36 @@ public class TestDummyDomainFile implements DomainFile { throw new UnsupportedOperationException(); } + ContentHandler getContentHandler() throws IOException { + if (contentType == null) { + throw new UnsupportedOperationException(); + } + return DomainObjectAdapter.getContentHandler(contentType); + } + @Override public String getContentType() { - throw new UnsupportedOperationException(); + return contentType; } @Override - public boolean isLinkFile() { - return false; + public boolean isLink() { + try { + return getContentHandler() instanceof LinkHandler; + } + catch (IOException e) { + return false; // unknown content + } } @Override - public DomainFolder followLink() { - throw new UnsupportedOperationException(); + public LinkFileInfo getLinkInfo() { + return isLink() ? this : null; } @Override public Class getDomainObjectClass() { - throw new UnsupportedOperationException(); + return domainObjectClass; } @Override @@ -347,7 +398,7 @@ public class TestDummyDomainFile implements DomainFile { } @Override - public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { + public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException { throw new UnsupportedOperationException(); } @@ -405,4 +456,28 @@ public class TestDummyDomainFile implements DomainFile { } return name; } + + // + // LinkFileInfo methods + // + + @Override + public DomainFile getFile() { + return this; + } + + @Override + public LinkedGhidraFolder getLinkedFolder() { + throw new UnsupportedOperationException(); + } + + @Override + public String getLinkPath() { + throw new UnsupportedOperationException(); + } + + @Override + public String getAbsoluteLinkPath() throws IOException { + throw new UnsupportedOperationException(); + } } diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java index 82052b474e..dadf407eea 100644 --- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyDomainFolder.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,6 +23,7 @@ import java.util.List; import org.apache.commons.lang3.StringUtils; +import ghidra.framework.data.*; import ghidra.framework.store.FolderNotEmptyException; import ghidra.util.InvalidNameException; import ghidra.util.exception.CancelledException; @@ -54,6 +55,16 @@ public class TestDummyDomainFolder implements DomainFolder { throw new UnsupportedOperationException(); } + @Override + public boolean isSame(DomainFolder folder) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSameOrAncestor(DomainFolder folder) { + throw new UnsupportedOperationException(); + } + @Override public synchronized String getName() { return folderName; @@ -95,7 +106,7 @@ public class TestDummyDomainFolder implements DomainFolder { @Override public boolean isInWritableProject() { - throw new UnsupportedOperationException(); + return parent != null ? parent.isInWritableProject() : false; } @Override @@ -131,7 +142,14 @@ public class TestDummyDomainFolder implements DomainFolder { @Override public synchronized DomainFile createFile(String name, DomainObject obj, TaskMonitor monitor) throws InvalidNameException, IOException, CancelledException { - DomainFile file = new TestDummyDomainFile(this, name); + + String contentType = ContentHandler.UNKNOWN_CONTENT; + if (obj != null) { + ContentHandler ch = DomainObjectAdapter.getContentHandler(obj); + contentType = ch.getContentType(); + } + + DomainFile file = new TestDummyDomainFile(this, name, contentType); files.add(file); return file; } @@ -143,9 +161,20 @@ public class TestDummyDomainFolder implements DomainFolder { } @Override - public synchronized DomainFolder createFolder(String name) - throws InvalidNameException, IOException { - DomainFolder folder = new TestDummyDomainFolder(this, name); + public DomainFile createLinkFile(ProjectData sourceProjectData, String pathname, + boolean makeRelative, String linkFilename, LinkHandler lh) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public DomainFile createLinkFile(String ghidraUrl, String linkFilename, LinkHandler lh) + throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized TestDummyDomainFolder createFolder(String name) { + TestDummyDomainFolder folder = new TestDummyDomainFolder(this, name); subFolders.add(folder); return folder; } @@ -174,7 +203,7 @@ public class TestDummyDomainFolder implements DomainFolder { } @Override - public DomainFile copyToAsLink(DomainFolder newParent) throws IOException { + public DomainFile copyToAsLink(DomainFolder newParent, boolean relative) throws IOException { throw new UnsupportedOperationException(); } diff --git a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java index 32921a18bd..5ff2a78e7a 100644 --- a/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java +++ b/Ghidra/Framework/Project/src/test/java/ghidra/framework/model/TestDummyProjectData.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -42,6 +42,12 @@ public class TestDummyProjectData implements ProjectData { @Override public DomainFolder getFolder(String path) { + // stub + return getFolder(path, DomainFolderFilter.ALL_INTERNAL_FOLDERS_FILTER); + } + + @Override + public DomainFolder getFolder(String path, DomainFolderFilter filter) { // stub return null; } @@ -54,6 +60,12 @@ public class TestDummyProjectData implements ProjectData { @Override public DomainFile getFile(String path) { + // stub + return getFile(path, DomainFileFilter.ALL_INTERNAL_FILES_FILTER); + } + + @Override + public DomainFile getFile(String path, DomainFileFilter filter) { // stub return null; } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java index 7c64fdf1ac..bebd6b64f0 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/DataTypeArchiveLinkContentHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,32 +15,16 @@ */ package ghidra.program.database; -import java.io.IOException; - import javax.swing.Icon; import ghidra.framework.data.LinkHandler; -import ghidra.framework.data.URLLinkObject; -import ghidra.framework.model.DomainObject; -import ghidra.framework.store.FileSystem; -import ghidra.util.InvalidNameException; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; public class DataTypeArchiveLinkContentHandler extends LinkHandler { - public static final String ARCHIVE_LINK_CONTENT_TYPE = "ArchiveLink"; + public static DataTypeArchiveLinkContentHandler INSTANCE = + new DataTypeArchiveLinkContentHandler(); - @Override - public long createFile(FileSystem fs, FileSystem userfs, String path, String name, - DomainObject obj, TaskMonitor monitor) - throws IOException, InvalidNameException, CancelledException { - if (!(obj instanceof URLLinkObject)) { - throw new IOException("Unsupported domain object: " + obj.getClass().getName()); - } - return createFile((URLLinkObject) obj, ARCHIVE_LINK_CONTENT_TYPE, fs, path, name, - monitor); - } + public static final String ARCHIVE_LINK_CONTENT_TYPE = "ArchiveLink"; @Override public String getContentType() { diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java index 09d02a94c1..a5f7ee1e3c 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramLinkContentHandler.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,32 +15,15 @@ */ package ghidra.program.database; -import java.io.IOException; - import javax.swing.Icon; import ghidra.framework.data.LinkHandler; -import ghidra.framework.data.URLLinkObject; -import ghidra.framework.model.DomainObject; -import ghidra.framework.store.FileSystem; -import ghidra.util.InvalidNameException; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; public class ProgramLinkContentHandler extends LinkHandler { - public static final String PROGRAM_LINK_CONTENT_TYPE = "ProgramLink"; + public static ProgramLinkContentHandler INSTANCE = new ProgramLinkContentHandler(); - @Override - public long createFile(FileSystem fs, FileSystem userfs, String path, String name, - DomainObject obj, TaskMonitor monitor) - throws IOException, InvalidNameException, CancelledException { - if (!(obj instanceof URLLinkObject)) { - throw new IOException("Unsupported domain object: " + obj.getClass().getName()); - } - return createFile((URLLinkObject) obj, PROGRAM_LINK_CONTENT_TYPE, fs, path, name, - monitor); - } + public static final String PROGRAM_LINK_CONTENT_TYPE = "ProgramLink"; @Override public String getContentType() { diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/CodeManager.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/CodeManager.java index 1055d9d138..e63f4ce315 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/CodeManager.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/CodeManager.java @@ -1160,7 +1160,7 @@ public class CodeManager implements ErrorHandler, ManagerDB { * Get an iterator that contains the code units which have the specified property type defined. * Only code units starting within the address set specified will be returned by the iterator. * If the address set is null then check the entire program. - *
    + *

    * Standard property types are defined in the CodeUnit class. The property types are: *

      *
    • REFERENCE_PROPERTY
    • diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ExporterPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ExporterPluginScreenShots.java index 3326e43179..f7994f80c7 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ExporterPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ExporterPluginScreenShots.java @@ -15,6 +15,7 @@ */ package help.screenshot; +import java.awt.Dialog; import java.io.IOException; import javax.swing.JComboBox; @@ -27,6 +28,7 @@ import ghidra.app.util.exporter.Exporter; import ghidra.framework.model.*; import ghidra.framework.preferences.Preferences; import ghidra.program.model.listing.Program; +import ghidra.util.Swing; import ghidra.util.exception.CancelledException; import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; @@ -40,10 +42,15 @@ public class ExporterPluginScreenShots extends GhidraScreenShotGenerator { Preferences.setProperty(Preferences.LAST_EXPORT_DIRECTORY, "/path"); DomainFile df = createDomainFile(); - ExporterDialog dialog = new ExporterDialog(tool, df); - runSwing(() -> tool.showDialog(dialog), false); + + runSwing(() -> ExporterDialog.show(tool, df), false); + + Dialog dialog = waitForJDialog("Export Program_A"); + waitForSwing(); captureDialog(dialog); + + Swing.runNow(() -> dialog.dispose()); } @Test diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java index b960285efc..6fe25c5e00 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java @@ -37,8 +37,7 @@ import docking.wizard.WizardDialog; import generic.theme.GThemeDefaults.Colors; import ghidra.app.plugin.core.archive.RestoreDialog; import ghidra.framework.Application; -import ghidra.framework.data.DefaultProjectData; -import ghidra.framework.data.GhidraFileData; +import ghidra.framework.data.*; import ghidra.framework.main.*; import ghidra.framework.main.wizard.project.*; import ghidra.framework.model.*; @@ -56,10 +55,14 @@ import ghidra.util.exception.CancelledException; import ghidra.util.extensions.ExtensionDetails; import ghidra.util.task.TaskMonitor; import resources.MultiIcon; +import resources.icons.TranslateIcon; public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator { + + private static final String RIGHT_ARROW = "\u2b95"; private static final String OTHER_PROJECT = "Other_Project"; - Icon icon = (Icon) getInstanceField("CONVERT_ICON", ProjectChooseRepositoryWizardModel.class); + private Icon icon = + (Icon) getInstanceField("CONVERT_ICON", ProjectChooseRepositoryWizardModel.class); public FrontEndPluginScreenShots() { super(); @@ -139,8 +142,7 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator { TestDummyWizardModel panelMgr = new TestDummyWizardModel(panel, false, true, false, - "Change Shared Project Information", 600, 375, - new ProjectWizardData(), icon); + "Change Shared Project Information", 600, 375, new ProjectWizardData(), icon); WizardDialog wizard = new WizardDialog(panelMgr, false); wizard.show(); @@ -156,9 +158,8 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator { ProjectWizardData data = new ProjectWizardData(); data.setServerInfo(new ServerInfo("server1", 13100)); - TestDummyWizardModel panelMgr = - new TestDummyWizardModel<>(panel, false, true, false, - "Change Shared Project Information", 600, 180, data, icon); + TestDummyWizardModel panelMgr = new TestDummyWizardModel<>(panel, false, + true, false, "Change Shared Project Information", 600, 180, data, icon); WizardDialog wizard = new WizardDialog(panelMgr, false); wizard.show(); @@ -326,6 +327,40 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator { captureIconAndText(multiIcon, "Example"); } + @Test + public void testAbsoluteFileLinkIcon() { + Icon programIcon = ProgramContentHandler.PROGRAM_ICON; + MultiIcon multiIcon = new MultiIcon(programIcon); + multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1)); + captureIconAndText(multiIcon, "Example " + RIGHT_ARROW + " /data/Example"); + } + + @Test + public void testAbsoluteBrokenFileLinkIcon() { + Icon programIcon = ProgramContentHandler.PROGRAM_ICON; + MultiIcon multiIcon = new MultiIcon(programIcon); + multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1)); + Icon linkIcon = new BrokenLinkIcon(multiIcon); + captureIconAndText(linkIcon, "Example " + RIGHT_ARROW + " /data/Example"); + } + + @Test + public void testAbsoluteFolderLinkIcon() { + Icon folderIcon = DomainFolder.CLOSED_FOLDER_ICON; + MultiIcon multiIcon = new MultiIcon(folderIcon); + multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1)); + captureIconAndText(multiIcon, "Example " + RIGHT_ARROW + " /data/Example"); + } + + @Test + public void testAbsoluteBrokenFolderLinkIcon() { + Icon folderIcon = DomainFolder.CLOSED_FOLDER_ICON; + MultiIcon multiIcon = new MultiIcon(folderIcon); + multiIcon.addIcon(new TranslateIcon(LinkHandler.LINK_ICON, 0, 1)); + Icon linkIcon = new BrokenLinkIcon(multiIcon); + captureIconAndText(linkIcon, "Example " + RIGHT_ARROW + " /data/Example"); + } + @Test public void testProjectDataTable() throws CancelledException, IOException, InvalidNameException { @@ -337,8 +372,7 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator { FrontEndPlugin plugin = getPlugin(tool, FrontEndPlugin.class); JComponent projectDataPanel = (JComponent) getInstanceField("projectDataPanel", plugin); - JTabbedPane tabbedPane = - (JTabbedPane) getInstanceField("projectTab", projectDataPanel); + JTabbedPane tabbedPane = (JTabbedPane) getInstanceField("projectTab", projectDataPanel); tabbedPane.setSelectedIndex(1); setToolSize(800, 600); captureComponent(projectDataPanel); @@ -407,8 +441,7 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator { TestDummyWizardModel panelMgr = new TestDummyWizardModel(panel, false, true, false, - "Specify Repository Name on Server1", 600, 375, - new ProjectWizardData(), icon); + "Specify Repository Name on Server1", 600, 375, new ProjectWizardData(), icon); WizardDialog wizard = new WizardDialog(panelMgr, false); @@ -694,13 +727,12 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator { ProjectTestUtils.deleteProject(TEMP_DIR, OTHER_PROJECT); Project otherProject = ProjectTestUtils.getProject(TEMP_DIR, OTHER_PROJECT); Language language = getZ80_LANGUAGE(); - DomainFile otherFile = - ProjectTestUtils.createProgramFile(otherProject, "Program1", language, - language.getDefaultCompilerSpec(), null); + DomainFile otherFile = ProjectTestUtils.createProgramFile(otherProject, "Program1", + language, language.getDefaultCompilerSpec(), null); ProjectTestUtils.createProgramFile(otherProject, "Program2", language, language.getDefaultCompilerSpec(), null); - otherFile.copyToAsLink(projectData.getRootFolder()); + otherFile.copyToAsLink(projectData.getRootFolder(), false); otherProject.close(); diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginOpenProgramActionsTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginOpenProgramActionsTest.java new file mode 100644 index 0000000000..a6053e3a06 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/FrontEndPluginOpenProgramActionsTest.java @@ -0,0 +1,411 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.main.datatree; + +import static org.junit.Assert.*; + +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; +import javax.swing.tree.TreePath; + +import org.junit.*; + +import docking.ActionContext; +import docking.ComponentProvider; +import docking.action.DockingActionIf; +import docking.test.AbstractDockingTest; +import docking.tool.ToolConstants; +import docking.widgets.tree.GTreeNode; +import ghidra.framework.main.FrontEndTool; +import ghidra.framework.main.datatable.ProjectDataContext; +import ghidra.framework.model.*; +import ghidra.framework.options.ToolOptions; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.test.*; +import ghidra.util.task.TaskMonitor; + +/** + * Tests for opening files. + */ +public class FrontEndPluginOpenProgramActionsTest extends AbstractGhidraHeadedIntegrationTest { + + private FrontEndTool frontEndTool; + private TestEnv env; + private DataTree tree; + private DomainFolder rootFolder; + private GTreeNode rootNode; + + @Before + public void setUp() throws Exception { + env = new TestEnv(); + env.resetDefaultTools(); + + frontEndTool = env.getFrontEndTool(); + env.showFrontEndTool(); + setErrorGUIEnabled(false); + + tree = findComponent(frontEndTool.getToolFrame(), DataTree.class); + rootFolder = env.getProject().getProjectData().getRootFolder(); + Program p = ToyProgramBuilder.buildSimpleProgram("sample", this); + rootFolder.createFile("sample", p, TaskMonitor.DUMMY); + p.release(this); + + p = ToyProgramBuilder.buildSimpleProgram("x", this); + rootFolder.createFile("X", p, TaskMonitor.DUMMY); + p.release(this); + + rootNode = tree.getViewRoot(); + + waitForSwing(); + tree.expandPath(rootNode.getTreePath()); + waitForTree(); + } + + @After + public void tearDown() throws Exception { + waitForTree(); + env.dispose(); + } + + @Test + public void testOpenActionsEnabled() throws Exception { + setSelectionPath(rootNode.getTreePath()); + DockingActionIf openAction = getAction("Open File"); + assertTrue(!openAction.isEnabledForContext(getDomainFileActionContext(rootNode))); + + ToolChest tc = env.getProject().getLocalToolChest(); + ToolTemplate[] configs = tc.getToolTemplates(); + for (ToolTemplate config : configs) { + DockingActionIf action = getAction("Open" + config.getName()); + assertTrue(!action.isEnabledForContext(getDomainFileActionContext(rootNode))); + assertTrue(!openAction.isEnabledForContext(getDomainFileActionContext(rootNode))); + } + } + + @Test + public void testOpenInDefaultTool() throws Exception { + //Open File + GTreeNode npNode = rootNode.getChild("sample"); + setSelectionPath(npNode.getTreePath()); + waitForTree(); + DockingActionIf openAction = getAction("Open File"); + performAction(openAction, getFrontEndContext(), true); + verifyToolExistsAndCloseTool(); + } + + @Test + public void testOpenInDefaultToolMultipleNewTool() throws Exception { + + ToolOptions options = frontEndTool.getOptions(ToolConstants.TOOL_OPTIONS); + options.setEnum(FrontEndTool.DEFAULT_TOOL_LAUNCH_MODE, DefaultLaunchMode.NEW_TOOL); + + //Open 1st File + DomainFile sampleDf = openInDefaultTool("sample"); + PluginTool[] runningTools = env.getProject().getToolManager().getRunningTools(); + assertEquals(1, runningTools.length); + assertOpenFiles(runningTools[0], sampleDf); + + //Open 2nd File in new tool + DomainFile xDf = openInDefaultTool("X"); + + // NOTE: runningTools order may vary + runningTools = env.getProject().getToolManager().getRunningTools(); + assertEquals(2, runningTools.length); + DomainFile[] domainFiles0 = runningTools[0].getDomainFiles(); + assertEquals(1, domainFiles0.length); + DomainFile[] domainFiles1 = runningTools[1].getDomainFiles(); + assertEquals(1, domainFiles1.length); + if (sampleDf.equals(domainFiles0[0])) { + assertEquals(xDf, domainFiles1[0]); + } + else if (sampleDf.equals(domainFiles1[0])) { + assertEquals(xDf, domainFiles0[0]); + } + else { + fail("Unexpected open domain files"); + } + + exitTools(runningTools); + } + + @Test + public void testOpenInDefaultToolMultipleReuseTool() throws Exception { + + ToolOptions options = frontEndTool.getOptions(ToolConstants.TOOL_OPTIONS); + options.setEnum(FrontEndTool.DEFAULT_TOOL_LAUNCH_MODE, DefaultLaunchMode.REUSE_TOOL); + + //Open 1st File + DomainFile sampleDf = openInDefaultTool("sample"); + PluginTool[] runningTools = env.getProject().getToolManager().getRunningTools(); + assertEquals(1, runningTools.length); + assertOpenFiles(runningTools[0], sampleDf); + + //Open 2nd File in same tool + DomainFile xDf = openInDefaultTool("X"); + runningTools = env.getProject().getToolManager().getRunningTools(); + assertEquals(1, runningTools.length); + assertOpenFiles(runningTools[0], sampleDf, xDf); + + exitTools(runningTools); + } + + @Test + public void testOpenMultipleNewTool() throws Exception { + + Program p = ToyProgramBuilder.buildSimpleProgram("y", this); + rootFolder.createFile("Y", p, TaskMonitor.DUMMY); + p.release(this); + + ToolOptions options = frontEndTool.getOptions(ToolConstants.TOOL_OPTIONS); + options.setEnum(FrontEndTool.DEFAULT_TOOL_LAUNCH_MODE, DefaultLaunchMode.NEW_TOOL); + + //Open 1st File + DomainFile sampleDf = openInDefaultTool("sample"); + PluginTool[] runningTools = env.getProject().getToolManager().getRunningTools(); + assertEquals(1, runningTools.length); + assertOpenFiles(runningTools[0], sampleDf); + + String toolName = runningTools[0].getName(); + + //Open two additional files in new tool + openInTool(toolName, "X", "Y"); + + // NOTE: runningTools order may vary + runningTools = env.getProject().getToolManager().getRunningTools(); + assertEquals(2, runningTools.length); + + DomainFile[] domainFiles0 = runningTools[0].getDomainFiles(); + DomainFile[] domainFiles1 = runningTools[1].getDomainFiles(); + assertEquals(3, domainFiles0.length + domainFiles1.length); + + exitTools(runningTools); + } + + @Test + public void testOpenMultipleReuseTool() throws Exception { + + Program p = ToyProgramBuilder.buildSimpleProgram("y", this); + rootFolder.createFile("Y", p, TaskMonitor.DUMMY); + p.release(this); + + ToolOptions options = frontEndTool.getOptions(ToolConstants.TOOL_OPTIONS); + options.setEnum(FrontEndTool.DEFAULT_TOOL_LAUNCH_MODE, DefaultLaunchMode.REUSE_TOOL); + + //Open 1st File + DomainFile sampleDf = openInDefaultTool("sample"); + PluginTool[] runningTools = env.getProject().getToolManager().getRunningTools(); + assertEquals(1, runningTools.length); + assertOpenFiles(runningTools[0], sampleDf); + + String toolName = runningTools[0].getName(); + + //Open two additional files in same tool + openInTool(toolName, "X", "Y"); + + runningTools = env.getProject().getToolManager().getRunningTools(); + assertEquals(1, runningTools.length); + + DomainFile[] domainFiles0 = runningTools[0].getDomainFiles(); + assertEquals(3, domainFiles0.length); + + exitTools(runningTools); + } + + @Test + public void testOpenWith() throws Exception { + + GTreeNode npNode = rootNode.getChild("sample"); + setSelectionPath(npNode.getTreePath()); + waitForTree(); + + ToolChest tc = env.getProject().getLocalToolChest(); + ToolTemplate[] configs = tc.getToolTemplates(); + + DockingActionIf action = getAction("Open" + configs[0].getName()); + performAction(action, getFrontEndContext(), true); + verifyToolExistsAndCloseTool(); + } + + @Test + public void testOpenWithDoubleClick() throws Exception { + // make sure that the Code Browser tool is the default + ToolChest tc = env.getProject().getLocalToolChest(); + ToolTemplate[] configs = tc.getToolTemplates(); + ToolTemplate codeBrowserConfig = null; + for (ToolTemplate config : configs) { + if ("CodeBrowser".equals(config.getName())) { + codeBrowserConfig = config; + } + } + + if (codeBrowserConfig == null) { + Assert.fail("Unable to find the Code Browser config file."); + } + + // double click on the program node + GTreeNode npNode = rootNode.getChild("sample"); + JTree jTree = (JTree) invokeInstanceMethod("getJTree", tree); + Rectangle rect = jTree.getPathBounds(npNode.getTreePath()); + setSelectionPath(npNode.getTreePath()); + waitForTree(); + + clickMouse(jTree, MouseEvent.BUTTON1, rect.x, rect.y, 2, 0); + + // make sure that the tool is loaded and processes all of the tasks it launches + Window window = waitForToolLaunch(); + + // DEBUG: + if (window == null) { + // see if any tools have been launched + PluginTool[] runningTools = frontEndTool.getToolServices().getRunningTools(); + for (PluginTool tool : runningTools) { + System.err.println("\t\"" + tool.getName() + "\""); + JFrame toolFrame = tool.getToolFrame(); + System.err.println("\t\twith window: " + toolFrame.getTitle()); + } + + System.err.println("Open Windows: "); + System.err.println(getOpenWindowsAsString()); + } + + assertNotNull(window); + waitForBusyTool(env.getProject().getToolManager().getRunningTools()[0]); + waitForTasks(); + + verifyToolExistsAndCloseTool(); + } + + private Window waitForToolLaunch() { + + waitForSwing(); + + long start = System.currentTimeMillis(); + int tryCount = 0; + Window window = null; + while (window == null && tryCount < 5) { + ++tryCount; + window = waitForValueWithoutFailing(() -> { + return getWindowByTitleContaining(null, + "CodeBrowser: " + PROJECT_NAME + ":/sample"); + }); + } + + long total = System.currentTimeMillis() - start; + assertNotNull("Timed-out waiting for tool - " + total + " ms", window); + return window; + } + +//================================================================================================== +// Private Methods +//================================================================================================== + + private ActionContext getFrontEndContext() { + ComponentProvider provider = env.getFrontEndProvider(); + return runSwing(() -> provider.getActionContext(null)); + } + + private DomainFile openInDefaultTool(String fileName) throws Exception { + GTreeNode node = rootNode.getChild(fileName); + assertTrue("Expected domain file node", node instanceof DomainFileNode); + DomainFileNode fileNode = (DomainFileNode) node; + DomainFile domainFile = fileNode.getDomainFile(); + setSelectionPath(fileNode.getTreePath()); + waitForTree(); + DockingActionIf openAction = getAction("Open File"); + performAction(openAction, getFrontEndContext(), true); + return domainFile; + } + + private List openInTool(String toolName, String... fileNames) throws Exception { + + ToolServices toolServices = env.getProject().getToolServices(); + + ArrayList domainFiles = new ArrayList<>(); + for (String fileName : fileNames) { + DomainFile df = rootFolder.getFile(fileName); + assertNotNull(df); + domainFiles.add(df); + } + + runSwing(() -> toolServices.launchTool(toolName, domainFiles)); + waitForSwing(); + return domainFiles; + } + + private void assertOpenFiles(PluginTool tool, DomainFile... expectedDomainFiles) { + DomainFile[] domainFiles = tool.getDomainFiles(); + assertArrayEquals(expectedDomainFiles, domainFiles); + } + + private void exitTools(PluginTool... tools) { + runSwing(() -> { + for (PluginTool t : tools) { + t.close(); + } + }); + } + + private void verifyToolExistsAndCloseTool() { + PluginTool[] runningTools = env.getProject().getToolManager().getRunningTools(); + assertEquals(1, runningTools.length); + exitTools(runningTools[0]); + } + + private ActionContext getDomainFileActionContext(GTreeNode... nodes) { + List fileList = new ArrayList<>(); + List folderList = new ArrayList<>(); + for (GTreeNode node : nodes) { + if (node instanceof DomainFileNode fileNode) { + fileList.add(fileNode.getDomainFile()); + } + else if (node instanceof DomainFolderNode folderNode) { + folderList.add(folderNode.getDomainFolder()); + } + } + + return new ProjectDataContext(null, null, nodes[0], folderList, fileList, tree, true); + + } + + private DockingActionIf getAction(String actionName) { + DockingActionIf action = + AbstractDockingTest.getAction(frontEndTool, "FrontEndPlugin", actionName); + return action; + } + + private void setSelectionPath(final TreePath path) throws Exception { + SwingUtilities.invokeAndWait(() -> tree.setSelectionPath(path)); + } + + private void waitForTree() { + waitForSwing(); + while (tree.isBusy()) { + try { + Thread.sleep(10); + } + catch (InterruptedException e) { + // don't care + } + } + waitForSwing(); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteFromRepositoryTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteFromRepositoryTest.java new file mode 100644 index 0000000000..6cd77609b6 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteFromRepositoryTest.java @@ -0,0 +1,674 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.main.datatree; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import org.junit.*; + +import docking.ActionContext; +import docking.action.DockingActionIf; +import ghidra.framework.client.ClientUtil; +import ghidra.framework.data.FolderLinkContentHandler; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.LinkHandler.LinkStatus; +import ghidra.framework.model.*; +import ghidra.framework.protocol.ghidra.*; +import ghidra.framework.protocol.ghidra.GhidraURLQuery.LinkFileControl; +import ghidra.program.database.ProgramDB; +import ghidra.program.database.ProgramLinkContentHandler; +import ghidra.server.remote.ServerTestUtil; +import ghidra.test.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +public class ProjectCopyPasteFromRepositoryTest extends AbstractGhidraHeadedIntegrationTest { + + private String testDirPath; + private File serverRoot; + private URL viewURL; + + private FrontEndTestEnv env; + + @Before + public void setUp() throws Exception { + testDirPath = getTestDirectoryPath(); + + env = new FrontEndTestEnv(); + + startServer(); + } + + @After + public void tearDown() throws Exception { + + env.dispose(); + + killServer(); + + ClientUtil.clearRepositoryAdapter("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT); + } + + private void killServer() { + + if (serverRoot == null) { + return; + } + + ServerTestUtil.disposeServer(); + + FileUtilities.deleteDir(serverRoot); + } + + private void startServer() throws Exception { + + // Authorized user "test" is predefined within TestServer.zip + ServerTestUtil.setLocalUser("test"); + + // Create server instance + serverRoot = new File(testDirPath, "TestServer"); + + ServerTestUtil.createPopulatedTestServer(serverRoot.getAbsolutePath()); + + ServerTestUtil.startServer(serverRoot.getAbsolutePath(), + ServerTestUtil.GHIDRA_TEST_SERVER_PORT, -1, false, false, false); + + viewURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT, "Test"); + + addLinkedServerContent(); + } + + private void addRepoView() throws IOException { + + Project project = env.getFrontEndTool().getProject(); + ProjectData projectData = project.addProjectView(viewURL, true); + assertNotNull(projectData); + assertEquals(viewURL, projectData.getProjectLocator().getURL()); + + // validate the view was added to project + ProjectLocator[] projViews = project.getProjectViews(); + assertEquals(1, projViews.length); + } + + private void addLinkedServerContent() throws Exception { + + /** + * Initial server files: + * /foo + * /notepad + * /f1/bash + */ + + GhidraURLQuery.queryRepositoryUrl(viewURL, false, new GhidraURLResultHandlerAdapter() { + @Override + public void processResult(DomainFolder serverRootFolder, URL url, TaskMonitor monitor) + throws IOException, CancelledException { + + // + // Add folder link: /f1Link -> f1 + // + DomainFile linkFile = + serverRootFolder.createLinkFile(serverRootFolder.getProjectData(), "/f1", true, + "f1Link", FolderLinkContentHandler.INSTANCE); + assertNotNull(linkFile); + assertTrue(linkFile.isLink() && linkFile.getLinkInfo().isFolderLink()); + assertEquals("f1", linkFile.getLinkInfo().getLinkPath()); + linkFile.addToVersionControl("Add Folder Link", false, monitor); + + // + // Add file link: /bashLink -> f1/bash + // + linkFile = serverRootFolder.createLinkFile(serverRootFolder.getProjectData(), + "/f1/bash", true, "bashLink", ProgramLinkContentHandler.INSTANCE); + assertNotNull(linkFile); + assertTrue(linkFile.isLink() && !linkFile.getLinkInfo().isFolderLink()); + assertEquals("f1/bash", linkFile.getLinkInfo().getLinkPath()); + linkFile.addToVersionControl("Add File Link", false, monitor); + } + }, LinkFileControl.NO_FOLLOW, TaskMonitor.DUMMY); + + } + + @Test + public void testCopyPasteExternalFile() throws Exception { + + env.getRootFolder().createFolder("xyz"); + + addRepoView(); + + env.waitForTree(); + + // + // Select foo file from viewed repository and Copy + // + DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm()); + assertNotNull("repo data tree view not found", viewTreeHelper); + + DomainFileNode fooFile = viewTreeHelper.waitForFileNode("/foo"); + + final ActionContext copyActionContext = viewTreeHelper.getDomainFileActionContext(fooFile); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + // + // Select xyz folder and perform Paste + // + DomainFolderNode xyzNode = env.waitForFolderNode("/xyz"); + + final ActionContext pasteActionContext = env.getDomainFileActionContext(xyzNode); + + DockingActionIf pasteAction = getAction(env.getFrontEndTool(), "Paste"); + assertNotNull("Paste action not found", pasteAction); + + assertTrue(pasteAction.isAddToPopup(pasteActionContext)); + assertTrue(pasteAction.isEnabledForContext(pasteActionContext)); + runSwing(() -> pasteAction.actionPerformed(pasteActionContext)); + + // + // Verify paste of external file from repository to active project + // + DomainFileNode fooCopyNode = env.waitForFileNode("/xyz/foo"); + DomainFile file = fooCopyNode.getDomainFile(); + assertTrue(file.exists()); + assertFalse(file.isLink()); + + assertEquals(LinkStatus.NON_LINK, LinkHandler.getLinkFileStatus(file, null)); + } + + @Test + public void testCopyPasteExternalLinkFile() throws Exception { + + env.getRootFolder().createFolder("xyz"); + + addRepoView(); + + env.waitForTree(); + + // + // Select bashLink link-file from viewed repository and Copy + // + DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm()); + assertNotNull("repo data tree view not found", viewTreeHelper); + + DomainFileNode bashLinkFile = viewTreeHelper.waitForFileNode("/bashLink"); + + final ActionContext copyActionContext = + viewTreeHelper.getDomainFileActionContext(bashLinkFile); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + // + // Select xyz folder and perform Paste + // + DomainFolderNode xyzNode = env.waitForFolderNode("/xyz"); + + final ActionContext pasteActionContext = env.getDomainFileActionContext(xyzNode); + + DockingActionIf pasteAction = getAction(env.getFrontEndTool(), "Paste"); + assertNotNull("Paste action not found", pasteAction); + + assertTrue(pasteAction.isAddToPopup(pasteActionContext)); + assertTrue(pasteAction.isEnabledForContext(pasteActionContext)); + runSwing(() -> pasteAction.actionPerformed(pasteActionContext)); + + // + // Verify paste of external link-file from repository to active project + // + DomainFileNode bashLinkCopyNode = env.waitForFileNode("/xyz/bashLink"); + DomainFile file = bashLinkCopyNode.getDomainFile(); + assertTrue(file.exists()); + assertTrue(file.isLink()); + + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(file, null)); + assertFalse(file.getLinkInfo().isFolderLink()); + + // + // Verify external URL to the link referenced file is applied with normal copy + // + assertEquals(viewURL + "/f1/bash", file.getLinkInfo().getLinkPath()); + } + + @Test + public void testCopyPasteExternalFileAsLink() throws Exception { + + env.getRootFolder().createFolder("abc"); + + addRepoView(); + + env.waitForTree(); + + // + // Select /foo file from viewed project and Copy + // + DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm()); + assertNotNull("repo data tree view not found", viewTreeHelper); + + DomainFileNode fooFile = viewTreeHelper.waitForFileNode("/foo"); + + final ActionContext copyActionContext = viewTreeHelper.getDomainFileActionContext(fooFile); + + URL sharedFileURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT, + "Test", "/foo", null); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + // + // Select /abc folder and perform Paste Link + // + DomainFolderNode abcNode = env.waitForFolderNode("/abc"); + + final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(abcNode); + + DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), "Paste Link"); + assertNotNull("Paste Link action not found", pasteLinkAction); + + assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext)); + assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext)); + runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext)); + + // + // Verify external file paste as link + // + DomainFileNode fooCopyNode = env.waitForFileNode("/abc/foo"); + DomainFile file = fooCopyNode.getDomainFile(); + assertTrue(file.exists()); + assertTrue(file.isLink()); + LinkFileInfo linkInfo = file.getLinkInfo(); + assertFalse(linkInfo.isFolderLink()); + assertTrue(linkInfo.isExternalLink()); + + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(file, null)); + + assertEquals(sharedFileURL.toExternalForm(), linkInfo.getLinkPath()); + + assertEquals(sharedFileURL, fooFile.getDomainFile().getSharedProjectURL(null)); + + // + // Verify link open follows into repository to open domain object database + // + DomainObject dobj = null; + try { + dobj = file.getDomainObject(this, false, false, TaskMonitor.DUMMY); + assertTrue(dobj instanceof ProgramDB); + assertFalse(dobj.canSave()); + assertTrue(dobj.isChangeable()); + } + finally { + if (dobj != null) { + dobj.release(this); + } + } + } + + @Test + public void testCopyPasteExternalLinkFileAsLink() throws Exception { + + env.getRootFolder().createFolder("abc"); + + addRepoView(); + + env.waitForTree(); + + // + // Select /bashLink file from viewed project and Copy + // + DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm()); + assertNotNull("repo data tree view not found", viewTreeHelper); + + DomainFileNode bashLinkFile = viewTreeHelper.waitForFileNode("/bashLink"); + + final ActionContext copyActionContext = + viewTreeHelper.getDomainFileActionContext(bashLinkFile); + + URL sharedFileURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT, + "Test", "/bashLink", null); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + // + // Select /abc folder and perform Paste Link + // + DomainFolderNode abcNode = env.waitForFolderNode("/abc"); + + final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(abcNode); + + DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), "Paste Link"); + assertNotNull("Paste Link action not found", pasteLinkAction); + + assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext)); + assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext)); + runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext)); + + // + // Verify external link-file paste as link + // + DomainFileNode bashLinkCopyNode = env.waitForFileNode("/abc/bashLink"); + DomainFile file = bashLinkCopyNode.getDomainFile(); + assertTrue(file.exists()); + assertTrue(file.isLink()); + LinkFileInfo linkInfo = file.getLinkInfo(); + assertFalse(linkInfo.isFolderLink()); + assertTrue(linkInfo.isExternalLink()); + + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(file, null)); + + assertEquals(sharedFileURL.toExternalForm(), linkInfo.getLinkPath()); + + assertEquals(sharedFileURL, bashLinkFile.getDomainFile().getSharedProjectURL(null)); + + // + // Verify link open follows double-hop into repository to open domain object database + // + DomainObject dobj = null; + try { + dobj = file.getDomainObject(this, false, false, TaskMonitor.DUMMY); + assertTrue(dobj instanceof ProgramDB); + assertFalse(dobj.canSave()); + assertTrue(dobj.isChangeable()); + } + finally { + if (dobj != null) { + dobj.release(this); + } + } + } + + @Test + public void testCopyPastExternalFolder() throws Exception { + + env.getRootFolder().createFolder("xyz"); + + addRepoView(); + + env.waitForTree(); + + // + // Select /f1 folder from viewed project and Copy + // + DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm()); + assertNotNull("repo data tree view not found", viewTreeHelper); + + DomainFolderNode f1Folder = viewTreeHelper.waitForFolderNode("/f1"); + + final ActionContext copyActionContext = viewTreeHelper.getDomainFileActionContext(f1Folder); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + // + // Select xyz folder and perform Paste + // + DomainFolderNode xyzNode = env.waitForFolderNode("/xyz"); + + final ActionContext pasteActionContext = env.getDomainFileActionContext(xyzNode); + + DockingActionIf pasteAction = getAction(env.getFrontEndTool(), "Paste"); + assertNotNull("Paste action not found", pasteAction); + + assertTrue(pasteAction.isAddToPopup(pasteActionContext)); + assertTrue(pasteAction.isEnabledForContext(pasteActionContext)); + runSwing(() -> pasteAction.actionPerformed(pasteActionContext)); + + // + // Verify external folder paste (full folder copy) with its content file + // + DomainFolderNode f1CopyNode = env.waitForFolderNode("/xyz/f1"); + DomainFolder folder = f1CopyNode.getDomainFolder(); + assertTrue(!folder.isEmpty()); + + DomainFile file = folder.getFile("bash"); + assertNotNull(file); + assertTrue(file.exists()); + + assertEquals(LinkStatus.NON_LINK, LinkHandler.getLinkFileStatus(file, null)); + } + + @Test + public void testCopyPastExternalFolderAsLink() throws Exception { + + env.getRootFolder().createFolder("abc"); + + addRepoView(); + + env.waitForTree(); + + // + // Select f1 folder from viewed project and Copy + // + DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm()); + assertNotNull("repo data tree view not found", viewTreeHelper); + + DomainFolderNode f1Folder = viewTreeHelper.waitForFolderNode("/f1"); + + final ActionContext copyActionContext = viewTreeHelper.getDomainFileActionContext(f1Folder); + + URL sharedFolderURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT, + "Test", "/f1/", null); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + // + // Select abc folder and perform Paste Link + // + DomainFolderNode abcNode = env.waitForFolderNode("/abc"); + + final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(abcNode); + + DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), "Paste Link"); + assertNotNull("Paste Link action not found", pasteLinkAction); + + assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext)); + assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext)); + runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext)); + + // + // Verify external folder paste as link + // + DomainFileNode abcCopyNode = env.waitForFileNode("/abc/f1"); + DomainFile file = abcCopyNode.getDomainFile(); + assertTrue(file.exists()); + assertTrue(file.isLink()); + LinkFileInfo linkInfo = file.getLinkInfo(); + assertTrue(linkInfo.isFolderLink()); + assertTrue(linkInfo.isExternalLink()); + + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(file, null)); + + // + // Folder link-paths intentionally omit the trailing / so they can adapt to use + // of folder or another folder-link-file at the referenced location + // + String urlPath = sharedFolderURL.toExternalForm(); // will end with '/' + urlPath = urlPath.substring(0, urlPath.length() - 1); // strip trailing '/' + + assertEquals(urlPath, linkInfo.getLinkPath()); + + LinkedDomainFolder linkedFolder = linkInfo.getLinkedFolder(); + assertNotNull(linkedFolder); + assertTrue(linkedFolder.isLinked()); + assertEquals(f1Folder.getDomainFolder(), linkedFolder.getRealFolder()); + + ProjectData projectData = env.getFrontEndTool().getProject().getProjectData(); + + // + // Verify stored folder and its indirect folder content access via ProjectData + // + DomainFolder remoteFolder = projectData.getFolder("/abc/f1"); + assertNull(remoteFolder); // must use filter to allow externals + remoteFolder = projectData.getFolder("/abc/f1", DomainFolderFilter.ALL_FOLDERS_FILTER); + assertEquals(linkedFolder, remoteFolder); + assertEquals(sharedFolderURL, remoteFolder.getSharedProjectURL()); + + DomainFile remoteFile = projectData.getFile("/abc/f1/bash"); + assertNull(remoteFile); // must use filter to allow externals + remoteFile = projectData.getFile("/abc/f1/bash", DomainFileFilter.ALL_FILES_FILTER); + assertNotNull(remoteFile); + assertTrue(remoteFile.exists()); + URL sharedFileURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT, + "Test", "/f1", "bash", null); + assertEquals(sharedFileURL, remoteFile.getSharedProjectURL(null)); + + // + // Verify ability to open linked-folder content + // + DomainObject dobj = null; + try { + dobj = remoteFile.getDomainObject(this, false, false, TaskMonitor.DUMMY); + assertTrue(dobj instanceof ProgramDB); + assertFalse(dobj.canSave()); + assertTrue(dobj.isChangeable()); + } + finally { + if (dobj != null) { + dobj.release(this); + } + } + } + + @Test + public void testCopyPastExternalFolderLinkAsLink() throws Exception { + + env.getRootFolder().createFolder("abc"); + + addRepoView(); + + env.waitForTree(); + + // + // Select f1Link folder-link from viewed project and Copy + // + DataTreeHelper viewTreeHelper = env.getReadOnlyProjectTreeHelper(viewURL.toExternalForm()); + assertNotNull("repo data tree view not found", viewTreeHelper); + + DomainFileNode f1LinkFile = viewTreeHelper.waitForFileNode("/f1Link"); + + final ActionContext copyActionContext = + viewTreeHelper.getDomainFileActionContext(f1LinkFile); + + URL sharedFolderURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT, + "Test", "/f1Link", null); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + // + // Select abc folder and perform Paste Link + // + DomainFolderNode abcNode = env.waitForFolderNode("/abc"); + + final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(abcNode); + + DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), "Paste Link"); + assertNotNull("Paste Link action not found", pasteLinkAction); + + assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext)); + assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext)); + runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext)); + + DomainFileNode f1LinkCopyNode = env.waitForFileNode("/abc/f1Link"); + DomainFile file = f1LinkCopyNode.getDomainFile(); + assertTrue(file.exists()); + assertTrue(file.isLink()); + LinkFileInfo linkInfo = file.getLinkInfo(); + assertTrue(linkInfo.isFolderLink()); + assertTrue(linkInfo.isExternalLink()); + + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(file, null)); + + // + // Folder link-paths intentionally omit the trailing / so they can adapt to use + // of folder or another folder-link-file at the referenced location + // + String urlPath = sharedFolderURL.toExternalForm(); + + assertEquals(urlPath, linkInfo.getLinkPath()); + + LinkedDomainFolder linkedFolder = linkInfo.getLinkedFolder(); + assertNotNull(linkedFolder); + assertTrue(linkedFolder.isLinked()); + + assertEquals("/f1Link", linkedFolder.getLinkedPathname()); + + assertNotNull("Linked folder content not found", linkedFolder.getFile("bash")); + + // + // Verify stored folder and its double-hop indirect folder content access via ProjectData + // + ProjectData projectData = env.getFrontEndTool().getProject().getProjectData(); + DomainFile remoteFile = + projectData.getFile("/abc/f1Link/bash", DomainFileFilter.ALL_FILES_FILTER); + assertNotNull(remoteFile); + assertTrue(remoteFile.exists()); + URL sharedFileURL = GhidraURL.makeURL("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT, + "Test", "/f1Link", "bash", null); + assertEquals(sharedFileURL, remoteFile.getSharedProjectURL(null)); + + // + // Verify ability to open double-hop linked-folder content + // + DomainObject dobj = null; + try { + dobj = remoteFile.getDomainObject(this, false, false, TaskMonitor.DUMMY); + assertTrue(dobj instanceof ProgramDB); + assertFalse(dobj.canSave()); + assertTrue(dobj.isChangeable()); + } + finally { + if (dobj != null) { + dobj.release(this); + } + } + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteTest.java new file mode 100644 index 0000000000..8c312a8692 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectCopyPasteTest.java @@ -0,0 +1,400 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.main.datatree; + +import static org.junit.Assert.*; + +import org.junit.*; + +import docking.ActionContext; +import docking.action.DockingActionIf; +import ghidra.framework.client.ClientUtil; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.LinkHandler.LinkStatus; +import ghidra.framework.model.*; +import ghidra.program.database.ProgramDB; +import ghidra.program.model.listing.Program; +import ghidra.server.remote.ServerTestUtil; +import ghidra.test.*; +import ghidra.util.task.TaskMonitor; + +public class ProjectCopyPasteTest extends AbstractGhidraHeadedIntegrationTest { + + private FrontEndTestEnv env; + + private DomainFolder abcFolder; + private DomainFile programFile; + + @Before + public void setUp() throws Exception { + + env = new FrontEndTestEnv(); + + /** + /abc (folder) + foo (program file) + /xyz (empty folder) + **/ + + DomainFolder rootFolder = env.getRootFolder(); + + abcFolder = rootFolder.createFolder("abc"); + rootFolder.createFolder("xyz"); + + Program p = ToyProgramBuilder.buildSimpleProgram("foo", this); + programFile = abcFolder.createFile("foo", p, TaskMonitor.DUMMY); + p.release(this); + + env.waitForTree(); + } + + @After + public void tearDown() throws Exception { + + env.dispose(); + + ClientUtil.clearRepositoryAdapter("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT); + } + + @Test + public void testCopyPasteFile() throws Exception { + + // Select /abc/foo file and Copy + + DomainFileNode fooFile = env.waitForFileNode("/abc/foo"); + + final ActionContext copyActionContext = env.getDomainFileActionContext(fooFile); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + // Select /xyz folder and perform Paste + + DomainFolderNode xyzNode = env.waitForFolderNode("/xyz"); + + final ActionContext pasteActionContext = env.getDomainFileActionContext(xyzNode); + + DockingActionIf pasteAction = getAction(env.getFrontEndTool(), "Paste"); + assertNotNull("Paste action not found", pasteAction); + + assertTrue(pasteAction.isAddToPopup(pasteActionContext)); + assertTrue(pasteAction.isEnabledForContext(pasteActionContext)); + runSwing(() -> pasteAction.actionPerformed(pasteActionContext)); + + DomainFileNode fooCopyNode = env.waitForFileNode("/xyz/foo"); + DomainFile file = fooCopyNode.getDomainFile(); + assertTrue(file.exists()); + assertFalse(file.isLink()); + + assertEquals(LinkStatus.NON_LINK, LinkHandler.getLinkFileStatus(file, null)); + } + + @Test + public void testCopyPastInternalAbsoluteFileLink() throws Exception { + testCopyPastInternalFileLink("Paste Link"); + } + + @Test + public void testCopyPastInternalRelativeFileLink() throws Exception { + testCopyPastInternalFileLink("Paste Relative-Link"); + } + + private void testCopyPastInternalFileLink(String pastActionName) throws Exception { + + /** + /abc + foo (copied) + /xyz (pasted into) + foo -> (direct link) + foo.1 -> (link to direct link) + **/ + + boolean isRelative = pastActionName.contains("Relative"); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + // Select /abc/foo file and perform Copy + + DomainFileNode fooNode = env.waitForFileNode("/abc/foo"); + final ActionContext copyActionContext = env.getDomainFileActionContext(fooNode); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), pastActionName); + assertNotNull(pastActionName + " action not found", pasteLinkAction); + + // Select /xyz folder and perform Paste as Link + + DomainFolderNode xyzNode = env.waitForFolderNode("/xyz"); + final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(xyzNode); + + assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext)); + assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext)); + runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext)); + + DomainFileNode fooLinkNode = env.waitForFileNode("/xyz/foo"); + DomainFile file = fooLinkNode.getDomainFile(); + assertTrue(file.exists()); + assertTrue(file.isLink()); + LinkFileInfo linkInfo = file.getLinkInfo(); + assertFalse(linkInfo.isFolderLink()); + assertFalse(linkInfo.isExternalLink()); + assertEquals(isRelative ? "../abc/foo" : "/abc/foo", linkInfo.getLinkPath()); + assertNull(linkInfo.getLinkedFolder()); + + ProjectData projectData = env.getFrontEndTool().getProject().getProjectData(); + + DomainFile fooLinkFile = projectData.getFile("/xyz/foo"); + assertNotNull(fooLinkFile); + assertTrue(fooLinkFile.exists()); + + assertEquals(LinkStatus.INTERNAL, LinkHandler.getLinkFileStatus(fooLinkFile, null)); + + DomainObject dobj = fooLinkFile.getDomainObject(this, false, false, TaskMonitor.DUMMY); + try { + assertTrue(dobj instanceof ProgramDB); + assertTrue(dobj.canSave()); + assertTrue(dobj.isChangeable()); + assertEquals(programFile, dobj.getDomainFile()); + } + finally { + if (dobj != null) { + dobj.release(this); + } + } + + // Select /xyz/foo file and perform Copy + + final ActionContext copy2ActionContext = env.getDomainFileActionContext(fooLinkNode); + + assertTrue(copyAction.isAddToPopup(copy2ActionContext)); + assertTrue(copyAction.isEnabledForContext(copy2ActionContext)); + runSwing(() -> copyAction.actionPerformed(copy2ActionContext)); + + // Select /xyz folder and perform Paste as Link + + assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext)); + assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext)); + runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext)); + + fooLinkNode = env.waitForFileNode("/xyz/foo.1"); + file = fooLinkNode.getDomainFile(); + assertTrue(file.exists()); + assertTrue(file.isLink()); + linkInfo = file.getLinkInfo(); + assertFalse(linkInfo.isFolderLink()); + assertFalse(linkInfo.isExternalLink()); + assertEquals(isRelative ? "foo" : "/xyz/foo", linkInfo.getLinkPath()); + assertNull(linkInfo.getLinkedFolder()); + + fooLinkFile = projectData.getFile("/xyz/foo.1"); + assertNotNull(fooLinkFile); + assertTrue(fooLinkFile.exists()); + + assertEquals(LinkStatus.INTERNAL, LinkHandler.getLinkFileStatus(fooLinkFile, null)); + + dobj = fooLinkFile.getDomainObject(this, false, false, TaskMonitor.DUMMY); + try { + assertTrue(dobj instanceof ProgramDB); + assertTrue(dobj.canSave()); + assertTrue(dobj.isChangeable()); + assertEquals(programFile, dobj.getDomainFile()); + } + finally { + if (dobj != null) { + dobj.release(this); + } + } + } + + @Test + public void testCopyPastFolder() throws Exception { + + // Select /abc file from viewed project and Copy + + DomainFolderNode abcFolderNode = env.waitForFolderNode("/abc"); + + final ActionContext copyActionContext = env.getDomainFileActionContext(abcFolderNode); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + // Select /xyz folder and perform Paste + + DomainFolderNode xyzNode = env.waitForFolderNode("/xyz"); + + final ActionContext pasteActionContext = env.getDomainFileActionContext(xyzNode); + + DockingActionIf pasteAction = getAction(env.getFrontEndTool(), "Paste"); + assertNotNull("Paste action not found", pasteAction); + + assertTrue(pasteAction.isAddToPopup(pasteActionContext)); + assertTrue(pasteAction.isEnabledForContext(pasteActionContext)); + runSwing(() -> pasteAction.actionPerformed(pasteActionContext)); + + DomainFolderNode abcCopyNode = env.waitForFolderNode("/xyz/abc"); + DomainFolder folder = abcCopyNode.getDomainFolder(); + assertTrue(!folder.isEmpty()); + + DomainFile file = folder.getFile("foo"); + assertNotNull(file); + assertTrue(file.exists()); + + } + + @Test + public void testCopyPastInternalAbsoluteFolderLink() throws Exception { + testCopyPastInternalFolderLink("Paste Link"); + } + + @Test + public void testCopyPastInternalRelativeFolderLink() throws Exception { + testCopyPastInternalFolderLink("Paste Relative-Link"); + } + + private void testCopyPastInternalFolderLink(String pastActionName) throws Exception { + + /** + /abc (copied) + foo + /xyz (pasted into) + abc -> (direct link) + abc.1 -> (link to direct link) + **/ + + boolean isRelative = pastActionName.contains("Relative"); + + DockingActionIf copyAction = getAction(env.getFrontEndTool(), "Copy"); + assertNotNull("Copy action not found", copyAction); + + // Select /abc folder and perform Copy + + DomainFolderNode abcNode = env.waitForFolderNode("/abc"); + final ActionContext copyActionContext = env.getDomainFileActionContext(abcNode); + + assertTrue(copyAction.isAddToPopup(copyActionContext)); + assertTrue(copyAction.isEnabledForContext(copyActionContext)); + runSwing(() -> copyAction.actionPerformed(copyActionContext)); + + // Select /xyz folder and perform Paste as Link /xyz/abc + + DockingActionIf pasteLinkAction = getAction(env.getFrontEndTool(), pastActionName); + assertNotNull(pastActionName + " action not found", pasteLinkAction); + + DomainFolderNode xyzNode = env.waitForFolderNode("/xyz"); + final ActionContext pasteLinkActionContext = env.getDomainFileActionContext(xyzNode); + + assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext)); + assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext)); + runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext)); + + final DomainFileNode xyzAbcLinkNode = env.waitForFileNode("/xyz/abc"); + final DomainFile xyzAbcLinkFile = xyzAbcLinkNode.getDomainFile(); + assertTrue(xyzAbcLinkFile.exists()); + assertTrue(xyzAbcLinkFile.isLink()); + LinkFileInfo xyzAbcLinkInfo = xyzAbcLinkFile.getLinkInfo(); + assertTrue(xyzAbcLinkInfo.isFolderLink()); + assertFalse(xyzAbcLinkInfo.isExternalLink()); + assertEquals(isRelative ? "../abc" : "/abc", xyzAbcLinkInfo.getLinkPath()); + + assertEquals(LinkStatus.INTERNAL, LinkHandler.getLinkFileStatus(xyzAbcLinkFile, null)); + + final LinkedDomainFolder xyzAbcLinkedFolder = xyzAbcLinkInfo.getLinkedFolder(); + assertNotNull(xyzAbcLinkedFolder); + assertTrue(xyzAbcLinkedFolder.isLinked()); + assertEquals(abcFolder, xyzAbcLinkedFolder.getRealFolder()); + + ProjectData projectData = env.getFrontEndTool().getProject().getProjectData(); + + DomainFile fooFile = projectData.getFile("/xyz/abc/foo"); + assertNotNull(fooFile); + assertTrue(fooFile.exists()); + + DomainObject dobj = fooFile.getDomainObject(this, false, false, TaskMonitor.DUMMY); + try { + assertTrue(dobj instanceof ProgramDB); + assertTrue(dobj.canSave()); + assertTrue(dobj.isChangeable()); + assertEquals(programFile, dobj.getDomainFile()); + } + finally { + if (dobj != null) { + dobj.release(this); + } + } + + // Select /xyz/abc linked-folder and perform Copy + + final ActionContext copy2ActionContext = env.getDomainFileActionContext(xyzAbcLinkNode); + + assertTrue(copyAction.isAddToPopup(copy2ActionContext)); + assertTrue(copyAction.isEnabledForContext(copy2ActionContext)); + runSwing(() -> copyAction.actionPerformed(copy2ActionContext)); + + // Select /xyz and perform Paste as Link /xyz/abc.1 + + assertTrue(pasteLinkAction.isAddToPopup(pasteLinkActionContext)); + assertTrue(pasteLinkAction.isEnabledForContext(pasteLinkActionContext)); + runSwing(() -> pasteLinkAction.actionPerformed(pasteLinkActionContext)); + + final DomainFileNode xyzAbc1CopyNode = env.waitForFileNode("/xyz/abc.1"); + DomainFile xyzAbc1LinkFile = xyzAbc1CopyNode.getDomainFile(); + assertTrue(xyzAbc1LinkFile.exists()); + assertTrue(xyzAbc1LinkFile.isLink()); + LinkFileInfo xyzAbc1LinkInfo = xyzAbc1LinkFile.getLinkInfo(); + assertTrue(xyzAbc1LinkInfo.isFolderLink()); + assertFalse(xyzAbc1LinkInfo.isExternalLink()); + assertEquals(isRelative ? "abc" : "/xyz/abc", xyzAbc1LinkInfo.getLinkPath()); + + assertEquals(LinkStatus.INTERNAL, LinkHandler.getLinkFileStatus(xyzAbc1LinkFile, null)); + + final LinkedDomainFolder xyzAbc1LinkedFolder = xyzAbc1LinkInfo.getLinkedFolder(); + assertNotNull(xyzAbc1LinkedFolder); + assertTrue(xyzAbc1LinkedFolder.isLinked()); + + assertEquals(xyzAbcLinkedFolder.getRealFolder(), xyzAbc1LinkedFolder.getRealFolder()); + + fooFile = projectData.getFile("/xyz/abc.1/foo"); + assertNotNull(fooFile); + assertTrue(fooFile.exists()); + + dobj = fooFile.getDomainObject(this, false, false, TaskMonitor.DUMMY); + try { + assertTrue(dobj instanceof ProgramDB); + assertTrue(dobj.canSave()); + assertTrue(dobj.isChangeable()); + assertEquals(programFile, dobj.getDomainFile()); + } + finally { + if (dobj != null) { + dobj.release(this); + } + } + + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTest.java new file mode 100644 index 0000000000..722997904f --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/main/datatree/ProjectLinkFileStatusTest.java @@ -0,0 +1,439 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.framework.main.datatree; + +import static org.junit.Assert.*; + +import java.util.function.BooleanSupplier; + +import org.junit.*; + +import ghidra.framework.client.ClientUtil; +import ghidra.framework.data.FolderLinkContentHandler; +import ghidra.framework.data.LinkHandler; +import ghidra.framework.data.LinkHandler.LinkStatus; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.DomainFolder; +import ghidra.program.database.DataTypeArchiveDB; +import ghidra.program.database.ProgramLinkContentHandler; +import ghidra.program.model.listing.Program; +import ghidra.server.remote.ServerTestUtil; +import ghidra.test.*; +import ghidra.util.exception.DuplicateFileException; +import ghidra.util.task.TaskMonitor; + +public class ProjectLinkFileStatusTest extends AbstractGhidraHeadedIntegrationTest { + + private FrontEndTestEnv env; + + private DomainFolder abcFolder; + private DomainFolder xyzFolder; + private DomainFile programFile; + + @Before + public void setUp() throws Exception { + + env = new FrontEndTestEnv(); + + /** + /abc/ (folder) + abc -> /xyz/abc (circular) + foo (program file) + /xyz/ + abc -> /abc (folder link) + abc -> (circular) + foo + foo -> /abc/foo (program link) + **/ + + DomainFolder rootFolder = env.getRootFolder(); + + abcFolder = rootFolder.createFolder("abc"); + xyzFolder = rootFolder.createFolder("xyz"); + DomainFile abcLinkFile = abcFolder.copyToAsLink(xyzFolder, false); + abcLinkFile.copyToAsLink(abcFolder, false); + + Program p = ToyProgramBuilder.buildSimpleProgram("foo", this); + programFile = abcFolder.createFile("foo", p, TaskMonitor.DUMMY); + p.release(this); + + programFile.copyToAsLink(xyzFolder, false); + + env.waitForTree(); + } + + @After + public void tearDown() throws Exception { + if (env != null) { + env.dispose(); + } + ClientUtil.clearRepositoryAdapter("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT); + } + + @Test + public void testNonFileLink() throws Exception { + DomainFileNode fileNode = env.waitForFileNode("/abc/foo"); + assertEquals(LinkStatus.NON_LINK, + LinkHandler.getLinkFileStatus(fileNode.getDomainFile(), null)); + } + + @Test + public void testExternalFileLink() throws Exception { + + // + // Create external program file-link /abc/A to remote repository + // + DomainFile linkFile = abcFolder.createLinkFile("ghidra://localhost/Test/A", "A", + ProgramLinkContentHandler.INSTANCE); + + env.waitForTree(); // give time for ChangeManager to update + + // + // Verify /abc/A external program file-link exists with correct status and display name + // + DomainFileNode nodeA = waitForFileNode("/abc/A"); + assertFalse(nodeA.isFolderLink()); + assertEquals(linkFile, nodeA.getDomainFile()); + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null)); + String displayName = runSwing(() -> nodeA.getDisplayText()); + assertTrue("Unexpected node display name: " + displayName, + displayName.contains("localhost[Test]:/A")); + + // + // Create external program file-link /abc/B to local project + // + linkFile = abcFolder.createLinkFile("ghidra:/x/y/Test?/B", "B", + ProgramLinkContentHandler.INSTANCE); + + env.waitForTree(); // give time for ChangeManager to update + + // + // Verify /abc/B external program file-link exists with correct status and display name + // + DomainFileNode nodeB = waitForFileNode("/abc/B"); + assertFalse(nodeB.isFolderLink()); + assertEquals(linkFile, nodeB.getDomainFile()); + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null)); + displayName = runSwing(() -> nodeB.getDisplayText()); + assertTrue("Unexpected node display name: " + displayName, displayName.contains("Test:/B")); + + // + // Remove /abc/foo file + // + DomainFile fooFile = abcFolder.getFile("foo"); + assertNotNull(fooFile); + fooFile.delete(); + + // + // Replace deleted file with external program file-link to local project + // which sets-up indirect link path from /xyz/foo -> /abc/foo -> local project file + // + linkFile = abcFolder.createLinkFile("ghidra:/x/y/Test?/foo", "foo", + ProgramLinkContentHandler.INSTANCE); + + waitForSwing(); // give a chance for ChangeManager to be notified + + env.waitForTree(); // give time for ChangeManager to update + + // + // Verify /abc/foo external program file-link exists with correct status and display name + // + DomainFileNode fooNode = waitForFileNode("/abc/foo"); + assertFalse(fooNode.isFolderLink()); + assertEquals(linkFile, fooNode.getDomainFile()); + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null)); + displayName = runSwing(() -> fooNode.getDisplayText()); + if (!displayName.contains("Test:/foo")) { + int junk = 0; + } + assertTrue("Unexpected node display name: " + displayName, + displayName.contains("Test:/foo")); + + // + // Check pre-existing file-link /xyz/foo reflects external status + // + DomainFileNode fooLinkNode = waitForFileNode("/xyz/foo"); + assertEquals(LinkStatus.EXTERNAL, + LinkHandler.getLinkFileStatus(fooLinkNode.getDomainFile(), null)); + } + + @Test + public void testExternalFolderLink() throws Exception { + + // NOTE: Only refer to root repo folder with remote URL to avoid unwanted connection attempt + + // + // Create external folder-link /abc/A to remote repository + // + DomainFile linkFile = abcFolder.createLinkFile("ghidra://localhost/Test/", "A", + FolderLinkContentHandler.INSTANCE); + + env.waitForTree(); // give time for ChangeManager to update + + // + // Verify /abc/A external folder-link exists with correct status and display name + // + DomainFileNode nodeA = waitForFileNode("/abc/A"); + assertTrue(nodeA.isFolderLink()); + assertEquals(linkFile, nodeA.getDomainFile()); + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null)); + String displayName = runSwing(() -> nodeA.getDisplayText()); + assertTrue("Unexpected node display name: " + displayName, + displayName.contains("localhost[Test]:/")); + + // + // Create external folder-link /abc/B to local project + // + linkFile = + abcFolder.createLinkFile("ghidra:/x/y/Test?/B", "B", FolderLinkContentHandler.INSTANCE); + + env.waitForTree(); // give time for ChangeManager to update + + // + // Verify /abc/B external folder-link exists with correct status and display name + // + DomainFileNode nodeB = waitForFileNode("/abc/B"); + assertTrue(nodeB.isFolderLink()); + assertEquals(linkFile, nodeB.getDomainFile()); + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null)); + displayName = runSwing(() -> nodeB.getDisplayText()); + assertTrue("Unexpected node display name: " + displayName, displayName.contains("Test:/B")); + + // + // Remove /abc folder and its children + // + DomainFolder rootFolder = abcFolder.getParent(); + abcFolder.getFile("abc").delete(); + abcFolder.getFile("foo").delete(); + abcFolder.getFile("A").delete(); + abcFolder.getFile("B").delete(); + abcFolder.delete(); + + // + // Remove /xyz/foo file to avoid remote access attempt to ghidra://localhost/Test/foo + // after /abc is replaced in the next step + // + DomainFile fooFile = xyzFolder.getFile("foo"); + assertNotNull(fooFile); + fooFile.delete(); + + // + // Replace deleted folder with external folder-link to local project + // which sets-up indirect link path from /xyz/abc -> /abc -> local project root folder + // + linkFile = rootFolder.createLinkFile("ghidra://localhost/Test/", "abc", + FolderLinkContentHandler.INSTANCE); + + env.waitForTree(); // give time for ChangeManager to update + + // + // Verify /abc external folder-link exists with correct status and display name + // + DomainFileNode abcLinkNode = waitForFileNode("/abc"); + assertTrue(abcLinkNode.isFolderLink()); + assertEquals(linkFile, abcLinkNode.getDomainFile()); + assertEquals(LinkStatus.EXTERNAL, LinkHandler.getLinkFileStatus(linkFile, null)); + displayName = runSwing(() -> abcLinkNode.getDisplayText()); + assertTrue("Unexpected node display name: " + displayName, + displayName.contains("localhost[Test]:/")); + + // + // Check pre-existing folder-link /xyz/abc reflects external status + // + DomainFileNode abcLinkNode2 = waitForFileNode("/xyz/abc"); + assertEquals(LinkStatus.EXTERNAL, + LinkHandler.getLinkFileStatus(abcLinkNode2.getDomainFile(), null)); + } + + @Test + public void testBrokenFolderLink() throws Exception { + + // + // Verify broken folder-link status for /abc/abc which has circular reference + // + DomainFileNode abcAbcLinkNode = waitForFileNode("/abc/abc"); + assertTrue(abcAbcLinkNode.isFolderLink()); + String displayName = runSwing(() -> abcAbcLinkNode.getDisplayText()); + assertTrue("Unexpected node display name: " + displayName, + displayName.endsWith(" /xyz/abc")); + assertEquals(LinkStatus.BROKEN, + LinkHandler.getLinkFileStatus(abcAbcLinkNode.getDomainFile(), null)); + String tooltip = abcAbcLinkNode.getToolTip().replace(" ", " "); + assertTrue(tooltip.contains("circular")); + + // + // Verify good folder-link internal status for /xyz/abc which has circular reference + // + DomainFileNode xyzAbcLinkNode = waitForFileNode("/xyz/abc"); + assertTrue(xyzAbcLinkNode.isFolderLink()); + displayName = runSwing(() -> xyzAbcLinkNode.getDisplayText()); + assertTrue("Unexpected node display name: " + displayName, displayName.endsWith(" /abc")); + assertEquals(LinkStatus.INTERNAL, + LinkHandler.getLinkFileStatus(xyzAbcLinkNode.getDomainFile(), null)); + + // + // Verify broken folder-link status for /xyz/abc/abc which has circular reference + // + DomainFileNode abcLinkedNode = waitForFileNode("/xyz/abc/abc"); + assertTrue(abcLinkedNode.isFolderLink()); + displayName = runSwing(() -> abcLinkedNode.getDisplayText()); + assertTrue("Unexpected node display name: " + displayName, + displayName.endsWith(" /xyz/abc")); + assertEquals(LinkStatus.BROKEN, + LinkHandler.getLinkFileStatus(abcLinkedNode.getDomainFile(), null)); + tooltip = abcLinkedNode.getToolTip().replace(" ", " "); + assertTrue(tooltip.contains("circular")); + + // + // Rename folder /abc to /ABC causing folder-link /xyz/abc to become broken + // + abcFolder = abcFolder.setName("ABC"); + + env.waitForTree(); // give time for ChangeManager to update + + // Verify /abc node not found + assertNull(env.getRootNode().getChild("abc")); + + // + // Verify broken folder-link status for /ABC/abc which has circular reference + // + DomainFileNode ABCAbcLinkNode = waitForFileNode("/ABC/abc"); + assertTrue(ABCAbcLinkNode.isFolderLink()); + displayName = runSwing(() -> ABCAbcLinkNode.getDisplayText()); + assertTrue("Unexpected node display name: " + displayName, + displayName.endsWith(" /xyz/abc")); + assertEquals(LinkStatus.BROKEN, + LinkHandler.getLinkFileStatus(ABCAbcLinkNode.getDomainFile(), null)); + tooltip = ABCAbcLinkNode.getToolTip().replace(" ", " "); + assertTrue(tooltip.contains("folder not found: /abc")); + + env.waitForTree(); // give time for ChangeManager to update + + // + // Verify that folder-link /xyz/abc is broken due to missing /abc + // + DomainFileNode n = waitForFileNode("/xyz/abc"); // wait for refresh + assertTrue(n == xyzAbcLinkNode); + assertTrue(xyzAbcLinkNode.isFolderLink()); + assertEquals(LinkStatus.BROKEN, + LinkHandler.getLinkFileStatus(xyzAbcLinkNode.getDomainFile(), null)); + tooltip = xyzAbcLinkNode.getToolTip().replace(" ", " "); + assertTrue("Unexpected tooltip: " + tooltip, tooltip.contains("folder not found: /abc")); + + // + // Attempt conflicting folder-link creation + // + DomainFile linkFile = abcFolder.getParent() + .createLinkFile("ghidra://localhost/Test/ABC", "ABC", + FolderLinkContentHandler.INSTANCE); + assertEquals("ABC.1", linkFile.getName()); // link forced to have unqiue name + + // + // Try to force folder vs folder-link name conflict + // While it won't be allowed it could occur in-the-wild due to shared project content + // (case not tested here) + // + + try { + linkFile.setName("ABC"); // trigger folder name conflict for folder-link + fail("Expected DuplicateFileException"); + } + catch (DuplicateFileException e) { + // expected for link file + } + + try { + abcFolder.setName("ABC.1"); + fail("Expected DuplicateFileException"); + } + catch (DuplicateFileException e) { + // expected for link file + } + + } + + @Test + public void testBrokenFileLink() throws Exception { + + // + // Verify good internal file-link status for /xyz/foo -> /abc/foo + // + DomainFileNode linkNode = waitForFileNode("/xyz/foo"); + assertEquals(LinkStatus.INTERNAL, + LinkHandler.getLinkFileStatus(linkNode.getDomainFile(), null)); + + // + // Copy program file /abc/foo as relative file-link /abc/foo.1 and verify good internal file-link status + // + DomainFile relativeProgramLink = programFile.copyToAsLink(abcFolder, true); + assertEquals("/abc/foo.1", relativeProgramLink.getPathname()); + assertEquals(LinkStatus.INTERNAL, LinkHandler.getLinkFileStatus(relativeProgramLink, null)); + + // + // Delete program file /abc/foo and verify that file-link /abc/foo.1 becomes broken + // + programFile.delete(); + assertEquals(LinkStatus.BROKEN, LinkHandler.getLinkFileStatus(relativeProgramLink, null)); + + env.waitForTree(); // give time for ChangeManager to update + + // + // Verify broken /xyz/foo file link status due to deleted file /abc/foo + // + linkNode = waitForFileNode("/xyz/foo"); + assertEquals(LinkStatus.BROKEN, + LinkHandler.getLinkFileStatus(linkNode.getDomainFile(), null)); + String tooltip = linkNode.getToolTip().replace(" ", " "); + assertTrue(tooltip.contains("file not found: /abc/foo")); + + // + // Create DataTypeArchive project file /abc/foo + // + DataTypeArchiveDB dtm = new DataTypeArchiveDB(abcFolder, "foo", this); + dtm.save(null, TaskMonitor.DUMMY); + dtm.release(this); + + env.waitForTree(); // give time for ChangeManager to update + + // + // Verify that Program file-link is now broken due to incompatible content for /abc/foo + // + linkNode = waitForFileNode("/xyz/foo"); + assertEquals(LinkStatus.BROKEN, + LinkHandler.getLinkFileStatus(linkNode.getDomainFile(), null)); + env.waitForSwing(); + tooltip = linkNode.getToolTip().replace(" ", " "); + assertTrue("Unexpected tooltip: " + tooltip, + tooltip.contains("incompatible content-type: /abc/foo")); + + } + + private DomainFileNode waitForFileNode(String path) { + DomainFileNode fileNode = env.waitForFileNode(path); + waitForRefresh(fileNode); + return fileNode; + } + + private void waitForRefresh(DomainFileNode fileNode) { + waitFor(new BooleanSupplier() { + @Override + public boolean getAsBoolean() { + return !fileNode.hasPendingRefresh(); + } + }); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java index d3a5c3e683..1947134da3 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/test/DataTreeHelper.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -130,11 +130,11 @@ public class DataTreeHelper { for (int i = 0; i < nodes.length; i++) { GTreeNode node = nodes[i]; treePaths[i] = node.getTreePath(); - if (node instanceof DomainFileNode) { - fileList.add(((DomainFileNode) node).getDomainFile()); + if (node instanceof DomainFileNode fileNode) { + fileList.add(fileNode.getDomainFile()); } - else if (node instanceof DomainFolderNode) { - folderList.add(((DomainFolderNode) node).getDomainFolder()); + else if (node instanceof DomainFolderNode folderNode) { + folderList.add(folderNode.getDomainFolder()); } }