diff --git a/Ghidra/Features/Base/data/ExtensionPoint.manifest b/Ghidra/Features/Base/data/ExtensionPoint.manifest index 9fb4bec9b99..6fada203547 100644 --- a/Ghidra/Features/Base/data/ExtensionPoint.manifest +++ b/Ghidra/Features/Base/data/ExtensionPoint.manifest @@ -21,3 +21,4 @@ ChecksumAlgorithm OverviewColorService DWARFFunctionFixup ElfInfoProducer +FSBFileHandler diff --git a/Ghidra/Features/Base/data/base.file.extensions.icons.theme.properties b/Ghidra/Features/Base/data/base.file.extensions.icons.theme.properties index d2f488893eb..d4bb5cd4d26 100644 --- a/Ghidra/Features/Base/data/base.file.extensions.icons.theme.properties +++ b/Ghidra/Features/Base/data/base.file.extensions.icons.theme.properties @@ -38,5 +38,6 @@ icon.fsbrowser.file.extension.zip = images/oxygen/16x16/application-x-bzi icon.fsbrowser.file.substring.release. = images/famfamfam_silk_icons_v013/bullet_purple.png icon.fsbrowser.file.overlay.imported = EMPTY_ICON{images/checkmark_green.gif[size(8,8)][move(8,8)]} // lower right quadrant -icon.fsbrowser.file.overlay.filesystem = EMPTY_ICON{images/ledgreen.png[size(8,8)][move(0,8)]} // lower left quadrant -icon.fsbrowser.file.overlay.missing.password = EMPTY_ICON{images/lock.png[size(8,8)][move(8,0)]} // upper right quadrant \ No newline at end of file +icon.fsbrowser.file.overlay.filesystem = EMPTY_ICON{images/ledgreen.png[size(8,8)][move(0,8)]} // lower left quadrant +icon.fsbrowser.file.overlay.link = EMPTY_ICON{icon.content.handler.link[move(0,8)]} // lower-left quadrant +icon.fsbrowser.file.overlay.missing.password = EMPTY_ICON{images/lock.png[size(8,8)][move(8,0)]} // upper right quadrant diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/AbstractProgramLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/AbstractProgramLoader.java index 8944f0a9e98..da2c57d2568 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/AbstractProgramLoader.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/AbstractProgramLoader.java @@ -28,7 +28,6 @@ import ghidra.formats.gfilesystem.FSRL; import ghidra.framework.model.*; import ghidra.framework.store.LockException; -import ghidra.plugin.importer.ProgramMappingService; import ghidra.program.database.ProgramDB; import ghidra.program.database.function.OverlappingFunctionException; import ghidra.program.model.address.*; @@ -367,8 +366,7 @@ public static void setProgramProperties(Program prog, ByteProvider provider, if (fsrl.getMD5() == null) { fsrl = fsrl.withMD5(md5); } - prog.getOptions(Program.PROGRAM_INFO) - .setString(ProgramMappingService.PROGRAM_SOURCE_FSRL, fsrl.toString()); + FSRL.writeToProgramInfo(prog, fsrl); } prog.setExecutableMD5(md5); String sha256 = computeBinarySHA256(provider); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/AbstractFileSystem.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/AbstractFileSystem.java index b11a7c90b72..01abcb92966 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/AbstractFileSystem.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/AbstractFileSystem.java @@ -15,6 +15,7 @@ */ package ghidra.formats.gfilesystem; +import java.io.IOException; import java.util.Comparator; import java.util.List; @@ -65,6 +66,11 @@ public GFile lookup(String path) { return fsIndex.lookup(null, path, getFilenameComparator()); } + @Override + public GFile getRootDir() { + return fsIndex.getRootDir(); + } + @Override public List getListing(GFile directory) { return fsIndex.getListing(directory); @@ -75,4 +81,9 @@ public int getFileCount() { return fsIndex.getFileCount(); } + @Override + public GFile resolveSymlinks(GFile file) throws IOException { + return fsIndex.resolveSymlinks(file); + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java index daf75db4642..7c561acff8a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java @@ -19,7 +19,6 @@ import java.net.MalformedURLException; import java.util.*; -import ghidra.plugin.importer.ProgramMappingService; import ghidra.program.model.listing.Program; import ghidra.util.SystemUtilities; @@ -64,6 +63,7 @@ */ public class FSRL { public static final String PARAM_MD5 = "MD5"; + public static final String FSRL_OPTION_NAME = "FSRL"; /** * Returns the {@link FSRL} stored in a {@link Program}'s properties, or null if not present @@ -73,8 +73,7 @@ public class FSRL { * @return {@link FSRL} from program's properties, or null if not present or invalid */ public static FSRL fromProgram(Program program) { - String fsrlStr = program.getOptions(Program.PROGRAM_INFO) - .getString(ProgramMappingService.PROGRAM_SOURCE_FSRL, null); + String fsrlStr = program.getOptions(Program.PROGRAM_INFO).getString(FSRL_OPTION_NAME, null); if (fsrlStr != null) { try { return FSRL.fromString(fsrlStr); @@ -86,6 +85,16 @@ public static FSRL fromProgram(Program program) { return null; } + /** + * Writes a FSRL value to a {@link Program}'s properties. + * + * @param program {@link Program} + * @param fsrl {@link FSRL} to write + */ + public static void writeToProgramInfo(Program program, FSRL fsrl) { + program.getOptions(Program.PROGRAM_INFO).setString(FSRL_OPTION_NAME, fsrl.toString()); + } + /** * Creates a {@link FSRL} from a raw string. The parent portions of the FSRL * are not intern()'d so will not be shared with other FSRL instances. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSUtilities.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSUtilities.java index 880fbe7649a..6d42b0bb27b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSUtilities.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSUtilities.java @@ -20,6 +20,8 @@ import java.net.MalformedURLException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; @@ -31,6 +33,7 @@ import docking.widgets.OptionDialog; import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; +import ghidra.formats.gfilesystem.fileinfo.FileType; import ghidra.util.*; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; @@ -400,9 +403,7 @@ public static List getLines(ByteProvider byteProvider) throws IOExceptio public static String getFileMD5(File f, TaskMonitor monitor) throws IOException, CancelledException { try (FileInputStream fis = new FileInputStream(f)) { - monitor.initialize(f.length()); - monitor.setMessage("Hashing file: " + f.getName()); - return getMD5(fis, monitor); + return getMD5(fis, f.getName(), f.length(), monitor); } } @@ -418,9 +419,7 @@ public static String getFileMD5(File f, TaskMonitor monitor) public static String getMD5(ByteProvider provider, TaskMonitor monitor) throws IOException, CancelledException { try (InputStream is = provider.getInputStream(0)) { - monitor.initialize(provider.length()); - monitor.setMessage("Hashing file: " + provider.getName()); - return getMD5(is, monitor); + return getMD5(is, provider.getName(), provider.length(), monitor); } } @@ -428,21 +427,39 @@ public static String getMD5(ByteProvider provider, TaskMonitor monitor) * Calculate the hash of an {@link InputStream}. * * @param is {@link InputStream} + * @param name of the inputstream + * @param expectedLength the length of the inputstream * @param monitor {@link TaskMonitor} to update * @return md5 as a hex encoded string, never null * @throws IOException if error * @throws CancelledException if cancelled */ - public static String getMD5(InputStream is, TaskMonitor monitor) - throws IOException, CancelledException { + public static String getMD5(InputStream is, String name, long expectedLength, + TaskMonitor monitor) throws IOException, CancelledException { try { + long startms = System.currentTimeMillis(); + long prevElapsed = startms; + + monitor.initialize(expectedLength, "Hashing %s".formatted(name)); + MessageDigest messageDigest = MessageDigest.getInstance(HashUtilities.MD5_ALGORITHM); - byte[] buf = new byte[16 * 1024]; + int bufSize = (int) Math.max(1024, Math.min(expectedLength, 1024 * 1024)); + byte[] buf = new byte[bufSize]; int bytesRead; + long totalBytesRead = 0; while ((bytesRead = is.read(buf)) >= 0) { messageDigest.update(buf, 0, bytesRead); - monitor.incrementProgress(bytesRead); - monitor.checkCancelled(); + totalBytesRead += bytesRead; + monitor.increment(bytesRead); + + long now = System.currentTimeMillis(); + if (now - prevElapsed > 5000 /*5 seconds*/ && totalBytesRead > bufSize) { + prevElapsed = now; + long elapsed = now - startms; + long rate = (long) (totalBytesRead / (elapsed / 1000f)); + monitor.setMessage( + "Hashing %s %s/s".formatted(name, FileUtilities.formatLength(rate))); + } } return NumericUtilities.convertBytesToString(messageDigest.digest()); } @@ -587,4 +604,50 @@ public static void uncheckedClose(Closeable c, String msg) { e); } } + + public static boolean isSymlink(File f) { + try { + return f != null && Files.isSymbolicLink(f.toPath()); + } + catch (IllegalArgumentException e) { + return false; + } + } + + /** + * Returns the destination of a symlink, or null if not a symlink or other error + * + * @param f {@link File} that is a symlink + * @return destination path string of the symlink, or null if not symlink + */ + public static String readSymlink(File f) { + try { + Path symlink = Files.readSymbolicLink(f.toPath()); + return symlink.toString(); + } + catch (Throwable th) { + // ignore and return null + } + return null; + } + + public static FileType getFileType(File f) { + try { + Path p = f.toPath(); + if (Files.isSymbolicLink(p)) { + return FileType.SYMBOLIC_LINK; + } + if (Files.isDirectory(p)) { + return FileType.DIRECTORY; + } + if (Files.isRegularFile(p)) { + return FileType.FILE; + } + } + catch (IllegalArgumentException e) { + // fall thru + } + return FileType.UNKNOWN; + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFile.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFile.java index c0669c8a059..6f5717e3fdb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFile.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFile.java @@ -31,53 +31,49 @@ public interface GFile { * The {@link GFileSystem} that owns this file. * @return {@link GFileSystem} that owns this file. */ - public GFileSystem getFilesystem(); + GFileSystem getFilesystem(); /** * The {@link FSRL} of this file. * * @return {@link FSRL} of this file. */ - public FSRL getFSRL(); + FSRL getFSRL(); /** * The parent directory of this file. * * @return parent {@link GFile} directory of this file. */ - public GFile getParentFile(); + GFile getParentFile(); /** * The path and filename of this file, relative to its owning filesystem. * * @return path and filename of this file, relative to its owning filesystem. */ - public String getPath(); + String getPath(); /** * The name of this file. * * @return name of this file. */ - public String getName(); + String getName(); /** * Returns true if this is a directory. *

* @return boolean true if this file is a directory, false otherwise. */ - public boolean isDirectory(); + boolean isDirectory(); /** * Returns the length of this file, or -1 if not known. * * @return number of bytes in this file. */ - public long getLength(); - - default public long getLastModified() { - return -1; - } + long getLength(); /** * Returns a listing of files in this sub-directory. @@ -85,7 +81,7 @@ default public long getLastModified() { * @return {@link List} of {@link GFile} instances. * @throws IOException if not a directory or error when accessing files. */ - default public List getListing() throws IOException { + default List getListing() throws IOException { return getFilesystem().getListing(this); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileLocal.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileLocal.java index 9bc768407ac..8a7f77b121a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileLocal.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileLocal.java @@ -16,6 +16,7 @@ package ghidra.formats.gfilesystem; import java.io.File; +import java.util.Objects; /** * {@link GFile} implementation that refers to a real java.io.File on the local @@ -85,11 +86,6 @@ public long getLength() { return f.length(); } - @Override - public long getLastModified() { - return f.lastModified(); - } - public File getLocalFile() { return f; } @@ -99,4 +95,23 @@ public String toString() { return "Local " + f.toString() + " with path " + path; } + @Override + public int hashCode() { + return Objects.hash(f, fs, fsrl, parent, path); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof GFileLocal)) { + return false; + } + GFileLocal other = (GFileLocal) obj; + return Objects.equals(f, other.f) && Objects.equals(fs, other.fs) && + Objects.equals(fsrl, other.fsrl) && Objects.equals(parent, other.parent) && + Objects.equals(path, other.path); + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystem.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystem.java index 062ce153d26..865ce2d1583 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystem.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystem.java @@ -52,7 +52,7 @@ public interface GFileSystem extends Closeable, ExtensionPoint { * * @return string filesystem volume name. */ - public String getName(); + String getName(); /** * Returns the type of this file system. @@ -62,7 +62,7 @@ public interface GFileSystem extends Closeable, ExtensionPoint { * * @return type string */ - default public String getType() { + default String getType() { return FSUtilities.getFilesystemTypeFromClass(this.getClass()); } @@ -74,7 +74,7 @@ default public String getType() { * * @return description string */ - default public String getDescription() { + default String getDescription() { return FSUtilities.getFilesystemDescriptionFromClass(this.getClass()); } @@ -83,21 +83,21 @@ default public String getDescription() { * * @return {@link FSRLRoot} of this filesystem. */ - public FSRLRoot getFSRL(); + FSRLRoot getFSRL(); /** * Returns true if the filesystem has been {@link #close() closed} * * @return boolean true if the filesystem has been closed. */ - public boolean isClosed(); + boolean isClosed(); /** * Indicates if this filesystem is a static snapshot or changes. * * @return boolean true if the filesystem is static or false if dynamic content. */ - default public boolean isStatic() { + default boolean isStatic() { return true; } @@ -107,14 +107,14 @@ default public boolean isStatic() { *

* @return {@link FileSystemRefManager} that manages references to this filesystem. */ - public FileSystemRefManager getRefManager(); + FileSystemRefManager getRefManager(); /** * Returns the number of files in the filesystem, if known, otherwise -1 if not known. * * @return number of files in this filesystem, -1 if not known. */ - default public int getFileCount() { + default int getFileCount() { return -1; } @@ -127,7 +127,23 @@ default public int getFileCount() { * @return {@link GFile} instance of requested file, null if not found. * @throws IOException if IO error when looking up file. */ - public GFile lookup(String path) throws IOException; + GFile lookup(String path) throws IOException; + + /** + * Returns the file system's root directory. + *

+ * Note: using {@code null} when calling {@link #getListing(GFile)} is also valid. + * + * @return file system's root directory + */ + default GFile getRootDir() { + try { + return lookup(null); + } + catch (IOException e) { + return null; + } + } /** * Returns an {@link InputStream} that contains the contents of the specified {@link GFile}. @@ -141,7 +157,7 @@ default public int getFileCount() { * @throws IOException if IO problem * @throws CancelledException if user cancels. */ - default public InputStream getInputStream(GFile file, TaskMonitor monitor) + default InputStream getInputStream(GFile file, TaskMonitor monitor) throws IOException, CancelledException { return getInputStreamHelper(file, this, monitor); } @@ -158,7 +174,7 @@ default public InputStream getInputStream(GFile file, TaskMonitor monitor) * @throws IOException if error * @throws CancelledException if user cancels */ - public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException; /** @@ -169,7 +185,7 @@ public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) * @return {@link List} of {@link GFile} instances of file in the requested directory. * @throws IOException if IO problem. */ - public List getListing(GFile directory) throws IOException; + List getListing(GFile directory) throws IOException; /** * Returns a container of {@link FileAttribute} values. @@ -181,10 +197,23 @@ public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) * @param monitor {@link TaskMonitor} * @return {@link FileAttributes} instance (possibly read-only), maybe empty but never null */ - default public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + default FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { return FileAttributes.EMPTY; } + /** + * Converts the specified (symlink) file into it's destination, or if not a symlink, + * returns the original file unchanged. + * + * @param file symlink file to follow + * @return destination of symlink, or original file if not a symlink + * @throws IOException if error following symlink path, typically outside of the hosting + * file system + */ + default GFile resolveSymlinks(GFile file) throws IOException { + return null; + } + /** * Default implementation of getting an {@link InputStream} from a {@link GFile}'s * {@link ByteProvider}. @@ -197,7 +226,7 @@ default public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) * @throws CancelledException if canceled * @throws IOException if error */ - public static InputStream getInputStreamHelper(GFile file, GFileSystem fs, TaskMonitor monitor) + static InputStream getInputStreamHelper(GFile file, GFileSystem fs, TaskMonitor monitor) throws CancelledException, IOException { ByteProvider bp = fs.getByteProvider(file, monitor); return (bp != null) ? new ByteProviderInputStream.ClosingInputStream(bp) : null; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GIconProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GIconProvider.java index 200547b9538..28accbccd50 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GIconProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GIconProvider.java @@ -43,7 +43,7 @@ public interface GIconProvider { * @throws IOException if problem reading or converting image. * @throws CancelledException if user cancels. */ - public Icon getIcon(GFile file, TaskMonitor monitor) throws IOException, CancelledException; + Icon getIcon(GFile file, TaskMonitor monitor) throws IOException, CancelledException; /** * Helper static method that will get an Icon from a data file. @@ -54,11 +54,11 @@ public interface GIconProvider { * file couldn't be converted into an image. * @throws CancelledException if the user cancels. */ - public static Icon getIconForFile(GFile file, TaskMonitor monitor) throws CancelledException { + static Icon getIconForFile(GFile file, TaskMonitor monitor) throws CancelledException { try { GFileSystem fs = file.getFilesystem(); - if (fs instanceof GIconProvider) { - return ((GIconProvider) fs).getIcon(file, monitor); + if (fs instanceof GIconProvider iconProviderFS) { + return iconProviderFS.getIcon(file, monitor); } try (InputStream is = file.getFilesystem().getInputStream(file, monitor)) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystem.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystem.java index 91c03b08196..ddf23e24c36 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystem.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystem.java @@ -18,7 +18,7 @@ import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; import java.io.*; -import java.nio.file.*; +import java.nio.file.AccessMode; import java.util.*; import org.apache.commons.collections4.map.ReferenceMap; @@ -29,6 +29,7 @@ import ghidra.formats.gfilesystem.factory.GFileSystemFactory; import ghidra.formats.gfilesystem.factory.GFileSystemFactoryIgnore; import ghidra.formats.gfilesystem.fileinfo.*; +import ghidra.framework.OperatingSystem; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -55,14 +56,17 @@ public static LocalFileSystem makeGlobalRootFS() { return new LocalFileSystem(FSRLRoot.makeRoot(FSTYPE)); } - private final List emptyDir = List.of(); private final FSRLRoot fsFSRL; + private final GFile rootDir; private final FileSystemRefManager refManager = new FileSystemRefManager(this); private final ReferenceMap fileFingerprintToMD5Map = new ReferenceMap<>(); + private final boolean needsListRoots = + OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS; private LocalFileSystem(FSRLRoot fsrl) { this.fsFSRL = fsrl; + this.rootDir = GFileImpl.fromFSRL(this, null, fsFSRL.withPath("/"), true, -1); } boolean isSameFS(FSRL fsrl) { @@ -143,8 +147,9 @@ public boolean isStatic() { @Override public List getListing(GFile directory) { List results = new ArrayList<>(); + directory = Objects.requireNonNullElse(directory, rootDir); - if (directory == null) { + if (directory.equals(rootDir) && needsListRoots) { for (File f : File.listRoots()) { FSRL rootElemFSRL = fsFSRL.withPath(FSUtilities.normalizeNativePath(f.getName())); results.add(GFileImpl.fromFSRL(this, null, rootElemFSRL, f.isDirectory(), -1)); @@ -152,17 +157,17 @@ public List getListing(GFile directory) { } else { File localDir = new File(directory.getPath()); - if (!localDir.isDirectory() || Files.isSymbolicLink(localDir.toPath())) { - return emptyDir; + if (!localDir.isDirectory() || FSUtilities.isSymlink(localDir)) { + return List.of(); } File[] files = localDir.listFiles(); if (files == null) { - return emptyDir; + return List.of(); } for (File f : files) { - if (f.isFile() || f.isDirectory()) { + if (f.isFile() || f.isDirectory() || FSUtilities.isSymlink(f)) { FSRL newFileFSRL = directory.getFSRL().appendPath(f.getName()); results.add(GFileImpl.fromFSRL(this, directory, newFileFSRL, f.isDirectory(), f.length())); @@ -186,36 +191,14 @@ public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { * @return {@link FileAttributes} instance */ public FileAttributes getFileAttributes(File f) { - Path p = f.toPath(); - FileType fileType = fileToFileType(p); - Path symLinkDest = null; - try { - symLinkDest = fileType == FileType.SYMBOLIC_LINK ? Files.readSymbolicLink(p) : null; - } - catch (IOException e) { - // ignore and continue with symLinkDest == null - } + FileType fileType = FSUtilities.getFileType(f); + String symLinkDest = fileType == FileType.SYMBOLIC_LINK ? FSUtilities.readSymlink(f) : null; return FileAttributes.of( FileAttribute.create(NAME_ATTR, f.getName()), FileAttribute.create(FILE_TYPE_ATTR, fileType), FileAttribute.create(SIZE_ATTR, f.length()), FileAttribute.create(MODIFIED_DATE_ATTR, new Date(f.lastModified())), - symLinkDest != null - ? FileAttribute.create(SYMLINK_DEST_ATTR, symLinkDest.toString()) - : null); - } - - private static FileType fileToFileType(Path p) { - if (Files.isSymbolicLink(p)) { - return FileType.SYMBOLIC_LINK; - } - if (Files.isDirectory(p)) { - return FileType.DIRECTORY; - } - if (Files.isRegularFile(p)) { - return FileType.FILE; - } - return FileType.UNKNOWN; + symLinkDest != null ? FileAttribute.create(SYMLINK_DEST_ATTR, symLinkDest) : null); } @Override @@ -223,6 +206,11 @@ public FSRLRoot getFSRL() { return fsFSRL; } + @Override + public GFile getRootDir() { + return rootDir; + } + @Override public GFile lookup(String path) throws IOException { File f = lookupFile(null, path, null); @@ -259,6 +247,18 @@ ByteProvider getByteProvider(FSRL fsrl, TaskMonitor monitor) throws IOException return new FileByteProvider(f, fsrl, AccessMode.READ); } + @Override + public GFile resolveSymlinks(GFile file) throws IOException { + File f = getLocalFile(file.getFSRL()); + File canonicalFile = f.getCanonicalFile(); + if (f.equals(canonicalFile)) { + return file; + } + return GFileImpl.fromPathString(this, + FSUtilities.normalizeNativePath(canonicalFile.getPath()), null, + canonicalFile.isDirectory(), canonicalFile.length()); + } + @Override public String toString() { return "Local file system " + fsFSRL; @@ -327,13 +327,18 @@ public static File lookupFile(File baseDir, String path, Comparator name // If not using a comparator, or if the requested path is a // root element (eg "/", or "c:\\"), don't do per-directory-path lookups. - // On windows, getCanonicalFile() will return a corrected path using the case of - // the file element on the file system (eg. "c:/users" -> "c:/Users"), if the - // element exists. - return f.exists() ? f.getCanonicalFile() : null; + if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) { + // On windows, getCanonicalFile() will return a corrected path using the case of + // the file element on the file system (eg. "c:/users" -> "c:/Users"), if the + // element exists. + // We don't want to do this on unix-ish file systems as it will follow symlinks + f = f.getCanonicalFile(); + } + return FSUtilities.isSymlink(f) || f.exists() ? f : null; } - if (f.exists()) { + // Test the file's path using the name comparator + if (f.exists() && baseDir == null) { // try to short-cut by comparing the entire path string File canonicalFile = f.getCanonicalFile(); if (nameComp.compare(path, diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystemSub.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystemSub.java index 864654cab0a..17748e44bfe 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystemSub.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystemSub.java @@ -16,7 +16,6 @@ package ghidra.formats.gfilesystem; import java.io.*; -import java.nio.file.Files; import java.util.ArrayList; import java.util.List; @@ -97,7 +96,7 @@ public List getListing(GFile directory) throws IOException { return List.of(); } File localDir = getFileFromGFile(directory); - if (Files.isSymbolicLink(localDir.toPath())) { + if (FSUtilities.isSymlink(localDir)) { return List.of(); } @@ -112,7 +111,8 @@ public List getListing(GFile directory) throws IOException { String relPath = FSUtilities.normalizeNativePath(directory.getPath()); for (File f : localFiles) { - if (!(f.isFile() || f.isDirectory())) { + boolean isSymlink = FSUtilities.isSymlink(f); // check this manually to allow broken symlinks to appear in listing + if (!(isSymlink || f.isFile() || f.isDirectory())) { // skip non-file things continue; } @@ -133,7 +133,7 @@ public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { return rootFS.getFileAttributes(localFile); } catch (IOException e) { - // fail and return null + // fail and return empty } return FileAttributes.EMPTY; } @@ -148,6 +148,11 @@ public FSRLRoot getFSRL() { return fsFSRL; } + @Override + public GFile getRootDir() { + return rootGFile; + } + @Override public GFile lookup(String path) throws IOException { File f = LocalFileSystem.lookupFile(localfsRootDir, path, null); @@ -210,4 +215,14 @@ public String getMD5Hash(GFile file, boolean required, TaskMonitor monitor) throws CancelledException, IOException { return rootFS.getMD5Hash(file.getFSRL(), required, monitor); } + + @Override + public GFile resolveSymlinks(GFile file) throws IOException { + File f = getFileFromGFile(file); + File canonicalFile = f.getCanonicalFile(); + if (f.equals(canonicalFile)) { + return file; + } + return getGFile(canonicalFile); + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/FileSystemFactoryMgr.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/FileSystemFactoryMgr.java index ff2f2f145f0..b82fb6aec47 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/FileSystemFactoryMgr.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/FileSystemFactoryMgr.java @@ -15,7 +15,6 @@ */ package ghidra.formats.gfilesystem.factory; -import java.io.File; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; @@ -212,18 +211,13 @@ public boolean test(ByteProvider byteProvider, FileSystemService fsService, byte[] startBytes = byteProvider.readBytes(0, pboByteCount); for (FileSystemInfoRec fsir : sortedFactories) { try { - if (fsir.getFactory() instanceof GFileSystemProbeBytesOnly) { - GFileSystemProbeBytesOnly factoryProbe = - (GFileSystemProbeBytesOnly) fsir.getFactory(); - if (factoryProbe.getBytesRequired() <= startBytes.length) { - if (factoryProbe.probeStartBytes(containerFSRL, startBytes)) { - return true; - } + if (fsir.getFactory() instanceof GFileSystemProbeBytesOnly factoryProbe) { + if (factoryProbe.getBytesRequired() <= startBytes.length && + factoryProbe.probeStartBytes(containerFSRL, startBytes)) { + return true; } } - if (fsir.getFactory() instanceof GFileSystemProbeByteProvider) { - GFileSystemProbeByteProvider factoryProbe = - (GFileSystemProbeByteProvider) fsir.getFactory(); + if (fsir.getFactory() instanceof GFileSystemProbeByteProvider factoryProbe) { if (factoryProbe.probe(byteProvider, fsService, monitor)) { return true; } 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 3347a1e21e8..9deb1e2224b 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 @@ -301,7 +301,6 @@ public void projectClosed(Project project) { if (addToProgramAction != null) { addToProgramAction.setEnabled(false); } - ProgramMappingService.clear(); } @Override @@ -315,8 +314,6 @@ public void projectOpened(Project project) { if (addToProgramAction != null) { addToProgramAction.setEnabled(false); } - - ProgramMappingService.clear(); } private void setupImportAction() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterUtilities.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterUtilities.java index 67901e57bc0..e6850a5554c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterUtilities.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterUtilities.java @@ -151,8 +151,8 @@ public static void showImportDialog(PluginTool tool, ProgramManager programManag if (!isFSContainer) { // normal file; do a single-file import - importSingleFile(fullFsrl, destinationFolder, suggestedPath, tool, programManager, - monitor); + showImportSingleFileDialog(fullFsrl, destinationFolder, suggestedPath, tool, + programManager, monitor); return; } @@ -213,8 +213,8 @@ private static void importFromContainer(PluginTool tool, ProgramManager programM } if (choice == 1) { - importSingleFile(fullFsrl, destinationFolder, suggestedPath, tool, programManager, - monitor); + showImportSingleFileDialog(fullFsrl, destinationFolder, suggestedPath, tool, + programManager, monitor); } else if (choice == 2) { BatchImportDialog.showAndImport(tool, null, Arrays.asList(fullFsrl), destinationFolder, @@ -276,8 +276,9 @@ public static void showAddToProgramDialog(FSRL fsrl, Program program, PluginTool * to the destination filename * @param tool the parent UI component * @param programManager optional {@link ProgramManager} instance to open the imported file in + * @param monitor {@link TaskMonitor} */ - private static void importSingleFile(FSRL fsrl, DomainFolder destinationFolder, + public static void showImportSingleFileDialog(FSRL fsrl, DomainFolder destinationFolder, String suggestedPath, PluginTool tool, ProgramManager programManager, TaskMonitor monitor) { @@ -420,8 +421,6 @@ private static Set doPostImportProcessing(PluginTool pluginTool, monitor.checkCancelled(); if (loaded.getDomainObject() instanceof Program program) { - ProgramMappingService.createAssociation(fsrl, program); - if (programManager != null) { int openState = firstProgram ? ProgramManager.OPEN_CURRENT diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ProgramMappingService.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ProgramMappingService.java deleted file mode 100644 index de007208f60..00000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ProgramMappingService.java +++ /dev/null @@ -1,482 +0,0 @@ -/* ### - * 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.plugin.importer; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.util.*; - -import ghidra.app.services.ProgramManager; -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.FileSystemService; -import ghidra.framework.main.AppInfo; -import ghidra.framework.model.*; -import ghidra.framework.options.Options; -import ghidra.program.model.listing.Program; -import ghidra.util.Msg; -import ghidra.util.datastruct.FixedSizeHashMap; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - -/** - * Provides a best-effort[1] mapping / association between Ghidra Program/DomainFile - * objects and GFilesystem files (identified by their {@link FSRL}). - *

- * As there is no current feature that allows you to quickly query the metadata of - * Programs/DomainFile objects in the current project, finding a Program by its MD5 or by a - * original source location string is not easily possible. - *

- * Threadsafe. - *

- * The current implementation searches current open Ghidra Programs and maintains a - * short-lived, in-memory only mapping of FSRL->DomainFile paths - * (manually updated by users of the ProgramMappingService when - * they do an import or other operation that creates a Ghidra DomainFile by calling - * {@link #createAssociation(FSRL, DomainFile)} and friends.) - *

- * [1] - best-effort (adverb): meaning a dirty hack. - */ -public class ProgramMappingService { - public static final String PROGRAM_METADATA_MD5 = "Executable MD5"; - public static final String PROGRAM_SOURCE_FSRL = "FSRL"; - - private static final int FSRL_TO_PATH_MAP_SIZE = 1000; - - /** - * LRU mapping from FSRL to the project path string of a DomainFile object. - *

- * Limited in size to {@value #FSRL_TO_PATH_MAP_SIZE}. - */ - private static Map fsrlToProjectPathMap = - new FixedSizeHashMap<>(FSRL_TO_PATH_MAP_SIZE); - - private ProgramMappingService() { - // utils class; cannot instantiate - } - - /** - * Clears {@link ProgramMappingService} data. - *

- * This should be done whenever the project is opened/closed. - */ - public static void clear() { - synchronized (fsrlToProjectPathMap) { - fsrlToProjectPathMap.clear(); - } - } - - /** - * Returns true if there is a current open Ghidra {@link Program} that has metadata - * that links it to the specified {@link FSRL}. - *

- * (ie. an open program has a MD5 or FSRL metadata value that matches the fsrl param.) - * - * @param fsrl {@link FSRL} to search for in open program info. - * @return boolean true if found. - */ - public static boolean isFileOpen(FSRL fsrl) { - String expectedMD5 = fsrl.getMD5(); - - List openDomainFiles = findOpenFiles(); - - Object consumer = new Object(); - for (DomainFile df : openDomainFiles) { - DomainObject openedDomainObject = df.getOpenedDomainObject(consumer); - try { - if (openedDomainObject instanceof Program) { - Program program = (Program) openedDomainObject; - Options propertyList = program.getOptions(Program.PROGRAM_INFO); - String fsrlStr = - propertyList.getString(ProgramMappingService.PROGRAM_SOURCE_FSRL, null); - String md5 = - propertyList.getString(ProgramMappingService.PROGRAM_METADATA_MD5, null); - - if ((expectedMD5 != null && expectedMD5.equals(md5)) || - fsrl.isEquivalent(fsrlStr)) { - createAssociation(fsrl, program); - return true; - } - } - } - finally { - if (openedDomainObject != null && openedDomainObject.isUsedBy(consumer)) { - openedDomainObject.release(consumer); - } - } - } - return false; - } - - /** - * Returns true if the specified {@link FSRL} has a matched Ghidra {@link DomainFile} - * in the current project. - *

- * @param fsrl {@link FSRL} to search for - * @return boolean true if file exists in project. - */ - public static boolean isFileImportedIntoProject(FSRL fsrl) { - return isFileOpen(fsrl) || (getCachedDomainFileFor(fsrl) != null); - } - - /** - * Returns a reference to a {@link DomainFile} in the current {@link Project} that matches - * the specified {@link FSRL}. - *

- * This method only consults an internal fsrl-to-DomainFile mapping that is short-lived - * and not persisted. - *

- * @param fsrl {@link FSRL} to search for - * @return {@link DomainFile} that was previously associated via - * {@link #createAssociation(FSRL, DomainFile)} and friends. - */ - public static DomainFile getCachedDomainFileFor(FSRL fsrl) { - String path = null; - synchronized (fsrlToProjectPathMap) { - path = fsrlToProjectPathMap.get(fsrl); - if (path == null && fsrl.getMD5() != null) { - fsrl = fsrl.withMD5(null); - path = fsrlToProjectPathMap.get(fsrl); - } - } - - if (path == null) { - return null; - } - - DomainFile domainFile = getProjectFile(path); - if (domainFile == null) { - // The domainFile will be null if the cached path is no longer valid. Remove - // the stale path from the cache. - synchronized (fsrlToProjectPathMap) { - if (Objects.equals(fsrlToProjectPathMap.get(fsrl), path)) { - fsrlToProjectPathMap.remove(fsrl); - } - } - } - return domainFile; - } - - /** - * Creates a short-lived association between a {@link FSRL} and an open {@link Program}. - *

- * @param fsrl {@link FSRL} of where the {@link Program} was imported from. - * @param program {@link Program} to associate to. - */ - public static void createAssociation(FSRL fsrl, Program program) { - synchronized (fsrlToProjectPathMap) { - fsrlToProjectPathMap.put(fsrl, program.getDomainFile().getPathname()); - fsrlToProjectPathMap.put(fsrl.withMD5(null), program.getDomainFile().getPathname()); - } - } - - /** - * Creates a short-lived association between a {@link FSRL} and a {@link DomainFile}. - * - * @param fsrl {@link FSRL} of where the DomainFile was imported from. - * @param domainFile {@link DomainFile} to associate with - */ - public static void createAssociation(FSRL fsrl, DomainFile domainFile) { - createAssociation(fsrl, domainFile, false); - } - - private static void createAssociation(FSRL fsrl, DomainFile domainFile, - boolean onlyAddIfEnoughRoomInCache) { - synchronized (fsrlToProjectPathMap) { - if (!onlyAddIfEnoughRoomInCache || - fsrlToProjectPathMap.size() < FSRL_TO_PATH_MAP_SIZE) { - fsrlToProjectPathMap.put(fsrl, domainFile.getPathname()); - fsrlToProjectPathMap.put(fsrl.withMD5(null), domainFile.getPathname()); - } - } - } - - /** - * Attempts to create an association between the specified open {@code program} and - * any {@link FSRL} metadata found in the {@link Program}s properties. - *

- * Used by event handlers that get notified about a {@link Program} being opened to - * opportunistically link that program to its source FSRL if the metadata is present. - *

- * @param program {@link Program} to rummage around in its metadata looking for FSRL info. - */ - public static void createAutoAssocation(Program program) { - if (program != null) { - Options propertyList = program.getOptions(Program.PROGRAM_INFO); - String fsrlStr = - propertyList.getString(ProgramMappingService.PROGRAM_SOURCE_FSRL, null); - if (fsrlStr != null) { - try { - FSRL fsrl = FSRL.fromString(fsrlStr); - synchronized (fsrlToProjectPathMap) { - if (!fsrlToProjectPathMap.containsKey(fsrl)) { - fsrlToProjectPathMap.put(fsrl, program.getDomainFile().getPathname()); - } - } - } - catch (MalformedURLException e) { - Msg.error(ProgramMappingService.class, "Bad FSRL found: " + fsrlStr + - ", program: " + program.getDomainFile().getPathname()); - } - } - } - } - - /** - * Returns an open {@link Program} instance that matches the specified - * {@link FSRL}, either from the set of currently open programs, or by - * requesting the specified {@link ProgramManager} to - * open a {@link DomainFile} that was found to match this GFile. - *

- * @param fsrl {@link FSRL} of program original location. - * @param consumer Object that will be used to pin the matching Program open. Caller - * must release the consumer when done. - * @param programManager {@link ProgramManager} that will be used to open DomainFiles - * if necessary. - * @param openState one of {@link ProgramManager#OPEN_VISIBLE}, - * {@link ProgramManager#OPEN_HIDDEN}, {@link ProgramManager#OPEN_VISIBLE} - * @return {@link Program} which was imported from the specified FSRL, or null if not found. - */ - public static Program findMatchingProgramOpenIfNeeded(FSRL fsrl, Object consumer, - ProgramManager programManager, int openState) { - return findMatchingProgramOpenIfNeeded(fsrl, null, consumer, programManager, openState); - } - - /** - * Returns an open {@link Program} instance that matches the specified - * {@link FSRL}, either from the set of currently open programs, or by - * requesting the specified {@link ProgramManager} to - * open a {@link DomainFile} that was found to match this GFile. - *

- * @param fsrl {@link FSRL} of program original location. - * @param domainFile optional {@link DomainFile} that corresponds to the FSRL param. - * @param consumer Object that will be used to pin the matching Program open. Caller - * must release the consumer when done. - * @param programManager {@link ProgramManager} that will be used to open DomainFiles - * if necessary. - * @param openState one of {@link ProgramManager#OPEN_VISIBLE}, - * {@link ProgramManager#OPEN_HIDDEN}, {@link ProgramManager#OPEN_VISIBLE} - * @return {@link Program} which was imported from the specified FSRL, or null if not found. - */ - public static Program findMatchingProgramOpenIfNeeded(FSRL fsrl, DomainFile domainFile, - Object consumer, ProgramManager programManager, int openState) { - Program program = findMatchingOpenProgram(fsrl, consumer); - if (program != null) { - programManager.openProgram(program, openState); - return program; - } - DomainFile df = (domainFile == null) ? getCachedDomainFileFor(fsrl) : domainFile; - if (df == null || programManager == null) { - return null; - } - - program = programManager.openProgram(df, DomainFile.DEFAULT_VERSION, openState); - if (program != null) { - program.addConsumer(consumer); - } - return program; - } - - /** - * Returns a currently open Ghidra {@link Program} that has metadata that links it - * to the specified {@code file} parameter. - *

- * (ie. an open program has a MD5 or FSRL metadata value that matches the file) - *

- * See also {@link #isFileOpen(FSRL)}. - *

- * @param fsrl {@link FSRL} to use when inspecting each open Program's metadata. - * @param consumer Object that will be used to pin the matching Program open. Caller - * must release the consumer when done. - * @return Already open {@link Program} that has matching metadata, or null if not found. - */ - public static Program findMatchingOpenProgram(FSRL fsrl, Object consumer) { - String expectedMD5 = fsrl.getMD5(); - - // use a temp consumer to hold the domainObject open because the caller-supplied - // consumer might already have been used to open one of the files we are querying. - Object tmpConsumer = new Object(); - List openDomainFiles = getOpenFiles(); - for (DomainFile df : openDomainFiles) { - DomainObject openedDomainObject = df.getOpenedDomainObject(tmpConsumer); - try { - if (openedDomainObject instanceof Program) { - Program program = (Program) openedDomainObject; - Options propertyList = program.getOptions(Program.PROGRAM_INFO); - String fsrlStr = - propertyList.getString(ProgramMappingService.PROGRAM_SOURCE_FSRL, null); - String md5 = - propertyList.getString(ProgramMappingService.PROGRAM_METADATA_MD5, null); - - if ((expectedMD5 != null && expectedMD5.equals(md5)) || - fsrl.isEquivalent(fsrlStr)) { - // lock the domain file with the caller-supplied consumer now that - // we've found it. - df.getOpenedDomainObject(consumer); - return program; - } - } - } - finally { - if (openedDomainObject != null) { - openedDomainObject.release(tmpConsumer); - } - } - } - return null; - } - - /** - * Recursively searches the current active {@link Project} for {@link DomainFile}s that - * have metadata that matches a {@link FSRL} in the specified list. - *

- * Warning, this operation is expensive and should only be done in a Task thread. - *

- * @param fsrls List of {@link FSRL} to match against the metadata of each DomainFile in Project. - * @param monitor {@link TaskMonitor} to watch for cancel and update with progress. - * @return Map of FSRLs to {@link DomainFile}s of the found files, never null. - */ - public static Map searchProjectForMatchingFiles(List fsrls, - TaskMonitor monitor) { - - Project project = AppInfo.getActiveProject(); - if (project == null) { - // this should not be possible if this call is being run as a task - return Collections.emptyMap(); - } - - ProjectData projectData = project.getProjectData(); - int fc = projectData.getFileCount(); - if (fc > 0) { - monitor.setShowProgressValue(true); - monitor.setMaximum(fc); - monitor.setProgress(0); - } - else { - monitor.setIndeterminate(true); - } - monitor.setMessage("Searching project for matching files"); - - Map fsrlsToFindByMD5; - try { - fsrlsToFindByMD5 = buildFullyQualifiedFSRLMap(fsrls, monitor); - } - catch (CancelledException ce) { - Msg.info(ProgramMappingService.class, "Canceling project search"); - return Collections.emptyMap(); - } - - Map results = new HashMap<>(); - - Iterable files = ProjectDataUtils.descendantFiles(projectData.getRootFolder()); - for (DomainFile domainFile : files) { - if (monitor.isCancelled() || fsrlsToFindByMD5.isEmpty()) { - break; - } - - monitor.incrementProgress(1); - Map metadata = domainFile.getMetadata(); - - FSRL dfFSRL = getFSRLFromMetadata(metadata, domainFile); - if (dfFSRL != null) { - // side effect: create association between the FSRL in the DomainFile's props - // to the DomainFile's path if there is room in the cache. - // (ie. don't blow out the cache for files that haven't been requested yet) - createAssociation(dfFSRL, domainFile, true); - } - String dfMD5 = (dfFSRL != null) ? dfFSRL.getMD5() : getMD5FromMetadata(metadata); - if (dfMD5 != null) { - FSRL matchedFSRL = fsrlsToFindByMD5.get(dfMD5); - if (matchedFSRL != null) { - results.put(matchedFSRL, domainFile); - fsrlsToFindByMD5.remove(dfMD5); - } - } - } - - return results; - } - - private static String getMD5FromMetadata(Map metadata) { - return metadata.get(PROGRAM_METADATA_MD5); - } - - private static FSRL getFSRLFromMetadata(Map metadata, DomainFile domainFile) { - String dfFSRLStr = metadata.get(PROGRAM_SOURCE_FSRL); - if (dfFSRLStr != null) { - try { - FSRL dfFSRL = FSRL.fromString(dfFSRLStr); - return dfFSRL; - } - catch (MalformedURLException e) { - Msg.warn(ProgramMappingService.class, - "Domain file " + domainFile.getPathname() + " has a bad FSRL: " + dfFSRLStr); - } - } - return null; - } - - private static DomainFile getProjectFile(String path) { - - Project project = AppInfo.getActiveProject(); - if (project != null) { - ProjectData data = project.getProjectData(); - if (data != null) { - return data.getFile(path); - } - } - return null; - } - - private static List getOpenFiles() { - - List files = new ArrayList<>(); - Project project = AppInfo.getActiveProject(); - if (project != null) { - files = project.getOpenData(); - } - return files; - } - - private static List findOpenFiles() { - - List files = new ArrayList<>(); - Project project = AppInfo.getActiveProject(); - if (project != null) { - ProjectData data = project.getProjectData(); - if (data != null) { - data.findOpenFiles(files); - } - } - return files; - } - - private static Map buildFullyQualifiedFSRLMap(List fsrls, - TaskMonitor monitor) throws CancelledException { - Map result = new HashMap<>(); - for (FSRL fsrl : fsrls) { - try { - FSRL fqFSRL = FileSystemService.getInstance().getFullyQualifiedFSRL(fsrl, monitor); - String expectedMD5 = fqFSRL.getMD5(); - result.put(expectedMD5, fsrl); - } - catch (IOException e) { - // ignore and continue - } - } - return result; - } - -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ProjectIndexService.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ProjectIndexService.java new file mode 100644 index 00000000000..685395a2a61 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ProjectIndexService.java @@ -0,0 +1,223 @@ +/* ### + * 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.plugin.importer; + +import java.net.MalformedURLException; +import java.util.*; +import java.util.function.BiFunction; + +import ghidra.formats.gfilesystem.FSRL; +import ghidra.framework.model.*; +import ghidra.util.Swing; +import ghidra.util.task.TaskMonitor; + +/** + * An in-memory index of FSRL-to-domainfile in the current project. + */ +public class ProjectIndexService implements DomainFolderChangeListener { + + public static ProjectIndexService getInstance() { + return SingletonHolder.instance; + } + + private static class SingletonHolder { + private static final ProjectIndexService instance = new ProjectIndexService(); + } + + public enum IndexType { + MD5("Executable MD5"), FSRL("FSRL"); + + private String metadataKey; + + IndexType(String metadataKey) { + this.metadataKey = metadataKey; + } + + public String getMetadataKey() { + return metadataKey; + } + } + + /** + * @param indexType IndexType enum + * @param mappingFunc bifunc that returns value that will be used to lookup the file + * @param indexedFiles map of index keyvalue to fileId (either string or list of strings) + * + */ + record IndexInfo(IndexType indexType, + BiFunction, Object> mappingFunc, + Map indexedFiles) { + IndexInfo(IndexType indexType, + BiFunction, Object> mappingFunc) { + this(indexType, mappingFunc, new HashMap<>()); + } + } + + private Project project; + private List indexes; + + private ProjectIndexService() { + this.indexes = List.of(new IndexInfo(IndexType.MD5, this::getMD5), + new IndexInfo(IndexType.FSRL, this::getFSRL)); + } + + public synchronized void clearProject() { + if (project != null) { + project.getProjectData().removeDomainFolderChangeListener(this); + for (IndexInfo index : indexes) { + index.indexedFiles.clear(); + } + project = null; + } + } + + public void setProject(Project newProject, TaskMonitor monitor) { + synchronized (this) { + if (newProject == project) { + return; + } + clearProject(); + project = newProject; + + if (project != null) { + indexes = List.of(new IndexInfo(IndexType.MD5, this::getMD5), + new IndexInfo(IndexType.FSRL, this::getFSRL)); + ProjectData projectData = project.getProjectData(); + projectData.removeDomainFolderChangeListener(this); + projectData.addDomainFolderChangeListener(this); + } + } + + if (newProject != null) { + // index outside of sync lock to allow concurrent lookups + indexProject(newProject.getProjectData(), monitor); + } + } + + @Override + public void domainFileAdded(DomainFile file) { + indexFile(file); + } + + @Override + public void domainFileRemoved(DomainFolder parent, String name, String fileID) { + removeFile(fileID); + } + + private void indexProject(ProjectData projectData, TaskMonitor monitor) { + int fileCount = projectData.getFileCount(); + if (fileCount < 0) { + return; + } + monitor.initialize(fileCount, "Indexing Project Metadata"); + for (DomainFile df : ProjectDataUtils.descendantFiles(projectData.getRootFolder())) { + monitor.incrementProgress(); + if (monitor.isCancelled()) { + break; + } + indexFile(df); + if (monitor.getProgress() % 10 == 0) { + Swing.allowSwingToProcessEvents(); + } + } + } + + private String getMD5(DomainFile file, Map metadata) { + return metadata.get(IndexType.MD5.metadataKey); + } + + private FSRL getFSRL(DomainFile file, Map metadata) { + String fsrlStr = metadata.get(IndexType.FSRL.metadataKey); + try { + return fsrlStr != null ? FSRL.fromString(fsrlStr).withMD5(null) : null; + } + catch (MalformedURLException e) { + return null; + } + } + + public synchronized List lookupFiles(IndexType keyType, Object keyValue) { + IndexInfo index = indexes.get(keyType.ordinal()); + Object fileInfo = index.indexedFiles.get(keyValue); + List fileIds; + if (fileInfo instanceof String fileIdStr) { + fileIds = List.of(fileIdStr); + } + else if (fileInfo instanceof List fileInfoList) { + fileIds = fileInfoList; + } + else { + fileIds = List.of(); + } + return fileIds.stream() + .map(fileId -> project.getProjectData().getFileByID(fileId)) + .filter(Objects::nonNull) + .toList(); + } + + public DomainFile findFirstByFSRL(FSRL fsrl) { + fsrl = fsrl.withMD5(null); + List files = lookupFiles(IndexType.FSRL, fsrl); + return !files.isEmpty() ? files.get(0) : null; + } + + private synchronized void indexFile(DomainFile file) { + Map metadata = file.getMetadata(); + for (IndexInfo index : indexes) { + Object indexedValue = index.mappingFunc.apply(file, metadata); + if (indexedValue != null) { + Object fileInfo = index.indexedFiles.get(indexedValue); + if (fileInfo == null) { + index.indexedFiles.put(indexedValue, file.getFileID()); + } + else if (fileInfo instanceof List fileInfoList) { + ((List) fileInfoList).add(file.getFileID()); + } + else if (fileInfo instanceof String prevFileId) { + String newFileId = file.getFileID(); + if (newFileId.equals(prevFileId)) { + // don't need to do anything + continue; + } + List fileInfoList = new ArrayList<>(); + fileInfoList.add(prevFileId); + fileInfoList.add(newFileId); + index.indexedFiles.put(indexedValue, fileInfoList); + } + } + } + } + + private synchronized void removeFile(String fileId) { + // brute force search through all entries to remove the file + for (IndexInfo index : indexes) { + for (Iterator it = index.indexedFiles.values().iterator(); it + .hasNext();) { + Object fileInfo = it.next(); + if (fileInfo instanceof String fileIdStr && fileIdStr.equals(fileId)) { + it.remove(); + } + else if (fileInfo instanceof List fileInfoList) { + fileInfoList.remove(fileId); + if (fileInfoList.isEmpty()) { + it.remove(); + } + } + } + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionContext.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionContext.java index 3df001321ee..73730a70c24 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionContext.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionContext.java @@ -16,19 +16,17 @@ package ghidra.plugins.fsbrowser; import java.awt.event.MouseEvent; -import java.io.IOException; import java.util.ArrayList; import java.util.List; -import javax.swing.tree.TreePath; - import docking.DefaultActionContext; import docking.widgets.tree.GTree; -import docking.widgets.tree.GTreeNode; -import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.framework.model.DomainFile; +import ghidra.plugin.importer.ProjectIndexService; /** - * {@link FileSystemBrowserPlugin}-specific action. + * {@link FSBComponentProvider} context for actions */ public class FSBActionContext extends DefaultActionContext { @@ -42,12 +40,27 @@ public class FSBActionContext extends DefaultActionContext { * @param event MouseEvent that caused the update, or null * @param gTree {@link FileSystemBrowserPlugin} provider tree. */ - public FSBActionContext(FileSystemBrowserComponentProvider provider, FSBNode[] selectedNodes, - MouseEvent event, GTree gTree) { + public FSBActionContext(FSBComponentProvider provider, + List selectedNodes, MouseEvent event, GTree gTree) { super(provider, selectedNodes, gTree); this.gTree = gTree; } + @Override + public FSBComponentProvider getComponentProvider() { + return (FSBComponentProvider) super.getComponentProvider(); + } + + @Override + public List getContextObject() { + return getSelectedNodes(); + } + + @Override + public GTree getSourceComponent() { + return gTree; + } + /** * Returns true if the GTree is not busy * @return boolean true if GTree is not busy @@ -79,9 +92,7 @@ public GTree getTree() { * @return boolean true if there are selected nodes in the browser tree */ public boolean hasSelectedNodes() { - FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); - - return selectedNodes.length > 0; + return !getSelectedNodes().isEmpty(); } /** @@ -90,7 +101,7 @@ public boolean hasSelectedNodes() { * @return list of currently selected tree nodes */ public List getSelectedNodes() { - return List.of((FSBNode[]) getContextObject()); + return (List) super.getContextObject(); } /** @@ -104,27 +115,19 @@ public List getSelectedNodes() { * selected */ public FSRL getFSRL(boolean dirsOk) { - FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); - if (selectedNodes.length != 1) { + List selectedNodes = getSelectedNodes(); + if (selectedNodes.size() != 1) { return null; } - FSBNode node = selectedNodes[0]; + FSBNode node = selectedNodes.get(0); FSRL fsrl = node.getFSRL(); - if (!dirsOk && node instanceof FSBRootNode && fsrlHasContainer(fsrl.getFS())) { + if (!dirsOk && node instanceof FSBRootNode fsRootNode && + fsRootNode.getContainer() != null) { // 'convert' a file system root node back into its container file - return fsrl.getFS().getContainer(); - } - - boolean isDir = (node instanceof FSBDirNode) || (node instanceof FSBRootNode); - if (isDir && !dirsOk) { - return null; + return fsRootNode.getContainer(); } - return fsrl; - } - - private boolean fsrlHasContainer(FSRLRoot fsFSRL) { - return fsFSRL.hasContainer() && !fsFSRL.getProtocol().equals(LocalFileSystem.FSTYPE); + return node.isLeaf() || dirsOk ? fsrl : null; } /** @@ -132,10 +135,9 @@ private boolean fsrlHasContainer(FSRLRoot fsFSRL) { * @return boolean true if the currently selected items are all directory items */ public boolean isSelectedAllDirs() { - FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); + List selectedNodes = getSelectedNodes(); for (FSBNode node : selectedNodes) { - boolean isDir = (node instanceof FSBDirNode) || (node instanceof FSBRootNode); - if (!isDir) { + if (node.isLeaf()) { return false; } } @@ -148,25 +150,8 @@ public boolean isSelectedAllDirs() { * @return the currently selected tree node, or null if no nodes or more than 1 node is selected */ public FSBNode getSelectedNode() { - FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); - return selectedNodes.length == 1 ? selectedNodes[0] : null; - } - - /** - * Returns the FSBRootNode that contains the currently selected tree node. - * - * @return FSBRootNode that contains the currently selected tree node, or null nothing - * selected - */ - public FSBRootNode getRootOfSelectedNode() { - return getRootOfNode(getSelectedNode()); - } - - private FSBRootNode getRootOfNode(GTreeNode tmp) { - while (tmp != null && !(tmp instanceof FSBRootNode)) { - tmp = tmp.getParent(); - } - return (tmp instanceof FSBRootNode) ? (FSBRootNode) tmp : null; + List selectedNodes = getSelectedNodes(); + return selectedNodes.size() == 1 ? selectedNodes.get(0) : null; } /** @@ -175,11 +160,10 @@ private FSBRootNode getRootOfNode(GTreeNode tmp) { * @return returns the number of selected nodes in the tree. */ public int getSelectedCount() { - FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); - return selectedNodes.length; + return getSelectedNodes().size(); } - private List getFSRLsFromNodes(FSBNode[] nodes, boolean dirsOk) { + private List getFSRLsFromNodes(List nodes, boolean dirsOk) { List fsrls = new ArrayList<>(); for (FSBNode node : nodes) { FSRL fsrl = node.getFSRL(); @@ -206,7 +190,7 @@ private List getFSRLsFromNodes(FSBNode[] nodes, boolean dirsOk) { * @return list of FSRLs of the currently selected items, maybe empty but never null */ public List getFSRLs(boolean dirsOk) { - FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); + List selectedNodes = getSelectedNodes(); return getFSRLsFromNodes(selectedNodes, dirsOk); } @@ -228,41 +212,6 @@ public FSRL getFileFSRL() { return getFSRL(false); } - /** - * Converts the tree-node hierarchy of the currently selected item into a string path using - * "/" separators. - * - * @return string path of the currently selected tree item - */ - public String getFormattedTreePath() { - FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); - if (selectedNodes.length != 1) { - return null; - } - TreePath treePath = selectedNodes[0].getTreePath(); - StringBuilder path = new StringBuilder(); - for (Object pathElement : treePath.getPath()) { - if (pathElement instanceof FSBNode) { - FSBNode node = (FSBNode) pathElement; - FSRL fsrl = node.getFSRL(); - if (path.length() != 0) { - path.append("/"); - } - String s; - if (fsrl instanceof FSRLRoot) { - s = fsrl.getFS().hasContainer() ? fsrl.getFS().getContainer().getName() - : "/"; - } - else { - s = fsrl.getName(); - } - path.append(s); - } - } - - return path.toString(); - } - /** * Returns the FSRL of the currently selected item, if it is a 'loadable' item. * @@ -271,82 +220,18 @@ public String getFormattedTreePath() { */ public FSRL getLoadableFSRL() { FSBNode node = getSelectedNode(); - if (node == null) { - return null; - } - FSRL fsrl = node.getFSRL(); - if ((node instanceof FSBDirNode) || (node instanceof FSBRootNode)) { - FSBRootNode rootNode = getRootOfSelectedNode(); - GFileSystem fs = rootNode.getFSRef().getFilesystem(); - if (fs instanceof GFileSystemProgramProvider) { - GFile gfile; - try { - gfile = fs.lookup(node.getFSRL().getPath()); - if (gfile != null && - ((GFileSystemProgramProvider) fs).canProvideProgram(gfile)) { - return fsrl; - } - } - catch (IOException e) { - // ignore error and fall thru to normal file handling - } - } - } - if (node instanceof FSBRootNode && fsrl.getFS().hasContainer()) { - // 'convert' a file system root node back into its container file - return fsrl.getFS().getContainer(); - } - return (node instanceof FSBFileNode) ? fsrl : null; + return node != null ? node.getLoadableFSRL() : null; } - /** - * Returns a list of FSRLs of the currently selected loadable items. - * - * @return list of FSRLs of currently selected loadable items, maybe empty but never null - */ - public List getLoadableFSRLs() { - FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); - - List fsrls = new ArrayList<>(); - for (FSBNode node : selectedNodes) { - FSRL fsrl = node.getFSRL(); - - FSRL validated = vaildateFsrl(fsrl, node); - if (validated != null) { - fsrls.add(validated); - continue; - } - else if (node instanceof FSBRootNode && fsrl.getFS().hasContainer()) { - // 'convert' a file system root node back into its container file - fsrls.add(fsrl.getFS().getContainer()); - } - else if (node instanceof FSBFileNode) { - fsrls.add(fsrl); + public boolean hasSelectedLinkedNodes() { + ProjectIndexService projectIndex = getComponentProvider().getProjectIndex(); + for (FSBNode node : getSelectedNodes()) { + DomainFile df = projectIndex.findFirstByFSRL(node.getFSRL()); + if (df != null) { + return true; } } - return fsrls; - } - - private FSRL vaildateFsrl(FSRL fsrl, FSBNode node) { - if ((node instanceof FSBDirNode) || (node instanceof FSBRootNode)) { - FSBRootNode rootNode = getRootOfNode(node); - GFileSystem fs = rootNode.getFSRef().getFilesystem(); - if (fs instanceof GFileSystemProgramProvider) { - GFile gfile; - try { - gfile = fs.lookup(node.getFSRL().getPath()); - if (gfile != null && - ((GFileSystemProgramProvider) fs).canProvideProgram(gfile)) { - return fsrl; - } - } - catch (IOException e) { - // ignore error and return null - } - } - } - - return null; + return false; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java deleted file mode 100644 index 0884965b754..00000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java +++ /dev/null @@ -1,1113 +0,0 @@ -/* ### - * 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.plugins.fsbrowser; - -import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; -import static java.util.Map.*; - -import java.awt.Component; -import java.io.*; -import java.util.*; -import java.util.function.Function; - -import javax.swing.*; - -import org.apache.commons.io.FilenameUtils; - -import docking.action.DockingAction; -import docking.action.builder.ActionBuilder; -import docking.widgets.OptionDialog; -import docking.widgets.SelectFromListDialog; -import docking.widgets.dialogs.MultiLineMessageDialog; -import docking.widgets.filechooser.GhidraFileChooser; -import docking.widgets.filechooser.GhidraFileChooserMode; -import docking.widgets.label.GIconLabel; -import docking.widgets.tree.GTree; -import docking.widgets.tree.GTreeNode; -import ghidra.app.services.ProgramManager; -import ghidra.app.util.bin.ByteProvider; -import ghidra.app.util.importer.LibrarySearchPathManager; -import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.crypto.CachedPasswordProvider; -import ghidra.formats.gfilesystem.crypto.CryptoProviders; -import ghidra.formats.gfilesystem.fileinfo.*; -import ghidra.framework.main.AppInfo; -import ghidra.framework.model.DomainFile; -import ghidra.framework.plugintool.PluginTool; -import ghidra.plugin.importer.ImporterUtilities; -import ghidra.plugin.importer.ProgramMappingService; -import ghidra.plugins.fsbrowser.tasks.GFileSystemExtractAllTask; -import ghidra.plugins.importer.batch.BatchImportDialog; -import ghidra.program.model.listing.Program; -import ghidra.util.*; -import ghidra.util.exception.CancelledException; -import ghidra.util.exception.CryptoException; -import ghidra.util.task.TaskLauncher; -import ghidra.util.task.TaskMonitor; -import utilities.util.FileUtilities; - -/** - * Handles the menu actions for the {@link FileSystemBrowserComponentProvider}. - * - * Visible to just this package. - */ -class FSBActionManager { - private static final int MAX_PROJECT_SIZE_TO_SEARCH_WITHOUT_WARNING_USER = 1000; - private static final int MAX_TEXT_FILE_LEN = 64 * 1024; - - /* package visibility menu actions */ - DockingAction actionImport; - DockingAction actionOpenPrograms; - DockingAction actionOpenFileSystemChooser; - DockingAction actionOpenFileSystemNewWindow; - DockingAction actionOpenFileSystemNested; - DockingAction actionGetInfo; - DockingAction actionListMountedFileSystems; - DockingAction actionViewAsText; - DockingAction actionViewAsImage; - DockingAction actionExportAll; - DockingAction actionExport; - DockingAction actionExpand; - DockingAction actionCollapse; - DockingAction actionImportBatch; - DockingAction actionAddToProgram; - DockingAction actionLibrarySearchPath; - DockingAction actionCloseFileSystem; - DockingAction actionClearCachedPasswords; - /* end package visibility */ - - protected FileSystemBrowserPlugin plugin; - protected FileSystemBrowserComponentProvider provider; - - private GTree gTree; - - private GhidraFileChooser chooserExport; - private GhidraFileChooser chooserExportAll; - - private List actions = new ArrayList<>(); - private FileSystemService fsService = FileSystemService.getInstance(); - - FSBActionManager(FileSystemBrowserPlugin plugin, FileSystemBrowserComponentProvider provider, - GTree gTree) { - - this.plugin = plugin; - this.provider = provider; - - this.gTree = gTree; - - createActions(); - } - - private void createActions() { - actions.add((actionCloseFileSystem = createCloseAction())); - actions.add((actionOpenPrograms = createOpenProgramsAction())); - actions.add((actionImport = createImportAction())); - actions.add((actionImportBatch = createBatchImportAction())); - actions.add((actionAddToProgram = createAddToProgramAction())); - actions.add((actionLibrarySearchPath = createLibrarySearchPathAction())); - actions.add((actionOpenFileSystemNewWindow = createOpenFileSystemNewWindowAction())); - actions.add((actionOpenFileSystemNested = createOpenFileSystemNestedAction())); - actions.add((actionOpenFileSystemChooser = createOpenNewFileSystemAction())); - actions.add((actionExpand = createExpandAllAction())); - actions.add((actionCollapse = createCollapseAllAction())); - actions.add((actionViewAsImage = createViewAsImageAction())); - actions.add((actionViewAsText = createViewAsTextAction())); - actions.add((actionExport = createExportAction())); - actions.add((actionExportAll = createExportAllAction())); - actions.add((actionGetInfo = createGetInfoAction())); - actions.add((actionListMountedFileSystems = createListMountedFilesystemsAction())); - actions.add((actionClearCachedPasswords = createClearCachedPasswordsAction())); - actions.add(createRefreshAction()); - } - - private void removeActions() { - for (DockingAction action : actions) { - plugin.getTool().removeLocalAction(provider, action); - } - } - - public void registerComponentActionsInTool() { - for (DockingAction action : actions) { - plugin.getTool().addLocalAction(provider, action); - } - } - - public void dispose() { - - if (chooserExport != null) { - chooserExport.dispose(); - } - - if (chooserExportAll != null) { - chooserExportAll.dispose(); - } - - removeActions(); - } - - private void openProgramFromFile(FSRL file, FSBNode node, String suggestedDestinationPath) { - ProgramManager pm = FSBUtils.getProgramManager(plugin.getTool(), false); - if (pm == null) { - return; - } - - gTree.runTask(monitor -> { - boolean success = doOpenProgramFromFile(file, suggestedDestinationPath, pm, monitor); - if (!success) { - if (!ensureFileAccessable(file, node, monitor)) { - return; - } - ImporterUtilities.showImportDialog(plugin.getTool(), pm, file, null, - suggestedDestinationPath, monitor); - } - }); - } - - private boolean doOpenProgramFromFile(FSRL fsrl, String suggestedDestinationPath, - ProgramManager programManager, TaskMonitor monitor) { - - Object consumer = new Object(); - Program program = ProgramMappingService.findMatchingProgramOpenIfNeeded(fsrl, consumer, - programManager, ProgramManager.OPEN_CURRENT); - - if (program != null) { - program.release(consumer); - return true; - } - - return searchProjectForMatchingFileOrFail(fsrl, suggestedDestinationPath, programManager, - monitor); - } - - private boolean searchProjectForMatchingFileOrFail(FSRL fsrl, String suggestedDestinationPath, - ProgramManager programManager, TaskMonitor monitor) { - boolean doSearch = isProjectSmallEnoughToSearchWithoutWarningUser() || - OptionDialog.showYesNoDialog(null, "Search Project for matching program?", - "Search entire Project for matching program? (WARNING, could take large amount of time)") == OptionDialog.YES_OPTION; - - Map matchedFSRLs = - doSearch ? ProgramMappingService.searchProjectForMatchingFiles(List.of(fsrl), monitor) - : Map.of(); - - DomainFile domainFile = matchedFSRLs.get(fsrl); - if (domainFile != null) { - ProgramMappingService.createAssociation(fsrl, domainFile); - showProgramInProgramManager(fsrl, domainFile, programManager, true); - return true; - } - return false; - } - - /** - * Helper function to let {@link FileSystemBrowserComponentProvider fsb components} - * open selected files in a code browser that this plugin is tracking. - *

- * If there is no {@link ProgramManager} associated with the current tool, one will - * be searched for and the user may be prompted for confirmation, or warned if - * no PM found. - * - * @param files List of {@link FSRL} files to open in the active {@link ProgramManager}. - */ - private void openProgramsFromFiles(List files) { - ProgramManager pm = FSBUtils.getProgramManager(plugin.getTool(), false); - if (pm == null) { - return; - } - - gTree.runTask(monitor -> { - List unmatchedFiles = doOpenProgramsFromFiles(files, pm, monitor); - - if (unmatchedFiles.size() == 1) { - ImporterUtilities.showImportDialog(plugin.getTool(), pm, unmatchedFiles.get(0), - null, null, monitor); - } - else if (unmatchedFiles.size() > 1) { - BatchImportDialog.showAndImport(plugin.getTool(), null, unmatchedFiles, null, pm); - } - }); - - } - - /** - * Opens the Ghidra {@link Program}s that were previously imported from the specified - * {@link FSRL files}. - *

- * Relies on {@link ProgramMappingService#findMatchingProgramOpenIfNeeded(FSRL, Object, ProgramManager, int)} - * and if that fails, will search the entire project for the file if the user so chooses. - * - * @param fsrls {@link List} of {@link FSRL}s of the files to search for. - * @param programManager {@link ProgramManager} to use to open the programs, null ok. - * @param monitor {@link TaskMonitor} to watch for cancel and update with progress. - * @return list of unmatched files that need to be imported - */ - private List doOpenProgramsFromFiles(List fsrls, ProgramManager programManager, - TaskMonitor monitor) { - - int programsOpened = 0; - List unmatchedFiles = new ArrayList<>(); - Object consumer = new Object(); - for (FSRL fsrl : fsrls) { - Program program = ProgramMappingService.findMatchingProgramOpenIfNeeded(fsrl, consumer, - programManager, - (programsOpened == 0) ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE); - - if (program == null) { - unmatchedFiles.add(fsrl); - continue; - } - - program.release(consumer); - programsOpened++; - } - - // UnmatchedFiles contains any files that had no association to a Program - // Give the user a chance to search the project for it, and import it if not found - if (!unmatchedFiles.isEmpty()) { - unmatchedFiles = searchProjectForMatchingFilesOrFail(unmatchedFiles, programManager, - monitor, programsOpened); - } - - return unmatchedFiles; - } - - private List searchProjectForMatchingFilesOrFail(List fsrlList, - ProgramManager programManager, TaskMonitor monitor, int programsOpened) { - boolean doSearch = isProjectSmallEnoughToSearchWithoutWarningUser() || - OptionDialog.showYesNoDialog(null, "Search Project for matching programs?", - "Search entire Project for matching programs? " + - "(WARNING, could take large amount of time)") == OptionDialog.YES_OPTION; - - Map matchedFSRLs = - doSearch ? ProgramMappingService.searchProjectForMatchingFiles(fsrlList, monitor) - : Map.of(); - - List unmatchedFSRLs = new ArrayList<>(); - for (FSRL fsrl : fsrlList) { - DomainFile domainFile = matchedFSRLs.get(fsrl); - if (domainFile != null) { - ProgramMappingService.createAssociation(fsrl, domainFile); - } - if (showProgramInProgramManager(fsrl, domainFile, programManager, - programsOpened == 0)) { - programsOpened++; - } - else { - unmatchedFSRLs.add(fsrl); - } - } - - return unmatchedFSRLs; - } - - private boolean isProjectSmallEnoughToSearchWithoutWarningUser() { - int fc = AppInfo.getActiveProject().getProjectData().getFileCount(); - return fc >= 0 && fc < MAX_PROJECT_SIZE_TO_SEARCH_WITHOUT_WARNING_USER; - } - - private boolean showProgramInProgramManager(FSRL fsrl, DomainFile domainFile, - ProgramManager programManager, boolean show) { - Program program = null; - Object consumer = new Object(); - try { - program = ProgramMappingService.findMatchingProgramOpenIfNeeded(fsrl, domainFile, - consumer, programManager, - show ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE); - return (program != null); - } - finally { - if (program != null) { - program.release(consumer); - } - } - - } - - //---------------------------------------------------------------------------------- - // DockingActions - //---------------------------------------------------------------------------------- - private DockingAction createExportAction() { - return new ActionBuilder("FSB Export", plugin.getName()).withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getFileFSRL() != null) - .popupMenuIcon(ImageManager.EXTRACT) - .popupMenuPath("Export...") - .popupMenuGroup("F", "C") - .onAction(ac -> { - FSRL fsrl = ac.getFileFSRL(); - if (fsrl == null) { - return; - } - if (chooserExport == null) { - chooserExport = new GhidraFileChooser(provider.getComponent()); - chooserExport.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY); - chooserExport.setTitle("Select Where To Export File"); - chooserExport.setApproveButtonText("Export"); - } - File selectedFile = - new File(chooserExport.getCurrentDirectory(), fsrl.getName()); - chooserExport.setSelectedFile(selectedFile); - File outputFile = chooserExport.getSelectedFile(); - if (outputFile == null) { - return; - } - if (outputFile.exists()) { - int answer = OptionDialog.showYesNoDialog(provider.getComponent(), - "Confirm Overwrite", outputFile.getAbsolutePath() + "\n" + - "The file already exists.\n" + "Do you want to overwrite it?"); - if (answer == OptionDialog.NO_OPTION) { - return; - } - } - gTree.runTask( - monitor -> doExtractFile(fsrl, outputFile, ac.getSelectedNode(), monitor)); - }) - .build(); - } - - private DockingAction createExportAllAction() { - return new ActionBuilder("FSB Export All", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.isSelectedAllDirs()) - .popupMenuIcon(ImageManager.EXTRACT) - .popupMenuPath("Export All...") - .popupMenuGroup("F", "C") - .onAction(ac -> { - FSRL fsrl = ac.getFSRL(true); - if (fsrl == null) { - return; - } - if (fsrl instanceof FSRLRoot) { - fsrl = fsrl.appendPath("/"); - } - if (chooserExportAll == null) { - chooserExportAll = new GhidraFileChooser(provider.getComponent()); - chooserExportAll - .setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); - chooserExportAll.setTitle("Select Export Directory"); - chooserExportAll.setApproveButtonText("Export All"); - } - chooserExportAll.setSelectedFile(null); - File outputFile = chooserExportAll.getSelectedFile(); - if (outputFile == null) { - return; - } - - if (!outputFile.isDirectory()) { - Msg.showInfo(this, provider.getComponent(), "Export All", - "Selected file is not a directory."); - return; - } - Component parentComp = plugin.getTool().getActiveWindow(); - TaskLauncher - .launch(new GFileSystemExtractAllTask(fsrl, outputFile, parentComp)); - }) - .build(); - } - - private DockingAction createViewAsImageAction() { - return new ActionBuilder("FSB View As Image", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getFileFSRL() != null) - .popupMenuIcon(ImageManager.VIEW_AS_IMAGE) - .popupMenuPath("View As Image") - .popupMenuGroup("G") - .onAction(ac -> { - FSRL fsrl = ac.getFileFSRL(); - if (fsrl != null) { - gTree.runTask( - monitor -> doViewAsImage(fsrl, ac.getSelectedNode(), monitor)); - } - }) - .build(); - } - - private DockingAction createViewAsTextAction() { - return new ActionBuilder("FSB View As Text", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getFileFSRL() != null) - .popupMenuIcon(ImageManager.VIEW_AS_TEXT) - .popupMenuPath("View As Text") - .popupMenuGroup("G") - .onAction(ac -> { - FSRL fsrl = ac.getFileFSRL(); - if (fsrl != null) { - gTree.runTask(monitor -> doViewAsText(fsrl, ac.getSelectedNode(), monitor)); - } - }) - .build(); - } - - private DockingAction createListMountedFilesystemsAction() { - return new ActionBuilder("FSB List Mounted Filesystems", plugin.getName()) - .description("List Mounted Filesystems") - .withContext(FSBActionContext.class) - .enabledWhen(FSBActionContext::notBusy) - .toolBarIcon(ImageManager.LIST_MOUNTED) - .toolBarGroup("ZZZZ") - .popupMenuIcon(ImageManager.LIST_MOUNTED) - .popupMenuPath("List Mounted Filesystems") - .popupMenuGroup("L") - .onAction(ac -> { - FSRLRoot fsFSRL = SelectFromListDialog.selectFromList( - fsService.getMountedFilesystems(), "Select filesystem", - "Choose filesystem to view", f -> f.toPrettyString()); - - FileSystemRef fsRef; - if (fsFSRL != null && - (fsRef = fsService.getMountedFilesystem(fsFSRL)) != null) { - plugin.createNewFileSystemBrowser(fsRef, true); - } - }) - .build(); - } - - private DockingAction createExpandAllAction() { - return new ActionBuilder("FSB Expand All", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen( - ac -> ac.notBusy() && ac.getSelectedCount() == 1 && ac.isSelectedAllDirs()) - .popupMenuIcon(ImageManager.EXPAND_ALL) - .popupMenuPath("Expand All") - .popupMenuGroup("B", "A") - .onAction(ac -> { - FSBNode selectedNode = ac.getSelectedNode(); - if (selectedNode != null) { - gTree.expandTree(selectedNode); - } - }) - .build(); - } - - private DockingAction createCollapseAllAction() { - return new ActionBuilder("FSB Collapse All", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen( - ac -> ac.notBusy() && ac.getSelectedCount() == 1 && ac.isSelectedAllDirs()) - .popupMenuIcon(ImageManager.COLLAPSE_ALL) - .popupMenuPath("Collapse All") - .popupMenuGroup("B", "B") - .onAction(ac -> { - FSBNode selectedNode = ac.getSelectedNode(); - if (selectedNode != null) { - gTree.collapseAll(selectedNode); - } - }) - .build(); - } - - private DockingAction createGetInfoAction() { - return new ActionBuilder("FSB Get Info", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getFSRL(true) != null) - .popupMenuPath("Get Info") - .popupMenuGroup("A", "A") - .popupMenuIcon(ImageManager.INFO) - .description("Show information about a file") - .onAction(ac -> { - FSRL fsrl = ac.getFSRL(true); - gTree.runTask(monitor -> showInfoForFile(fsrl, monitor)); - }) - .build(); - } - - private DockingAction createOpenFileSystemNewWindowAction() { - return new ActionBuilder("FSB Open File System In New Window", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getSelectedNode() instanceof FSBFileNode) - .popupMenuIcon(ImageManager.OPEN_FILE_SYSTEM) - .popupMenuPath("Open File System in new window") - .popupMenuGroup("C") - .onAction(ac -> { - if (!(ac.getSelectedNode() instanceof FSBFileNode) || - ac.getSelectedNode().getFSRL() == null) { - return; - } - FSBFileNode selectedNode = (FSBFileNode) ac.getSelectedNode(); - FSRL containerFSRL = selectedNode.getFSRL(); - if (containerFSRL != null) { - gTree.runTask(monitor -> { - doOpenFileSystem(containerFSRL, selectedNode, false, monitor); - }); - } - }) - .build(); - } - - private DockingAction createOpenFileSystemNestedAction() { - return new ActionBuilder("FSB Open File System Nested", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getSelectedNode() instanceof FSBFileNode) - .popupMenuIcon(ImageManager.OPEN_FILE_SYSTEM) - .popupMenuPath("Open File System") - .popupMenuGroup("C") - .onAction(ac -> { - if (!(ac.getSelectedNode() instanceof FSBFileNode) || - ac.getSelectedNode().getFSRL() == null) { - return; - } - FSBFileNode selectedNode = (FSBFileNode) ac.getSelectedNode(); - FSRL containerFSRL = selectedNode.getFSRL(); - if (containerFSRL != null) { - gTree.runTask(monitor -> { - doOpenFileSystem(containerFSRL, selectedNode, true, monitor); - }); - } - }) - .build(); - } - - private DockingAction createOpenNewFileSystemAction() { - return new ActionBuilder("FSB Open File System Chooser", plugin.getName()) - .description("Open File System Chooser") - .withContext(FSBActionContext.class) - .enabledWhen(FSBActionContext::notBusy) - .toolBarIcon(ImageManager.OPEN_FILE_SYSTEM) - .toolBarGroup("B") - .onAction(ac -> plugin.openFileSystem()) - .build(); - } - - private DockingAction createOpenProgramsAction() { - return new ActionBuilder("FSB Open Programs", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && plugin.hasProgramManager() && - !ac.getLoadableFSRLs().isEmpty()) - .popupMenuIcon(ImageManager.OPEN_ALL) - .popupMenuPath("Open Program(s)") - .popupMenuGroup("D", "B") - .onAction(ac -> { - if (!plugin.hasProgramManager()) { - Msg.showInfo(this, plugin.getTool().getActiveWindow(), "Open Program Error", - "There is no tool currently open that can be used to show a program."); - return; - } - List files = ac.getLoadableFSRLs(); - if (files.size() == 1) { - String treePath = - FilenameUtils.getFullPathNoEndSeparator(ac.getFormattedTreePath()); - openProgramFromFile(files.get(0), ac.getSelectedNodes().get(0), treePath); - } - else if (files.size() > 1) { - openProgramsFromFiles(files); - } - }) - .build(); - } - - private DockingAction createCloseAction() { - return new ActionBuilder("FSB Close", plugin.getName()).withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getSelectedNode() instanceof FSBRootNode) - .description("Close") - .toolBarIcon(ImageManager.CLOSE) - .toolBarGroup("ZZZZ") - .popupMenuIcon(ImageManager.CLOSE) - .popupMenuPath("Close") - .popupMenuGroup("ZZZZ") - .onAction(ac -> { - FSBNode selectedNode = ac.getSelectedNode(); - if (!(selectedNode instanceof FSBRootNode)) { - return; - } - FSBRootNode node = (FSBRootNode) selectedNode; - if (node.getParent() == null) { - // Close entire window - if (OptionDialog.showYesNoDialog(provider.getComponent(), - "Close File System", - "Do you want to close the filesystem browser for " + node.getName() + - "?") == OptionDialog.YES_OPTION) { - provider.componentHidden(); // cause component to close itself - } - } - else { - // Close file system that is nested in the container's tree and swap - // in the saved node that was the original container file - gTree.runTask(monitor -> node.swapBackPrevModelNodeAndDispose()); - } - }) - .build(); - } - - private DockingAction createImportAction() { - return new ActionBuilder("FSB Import Single", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getLoadableFSRL() != null) - .popupMenuIcon(ImageManager.IMPORT) - .popupMenuPath("Import") - .popupMenuGroup("F", "A") - .onAction(ac -> { - FSRL fsrl = ac.getLoadableFSRL(); - if (fsrl == null) { - return; - } - - String treePath = ac.getFormattedTreePath(); - String suggestedPath = - FilenameUtils.getFullPathNoEndSeparator(treePath).replaceAll(":/", "/"); - - PluginTool tool = plugin.getTool(); - ProgramManager pm = FSBUtils.getProgramManager(tool, false); - - gTree.runTask(monitor -> { - if (!ensureFileAccessable(fsrl, ac.getSelectedNode(), monitor)) { - return; - } - ImporterUtilities.showImportDialog(tool, pm, fsrl, null, suggestedPath, - monitor); - }); - }) - .build(); - } - - private DockingAction createBatchImportAction() { - return new ActionBuilder("FSB Import Batch", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getSelectedCount() > 0) - .popupMenuIcon(ImageManager.IMPORT) - .popupMenuPath("Batch Import") - .popupMenuGroup("F", "B") - .onAction(ac -> { - // Do some fancy selection logic. - // If the user selected a combination of files and folders, - // ignore the folders. - // If they only selected folders, leave them in the list. - List files = ac.getFSRLs(true); - if (files.isEmpty()) { - return; - } - - boolean allDirs = ac.isSelectedAllDirs(); - if (files.size() > 1 && !allDirs) { - files = ac.getFileFSRLs(); - } - - BatchImportDialog.showAndImport(plugin.getTool(), null, files, null, - FSBUtils.getProgramManager(plugin.getTool(), false)); - }) - .build(); - } - - private DockingAction createAddToProgramAction() { - return new ActionBuilder("FSB Add To Program", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getLoadableFSRL() != null) - .popupMenuIcon(ImageManager.IMPORT) - .popupMenuPath("Add To Program") - .popupMenuGroup("F", "C") - .onAction(ac -> { - FSRL fsrl = ac.getLoadableFSRL(); - if (fsrl == null) { - return; - } - - gTree.runTask(monitor -> { - if (!ensureFileAccessable(fsrl, ac.getSelectedNode(), monitor)) { - return; - } - - PluginTool tool = plugin.getTool(); - ProgramManager pm = FSBUtils.getProgramManager(tool, false); - Program program = null; - if (pm == null || (program = pm.getCurrentProgram()) == null) { - Msg.showError(this, gTree, "Unable To Add To Program", - "No programs are open"); - return; - } - ImporterUtilities.showAddToProgramDialog(fsrl, program, tool, monitor); - }); - - }) - .build(); - } - - private DockingAction createLibrarySearchPathAction() { - return new ActionBuilder("FSB Add Library Search Path", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getFSRL(true) != null) - .popupMenuPath("Add Library Search Path") - .popupMenuGroup("F", "D") - .popupMenuIcon(ImageManager.LIBRARY) - .description("Add file/folder to library search paths") - .onAction(ac -> { - try { - FSRL fsrl = ac.getFSRL(true); - LocalFileSystem localFs = fsService.getLocalFS(); - String path = fsService.isLocal(fsrl) ? localFs.getLocalFile(fsrl).getPath() - : fsrl.toString(); - if (LibrarySearchPathManager.addPath(path)) { - Msg.showInfo(this, gTree, "Add Library Search Path", - "Added '%s' to library search paths.".formatted(fsrl)); - } - else { - Msg.showInfo(this, gTree, "Add Library Search Path", - "Library search path '%s' already exists.".formatted(fsrl)); - } - } - catch (IOException e) { - Msg.showError(this, gTree, "Add Library Search Path", e); - } - }) - .build(); - } - - private DockingAction createClearCachedPasswordsAction() { - return new ActionBuilder("FSB Clear Cached Passwords", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(FSBActionContext::notBusy) - .popupMenuPath("Clear Cached Passwords") - .popupMenuGroup("Z", "B") - .description("Clear cached container file passwords") - .onAction(ac -> { - CachedPasswordProvider ccp = - CryptoProviders.getInstance().getCachedCryptoProvider(); - int preCount = ccp.getCount(); - ccp.clearCache(); - Msg.info(this, "Cleared " + (preCount - ccp.getCount()) + " cached passwords."); - }) - .build(); - } - - private DockingAction createRefreshAction() { - return new ActionBuilder("FSB Refresh", plugin.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.hasSelectedNodes()) - .popupMenuPath("Refresh") - .popupMenuGroup("Z", "Z") - .description("Refresh file info") - .onAction( - ac -> gTree.runTask(monitor -> doRefreshInfo(ac.getSelectedNodes(), monitor))) - .build(); - } - - //---------------------------------------------------------------------------------- - // end DockingActions - //---------------------------------------------------------------------------------- - - private void doExtractFile(FSRL fsrl, File outputFile, FSBNode node, TaskMonitor monitor) { - if (!ensureFileAccessable(fsrl, node, monitor)) { - return; - } - monitor.setMessage("Exporting..."); - try (ByteProvider fileBP = fsService.getByteProvider(fsrl, false, monitor)) { - long bytesCopied = FSUtilities.copyByteProviderToFile(fileBP, outputFile, monitor); - Msg.info(this, "Exported " + fsrl.getName() + " to " + outputFile + ", " + bytesCopied + - " bytes copied."); - } - catch (IOException | CancelledException | UnsupportedOperationException e) { - FSUtilities.displayException(this, plugin.getTool().getActiveWindow(), - "Error Exporting File", e.getMessage(), e); - } - } - - /* - * run on gTree task thread - */ - private void doOpenFileSystem(FSRL containerFSRL, FSBFileNode node, boolean nested, - TaskMonitor monitor) { - try { - if (!ensureFileAccessable(containerFSRL, node, monitor)) { - return; - } - - monitor.setMessage("Probing " + containerFSRL.getName() + " for filesystems"); - FileSystemRef ref = fsService.probeFileForFilesystem(containerFSRL, monitor, - FileSystemProbeConflictResolver.GUI_PICKER); - if (ref == null) { - Msg.showWarn(this, plugin.getTool().getActiveWindow(), "Open Filesystem", - "No filesystem detected in " + containerFSRL.getName()); - return; - } - - Swing.runLater(() -> { - if (nested) { - FSBFileNode modelFileNode = - (FSBFileNode) gTree.getModelNodeForPath(node.getTreePath()); - - FSBRootNode nestedRootNode = new FSBRootNode(ref, modelFileNode); - try { - nestedRootNode.setChildren(nestedRootNode.generateChildren(monitor)); - } - catch (CancelledException e) { - Msg.warn(this, "Failed to populate FSB root node with children"); - } - - int indexInParent = modelFileNode.getIndexInParent(); - GTreeNode parent = modelFileNode.getParent(); - parent.removeNode(modelFileNode); - parent.addNode(indexInParent, nestedRootNode); - gTree.expandPath(nestedRootNode); - provider.contextChanged(); - } - else { - plugin.createNewFileSystemBrowser(ref, true); - } - }); - } - catch (IOException | CancelledException e) { - FSUtilities.displayException(this, plugin.getTool().getActiveWindow(), - "Open Filesystem", "Error opening filesystem for " + containerFSRL.getName(), e); - } - } - - private void doViewAsImage(FSRL fsrl, FSBNode node, TaskMonitor monitor) { - if (!ensureFileAccessable(fsrl, node, monitor)) { - return; - } - - Component parent = plugin.getTool().getActiveWindow(); - try (RefdFile refdFile = fsService.getRefdFile(fsrl, monitor)) { - - Icon icon = GIconProvider.getIconForFile(refdFile.file, monitor); - if (icon == null) { - Msg.showError(this, parent, "Unable To View Image", - "Unable to view " + fsrl.getName() + " as an image."); - return; - } - Swing.runLater(() -> { - JLabel label = new GIconLabel(icon); - JOptionPane.showMessageDialog(null, label, "Image Viewer: " + fsrl.getName(), - JOptionPane.INFORMATION_MESSAGE); - }); - } - catch (IOException | CancelledException e) { - FSUtilities.displayException(this, parent, "Error Viewing Image File", e.getMessage(), - e); - } - } - - private void doViewAsText(FSRL fsrl, FSBNode node, TaskMonitor monitor) { - if (!ensureFileAccessable(fsrl, node, monitor)) { - return; - } - - Component parent = plugin.getTool().getActiveWindow(); - try (ByteProvider fileBP = fsService.getByteProvider(fsrl, false, monitor)) { - - if (fileBP.length() > MAX_TEXT_FILE_LEN) { - Msg.showInfo(this, parent, "View As Text Failed", - "File too large to view as text inside Ghidra. " + - "Please use the \"EXPORT\" action."); - return; - } - if (fileBP.length() == 0) { - Msg.showInfo(this, parent, "View As Text Failed", - "File " + fsrl.getName() + " is empty (0 bytes)."); - return; - } - - try { - ByteArrayInputStream bais = - new ByteArrayInputStream(fileBP.readBytes(0, fileBP.length())); - - String text = FileUtilities.getText(bais); - Swing.runLater(() -> { - new TextEditorComponentProvider(fsrl.getName(), text); - }); - } - catch (IOException e) { - Msg.showError(this, parent, "View As Text Failed", - "Error when trying to view text file " + fsrl.getName(), e); - } - } - catch (IOException | CancelledException e) { - FSUtilities.displayException(this, parent, "Error Viewing Text File", e.getMessage(), - e); - } - } - - void doRefreshInfo(List nodes, TaskMonitor monitor) { - Set rootNodes = new HashSet<>(); - for (FSBNode node : nodes) { - if (node instanceof FSBFileNode) { - // for each file node, if it's password attr info is out of date, build a unique - // list of the containing root nodes that will later be used to refresh the - // entire FS - if (((FSBFileNode) node).needsFileAttributesUpdate(monitor)) { - rootNodes.add(node.getFSBRootNode()); - } - } - else if (node instanceof FSBDirNode) { - // if the user selected a dir node, force the FS to be refreshed - rootNodes.add(node.getFSBRootNode()); - } - else if (node instanceof FSBRootNode) { - rootNodes.add((FSBRootNode) node); - } - } - try { - for (FSBRootNode rootNode : rootNodes) { - rootNode.updateFileAttributes(monitor); - } - gTree.refilterLater(); // force the changed modelNodes to be recloned and displayed (if filter active) - } - catch (CancelledException e) { - // stop - } - Swing.runLater(() -> gTree.repaint()); - } - - private boolean ensureFileAccessable(FSRL fsrl, FSBNode node, TaskMonitor monitor) { - - FSBFileNode fileNode = (node instanceof FSBFileNode) ? (FSBFileNode) node : null; - - monitor.initialize(0); - monitor.setMessage("Testing file access"); - boolean wasMissingPasword = (fileNode != null) ? fileNode.hasMissingPassword() : false; - try (ByteProvider bp = fsService.getByteProvider(fsrl, false, monitor)) { - // if we can get here and it used to have a missing password, update the node's status - if (fileNode != null && wasMissingPasword) { - doRefreshInfo(List.of(fileNode), monitor); - } - return true; - } - catch (CryptoException e) { - Msg.showWarn(this, gTree, "Crypto / Password Error", - "Unable to access the specified file.\n" + - "This could be caused by not entering the correct password or because of missing crypto information.\n\n" + - e.getMessage()); - return false; - } - catch (IOException e) { - Msg.showError(this, gTree, "File IO Error", - "Unable to access the specified file.\n\n" + e.getMessage(), e); - return false; - } - catch (CancelledException e) { - return false; - } - - } - - //--------------------------------------------------------------------------------------------- - // static lookup tables for rendering file attributes - //--------------------------------------------------------------------------------------------- - private static final Function PLAIN_TOSTRING = o -> o.toString(); - private static final Function SIZE_TOSTRING = - o -> (o instanceof Long) ? FSUtilities.formatSize((Long) o) : o.toString(); - private static final Function UNIX_ACL_TOSTRING = - o -> (o instanceof Number) ? String.format("%05o", (Number) o) : o.toString(); - private static final Function DATE_TOSTRING = - o -> (o instanceof Date) ? FSUtilities.formatFSTimestamp((Date) o) : o.toString(); - private static final Function FSRL_TOSTRING = - o -> (o instanceof FSRL) ? ((FSRL) o).toPrettyString().replace("|", "|\n\t") : o.toString(); - - private static final Map> FAT_TOSTRING_FUNCS = - Map.ofEntries(entry(FSRL_ATTR, FSRL_TOSTRING), entry(SIZE_ATTR, SIZE_TOSTRING), - entry(COMPRESSED_SIZE_ATTR, SIZE_TOSTRING), entry(CREATE_DATE_ATTR, DATE_TOSTRING), - entry(MODIFIED_DATE_ATTR, DATE_TOSTRING), entry(ACCESSED_DATE_ATTR, DATE_TOSTRING), - entry(UNIX_ACL_ATTR, UNIX_ACL_TOSTRING)); - - /** - * Shows a dialog with information about the specified file. - * - * @param fsrl {@link FSRL} of the file to display info about. - * @param monitor {@link TaskMonitor} to monitor and update when accessing the filesystems. - */ - private void showInfoForFile(FSRL fsrl, TaskMonitor monitor) { - if (fsrl == null) { - Msg.showError(this, null, "Missing File", "Unable to retrieve information"); - return; - } - - // if looking at the root of a nested file system, also include its parent container - List fsrls = (fsrl instanceof FSRLRoot && ((FSRLRoot) fsrl).hasContainer()) - ? List.of(((FSRLRoot) fsrl).getContainer(), fsrl) - : List.of(fsrl); - String title = "Info about " + fsrls.get(0).getName(); - List fattrs = new ArrayList<>(); - for (FSRL fsrl2 : fsrls) { - try { - fattrs.add(getAttrsFor(fsrl2, monitor)); - } - catch (IOException e) { - Msg.warn(this, "Failed to get info for file " + fsrl2, e); - } - catch (CancelledException e) { - return; - } - } - String html = getHTMLInfoStringForAttributes(fattrs); - - MultiLineMessageDialog.showMessageDialog(plugin.getTool().getActiveWindow(), title, null, - html, MultiLineMessageDialog.INFORMATION_MESSAGE); - } - - private FileAttributes getAttrsFor(FSRL fsrl, TaskMonitor monitor) - throws CancelledException, IOException { - try (RefdFile refdFile = fsService.getRefdFile(fsrl, monitor)) { - GFileSystem fs = refdFile.fsRef.getFilesystem(); - GFile file = refdFile.file; - FileAttributes fattrs = fs.getFileAttributes(file, monitor); - if (fattrs == null) { - fattrs = FileAttributes.EMPTY; - } - fattrs = fattrs.clone(); - DomainFile associatedDomainFile = ProgramMappingService.getCachedDomainFileFor(fsrl); - if (associatedDomainFile != null) { - fattrs.add(PROJECT_FILE_ATTR, associatedDomainFile.getPathname()); - } - - if (!fattrs.contains(NAME_ATTR)) { - fattrs.add(NAME_ATTR, file.getName()); - } - if (!fattrs.contains(PATH_ATTR)) { - fattrs.add(PATH_ATTR, FilenameUtils.getFullPath(file.getPath())); - } - if (!fattrs.contains(FSRL_ATTR)) { - fattrs.add(FSRL_ATTR, file.getFSRL()); - } - return fattrs; - } - } - - private String getHTMLInfoStringForAttributes(List fileAttributesList) { - StringBuilder sb = new StringBuilder("\n\n"); - sb.append("\n"); - for (FileAttributes fattrs : fileAttributesList) { - if (fattrs != fileAttributesList.get(0)) { - // not first element, put a visual divider line - sb.append(""); - } - List> sortedAttribs = fattrs.getAttributes(); - Collections.sort(sortedAttribs, (o1, o2) -> Integer - .compare(o1.getAttributeType().ordinal(), o2.getAttributeType().ordinal())); - - FileAttributeTypeGroup group = null; - for (FileAttribute attr : sortedAttribs) { - if (attr.getAttributeType().getGroup() != group) { - group = attr.getAttributeType().getGroup(); - if (group != FileAttributeTypeGroup.GENERAL_INFO) { - sb.append("\n"); - } - } - String valStr = - FAT_TOSTRING_FUNCS.getOrDefault(attr.getAttributeType(), PLAIN_TOSTRING) - .apply(attr.getAttributeValue()); - - String html = HTMLUtilities.escapeHTML(valStr); - html = html.replace("\n", "
\n"); - sb.append("\n"); - } - } - sb.append("
PropertyValue

") - .append(group.getDescriptiveName()) - .append("
") - .append(attr.getAttributeDisplayName()) - .append(":") - .append(html) - .append("
"); - return sb.toString(); - } - -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBComponentProvider.java new file mode 100644 index 00000000000..d5b8e32b55d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBComponentProvider.java @@ -0,0 +1,576 @@ +/* ### + * 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.plugins.fsbrowser; + +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + +import java.awt.Component; +import java.awt.event.MouseEvent; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import docking.*; +import docking.action.DockingAction; +import docking.action.DockingActionIf; +import docking.actions.PopupActionProvider; +import docking.event.mouse.GMouseListenerAdapter; +import docking.widgets.tree.GTree; +import docking.widgets.tree.GTreeNode; +import docking.widgets.tree.support.GTreeRenderer; +import generic.theme.GThemeDefaults.Colors.Palette; +import ghidra.app.services.ProgramManager; +import ghidra.app.util.bin.ByteProvider; +import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; +import ghidra.framework.model.*; +import ghidra.framework.plugintool.ComponentProviderAdapter; +import ghidra.plugin.importer.ImporterUtilities; +import ghidra.plugin.importer.ProjectIndexService; +import ghidra.program.model.listing.Program; +import ghidra.util.*; +import ghidra.util.classfinder.ClassSearcher; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.CryptoException; +import ghidra.util.task.MonitoredRunnable; +import ghidra.util.task.TaskMonitor; + +/** + * Plugin component provider for the {@link FileSystemBrowserPlugin}. + *

+ * An instance of this class is created for each file system browser window (w/tree). + *

+ * See the {@link FSBFileHandler} interface for how to add actions to this component. + */ +public class FSBComponentProvider extends ComponentProviderAdapter + implements FileSystemEventListener, PopupActionProvider { + private static final String TITLE = "Filesystem Viewer"; + + private FSBIcons fsbIcons = FSBIcons.getInstance(); + private FileSystemService fsService = FileSystemService.getInstance(); + private ProjectIndexService projectIndex = ProjectIndexService.getInstance(); + + private FileSystemBrowserPlugin plugin; + private GTree gTree; + private FSBRootNode rootNode; + private List fileHandlers = List.of(); + private ProgramManager pm; + + /** + * Creates a new {@link FSBComponentProvider} instance, taking + * ownership of the passed-in {@link FileSystemRef fsRef}. + * + * @param plugin parent plugin + * @param fsRef {@link FileSystemRef} to a {@link GFileSystem}. + */ + public FSBComponentProvider(FileSystemBrowserPlugin plugin, FileSystemRef fsRef) { + super(plugin.getTool(), fsRef.getFilesystem().getName(), plugin.getName()); + + this.plugin = plugin; + this.rootNode = new FSBRootNode(fsRef); + this.pm = plugin.getTool().getService(ProgramManager.class); + + setTransient(); + setIcon(FSBIcons.PHOTO); + + initTree(); + fsRef.getFilesystem().getRefManager().addListener(this); + initFileHandlers(); + + setHelpLocation( + new HelpLocation("FileSystemBrowserPlugin", "FileSystemBrowserIntroduction")); + + } + + void initFileHandlers() { + FSBFileHandlerContext context = + new FSBFileHandlerContext(plugin, this, fsService, projectIndex); + fileHandlers = ClassSearcher.getInstances(FSBFileHandler.class); + for (FSBFileHandler fileHandler : fileHandlers) { + fileHandler.init(context); + } + fileHandlers.add(new DefaultFileHandler()); + plugin.getTool().addPopupActionProvider(this); // delegate to fileHandler's getPopupProviderActions() + } + + void initTree() { + gTree = new GTree(rootNode); + gTree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); + gTree.getSelectionModel().addTreeSelectionListener(e -> { + tool.contextChanged(FSBComponentProvider.this); + TreePath[] paths = gTree.getSelectionPaths(); + if (paths.length == 1) { + GTreeNode clickedNode = (GTreeNode) paths[0].getLastPathComponent(); + handleSingleClick(clickedNode); + } + }); + gTree.addMouseListener(new GMouseListenerAdapter() { + @Override + public void doubleClickTriggered(MouseEvent e) { + if (handleDoubleClick(gTree.getNodeForLocation(e.getX(), e.getY()))) { + e.consume(); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + super.mouseClicked(e); + if (!e.isConsumed()) { + handleSingleClick(gTree.getNodeForLocation(e.getX(), e.getY())); + } + } + }); + gTree.setCellRenderer(new GTreeRenderer() { + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, + boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { + + super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, + hasFocus); + + if (value instanceof FSBRootNode fsRootNode) { + renderFS(fsRootNode, selected); + } + else if (value instanceof FSBDirNode) { + // do nothing special, but exclude FSBFileNode + } + else if (value instanceof FSBFileNode fileNode) { + renderFile(fileNode, selected); + } + + return this; + } + + private void renderFS(FSBRootNode node, boolean selected) { + FileSystemRef nodeFSRef = node.getFSRef(); + if (nodeFSRef == null || nodeFSRef.getFilesystem() == null) { + return; + } + Icon image = fsbIcons.getIcon(node.getContainerName(), + List.of(FSBIcons.FILESYSTEM_OVERLAY_ICON)); + setIcon(image); + } + + private void renderFile(FSBFileNode node, boolean selected) { + FSRL fsrl = node.getFSRL(); + String filename = fsrl.getName(); + List overlays = new ArrayList<>(4); + + DomainFile df = projectIndex.findFirstByFSRL(fsrl); + if (df != null) { + overlays.add(FSBIcons.IMPORTED_OVERLAY_ICON); + + if (plugin.isOpen(df)) { + // TODO: change this to a OVERLAY_OPEN option when fetching icon + setForeground(selected ? Palette.CYAN : Palette.MAGENTA); + } + } + if (fsService.isFilesystemMountedAt(fsrl)) { + overlays.add(FSBIcons.FILESYSTEM_OVERLAY_ICON); + } + if (node.isSymlink()) { + overlays.add(FSBIcons.LINK_OVERLAY_ICON); + } + if (node.hasMissingPassword()) { + overlays.add(FSBIcons.MISSING_PASSWORD_OVERLAY_ICON); + } + + Icon icon = fsbIcons.getIcon(filename, overlays); + setIcon(icon); + + } + }); + } + + public FileSystemBrowserPlugin getPlugin() { + return plugin; + } + + /** + * + * @return this provider's GTree. + */ + public GTree getGTree() { + return gTree; + } + + FSRL getFSRL() { + return rootNode != null ? rootNode.getFSRL() : null; + } + + public ProjectIndexService getProjectIndex() { + return projectIndex; + } + + void dispose() { + plugin.getTool().removePopupActionProvider(this); + + if (rootNode != null && rootNode.getFSRef() != null && !rootNode.getFSRef().isClosed()) { + rootNode.getFSRef().getFilesystem().getRefManager().removeListener(this); + } + fileHandlers.clear(); + if (gTree != null) { + gTree.setCellRenderer(null); // avoid npe's in the cellrenderer when disposed + gTree.dispose(); // calls dispose() on tree's rootNode, which will release the fsRefs + } + removeFromTool(); + rootNode = null; + plugin = null; + gTree = null; + } + + @Override + public void componentHidden() { + // if the component is 'closed', nuke ourselves + if (plugin != null) { + plugin.removeFileSystemBrowserComponent(this); + dispose(); + } + } + + @Override + public List getPopupActions(Tool tool, ActionContext context) { + List results = new ArrayList<>(); + for (FSBFileHandler fileHandler : fileHandlers) { + List actions = fileHandler.getPopupProviderActions(); + results.addAll(actions); + } + return results; + } + + public void afterAddedToTool() { + fileHandlers.stream() + .flatMap(fh -> fh.createActions().stream()) + .forEach(this::addLocalAction); + + setProject(tool.getProject()); + } + + public void setProject(Project project) { + gTree.runTask(monitor -> { + projectIndex.setProject(project, monitor); + Swing.runLater(() -> gTree.repaint()); // icons might need repainting after new info is available + }); + } + + @Override + public void onFilesystemClose(GFileSystem fs) { + Msg.info(this, "File system " + fs.getFSRL() + " was closed! Closing browser window"); + Swing.runIfSwingOrRunLater(() -> componentHidden()); + } + + @Override + public void onFilesystemRefChange(GFileSystem fs, FileSystemRefManager refManager) { + // nothing + } + + public void runTask(MonitoredRunnable runnableTask) { + gTree.runTask(runnableTask); + } + + /*****************************************/ + + private boolean handleSingleClick(GTreeNode clickedNode) { + if (clickedNode instanceof FSBFileNode fileNode) { + for (FSBFileHandler handler : fileHandlers) { + if (handler.fileFocused(fileNode)) { + return true; + } + } + } + return false; + } + + private boolean handleDoubleClick(GTreeNode clickedNode) { + if (clickedNode instanceof FSBFileNode fileNode) { + for (FSBFileHandler handler : fileHandlers) { + if (handler.fileDefaultAction(fileNode)) { + return true; + } + } + } + return false; + } + + /*****************************************/ + + @Override + public FSBActionContext getActionContext(MouseEvent event) { + return new FSBActionContext(this, getSelectedNodes(event), event, gTree); + } + + private List getSelectedNodes(MouseEvent event) { + TreePath[] selectionPaths = gTree.getSelectionPaths(); + List list = new ArrayList<>(selectionPaths.length); + for (TreePath selectionPath : selectionPaths) { + Object lastPathComponent = selectionPath.getLastPathComponent(); + if (lastPathComponent instanceof FSBNode fsbNode) { + list.add(fsbNode); + } + } + if (list.isEmpty() && event != null) { + Object source = event.getSource(); + if (source instanceof JTree sourceTree && gTree.isMyJTree(sourceTree)) { + int x = event.getX(); + int y = event.getY(); + GTreeNode nodeAtEventLocation = gTree.getNodeForLocation(x, y); + if (nodeAtEventLocation != null && nodeAtEventLocation instanceof FSBNode fsbNode) { + list.add(fsbNode); + } + } + } + return list; + } + + @Override + public JComponent getComponent() { + return gTree; + } + + @Override + public String getName() { + return TITLE; + } + + @Override + public WindowPosition getDefaultWindowPosition() { + return WindowPosition.WINDOW; + } + + public boolean ensureFileAccessable(FSRL fsrl, FSBNode node, TaskMonitor monitor) { + + FSBFileNode fileNode = (node instanceof FSBFileNode) ? (FSBFileNode) node : null; + + monitor.initialize(0); + monitor.setMessage("Testing file access"); + boolean wasMissingPasword = (fileNode != null) ? fileNode.hasMissingPassword() : false; + try (ByteProvider bp = fsService.getByteProvider(fsrl, false, monitor)) { + // if we can get here and it used to have a missing password, update the node's status + if (wasMissingPasword) { + doRefreshInfo(List.of(fileNode), monitor); + } + return true; + } + catch (CryptoException e) { + Msg.showWarn(this, gTree, "Crypto / Password Error", + "Unable to access the specified file.\n" + + "This could be caused by not entering the correct password or because of missing crypto information.\n\n" + + e.getMessage()); + return false; + } + catch (IOException e) { + Msg.showError(this, gTree, "File IO Error", + "Unable to access the specified file.\n\n" + e.getMessage(), e); + return false; + } + catch (CancelledException e) { + return false; + } + + } + + public boolean openFileSystem(FSBNode node, boolean nested) { + if (!(node instanceof FSBFileNode fileNode) || fileNode.getFSRL() == null) { + return false; + } + FSRL fsrl = fileNode.getFSRL(); + gTree.runTask(monitor -> { + if (!ensureFileAccessable(fsrl, fileNode, monitor)) { + return; + } + if (!doOpenFileSystem(fsrl, fileNode, nested, monitor)) { + return; + } + }); + return true; + } + + /* + * run on gTree task thread + */ + boolean doOpenFileSystem(FSRL containerFSRL, FSBFileNode node, boolean nested, + TaskMonitor monitor) { + try { + monitor.setMessage("Probing " + containerFSRL.getName() + " for filesystems"); + FileSystemRef ref = fsService.probeFileForFilesystem(containerFSRL, monitor, + FileSystemProbeConflictResolver.GUI_PICKER); + if (ref == null) { + Msg.showWarn(this, plugin.getTool().getActiveWindow(), "Open Filesystem", + "No filesystem detected in " + containerFSRL.getName()); + return false; + } + + Swing.runLater(() -> { + if (nested) { + FSBFileNode modelFileNode = + (FSBFileNode) gTree.getModelNodeForPath(node.getTreePath()); + + FSBRootNode nestedRootNode = new FSBRootNode(ref, modelFileNode); + + int indexInParent = modelFileNode.getIndexInParent(); + GTreeNode parent = modelFileNode.getParent(); + parent.removeNode(modelFileNode); + parent.addNode(indexInParent, nestedRootNode); + gTree.expandPath(nestedRootNode); + try { + nestedRootNode.init(monitor); + } + catch (CancelledException e) { + Msg.warn(this, "Failed to populate FSB root node with children"); + } + contextChanged(); + } + else { + plugin.createNewFileSystemBrowser(ref, true); + } + }); + return true; + } + catch (IOException | CancelledException e) { + FSUtilities.displayException(this, plugin.getTool().getActiveWindow(), + "Open Filesystem", "Error opening filesystem for " + containerFSRL.getName(), e); + return false; + } + } + + void doRefreshInfo(List nodes, TaskMonitor monitor) { + try { + for (FSBNode node : nodes) { + node.refreshNode(monitor); + } + gTree.refilterLater(); // force the changed modelNodes to be recloned and displayed (if filter active) + } + catch (CancelledException e) { + // stop + } + Swing.runLater(() -> gTree.repaint()); + } + + //--------------------------------------------------------------------------------------------- + + private class DefaultFileHandler implements FSBFileHandler { + + @Override + public void init(FSBFileHandlerContext context) { + // empty + } + + @Override + public List createActions() { + return List.of(); + } + + @Override + public boolean fileFocused(FSBFileNode fileNode) { + + FSRL fsrl = fileNode.getFSRL(); + if (fsrl != null) { + if (pm != null) { + // if this tool is a codebrowser-ish tool, switch focus to the matching focused file + DomainFile df = projectIndex.findFirstByFSRL(fsrl); + DomainObject domObj; + if (df != null && (domObj = df.getOpenedDomainObject(this)) != null) { + domObj.release(this); + if (domObj instanceof Program program) { + runTask(monitor -> pm.setCurrentProgram(program)); + } + return true; + } + } + + if (fileNode.hasMissingPassword()) { + runTask(monitor -> doRefreshInfo(List.of(fileNode), monitor)); + } + } + return false; + } + + @Override + public boolean fileDefaultAction(FSBFileNode fileNode) { + FSRL fsrl = fileNode.getFSRL(); + if (fsrl == null) { + return false; + } + + if (fileNode.isSymlink()) { + gotoSymlinkDest(fileNode); + return true; + } + + if (!fileNode.isLeaf()) { + return false; + } + + runTask(monitor -> { + if (!ensureFileAccessable(fsrl, fileNode, monitor)) { + return; + } + try { + FSRL fullFsrl = fsService.getFullyQualifiedFSRL(fsrl, monitor); + if (fsService.isFileFilesystemContainer(fullFsrl, monitor)) { + doOpenFileSystem(fullFsrl, fileNode, true, monitor); + return; + } + + DomainFile df = projectIndex.findFirstByFSRL(fsrl); + OpenWithTarget openWithTarget = OpenWithTarget.getDefault(plugin.getTool()); + if (df != null && openWithTarget != null) { + Swing.runLater(() -> openWithTarget.open(List.of(df))); + return; + } + ImporterUtilities.showImportSingleFileDialog(fullFsrl, null, + fileNode.getFormattedTreePath(), plugin.getTool(), openWithTarget.getPm(), + monitor); + } + catch (IOException | CancelledException e) { + // fall thru + } + }); + + return true; + } + + private void gotoSymlinkDest(FSBFileNode fileNode) { + GFile file = fileNode.file; + try { + FSBRootNode fsRootNode = fileNode.getFSBRootNode(); + GFile destFile = file.getFilesystem().resolveSymlinks(file); + if (destFile != null && fsRootNode != null) { + gTree.runTask(monitor -> { + FSBNode destNode = fsRootNode.getGFileFSBNode(destFile, monitor); + if (destNode != null) { + Swing.runLater(() -> gTree.setSelectedNodes(destNode)); + } + }); + return; + } + } + catch (IOException e) { + // fall thru + } + FileAttributes fattrs = file.getFilesystem().getFileAttributes(file, null); + String symlinkDest = fattrs.get(SYMLINK_DEST_ATTR, String.class, null); + plugin.getTool() + .setStatusInfo("Unable to resolve symlink [%s]".formatted(symlinkDest), true); + } + + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBDirNode.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBDirNode.java index 092716faa03..b48b84c21fd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBDirNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBDirNode.java @@ -19,6 +19,7 @@ import java.util.List; import docking.widgets.tree.GTreeNode; +import ghidra.formats.gfilesystem.FSRL; import ghidra.formats.gfilesystem.GFile; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -44,16 +45,8 @@ public List generateChildren(TaskMonitor monitor) throws CancelledExc } @Override - public void updateFileAttributes(TaskMonitor monitor) { - for (GTreeNode node : getChildren()) { - if (node instanceof FSBFileNode) { - ((FSBFileNode) node).updateFileAttributes(monitor); - } - if (monitor.isCancelled()) { - break; - } - } - super.updateFileAttributes(monitor); + public void refreshNode(TaskMonitor monitor) throws CancelledException { + refreshChildren(monitor); } @Override @@ -61,4 +54,9 @@ public boolean isLeaf() { return false; } + @Override + public FSRL getLoadableFSRL() { + return getFSBRootNode().getProgramProviderFSRL(getFSRL()); + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileHandler.java new file mode 100644 index 00000000000..1172b4434cb --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileHandler.java @@ -0,0 +1,81 @@ +/* ### + * 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.plugins.fsbrowser; + +import java.util.List; + +import docking.action.DockingAction; +import ghidra.framework.plugintool.PluginTool; +import ghidra.util.classfinder.ExtensionPoint; + +/** + * Extension point, used by the {@link FSBComponentProvider} to create actions that appear + * in the fsb tree, and to delegate focus and default actions. + */ +public interface FSBFileHandler extends ExtensionPoint { + /** + * Called once after creation of each instance to provide useful info + * + * @param context references to useful objects and services + */ + void init(FSBFileHandlerContext context); + + /** + * Returns a list of {@link DockingAction}s that should be + * {@link PluginTool#addLocalAction(docking.ComponentProvider, docking.action.DockingActionIf) added} + * to the {@link FSBComponentProvider} tree as local actions. + * + * @return list of {@link DockingAction}s + */ + default List createActions() { + return List.of(); + } + + /** + * Called when a file node is focused in the {@link FSBComponentProvider} tree. + * + * @param fileNode {@link FSBFileNode} that was focused + * @return boolean true if action was taken + */ + default boolean fileFocused(FSBFileNode fileNode) { + return false; + } + + /** + * Called when a file node is the target of a 'default action' initiated by the user, such + * as a double click, etc. + * + * @param fileNode {@link FSBFileNode} that was acted upon + * @return boolean true if action was taken, false if no action was taken + */ + default boolean fileDefaultAction(FSBFileNode fileNode) { + return false; + } + + /** + * Returns a list of {@link DockingAction}s that should be added to a popup menu. Called + * each time a fsb browser tree popup menu is created. + *

+ * Only use this method to provide actions when the actions need to be created freshly + * for each popup event. Normal long-lived actions should be published by the + * {@link #createActions()} method. + * + * @return list of {@link DockingAction}s + */ + default List getPopupProviderActions() { + return List.of(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBAction.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileHandlerContext.java similarity index 55% rename from Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBAction.java rename to Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileHandlerContext.java index fbcb813e702..011f813de6d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileHandlerContext.java @@ -14,26 +14,18 @@ * limitations under the License. */ package ghidra.plugins.fsbrowser; -import docking.action.DockingAction; -import ghidra.framework.plugintool.Plugin; + +import ghidra.formats.gfilesystem.FileSystemService; +import ghidra.plugin.importer.ProjectIndexService; /** - * {@link FileSystemBrowserPlugin}-specific action. + * Context given to a {@link FSBFileHandler} instance when being initialized. + * + * @param plugin the FSB plugin + * @param fsbComponent the FSB component + * @param fsService the fs service + * @param projectIndex the project index */ -public abstract class FSBAction extends DockingAction { - - private final String menuText; - - public FSBAction(String menuText, Plugin plugin) { - this(menuText, menuText, plugin); - } - - public FSBAction(String name, String menuText, Plugin plugin) { - super("FSB " + name, plugin.getName()); - this.menuText = menuText; - } - - public String getMenuText() { - return menuText; - } -} +public record FSBFileHandlerContext(FileSystemBrowserPlugin plugin, + FSBComponentProvider fsbComponent, FileSystemService fsService, + ProjectIndexService projectIndex) {} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileNode.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileNode.java index 568c2f55159..4c6b6c4347b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileNode.java @@ -17,15 +17,16 @@ import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; +import java.util.Date; import java.util.List; import docking.widgets.tree.GTreeNode; -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.GFile; +import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.fileinfo.FileAttributeType; import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; /** * GTreeNode that represents a file on a filesystem. @@ -35,31 +36,76 @@ public class FSBFileNode extends FSBNode { protected GFile file; protected boolean isEncrypted; protected boolean hasPassword; + protected String symlinkDest; + protected long lastModified; FSBFileNode(GFile file) { this.file = file; } + @Override + public void init(TaskMonitor monitor) { + updateFileProps(monitor); + } + @Override public FSRL getFSRL() { return file.getFSRL(); } + @Override + public GFile getGFile() { + return file; + } + @Override public boolean isLeaf() { return true; } + @Override + public String getToolTip() { + if (symlinkDest != null) { + // unicode \u2192 is a -> right arrow + return "%s \u2192 %s".formatted(getName(), symlinkDest); + } + + long flen = file.getLength(); + String flenStr = flen >= 0 ? " - " + FileUtilities.formatLength(flen) : ""; + String lastModStr = + lastModified > 0 ? " - " + FSUtilities.formatFSTimestamp(new Date(lastModified)) : ""; + String pwInfo = isEncrypted && !hasPassword ? " (missing password)" : ""; + + return getName() + flenStr + lastModStr + pwInfo; + } + + public boolean isSymlink() { + return symlinkDest != null; + } + @Override public int hashCode() { return file.hashCode(); } - @Override - protected void updateFileAttributes(TaskMonitor monitor) { + private void updateFileProps(TaskMonitor monitor) { FileAttributes fattrs = file.getFilesystem().getFileAttributes(file, monitor); isEncrypted = fattrs.get(IS_ENCRYPTED_ATTR, Boolean.class, false); hasPassword = fattrs.get(HAS_GOOD_PASSWORD_ATTR, Boolean.class, false); + symlinkDest = fattrs.get(SYMLINK_DEST_ATTR, String.class, null); + Date lastModDate = fattrs.get(MODIFIED_DATE_ATTR, Date.class, null); + lastModified = lastModDate != null ? lastModDate.getTime() : 0; + } + + @Override + public void refreshNode(TaskMonitor monitor) throws CancelledException { + boolean wasMissingPassword = hasMissingPassword(); + + updateFileProps(monitor); + + if (wasMissingPassword != hasMissingPassword()) { + getFSBRootNode().setCryptoStatusUpdated(true); + } } @Override @@ -93,19 +139,9 @@ public boolean hasMissingPassword() { return isEncrypted && !hasPassword; } - /** - * Returns true if this node's password status has changed, calling for a complete refresh - * of the status of all files in the file system. - * - * @param monitor {@link TaskMonitor} - * @return boolean true if this nodes password status has changed - */ - public boolean needsFileAttributesUpdate(TaskMonitor monitor) { - if (hasMissingPassword()) { - updateFileAttributes(monitor); - return hasPassword; // if true then the attribute has changed and everything should be refreshed - } - return false; + @Override + public FSRL getLoadableFSRL() { + return getFSRL(); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBIcons.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBIcons.java new file mode 100644 index 00000000000..114fb336bed --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBIcons.java @@ -0,0 +1,155 @@ +/* ### + * 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.plugins.fsbrowser; + +import java.util.*; + +import javax.swing.Icon; + +import generic.theme.*; +import ghidra.formats.gfilesystem.FSUtilities; +import resources.MultiIcon; + +/** + * Static list of Icons for the file system browser plugin and its child windows. + *

+ * The {@link #getInstance() singleton instance} provides {@link Icon}s that represent the type + * and status of a file, based on a filename mapping and caller specified status overlays. + *

+ * Thread safe + */ +public class FSBIcons { + //@formatter:off + public static final Icon COPY = new GIcon("icon.plugin.fsbrowser.copy"); + public static final Icon CUT = new GIcon("icon.plugin.fsbrowser.cut"); + public static final Icon DELETE = new GIcon("icon.plugin.fsbrowser.delete"); + public static final Icon FONT = new GIcon("icon.plugin.fsbrowser.font"); + public static final Icon LOCKED = new GIcon("icon.plugin.fsbrowser.locked"); + public static final Icon NEW = new GIcon("icon.plugin.fsbrowser.new"); + public static final Icon PASTE = new GIcon("icon.plugin.fsbrowser.paste"); + public static final Icon REDO = new GIcon("icon.plugin.fsbrowser.redo"); + public static final Icon RENAME = new GIcon("icon.plugin.fsbrowser.rename"); + public static final Icon REFRESH = new GIcon("icon.plugin.fsbrowser.refresh"); + public static final Icon SAVE = new GIcon("icon.plugin.fsbrowser.save"); + public static final Icon SAVE_AS = new GIcon("icon.plugin.fsbrowser.save.as"); + public static final Icon UNDO = new GIcon("icon.plugin.fsbrowser.undo"); + public static final Icon UNLOCKED = new GIcon("icon.plugin.fsbrowser.unlocked"); + public static final Icon CLOSE = new GIcon("icon.plugin.fsbrowser.close"); + public static final Icon COLLAPSE_ALL = new GIcon("icon.plugin.fsbrowser.collapse.all"); + public static final Icon COMPRESS = new GIcon("icon.plugin.fsbrowser.compress"); + public static final Icon CREATE_FIRMWARE = new GIcon("icon.plugin.fsbrowser.create.firmware"); + public static final Icon EXPAND_ALL = new GIcon("icon.plugin.fsbrowser.expand.all"); + public static final Icon EXTRACT = new GIcon("icon.plugin.fsbrowser.extract"); + public static final Icon INFO = new GIcon("icon.plugin.fsbrowser.info"); + public static final Icon OPEN = new GIcon("icon.plugin.fsbrowser.open"); + public static final Icon OPEN_AS_BINARY = new GIcon("icon.plugin.fsbrowser.open.as.binary"); + public static final Icon OPEN_IN_LISTING = new GIcon("icon.plugin.fsbrowser.open.in.listing"); + public static final Icon OPEN_FILE_SYSTEM = new GIcon("icon.plugin.fsbrowser.open.file.system"); + public static final Icon PHOTO = new GIcon("icon.plugin.fsbrowser.photo"); + public static final Icon VIEW_AS_IMAGE = new GIcon("icon.plugin.fsbrowser.view.as.image"); + public static final Icon VIEW_AS_TEXT = new GIcon("icon.plugin.fsbrowser.view.as.text"); + public static final Icon ECLIPSE = new GIcon("icon.plugin.fsbrowser.eclipse"); + public static final Icon JAR = new GIcon("icon.plugin.fsbrowser.jar"); + public static final Icon IMPORT = new GIcon("icon.plugin.fsbrowser.import"); + public static final Icon iOS = new GIcon("icon.plugin.fsbrowser.ios"); + public static final Icon OPEN_ALL = new GIcon("icon.plugin.fsbrowser.open.all"); + public static final Icon LIST_MOUNTED = new GIcon("icon.plugin.fsbrowser.list.mounted"); + public static final Icon LIBRARY = new GIcon("icon.plugin.fsbrowser.library"); + + public static final Icon IMPORTED_OVERLAY_ICON = new GIcon("icon.fsbrowser.file.overlay.imported"); + public static final Icon FILESYSTEM_OVERLAY_ICON = new GIcon("icon.fsbrowser.file.overlay.filesystem"); + public static final Icon MISSING_PASSWORD_OVERLAY_ICON = new GIcon("icon.fsbrowser.file.overlay.missing.password"); + public static final Icon LINK_OVERLAY_ICON = new GIcon("icon.fsbrowser.file.overlay.link"); + public static final Icon DEFAULT_ICON = new GIcon("icon.fsbrowser.file.extension.default"); + //@formatter:on + + public static FSBIcons getInstance() { + return Singleton.INSTANCE; + } + + private static final class Singleton { + private static final FSBIcons INSTANCE = new FSBIcons(); + } + + private static final String EXTENSION_ICON_PREFIX = "icon.fsbrowser.file.extension"; + private static final String SUBSTRING_ICON_PREFIX = "icon.fsbrowser.file.substring"; + + private Map substringToIconMap = createSubstringMap(); + + private FSBIcons() { + // don't create instances of this class, use getInstance() instead + } + + private Map createSubstringMap() { + Map results = new HashMap<>(); + GThemeValueMap values = ThemeManager.getInstance().getCurrentValues(); + List icons = values.getIcons(); + for (IconValue iconValue : icons) { + String id = iconValue.getId(); + if (id.startsWith(SUBSTRING_ICON_PREFIX)) { + String substring = id.substring(SUBSTRING_ICON_PREFIX.length()); + results.put(substring, new GIcon(id)); + } + } + return results; + } + + /** + * Returns an {@link Icon} that represents a file's content based on its + * name. + * + * @param fileName name of file that an icon is being requested for. + * @param overlays optional list of overlay icons that + * should be overlaid on top of the base icon. These icons represent a + * status or feature independent of the file's base icon. + * @return {@link Icon} instance that best represents the named file, never + * null. + */ + public Icon getIcon(String fileName, List overlays) { + fileName = fileName.toLowerCase(); + String ext = FSUtilities.getExtension(fileName, 1); + if (ext != null) { + String iconId = EXTENSION_ICON_PREFIX + ext; + if (Gui.hasIcon(iconId)) { + Icon base = new GIcon(iconId); + return buildIcon(base, overlays); + } + } + + for (String substring : substringToIconMap.keySet()) { + if (fileName.indexOf(substring) != -1) { + return buildIcon(substringToIconMap.get(substring), overlays); + } + } + + // return default icon for generic file + return buildIcon(DEFAULT_ICON, overlays); + } + + private Icon buildIcon(Icon base, List overlays) { + if (overlays == null || overlays.isEmpty()) { + return base; + } + MultiIcon multiIcon = new MultiIcon(base); + for (Icon overlay : overlays) { + if (overlay != null) { + multiIcon.addIcon(overlay); + } + } + return multiIcon; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBNode.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBNode.java index f07cf9d77b3..77394ccffe1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBNode.java @@ -15,9 +15,12 @@ */ package ghidra.plugins.fsbrowser; +import java.io.IOException; import java.util.*; +import java.util.stream.Collectors; import javax.swing.Icon; +import javax.swing.tree.TreePath; import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeSlowLoadingNode; @@ -39,6 +42,14 @@ public abstract class FSBNode extends GTreeSlowLoadingNode { */ public abstract FSRL getFSRL(); + public void init(TaskMonitor monitor) throws CancelledException { + // nothing + } + + public GFile getGFile() { + return null; + } + @Override public String getToolTip() { return getName(); @@ -56,13 +67,136 @@ public String getName() { public FSBRootNode getFSBRootNode() { GTreeNode node = getParent(); - while (node != null && !(node instanceof FSBRootNode)) { + while (node != null) { + if (node instanceof FSBRootNode rootNode) { + return rootNode; + } node = node.getParent(); } - return (node instanceof FSBRootNode) ? (FSBRootNode) node : null; + return null; } - protected abstract void updateFileAttributes(TaskMonitor monitor) throws CancelledException; + public abstract void refreshNode(TaskMonitor monitor) throws CancelledException; + + protected void loadChildrenIfNeeded(TaskMonitor monitor) throws CancelledException { + if (!isLeaf() && !isLoaded()) { + doSetChildren(generateChildren(monitor)); + } + } + + private static Map getListing(GFile f) { + try { + List listing = f.getListing(); + return listing.stream().collect(Collectors.toMap(f1 -> f1.getFSRL(), f1 -> f1)); + } + catch (IOException e) { + return Map.of(); + } + } + + protected void refreshChildren(TaskMonitor monitor) + throws CancelledException { + GFile f = getGFile(); + if (f == null || !isLoaded() || isLeaf()) { + return; + } + Map currentFiles = getListing(f); + + int changeCount = 0; + boolean cryptoCausesFullRefresh = true; + boolean flagFSBRootNodeWithCryptoUpdate = false; + + List newNodes = new ArrayList<>(); + List currentChildren = new ArrayList<>(children()); + for (GTreeNode oldNode : currentChildren) { + monitor.increment(); + if (oldNode instanceof FSBNode fsbNode) { + GFile currentFile = currentFiles.get(fsbNode.getFSRL()); + if (fileMatchesNode(currentFile, fsbNode)) { + boolean checkPwUpdate = cryptoCausesFullRefresh && + fsbNode instanceof FSBFileNode fileNode && fileNode.hasMissingPassword(); + + fsbNode.refreshNode(monitor); + + flagFSBRootNodeWithCryptoUpdate |= checkPwUpdate && + fsbNode instanceof FSBFileNode fileNode && !fileNode.hasMissingPassword(); + + newNodes.add(fsbNode); // port old node over to new list + currentFiles.remove(fsbNode.getFSRL()); + } + else { + // by not adding to newNodes, the old node will disappear + changeCount++; + } + } + } + + // add any remaining GFiles as new nodes + changeCount += currentFiles.size(); + currentFiles.values() + .stream() + .map(f1 -> createNodeFromFile(f1, monitor)) + .forEach(newNodes::add); + + Collections.sort(newNodes, FSBNODE_NAME_TYPE_COMPARATOR); + + FSBRootNode fsbRootNode; + if (flagFSBRootNodeWithCryptoUpdate && (fsbRootNode = getFSBRootNode()) != null) { + fsbRootNode.setCryptoStatusUpdated(true); + } + + if (changeCount > 0) { + setChildren(newNodes); + } + } + + private boolean fileMatchesNode(GFile f, FSBNode node) { + if (f == null) { + return false; + } + if (node instanceof FSBFileNode fileNode && + f.isDirectory() != (fileNode instanceof FSBDirNode)) { + return false; + } + return true; + + } + + protected FSBFileNode findMatchingNode(GFile f, TaskMonitor monitor) throws CancelledException { + loadChildrenIfNeeded(monitor); + for (GTreeNode treeNode : children()) { + if (treeNode instanceof FSBFileNode fileNode) { + if (fileNode.file.equals(f)) { + return fileNode; + } + } + } + return null; + } + + public String getFormattedTreePath() { + TreePath treePath = getTreePath(); + StringBuilder path = new StringBuilder(); + for (Object pathElement : treePath.getPath()) { + if (pathElement instanceof FSBNode node) { + if (!path.isEmpty()) { + path.append("/"); + } + if (node instanceof FSBRootNode rootNode) { + FSRL fsContainer = rootNode.getContainer(); + if (fsContainer != null) { + path.append(fsContainer.getName()); + } + } + else { + path.append(node.getFSRL().getName()); + } + } + } + return path.toString(); + } + + abstract public FSRL getLoadableFSRL(); /** * Returns the {@link FSBRootNode} that represents the root of the file system that @@ -93,10 +227,7 @@ public static List createNodesFromFileList(List files, TaskMon List nodes = new ArrayList<>(files.size()); for (GFile child : files) { - FSBFileNode node = createNodeFromFile(child); - if (node.isLeaf()) { - node.updateFileAttributes(monitor); - } + FSBFileNode node = createNodeFromFile(child, monitor); nodes.add(node); } return nodes; @@ -108,8 +239,25 @@ public static List createNodesFromFileList(List files, TaskMon * @param file {@link GFile} to convert * @return a new {@link FSBFileNode} with type specific to the GFile's type. */ - public static FSBFileNode createNodeFromFile(GFile file) { - return file.isDirectory() ? new FSBDirNode(file) : new FSBFileNode(file); + public static FSBFileNode createNodeFromFile(GFile file, TaskMonitor monitor) { + FSBFileNode result = file.isDirectory() ? new FSBDirNode(file) : new FSBFileNode(file); + result.init(monitor); + return result; } + public static final Comparator FSBNODE_NAME_TYPE_COMPARATOR = (o1, o2) -> { + if (!(o1 instanceof FSBNode node1) || !(o2 instanceof FSBNode node2)) { + return 0; + } + GFile f1 = node1.getGFile(); + GFile f2 = node2.getGFile(); + int result = Boolean.compare(!f1.isDirectory(), !f2.isDirectory()); + if (result == 0) { + String n1 = Objects.requireNonNullElse(f1.getName(), ""); + String n2 = Objects.requireNonNullElse(f2.getName(), ""); + result = n1.compareToIgnoreCase(n2); + } + return result; + }; + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBRootNode.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBRootNode.java index d435e9f5614..8555b5b1858 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBRootNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBRootNode.java @@ -38,8 +38,8 @@ public class FSBRootNode extends FSBNode { private FileSystemRef fsRef; private FSBFileNode prevNode; - private List subRootNodes = new ArrayList<>(); private FSBRootNode modelNode; + private boolean cryptoStatusUpdated; FSBRootNode(FileSystemRef fsRef) { this(fsRef, null); @@ -60,11 +60,24 @@ public GTreeNode clone() throws CloneNotSupportedException { @Override public void dispose() { - releaseFSRefsIfModelNode(); + releaseFSRefIfModelNode(); super.dispose(); } - void swapBackPrevModelNodeAndDispose() { + @Override + public void init(TaskMonitor monitor) throws CancelledException { + setChildren(generateChildren(monitor)); + } + + public void setCryptoStatusUpdated(boolean cryptoStatusUpdated) { + this.cryptoStatusUpdated = cryptoStatusUpdated; + } + + boolean isCryptoStatusUpdated() { + return cryptoStatusUpdated; + } + + public void swapBackPrevModelNodeAndDispose() { if (this != modelNode) { modelNode.swapBackPrevModelNodeAndDispose(); return; @@ -76,34 +89,32 @@ void swapBackPrevModelNodeAndDispose() { dispose(); // releases the fsRef } + @Override + public GFile getGFile() { + return fsRef.getFilesystem().getRootDir(); + } + public FileSystemRef getFSRef() { return modelNode.fsRef; } - private void releaseFSRefsIfModelNode() { + private void releaseFSRefIfModelNode() { if (this != modelNode) { return; } - for (FSBRootNode subFSBRootNode : subRootNodes) { - subFSBRootNode.releaseFSRefsIfModelNode(); - } - subRootNodes.clear(); - FileSystemService.getInstance().releaseFileSystemImmediate(fsRef); fsRef = null; } @Override - public void updateFileAttributes(TaskMonitor monitor) throws CancelledException { + public void refreshNode(TaskMonitor monitor) throws CancelledException { if (this != modelNode) { - modelNode.updateFileAttributes(monitor); + modelNode.refreshNode(monitor); return; } - for (GTreeNode node : getChildren()) { - monitor.checkCancelled(); - if (node instanceof FSBFileNode) { - ((FSBFileNode) node).updateFileAttributes(monitor); - } + refreshChildren(monitor); + if (cryptoStatusUpdated) { + // do something to refresh children's status that may have been affected by crypto update } } @@ -141,6 +152,68 @@ public List generateChildren(TaskMonitor monitor) throws CancelledExc @Override public FSRL getFSRL() { - return modelNode.fsRef.getFilesystem().getFSRL(); + return modelNode != null && modelNode.fsRef != null + ? modelNode.fsRef.getFilesystem().getFSRL() + : null; + } + + public FSBNode getGFileFSBNode(GFile file, TaskMonitor monitor) { + List pathParts = splitGFilePath(file); + FSBNode fileNode = this; + for (int i = 1 /* skip root */; fileNode != null && i < pathParts.size(); i++) { + try { + fileNode = fileNode.findMatchingNode(pathParts.get(i), monitor); + } + catch (CancelledException e) { + return null; + } + } + return fileNode; } + + public FSRL getContainer() { + // use the rootDir's FSRL to sidestep issue with LocalFileSystemSub's non-standard fsFSRL + return fsRef != null + ? fsRef.getFilesystem().getRootDir().getFSRL().getFS().getContainer() + : null; + } + + public String getContainerName() { + return prevNode != null ? prevNode.getName() : "/"; + } + + private List splitGFilePath(GFile f) { + List result = new ArrayList<>(); + while (f != null) { + result.add(0, f); + f = f.getParentFile(); + } + return result; + } + + public FSRL getProgramProviderFSRL(FSRL fsrl) { + GFileSystem fs = fsRef.getFilesystem(); + if (fs instanceof GFileSystemProgramProvider programProviderFS) { + try { + GFile gfile = fs.lookup(fsrl.getPath()); + if (gfile != null && programProviderFS.canProvideProgram(gfile)) { + return fsrl; + } + } + catch (IOException e) { + // ignore error and fall thru + } + } + return null; + } + + @Override + public FSRL getLoadableFSRL() { + FSRL ppFSRL = getProgramProviderFSRL(getFSRL()); + if (ppFSRL != null) { + return ppFSRL; + } + return getContainer(); + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java deleted file mode 100644 index 0089879bb26..00000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java +++ /dev/null @@ -1,99 +0,0 @@ -/* ### - * 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.plugins.fsbrowser; - -import java.util.ArrayList; -import java.util.List; - -import docking.widgets.SelectFromListDialog; -import ghidra.app.services.ProgramManager; -import ghidra.framework.plugintool.PluginTool; -import ghidra.util.Msg; - -/** - * {@link FileSystemBrowserPlugin} utility methods that other things might find useful. - */ -public class FSBUtils { - - /** - * Returns the {@link ProgramManager} associated with this fs browser plugin. - *

- * When this FS Browser plugin is part of the front-end tool, this will search - * for an open CodeBrowser tool that can be used to handle programs. - *

- * When this FS Browser plugin is part of a CodeBrowser tool, this will just return - * the local ProgramManager / CodeBrowser. - * - * @param tool The plugin tool. - * @param allowUserPrompt boolean flag to allow this method to query the user to select - * a CodeBrowser. - * @return null if front-end and no open CodeBrowser, otherwise returns the local - * CodeBrowser ProgramManager service. - */ - public static ProgramManager getProgramManager(PluginTool tool, boolean allowUserPrompt) { - PluginTool pmTool = null; - ProgramManager pm = tool.getService(ProgramManager.class); - if (pm != null) { - pmTool = tool; - } - else { - List runningPMTools = FSBUtils.getRunningProgramManagerTools(tool); - if (runningPMTools.size() == 1) { - pmTool = runningPMTools.get(0); - } - else { - pmTool = allowUserPrompt ? selectPMTool(tool) : null; - } - } - return (pmTool != null) ? pmTool.getService(ProgramManager.class) : null; - } - - public static List getRunningProgramManagerTools(PluginTool tool) { - List pluginTools = new ArrayList<>(); - for (PluginTool runningTool : tool.getToolServices().getRunningTools()) { - PluginTool pt = runningTool; - ProgramManager pmService = pt.getService(ProgramManager.class); - if (pmService != null) { - pluginTools.add(pt); - } - } - return pluginTools; - } - - private static PluginTool selectPMTool(PluginTool tool) { - ProgramManager pm = tool.getService(ProgramManager.class); - if (pm != null) { - return tool; - } - - List pluginTools = FSBUtils.getRunningProgramManagerTools(tool); - - if (pluginTools.size() == 1) { - return pluginTools.get(0); - } - - if (pluginTools.isEmpty()) { - Msg.showWarn(tool, tool.getActiveWindow(), "No open tools", - "There are no open tools to use to open a program with"); - return null; - } - - PluginTool pt = SelectFromListDialog.selectFromList(pluginTools, "Select tool", - "Select a tool to use to open programs", pluginTool -> pluginTool.getName()); - return pt; - } - -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java deleted file mode 100644 index 122a1335d7c..00000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java +++ /dev/null @@ -1,124 +0,0 @@ -/* ### - * 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.plugins.fsbrowser; - -import java.util.*; - -import javax.swing.Icon; - -import generic.theme.*; -import ghidra.formats.gfilesystem.FSUtilities; -import resources.MultiIcon; - -/** - * Provides {@link Icon}s that represent the type and status of a file, based on - * a filename mapping and caller specified status overlays. - *

- * The mappings between a file's extension and its icon are stored in a resource - * file called "file_extension_icons.xml", which is read and parsed the first - * time this service is referenced. - *

- * Status overlays are also specified in the file_extension_icons.xml file, and - * are resized to be 1/2 the width and height of the icon they are being - * overlaid on. - *

- * Thread safe - *

- */ -public class FileIconService { - - private static final class Singleton { - private static final FileIconService INSTANCE = new FileIconService(); - } - - public static FileIconService getInstance() { - return Singleton.INSTANCE; - } - - public static final Icon IMPORTED_OVERLAY_ICON = - new GIcon("icon.fsbrowser.file.overlay.imported"); - public static final Icon FILESYSTEM_OVERLAY_ICON = - new GIcon("icon.fsbrowser.file.overlay.filesystem"); - public static final Icon MISSING_PASSWORD_OVERLAY_ICON = - new GIcon("icon.fsbrowser.file.overlay.missing.password"); - public static final Icon DEFAULT_ICON = new GIcon("icon.fsbrowser.file.extension.default"); - - private static final String EXTENSION_ICON_PREFIX = "icon.fsbrowser.file.extension"; - private static final String SUBSTRING_ICON_PREFIX = "icon.fsbrowser.file.substring"; - - private Map substringToIconMap = new HashMap<>(); - - private FileIconService() { - createSubstringMap(); - } - - private void createSubstringMap() { - GThemeValueMap values = ThemeManager.getInstance().getCurrentValues(); - List icons = values.getIcons(); - for (IconValue iconValue : icons) { - String id = iconValue.getId(); - if (id.startsWith(SUBSTRING_ICON_PREFIX)) { - String substring = id.substring(SUBSTRING_ICON_PREFIX.length()); - substringToIconMap.put(substring, new GIcon(id)); - } - } - } - - /** - * Returns an {@link Icon} that represents a file's content based on its - * name. - * - * @param fileName name of file that an icon is being requested for. - * @param overlays optional list of overlay icons that - * should be overlaid on top of the base icon. These icons represent a - * status or feature independent of the file's base icon. - * @return {@link Icon} instance that best represents the named file, never - * null. - */ - public Icon getIcon(String fileName, List overlays) { - fileName = fileName.toLowerCase(); - String ext = FSUtilities.getExtension(fileName, 1); - if (ext != null) { - String iconId = EXTENSION_ICON_PREFIX + ext; - if (Gui.hasIcon(iconId)) { - Icon base = new GIcon(iconId); - return buildIcon(base, overlays); - } - } - - for (String substring : substringToIconMap.keySet()) { - if (fileName.indexOf(substring) != -1) { - return buildIcon(substringToIconMap.get(substring), overlays); - } - } - - // return default icon for generic file - return buildIcon(DEFAULT_ICON, overlays); - } - - private Icon buildIcon(Icon base, List overlays) { - if (overlays == null || overlays.isEmpty()) { - return base; - } - MultiIcon multiIcon = new MultiIcon(base); - for (Icon overlay : overlays) { - if (overlay != null) { - multiIcon.addIcon(overlay); - } - } - return multiIcon; - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserComponentProvider.java deleted file mode 100644 index a8f32f6b824..00000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserComponentProvider.java +++ /dev/null @@ -1,339 +0,0 @@ -/* ### - * 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.plugins.fsbrowser; - -import java.awt.Component; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.List; - -import javax.swing.*; -import javax.swing.tree.TreePath; -import javax.swing.tree.TreeSelectionModel; - -import docking.WindowPosition; -import docking.event.mouse.GMouseListenerAdapter; -import docking.widgets.tree.GTree; -import docking.widgets.tree.GTreeNode; -import docking.widgets.tree.support.GTreeRenderer; -import generic.theme.GThemeDefaults.Colors.Palette; -import ghidra.app.services.ProgramManager; -import ghidra.formats.gfilesystem.*; -import ghidra.framework.plugintool.ComponentProviderAdapter; -import ghidra.plugin.importer.ProgramMappingService; -import ghidra.program.model.listing.Program; -import ghidra.util.*; - -/** - * Plugin component provider for the {@link FileSystemBrowserPlugin}. - *

- * An instance of this class is created for each file system browser window (w/tree). - *

- * Visible to just this package. - */ -class FileSystemBrowserComponentProvider extends ComponentProviderAdapter - implements FileSystemEventListener { - private static final String TITLE = "Filesystem Viewer"; - - private FileSystemBrowserPlugin plugin; - private FSBActionManager actionManager; - private GTree gTree; - private FSBRootNode rootNode; - private FileSystemService fsService = FileSystemService.getInstance(); - - /** - * Creates a new {@link FileSystemBrowserComponentProvider} instance, taking - * ownership of the passed-in {@link FileSystemRef fsRef}. - * - * @param plugin parent plugin - * @param fsRef {@link FileSystemRef} to a {@link GFileSystem}. - */ - public FileSystemBrowserComponentProvider(FileSystemBrowserPlugin plugin, FileSystemRef fsRef) { - super(plugin.getTool(), fsRef.getFilesystem().getName(), plugin.getName()); - - this.plugin = plugin; - this.rootNode = new FSBRootNode(fsRef); - - setTransient(); - setIcon(ImageManager.PHOTO); - - gTree = new GTree(rootNode); - gTree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); - gTree.getSelectionModel().addTreeSelectionListener(e -> { - tool.contextChanged(FileSystemBrowserComponentProvider.this); - TreePath[] paths = gTree.getSelectionPaths(); - if (paths.length == 1) { - GTreeNode clickedNode = (GTreeNode) paths[0].getLastPathComponent(); - handleSingleClick(clickedNode); - } - }); - gTree.addMouseListener(new GMouseListenerAdapter() { - @Override - public void doubleClickTriggered(MouseEvent e) { - handleDoubleClick(gTree.getNodeForLocation(e.getX(), e.getY())); - e.consume(); - } - - @Override - public void mouseClicked(MouseEvent e) { - super.mouseClicked(e); - if (!e.isConsumed()) { - handleSingleClick(gTree.getNodeForLocation(e.getX(), e.getY())); - } - } - }); - gTree.setCellRenderer(new GTreeRenderer() { - @Override - public Component getTreeCellRendererComponent(JTree tree, Object value, - boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { - - super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, - hasFocus); - - if (value instanceof FSBRootNode) { - renderFS((FSBRootNode) value, selected); - } - else if (value instanceof FSBDirNode) { - // do nothing special - } - else if (value instanceof FSBFileNode) { - renderFile((FSBFileNode) value, selected); - } - else if (value instanceof FSBNode) { - renderNode((FSBNode) value, selected); - } - - return this; - } - - private void renderFS(FSBRootNode node, boolean selected) { - FileSystemRef nodeFSRef = node.getFSRef(); - if (nodeFSRef == null || nodeFSRef.getFilesystem() == null) { - return; - } - FSRLRoot fsFSRL = nodeFSRef.getFilesystem().getFSRL(); - String containerFilename = - fsFSRL.hasContainer() ? fsFSRL.getContainer().getName() : "unknown"; - Icon image = FileIconService.getInstance() - .getIcon(containerFilename, - List.of(FileIconService.FILESYSTEM_OVERLAY_ICON)); - setIcon(image); - } - - private void renderFile(FSBFileNode node, boolean selected) { - FSRL fsrl = node.getFSRL(); - String filename = fsrl.getName(); - List overlays = new ArrayList<>(3); - - if (ProgramMappingService.isFileImportedIntoProject(fsrl)) { - overlays.add(FileIconService.IMPORTED_OVERLAY_ICON); - } - if (fsService.isFilesystemMountedAt(fsrl)) { - overlays.add(FileIconService.FILESYSTEM_OVERLAY_ICON); - } - if (node.hasMissingPassword()) { - overlays.add(FileIconService.MISSING_PASSWORD_OVERLAY_ICON); - } - - Icon icon = FileIconService.getInstance().getIcon(filename, overlays); - setIcon(icon); - - if (ProgramMappingService.isFileOpen(fsrl)) { - // TODO: change this to a OVERLAY_OPEN option when fetching icon - setForeground(selected ? Palette.CYAN : Palette.MAGENTA); - } - } - - private void renderNode(FSBNode node, boolean selected) { - // do nothing for now - } - }); - - actionManager = new FSBActionManager(plugin, this, gTree); - - // TODO: fix this Help stuff - setHelpLocation( - new HelpLocation("FileSystemBrowserPlugin", "FileSystemBrowserIntroduction")); - - fsRef.getFilesystem().getRefManager().addListener(this); - } - - /** - * For testing access only. - * - * @return this provider's GTree. - */ - GTree getGTree() { - return gTree; - } - - FSRL getFSRL() { - return rootNode != null ? rootNode.getFSRL() : null; - } - - FSBActionManager getActionManager() { - return actionManager; - } - - void dispose() { - if (rootNode != null && rootNode.getFSRef() != null && !rootNode.getFSRef().isClosed()) { - rootNode.getFSRef().getFilesystem().getRefManager().removeListener(this); - } - removeFromTool(); - if (actionManager != null) { - actionManager.dispose(); - actionManager = null; - } - if (gTree != null) { - gTree.dispose(); // calls dispose() on tree's rootNode, which will release the fsRefs - gTree = null; - } - rootNode = null; - plugin = null; - } - - @Override - public void componentHidden() { - // if the component is 'closed', nuke ourselves - if (plugin != null) { - plugin.removeFileSystemBrowserComponent(this); - dispose(); - } - } - - public void afterAddedToTool() { - actionManager.registerComponentActionsInTool(); - } - - @Override - public void onFilesystemClose(GFileSystem fs) { - Msg.info(this, "File system " + fs.getFSRL() + " was closed! Closing browser window"); - Swing.runIfSwingOrRunLater(() -> componentHidden()); - } - - @Override - public void onFilesystemRefChange(GFileSystem fs, FileSystemRefManager refManager) { - // nothing - } - - /*****************************************/ - - /** - * Finds an associated already open {@link Program} and makes it visible in the - * current tool's ProgramManager. - * - * @param fsrl {@link FSRL} of the file to attempt to quickly show if its already open in a PM. - * @return boolean true if already open program was found and it was switched to. - */ - private boolean quickShowProgram(FSRL fsrl) { - if (plugin.hasProgramManager()) { - ProgramManager programManager = FSBUtils.getProgramManager(plugin.getTool(), false); - if (programManager != null) { - Object consumer = new Object(); - Program program = ProgramMappingService.findMatchingOpenProgram(fsrl, consumer); - if (program != null) { - programManager.setCurrentProgram(program); - program.release(consumer); - return true; - } - } - } - - return false; - } - - private void handleSingleClick(GTreeNode clickedNode) { - if (clickedNode instanceof FSBFileNode) { - FSBFileNode node = (FSBFileNode) clickedNode; - if (node.getFSRL() != null) { - quickShowProgram(node.getFSRL()); - updatePasswordStatus(node); - } - } - } - - private void updatePasswordStatus(FSBFileNode node) { - // currently this is the only state that might change - // and that effect the node display - if (node.hasMissingPassword()) { - // check and see if its status has changed - gTree.runTask(monitor -> { - if (node.needsFileAttributesUpdate(monitor)) { - actionManager.doRefreshInfo(List.of(node), monitor); - } - }); - } - } - - private void handleDoubleClick(GTreeNode clickedNode) { - if (clickedNode instanceof FSBFileNode && clickedNode.isLeaf()) { - FSBFileNode node = (FSBFileNode) clickedNode; - - if (node.getFSRL() != null && !quickShowProgram(node.getFSRL())) { - actionManager.actionOpenPrograms.actionPerformed(getActionContext(null)); - } - } - } - - /*****************************************/ - - @Override - public FSBActionContext getActionContext(MouseEvent event) { - return new FSBActionContext(this, getSelectedNodes(event), event, gTree); - } - - private FSBNode[] getSelectedNodes(MouseEvent event) { - TreePath[] selectionPaths = gTree.getSelectionPaths(); - List list = new ArrayList<>(selectionPaths.length); - for (TreePath selectionPath : selectionPaths) { - Object lastPathComponent = selectionPath.getLastPathComponent(); - if (lastPathComponent instanceof FSBNode) { - list.add((FSBNode) lastPathComponent); - } - } - if (list.isEmpty() && event != null) { - Object source = event.getSource(); - int x = event.getX(); - int y = event.getY(); - if (source instanceof JTree) { - JTree sourceTree = (JTree) source; - if (gTree.isMyJTree(sourceTree)) { - GTreeNode nodeAtEventLocation = gTree.getNodeForLocation(x, y); - if (nodeAtEventLocation != null && nodeAtEventLocation instanceof FSBNode) { - list.add((FSBNode) nodeAtEventLocation); - } - } - } - } - return list.toArray(FSBNode[]::new); - } - - @Override - public JComponent getComponent() { - return gTree; - } - - @Override - public String getName() { - return TITLE; - } - - @Override - public WindowPosition getDefaultWindowPosition() { - return WindowPosition.WINDOW; - } - -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java index 9da72ef7548..eaa95d98528 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java @@ -33,16 +33,14 @@ import ghidra.app.events.ProgramActivatedPluginEvent; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.services.FileSystemBrowserService; -import ghidra.app.services.ProgramManager; import ghidra.formats.gfilesystem.*; import ghidra.framework.main.ApplicationLevelPlugin; import ghidra.framework.main.FrontEndService; -import ghidra.framework.model.Project; -import ghidra.framework.model.ProjectListener; +import ghidra.framework.model.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; import ghidra.plugin.importer.ImporterUtilities; -import ghidra.plugin.importer.ProgramMappingService; +import ghidra.plugin.importer.ProjectIndexService; import ghidra.util.Msg; import ghidra.util.Swing; import ghidra.util.exception.CancelledException; @@ -74,8 +72,9 @@ public class FileSystemBrowserPlugin extends Plugin /* package */ DockingAction showFileSystemImplsAction; private GhidraFileChooser chooserOpen; private FrontEndService frontEndService; - private Map currentBrowsers = new HashMap<>(); + private Map currentBrowsers = new HashMap<>(); private FileSystemService fsService; // don't use this directly, use fsService() instead + private File lastExportDirectory; public FileSystemBrowserPlugin(PluginTool tool) { super(tool); @@ -89,9 +88,6 @@ protected void init() { if (frontEndService != null) { frontEndService.addProjectListener(this); } - else { - FSBUtils.getProgramManager(tool, false); - } setupActions(); } @@ -122,7 +118,7 @@ protected void dispose() { chooserOpen.dispose(); } - for (FileSystemBrowserComponentProvider provider : currentBrowsers.values()) { + for (FSBComponentProvider provider : currentBrowsers.values()) { provider.dispose(); } currentBrowsers.clear(); @@ -143,19 +139,19 @@ public void openFileSystem(FSRL fsrl) { * @param fsRef {@link FileSystemRef} of open {@link GFileSystem} * @param show boolean true if the new browser component should be shown */ - /* package */ void createNewFileSystemBrowser(FileSystemRef fsRef, boolean show) { + public void createNewFileSystemBrowser(FileSystemRef fsRef, boolean show) { Swing.runIfSwingOrRunLater(() -> doCreateNewFileSystemBrowser(fsRef, show)); } private void doCreateNewFileSystemBrowser(FileSystemRef fsRef, boolean show) { FSRLRoot fsFSRL = fsRef.getFilesystem().getFSRL(); - FileSystemBrowserComponentProvider provider = currentBrowsers.get(fsFSRL); + FSBComponentProvider provider = currentBrowsers.get(fsFSRL); if (provider != null) { Msg.info(this, "Filesystem browser already open for " + fsFSRL); fsRef.close(); } else { - provider = new FileSystemBrowserComponentProvider(this, fsRef); + provider = new FSBComponentProvider(this, fsRef); currentBrowsers.put(fsFSRL, provider); getTool().addComponentProvider(provider, false); provider.afterAddedToTool(); @@ -168,7 +164,7 @@ private void doCreateNewFileSystemBrowser(FileSystemRef fsRef, boolean show) { } } - void removeFileSystemBrowserComponent(FileSystemBrowserComponentProvider componentProvider) { + void removeFileSystemBrowserComponent(FSBComponentProvider componentProvider) { if (componentProvider != null) { Swing.runIfSwingOrRunLater(() -> currentBrowsers.remove(componentProvider.getFSRL())); } @@ -179,7 +175,7 @@ void removeFileSystemBrowserComponent(FileSystemBrowserComponentProvider compone */ private void removeAllFileSystemBrowsers() { Swing.runIfSwingOrRunLater(() -> { - for (FileSystemBrowserComponentProvider fsbcp : new ArrayList<>( + for (FSBComponentProvider fsbcp : new ArrayList<>( currentBrowsers.values())) { fsbcp.dispose(); } @@ -187,29 +183,6 @@ private void removeAllFileSystemBrowsers() { }); } - @Override - public void processEvent(PluginEvent event) { - super.processEvent(event); - - if (event instanceof ProgramActivatedPluginEvent) { - ProgramActivatedPluginEvent pape = (ProgramActivatedPluginEvent) event; - ProgramMappingService.createAutoAssocation(pape.getActiveProgram()); - } - } - - @Override - public void projectClosed(Project project) { - removeAllFileSystemBrowsers(); - if (FileSystemService.isInitialized()) { - fsService().closeUnusedFileSystems(); - } - } - - @Override - public void projectOpened(Project project) { - // nada - } - private void openChooser(String title, String buttonText, boolean multiSelect) { if (chooserOpen == null) { chooserOpen = new GhidraFileChooser(tool.getActiveWindow()); @@ -253,7 +226,7 @@ private void doOpenFilesystem(FSRL containerFSRL, Component parent, TaskMonitor * Prompts the user to pick a file system container file to open using a local * filesystem browser and then displays that filesystem in a new fsb browser. */ - /* package */ void openFileSystem() { + public void openFileSystem() { Swing.runLater(this::doOpenFileSystem); } @@ -291,28 +264,61 @@ private FileSystemService fsService() { return fsService; } - /** - * Returns true if there is a {@link ProgramManager} associated with this FSB. - * - * @return boolean true if there is a ProgramManager. - */ - /* package */ boolean hasProgramManager() { - return tool.getService(ProgramManager.class) != null || - FSBUtils.getRunningProgramManagerTools(getTool()).size() == 1; - } - /** * For testing access only. * * @param fsFSRL {@link FSRLRoot} of browser component to fetch. * @return provider or null if not found. */ - /* package */ FileSystemBrowserComponentProvider getProviderFor(FSRLRoot fsFSRL) { - FileSystemBrowserComponentProvider provider = currentBrowsers.get(fsFSRL); + /* package */ FSBComponentProvider getProviderFor(FSRLRoot fsFSRL) { + FSBComponentProvider provider = currentBrowsers.get(fsFSRL); if (provider == null) { Msg.info(this, "Could not find browser for " + fsFSRL); return null; } return provider; } + + //-------------------------------------------------------------------------------------------- + @Override + public void processEvent(PluginEvent event) { + super.processEvent(event); + } + + @Override + public void projectClosed(Project project) { + removeAllFileSystemBrowsers(); + if (FileSystemService.isInitialized()) { + fsService().closeUnusedFileSystems(); + } + ProjectIndexService.getInstance().clearProject(); + } + + @Override + public void projectOpened(Project project) { + // there shouldn't be any fsb components open because the previous projectClosed would have + // removed all fsb trees, therefore, we don't need to update any of the components + // to tell them about the new project + } + + public boolean isOpen(DomainFile df) { + Object tmp = new Object(); + DomainObject openDF = df.getOpenedDomainObject(tmp); + if (openDF != null) { + openDF.release(tmp); + return true; + } + return false; + } + + public File getLastExportDirectory() { + return lastExportDirectory != null + ? lastExportDirectory + : new File(System.getProperty("user.home")); + } + + public void setLastExportDirectory(File lastExportDirectory) { + this.lastExportDirectory = lastExportDirectory; + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/ImageManager.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/ImageManager.java deleted file mode 100644 index 0928c4de9b8..00000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/ImageManager.java +++ /dev/null @@ -1,66 +0,0 @@ -/* ### - * 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.plugins.fsbrowser; - -import javax.swing.Icon; - -import generic.theme.GIcon; - -/** - * Static helper to register and load Icons for the file system browser plugin and its - * child windows. - *

- * Visible to just this package. - */ -public class ImageManager { - //@formatter:off - public final static Icon COPY = new GIcon("icon.plugin.fsbrowser.copy"); - public final static Icon CUT = new GIcon("icon.plugin.fsbrowser.cut"); - public final static Icon DELETE = new GIcon("icon.plugin.fsbrowser.delete"); - public final static Icon FONT = new GIcon("icon.plugin.fsbrowser.font"); - public final static Icon LOCKED = new GIcon("icon.plugin.fsbrowser.locked"); - public final static Icon NEW = new GIcon("icon.plugin.fsbrowser.new"); - public final static Icon PASTE = new GIcon("icon.plugin.fsbrowser.paste"); - public final static Icon REDO = new GIcon("icon.plugin.fsbrowser.redo"); - public final static Icon RENAME = new GIcon("icon.plugin.fsbrowser.rename"); - public final static Icon REFRESH = new GIcon("icon.plugin.fsbrowser.refresh"); - public final static Icon SAVE = new GIcon("icon.plugin.fsbrowser.save"); - public final static Icon SAVE_AS = new GIcon("icon.plugin.fsbrowser.save.as"); - public final static Icon UNDO = new GIcon("icon.plugin.fsbrowser.undo"); - public final static Icon UNLOCKED = new GIcon("icon.plugin.fsbrowser.unlocked"); - public final static Icon CLOSE = new GIcon("icon.plugin.fsbrowser.close"); - public final static Icon COLLAPSE_ALL = new GIcon("icon.plugin.fsbrowser.collapse.all"); - public final static Icon COMPRESS = new GIcon("icon.plugin.fsbrowser.compress"); - public final static Icon CREATE_FIRMWARE = new GIcon("icon.plugin.fsbrowser.create.firmware"); - public final static Icon EXPAND_ALL = new GIcon("icon.plugin.fsbrowser.expand.all"); - public final static Icon EXTRACT = new GIcon("icon.plugin.fsbrowser.extract"); - public final static Icon INFO = new GIcon("icon.plugin.fsbrowser.info"); - public final static Icon OPEN = new GIcon("icon.plugin.fsbrowser.open"); - public final static Icon OPEN_AS_BINARY = new GIcon("icon.plugin.fsbrowser.open.as.binary"); - public final static Icon OPEN_IN_LISTING = new GIcon("icon.plugin.fsbrowser.open.in.listing"); - public final static Icon OPEN_FILE_SYSTEM = new GIcon("icon.plugin.fsbrowser.open.file.system"); - public final static Icon PHOTO = new GIcon("icon.plugin.fsbrowser.photo"); - public final static Icon VIEW_AS_IMAGE = new GIcon("icon.plugin.fsbrowser.view.as.image"); - public final static Icon VIEW_AS_TEXT = new GIcon("icon.plugin.fsbrowser.view.as.text"); - public final static Icon ECLIPSE = new GIcon("icon.plugin.fsbrowser.eclipse"); - public final static Icon JAR = new GIcon("icon.plugin.fsbrowser.jar"); - public final static Icon IMPORT = new GIcon("icon.plugin.fsbrowser.import"); - public final static Icon iOS = new GIcon("icon.plugin.fsbrowser.ios"); - public final static Icon OPEN_ALL = new GIcon("icon.plugin.fsbrowser.open.all"); - public final static Icon LIST_MOUNTED = new GIcon("icon.plugin.fsbrowser.list.mounted"); - public final static Icon LIBRARY = new GIcon("icon.plugin.fsbrowser.library"); - //@formatter:on -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/OpenWithTarget.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/OpenWithTarget.java new file mode 100644 index 00000000000..be375440829 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/OpenWithTarget.java @@ -0,0 +1,202 @@ +/* ### + * 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.plugins.fsbrowser; + +import java.util.*; + +import javax.swing.Icon; + +import ghidra.app.services.ProgramManager; +import ghidra.framework.main.AppInfo; +import ghidra.framework.model.*; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.ProgramContentHandler; +import ghidra.program.model.listing.Program; + +/** + * Represents a way to open a {@link DomainFile} in a {@link ProgramManager} + */ +public class OpenWithTarget { + + /** + * Returns a list of all running tools and tool templates that can be used to open a domainfile. + * + * @return list of OpenWithTarget instances, maybe empty but not null + */ + public static List getAll() { + List results = new ArrayList<>(); + Project project = AppInfo.getActiveProject(); + if (project != null) { + results.addAll(getRunningTargets(project)); + + ToolTemplate defaultTT = project.getToolServices() + .getDefaultToolTemplate(ProgramContentHandler.PROGRAM_CONTENT_TYPE); + results.add(new OpenWithTarget(defaultTT.getName(), null, defaultTT.getIcon())); + + ToolTemplate[] templates = project.getLocalToolChest().getToolTemplates(); + for (ToolTemplate toolTemplate : templates) { + if (!toolTemplate.getName().equals(defaultTT.getName())) { + results.add( + new OpenWithTarget(toolTemplate.getName(), null, toolTemplate.getIcon())); + } + } + } + return results; + } + + /** + * Returns an OpenWithTarget, or null, that represents the specified tool's default ability + * to open a {@link DomainFile}. + * + * @param tool a {@link PluginTool} + * @return a {@link OpenWithTarget}, or null if the specified tool can't open a domain file + */ + public static OpenWithTarget getDefault(PluginTool tool) { + Project project = tool.getProject(); + if (project == null) { + return null; + } + ProgramManager pm = tool.getService(ProgramManager.class); + if (pm != null) { + return new OpenWithTarget(tool.getName(), pm, tool.getIcon()); + } + if (AppInfo.getFrontEndTool().getDefaultLaunchMode() == DefaultLaunchMode.REUSE_TOOL) { + List runningTargets = getRunningTargets(project); + if (!runningTargets.isEmpty()) { + return runningTargets.get(0); + } + } + ToolTemplate defaultTT = project.getToolServices() + .getDefaultToolTemplate(ProgramContentHandler.PROGRAM_CONTENT_TYPE); + return new OpenWithTarget(defaultTT.getName(), null, defaultTT.getIcon()); + } + + /** + * Returns an OpenWithTarget, or null, that represents a running {@link ProgramManager}. + * + * @param tool a {@link PluginTool} + * @return a {@link OpenWithTarget}, or null if there is no open {@link ProgramManager} + */ + public static OpenWithTarget getRunningProgramManager(PluginTool tool) { + Project project = tool.getProject(); + if (project == null) { + return null; + } + ProgramManager pm = tool.getService(ProgramManager.class); + if (pm != null) { + return new OpenWithTarget(tool.getName(), pm, tool.getIcon()); + } + List runningTargets = getRunningTargets(project); + return !runningTargets.isEmpty() ? runningTargets.get(0) : null; + } + + private final String name; + private final ProgramManager pm; + private final Icon icon; + + public OpenWithTarget(String name, ProgramManager pm, Icon icon) { + this.name = name; + this.pm = pm; + this.icon = icon; + } + + public String getName() { + return name; + } + + public ProgramManager getPm() { + return pm; + } + + public Icon getIcon() { + return icon; + } + + /** + * Opens the specified files, using whatever program manager / tool this instance represents. + *

+ * The first item in the list of files will be focused / made visible, the other items in the + * list will be opened but not focused. + * + * @param files {@link DomainFile}s to open + */ + public void open(List files) { + Project project = AppInfo.getActiveProject(); + if (project == null) { + return; + } + if (pm != null) { + openWithPM(files); + } + else { + openWithToolTemplate(project, files); + } + } + + private Map openWithPM(List files) { + Map results = new HashMap<>(); + for (DomainFile file : files) { + int openMode = + results.isEmpty() ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE; + Program program = pm.openProgram(file, DomainFile.DEFAULT_VERSION, openMode); + if (program != null) { + results.put(file, program); + } + } + return results; + } + + private Map openWithToolTemplate(Project project, List files) { + Map results = new HashMap<>(); + + PluginTool newTool = project.getToolServices().launchTool(name, files); + + ProgramManager newToolPM; + if (newTool != null && (newToolPM = newTool.getService(ProgramManager.class)) != null) { + Set fileSet = new HashSet<>(files); + for (Program openProgram : newToolPM.getAllOpenPrograms()) { + if (fileSet.contains(openProgram.getDomainFile())) { + results.put(openProgram.getDomainFile(), openProgram); + } + } + } + return results; + } + + //---------------------------------------------------------------------------------------------- + + private static List getRunningTargets(Project project) { + List results = new ArrayList<>(); + for (PluginTool runningTool : project.getToolManager().getRunningTools()) { + ProgramManager runningPM = runningTool.getService(ProgramManager.class); + if (runningPM != null) { + Program currentProgram = runningPM.getCurrentProgram(); + int programCount = runningPM.getAllOpenPrograms().length; + String descName = runningTool.getName(); + if (currentProgram != null) { + descName += ": " + currentProgram.getName(); + if (programCount > 1) { + descName += " (+%d more)".formatted(programCount - 1); + } + } + results.add(new OpenWithTarget(descName, runningPM, runningTool.getIcon())); + } + } + Collections.sort(results, (r1, r2) -> r2.name.compareTo(r1.name)); + return results; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/TextEditorComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/TextEditorComponentProvider.java index 72376a7748b..7668bfb5d18 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/TextEditorComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/TextEditorComponentProvider.java @@ -33,7 +33,6 @@ import docking.widgets.OptionDialog; import docking.widgets.filechooser.GhidraFileChooser; import generic.theme.*; -import ghidra.framework.main.AppInfo; import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.util.Msg; import ghidra.util.datastruct.FixedSizeStack; @@ -59,8 +58,9 @@ public class TextEditorComponentProvider extends ComponentProviderAdapter { private FixedSizeStack undoStack = new FixedSizeStack<>(MAX_UNDO_REDO_SIZE); private FixedSizeStack redoStack = new FixedSizeStack<>(MAX_UNDO_REDO_SIZE); - TextEditorComponentProvider(String textFileName, String text) { - super(AppInfo.getFrontEndTool(), TITLE, "TextEditorComponentProvider"); + public TextEditorComponentProvider(FileSystemBrowserPlugin plugin, String textFileName, + String text) { + super(plugin.getTool(), TITLE, "TextEditorComponentProvider"); this.textFileName = textFileName; initialize(text); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/AddToProgramFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/AddToProgramFSBFileHandler.java new file mode 100644 index 00000000000..7ef0f48ec66 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/AddToProgramFSBFileHandler.java @@ -0,0 +1,73 @@ +/* ### + * 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.plugins.fsbrowser.filehandlers; + +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.plugin.importer.ImporterUtilities; +import ghidra.plugins.fsbrowser.*; +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; + +public class AddToProgramFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder("FSB Add To Program", context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getLoadableFSRL() != null) + .popupMenuIcon(FSBIcons.IMPORT) + .popupMenuPath("Add To Program") + .popupMenuGroup("F", "C") + .onAction(ac -> { + FSRL fsrl = ac.getLoadableFSRL(); + if (fsrl == null) { + return; + } + OpenWithTarget openWith = + OpenWithTarget.getRunningProgramManager(context.plugin().getTool()); + if (openWith == null || openWith.getPm().getCurrentProgram() == null) { + Msg.showError(this, ac.getSourceComponent(), "Unable To Add To Program", + "No programs are open"); + return; + } + + FSBComponentProvider fsbComp = ac.getComponentProvider(); + Program program = openWith.getPm().getCurrentProgram(); + if (program != null) { + fsbComp.runTask(monitor -> { + if (fsbComp.ensureFileAccessable(fsrl, ac.getSelectedNode(), monitor)) { + ImporterUtilities.showAddToProgramDialog(fsrl, program, + fsbComp.getTool(), monitor); + } + }); + return; + } + }) + .build()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/BatchImportFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/BatchImportFSBFileHandler.java new file mode 100644 index 00000000000..b2a522333b6 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/BatchImportFSBFileHandler.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.plugins.fsbrowser.filehandlers; + +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.framework.plugintool.PluginTool; +import ghidra.plugins.fsbrowser.*; +import ghidra.plugins.importer.batch.BatchImportDialog; + +public class BatchImportFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder("FSB Import Batch", context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getSelectedCount() > 0) + .popupMenuIcon(FSBIcons.IMPORT) + .popupMenuPath("Batch Import") + .popupMenuGroup("F", "B") + .onAction(ac -> { + // Do some fancy selection logic. + // If the user selected a combination of files and folders, + // ignore the folders. + // If they only selected folders, leave them in the list. + List files = ac.getFSRLs(true); + if (files.isEmpty()) { + return; + } + + boolean allDirs = ac.isSelectedAllDirs(); + if (files.size() > 1 && !allDirs) { + files = ac.getFileFSRLs(); + } + + PluginTool tool = context.plugin().getTool(); + OpenWithTarget openWith = OpenWithTarget.getDefault(tool); + BatchImportDialog.showAndImport(tool, null, files, null, openWith.getPm()); + }) + .build()); + + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ClearCachedPwdFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ClearCachedPwdFSBFileHandler.java new file mode 100644 index 00000000000..e34d5fbf688 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ClearCachedPwdFSBFileHandler.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.plugins.fsbrowser.filehandlers; + +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import ghidra.formats.gfilesystem.crypto.CachedPasswordProvider; +import ghidra.formats.gfilesystem.crypto.CryptoProviders; +import ghidra.plugins.fsbrowser.*; +import ghidra.util.Msg; + +public class ClearCachedPwdFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder("FSB Clear Cached Passwords", context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(FSBActionContext::notBusy) + .popupMenuPath("Clear Cached Passwords") + .popupMenuGroup("Z", "B") + .description("Clear cached container file passwords") + .onAction(ac -> { + CachedPasswordProvider ccp = + CryptoProviders.getInstance().getCachedCryptoProvider(); + int preCount = ccp.getCount(); + ccp.clearCache(); + + String msg = + "Cleared %d cached passwords.".formatted(preCount - ccp.getCount()); + + Msg.info(this, msg); + context.fsbComponent().getPlugin().getTool().setStatusInfo(msg); + }) + .build()); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/CloseFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/CloseFSBFileHandler.java new file mode 100644 index 00000000000..eec6caceb57 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/CloseFSBFileHandler.java @@ -0,0 +1,70 @@ +/* ### + * 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.plugins.fsbrowser.filehandlers; + +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.widgets.OptionDialog; +import ghidra.plugins.fsbrowser.*; + +public class CloseFSBFileHandler implements FSBFileHandler { + + public static final String FSB_CLOSE = "FSB Close"; + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder(FSB_CLOSE, context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getSelectedNode() instanceof FSBRootNode) + .description("Close") + .toolBarIcon(FSBIcons.CLOSE) + .toolBarGroup("ZZZZ") + .popupMenuIcon(FSBIcons.CLOSE) + .popupMenuPath("Close") + .popupMenuGroup("ZZZZ") + .onAction(ac -> { + FSBNode selectedNode = ac.getSelectedNode(); + if (!(selectedNode instanceof FSBRootNode node)) { + return; + } + if (node.getParent() == null) { + // Close entire window + if (OptionDialog.showYesNoDialog(ac.getSourceComponent(), + "Close File System", + "Do you want to close the filesystem browser for %s?" + .formatted(node.getName())) == OptionDialog.YES_OPTION) { + ac.getComponentProvider().componentHidden(); // cause component to close itself + } + } + else { + // Close file system that is nested in the container's tree and swap + // in the saved node that was the original container file + ac.getComponentProvider() + .runTask(monitor -> node.swapBackPrevModelNodeAndDispose()); + } + }) + .build()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ExportFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ExportFSBFileHandler.java new file mode 100644 index 00000000000..fec8dfaef9e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ExportFSBFileHandler.java @@ -0,0 +1,146 @@ +/* ### + * 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.plugins.fsbrowser.filehandlers; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.widgets.OptionDialog; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import docking.widgets.tree.GTree; +import ghidra.app.util.bin.ByteProvider; +import ghidra.formats.gfilesystem.*; +import ghidra.plugins.fsbrowser.*; +import ghidra.plugins.fsbrowser.tasks.GFileSystemExtractAllTask; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskLauncher; +import ghidra.util.task.TaskMonitor; + +public class ExportFSBFileHandler implements FSBFileHandler { + public static final String FSB_EXPORT_ALL = "FSB Export All"; + public static final String FSB_EXPORT = "FSB Export"; + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder(FSB_EXPORT, context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getFileFSRL() != null) + .popupMenuIcon(FSBIcons.EXTRACT) + .popupMenuPath("Export...") + .popupMenuGroup("F", "C") + .onAction(ac -> { + FSRL fsrl = ac.getFileFSRL(); + if (fsrl == null) { + return; + } + GTree tree = ac.getTree(); + GhidraFileChooser chooser = new GhidraFileChooser(tree); + chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY); + chooser.setTitle("Select Where To Export File"); + chooser.setApproveButtonText("Export"); + chooser.setSelectedFile( + new File(context.plugin().getLastExportDirectory(), fsrl.getName())); + File selectedFile = chooser.getSelectedFile(); + chooser.dispose(); + if (selectedFile == null) { + return; + } + + if (selectedFile.exists()) { + int answer = OptionDialog.showYesNoDialog(tree, "Confirm Overwrite", + "%s\nThe file already exists.\nDo you want to overwrite it?" + .formatted(selectedFile.getAbsolutePath())); + if (answer == OptionDialog.NO_OPTION) { + return; + } + } + context.plugin().setLastExportDirectory(selectedFile.getParentFile()); + tree.runTask( + monitor -> doExtractFile(fsrl, selectedFile, ac.getSelectedNode(), + monitor)); + }) + .build(), + new ActionBuilder(FSB_EXPORT_ALL, context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.isSelectedAllDirs()) + .popupMenuIcon(FSBIcons.EXTRACT) + .popupMenuPath("Export All...") + .popupMenuGroup("F", "C") + .onAction(ac -> { + FSRL fsrl = ac.getFSRL(true); + if (fsrl == null) { + return; + } + GTree tree = ac.getTree(); + if (fsrl instanceof FSRLRoot) { + fsrl = fsrl.appendPath("/"); + } + GhidraFileChooser chooser = new GhidraFileChooser(tree); + chooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); + chooser.setTitle("Select Export Directory"); + chooser.setApproveButtonText("Export All"); + chooser.setCurrentDirectory(context.plugin().getLastExportDirectory()); + File selectedFile = chooser.getSelectedFile(); + chooser.dispose(); + if (selectedFile == null) { + return; + } + if (!selectedFile.isDirectory()) { + Msg.showInfo(this, tree, "Export All", + "Selected file is not a directory."); + return; + } + context.plugin().setLastExportDirectory(selectedFile); + + TaskLauncher.launch(new GFileSystemExtractAllTask(fsrl, selectedFile, tree)); + }) + .build()); + } + + private void doExtractFile(FSRL fsrl, File outputFile, FSBNode node, TaskMonitor monitor) { + if (!context.fsbComponent().ensureFileAccessable(fsrl, node, monitor)) { + return; + } + monitor.setMessage("Exporting..."); + try (ByteProvider fileBP = context.fsService().getByteProvider(fsrl, false, monitor)) { + monitor.initialize(fileBP.length(), "Exporting %s".formatted(fsrl.getName())); + long bytesCopied = FSUtilities.copyByteProviderToFile(fileBP, outputFile, monitor); + + String msg = "Exported %s to %s, %d bytes copied.".formatted(fsrl.getName(), outputFile, + bytesCopied); + + context.fsbComponent().getTool().setStatusInfo(msg); + Msg.info(this, msg); + } + catch (IOException | CancelledException | UnsupportedOperationException e) { + FSUtilities.displayException(this, context.plugin().getTool().getActiveWindow(), + "Error Exporting File", e.getMessage(), e); + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/GetInfoFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/GetInfoFSBFileHandler.java new file mode 100644 index 00000000000..cf2f50d8a2d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/GetInfoFSBFileHandler.java @@ -0,0 +1,183 @@ +/* ### + * 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.plugins.fsbrowser.filehandlers; + +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; +import static java.util.Map.*; + +import java.awt.Component; +import java.io.IOException; +import java.util.*; +import java.util.function.Function; + +import org.apache.commons.io.FilenameUtils; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.widgets.dialogs.MultiLineMessageDialog; +import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.fileinfo.*; +import ghidra.framework.model.DomainFile; +import ghidra.plugins.fsbrowser.*; +import ghidra.util.HTMLUtilities; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class GetInfoFSBFileHandler implements FSBFileHandler { + + public static final String FSB_GET_INFO = "FSB Get Info"; + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder(FSB_GET_INFO, context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getFSRL(true) != null) + .popupMenuPath("Get Info") + .popupMenuGroup("A", "A") + .popupMenuIcon(FSBIcons.INFO) + .description("Show information about a file") + .onAction(ac -> { + FSRL fsrl = ac.getFSRL(true); + FSBComponentProvider fsbComp = ac.getComponentProvider(); + fsbComp.runTask( + monitor -> showInfoForFile(ac.getSourceComponent(), fsrl, monitor)); + }) + .build()); + } + + private void showInfoForFile(Component parentComp, FSRL fsrl, TaskMonitor monitor) { + if (fsrl == null) { + Msg.showError(this, parentComp, "Missing File", "Unable to retrieve information"); + return; + } + + // if looking at the root of a nested file system, also include its parent container + List fsrls = (fsrl instanceof FSRLRoot && ((FSRLRoot) fsrl).hasContainer()) + ? List.of(((FSRLRoot) fsrl).getContainer(), fsrl) + : List.of(fsrl); + String title = "Info about " + fsrls.get(0).getName(); + List fattrs = new ArrayList<>(); + for (FSRL fsrl2 : fsrls) { + try { + fattrs.add(getAttrsFor(fsrl2, monitor)); + } + catch (IOException e) { + Msg.warn(this, "Failed to get info for file " + fsrl2, e); + } + catch (CancelledException e) { + return; + } + } + String html = getHTMLInfoStringForAttributes(fattrs); + + MultiLineMessageDialog.showMessageDialog(parentComp, title, null, html, + MultiLineMessageDialog.INFORMATION_MESSAGE); + } + + private FileAttributes getAttrsFor(FSRL fsrl, TaskMonitor monitor) + throws CancelledException, IOException { + try (RefdFile refdFile = context.fsService().getRefdFile(fsrl, monitor)) { + GFileSystem fs = refdFile.fsRef.getFilesystem(); + GFile file = refdFile.file; + FileAttributes fattrs = fs.getFileAttributes(file, monitor); + if (fattrs == null) { + fattrs = FileAttributes.EMPTY; + } + fattrs = fattrs.clone(); + + DomainFile associatedDomainFile = context.projectIndex().findFirstByFSRL(fsrl); + if (associatedDomainFile != null) { + fattrs.add(PROJECT_FILE_ATTR, associatedDomainFile.getPathname()); + } + + if (!fattrs.contains(NAME_ATTR)) { + fattrs.add(NAME_ATTR, file.getName()); + } + if (!fattrs.contains(PATH_ATTR)) { + fattrs.add(PATH_ATTR, FilenameUtils.getFullPath(file.getPath())); + } + if (!fattrs.contains(FSRL_ATTR)) { + fattrs.add(FSRL_ATTR, file.getFSRL()); + } + return fattrs; + } + } + + private String getHTMLInfoStringForAttributes(List fileAttributesList) { + StringBuilder sb = new StringBuilder("\n\n"); + sb.append("\n"); + for (FileAttributes fattrs : fileAttributesList) { + if (fattrs != fileAttributesList.get(0)) { + // not first element, put a visual divider line + sb.append(""); + } + List> sortedAttribs = fattrs.getAttributes(); + Collections.sort(sortedAttribs, (o1, o2) -> Integer + .compare(o1.getAttributeType().ordinal(), o2.getAttributeType().ordinal())); + + FileAttributeTypeGroup group = null; + for (FileAttribute attr : sortedAttribs) { + if (attr.getAttributeType().getGroup() != group) { + group = attr.getAttributeType().getGroup(); + if (group != FileAttributeTypeGroup.GENERAL_INFO) { + sb.append("\n"); + } + } + String valStr = + FAT_TOSTRING_FUNCS.getOrDefault(attr.getAttributeType(), PLAIN_TOSTRING) + .apply(attr.getAttributeValue()); + + String html = HTMLUtilities.escapeHTML(valStr); + html = html.replace("\n", "
\n"); + sb.append("\n"); + } + } + sb.append("
PropertyValue

") + .append(group.getDescriptiveName()) + .append("
") + .append(attr.getAttributeDisplayName()) + .append(":") + .append(html) + .append("
"); + return sb.toString(); + } + + //--------------------------------------------------------------------------------------------- + // static lookup tables for rendering file attributes + //--------------------------------------------------------------------------------------------- + private static final Function PLAIN_TOSTRING = o -> o.toString(); + private static final Function SIZE_TOSTRING = + o -> (o instanceof Long) ? FSUtilities.formatSize((Long) o) : o.toString(); + private static final Function UNIX_ACL_TOSTRING = + o -> (o instanceof Number) ? String.format("%05o", (Number) o) : o.toString(); + private static final Function DATE_TOSTRING = + o -> (o instanceof Date) ? FSUtilities.formatFSTimestamp((Date) o) : o.toString(); + private static final Function FSRL_TOSTRING = + o -> (o instanceof FSRL) ? ((FSRL) o).toPrettyString().replace("|", "|\n\t") : o.toString(); + + private static final Map> FAT_TOSTRING_FUNCS = + Map.ofEntries(entry(FSRL_ATTR, FSRL_TOSTRING), entry(SIZE_ATTR, SIZE_TOSTRING), + entry(COMPRESSED_SIZE_ATTR, SIZE_TOSTRING), entry(CREATE_DATE_ATTR, DATE_TOSTRING), + entry(MODIFIED_DATE_ATTR, DATE_TOSTRING), entry(ACCESSED_DATE_ATTR, DATE_TOSTRING), + entry(UNIX_ACL_ATTR, UNIX_ACL_TOSTRING)); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ImageFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ImageFSBFileHandler.java new file mode 100644 index 00000000000..0dc29075f25 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ImageFSBFileHandler.java @@ -0,0 +1,106 @@ +/* ### + * 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.plugins.fsbrowser.filehandlers; + +import java.awt.Component; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import javax.swing.*; + +import org.apache.commons.io.FilenameUtils; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.widgets.label.GIconLabel; +import ghidra.formats.gfilesystem.*; +import ghidra.plugins.fsbrowser.*; +import ghidra.util.Msg; +import ghidra.util.Swing; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class ImageFSBFileHandler implements FSBFileHandler { + public static final String FSB_VIEW_AS_IMAGE = "FSB View As Image"; + + private static final Set COMMON_IMAGE_EXTENSIONS = Set.of("png", "jpg", "jpeg", "gif"); + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public boolean fileDefaultAction(FSBFileNode fileNode) { + FSRL fsrl = fileNode.getFSRL(); + String extension = FilenameUtils.getExtension(fsrl.getName().toLowerCase()); + if (COMMON_IMAGE_EXTENSIONS.contains(extension)) { + FSBComponentProvider fsbComponent = context.fsbComponent(); + fsbComponent.runTask(monitor -> doViewAsImage(fileNode.getFSRL(), + fsbComponent.getComponent(), monitor)); + return true; + } + return false; + } + + @Override + public List createActions() { + DockingAction action = new ActionBuilder(FSB_VIEW_AS_IMAGE, context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getFileFSRL() != null) + .popupMenuIcon(FSBIcons.VIEW_AS_IMAGE) + .popupMenuPath("View As", "Image") + .popupMenuGroup("G") + .onAction(ac -> { + FSRL fsrl = ac.getFileFSRL(); + if (fsrl != null) { + ac.getTree() + .runTask(monitor -> doViewAsImage(fsrl, ac.getSourceComponent(), + monitor)); + } + }) + .build(); + action.getPopupMenuData().setParentMenuGroup("C"); + + return List.of(action); + } + + void doViewAsImage(FSRL fsrl, Component parent, TaskMonitor monitor) { + + try (RefdFile refdFile = context.fsService().getRefdFile(fsrl, monitor)) { + + Icon icon = GIconProvider.getIconForFile(refdFile.file, monitor); + if (icon == null) { + Msg.showError(this, parent, "Unable To View Image", + "Unable to view " + fsrl.getName() + " as an image."); + return; + } + Swing.runLater(() -> { + JLabel label = new GIconLabel(icon); + JOptionPane.showMessageDialog(null, label, "Image Viewer: " + fsrl.getName(), + JOptionPane.INFORMATION_MESSAGE); + }); + } + catch (IOException | CancelledException e) { + FSUtilities.displayException(this, parent, "Error Viewing Image File", e.getMessage(), + e); + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ImportFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ImportFSBFileHandler.java new file mode 100644 index 00000000000..c1bb5a69c7d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ImportFSBFileHandler.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.plugins.fsbrowser.filehandlers; + +import java.util.List; + +import org.apache.commons.io.FilenameUtils; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.plugin.importer.ImporterUtilities; +import ghidra.plugins.fsbrowser.*; + +public class ImportFSBFileHandler implements FSBFileHandler { + + public static final String FSB_IMPORT_SINGLE = "FSB Import Single"; + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder(FSB_IMPORT_SINGLE, context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getLoadableFSRL() != null) + .popupMenuIcon(FSBIcons.IMPORT) + .popupMenuPath("Import") + .popupMenuGroup("F", "A") + .onAction(ac -> { + FSBNode node = ac.getSelectedNode(); + FSRL fsrl = node.getLoadableFSRL(); + if (fsrl == null) { + return; + } + + String suggestedPath = FilenameUtils + .getFullPathNoEndSeparator(node.getFormattedTreePath()) + .replaceAll(":/", "/"); + + FSBComponentProvider fsbComp = ac.getComponentProvider(); + FileSystemBrowserPlugin plugin = fsbComp.getPlugin(); + OpenWithTarget openWith = OpenWithTarget.getDefault(plugin.getTool()); + + ac.getTree().runTask(monitor -> { + if (!fsbComp.ensureFileAccessable(fsrl, node, monitor)) { + return; + } + ImporterUtilities.showImportSingleFileDialog(fsrl, null, suggestedPath, + plugin.getTool(), openWith.getPm(), monitor); + }); + }) + .build()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/LibrarySearchPathFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/LibrarySearchPathFSBFileHandler.java new file mode 100644 index 00000000000..f4ed0240b51 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/LibrarySearchPathFSBFileHandler.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.plugins.fsbrowser.filehandlers; + +import java.awt.Component; +import java.io.IOException; +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import ghidra.app.util.importer.LibrarySearchPathManager; +import ghidra.formats.gfilesystem.*; +import ghidra.plugins.fsbrowser.*; +import ghidra.util.Msg; + +public class LibrarySearchPathFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder("FSB Add Library Search Path", context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getFSRL(true) != null) + .popupMenuPath("Add Library Search Path") + .popupMenuGroup("F", "D") + .popupMenuIcon(FSBIcons.LIBRARY) + .description("Add file/folder to library search paths") + .onAction(ac -> { + Component parentComp = context.fsbComponent().getComponent(); + try { + FSRL fsrl = ac.getFSRL(true); + FileSystemService fsService = context.fsService(); + LocalFileSystem localFs = fsService.getLocalFS(); + String path = fsService.isLocal(fsrl) + ? localFs.getLocalFile(fsrl).getPath() + : fsrl.toString(); + if (LibrarySearchPathManager.addPath(path)) { + Msg.showInfo(this, parentComp, "Add Library Search Path", + "Added '%s' to library search paths.".formatted(fsrl)); + } + else { + Msg.showInfo(this, parentComp, "Add Library Search Path", + "Library search path '%s' already exists.".formatted(fsrl)); + } + } + catch (IOException e) { + Msg.showError(this, parentComp, "Add Library Search Path", e); + } + }) + .build()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ListMountedFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ListMountedFSBFileHandler.java new file mode 100644 index 00000000000..1961485c413 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/ListMountedFSBFileHandler.java @@ -0,0 +1,61 @@ +/* ### + * 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.plugins.fsbrowser.filehandlers; + +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.widgets.SelectFromListDialog; +import ghidra.formats.gfilesystem.FSRLRoot; +import ghidra.formats.gfilesystem.FileSystemRef; +import ghidra.plugins.fsbrowser.*; + +public class ListMountedFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder("FSB List Mounted Filesystems", context.plugin().getName()) + .description("List Mounted Filesystems") + .withContext(FSBActionContext.class) + .enabledWhen(FSBActionContext::notBusy) + .toolBarIcon(FSBIcons.LIST_MOUNTED) + .toolBarGroup("ZZZZ") + .popupMenuIcon(FSBIcons.LIST_MOUNTED) + .popupMenuPath("List Mounted Filesystems") + .popupMenuGroup("L") + .onAction(ac -> { + FSRLRoot fsFSRL = SelectFromListDialog.selectFromList( + context.fsService().getMountedFilesystems(), "Select filesystem", + "Choose filesystem to view", f -> f.toPrettyString()); + + FileSystemRef fsRef; + if (fsFSRL != null && + (fsRef = context.fsService().getMountedFilesystem(fsFSRL)) != null) { + context.fsbComponent().getPlugin().createNewFileSystemBrowser(fsRef, true); + } + }) + .build()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/OpenFsFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/OpenFsFSBFileHandler.java new file mode 100644 index 00000000000..6e4f7fce2b0 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/OpenFsFSBFileHandler.java @@ -0,0 +1,76 @@ +/* ### + * 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.plugins.fsbrowser.filehandlers; + +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import ghidra.plugins.fsbrowser.*; + +public class OpenFsFSBFileHandler implements FSBFileHandler { + + public static final String FSB_OPEN_FILE_SYSTEM_CHOOSER = "FSB Open File System Chooser"; + public static final String FSB_OPEN_FILE_SYSTEM_IN_NEW_WINDOW = + "FSB Open File System In New Window"; + public static final String FSB_OPEN_FILE_SYSTEM_NESTED = "FSB Open File System Nested"; + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of( + new ActionBuilder(FSB_OPEN_FILE_SYSTEM_NESTED, context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && + ac.getSelectedNode() instanceof FSBFileNode fileNode && fileNode.isLeaf() && + !fileNode.isSymlink()) + .popupMenuIcon(FSBIcons.OPEN_FILE_SYSTEM) + .popupMenuPath("Open File System") + .popupMenuGroup("C") + .onAction( + ac -> ac.getComponentProvider().openFileSystem(ac.getSelectedNode(), true)) + .build(), + + new ActionBuilder(FSB_OPEN_FILE_SYSTEM_IN_NEW_WINDOW, context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && + ac.getSelectedNode() instanceof FSBFileNode fileNode && fileNode.isLeaf() && + !fileNode.isSymlink()) + .popupMenuIcon(FSBIcons.OPEN_FILE_SYSTEM) + .popupMenuPath("Open File System in new window") + .popupMenuGroup("C") + .onAction( + ac -> ac.getComponentProvider().openFileSystem(ac.getSelectedNode(), false)) + .build(), + + new ActionBuilder(FSB_OPEN_FILE_SYSTEM_CHOOSER, context.plugin().getName()) + .description("Open File System Chooser") + .withContext(FSBActionContext.class) + .enabledWhen(FSBActionContext::notBusy) + .toolBarIcon(FSBIcons.OPEN_FILE_SYSTEM) + .toolBarGroup("B") + .onAction(ac -> context.plugin().openFileSystem()) + .build() + ); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/OpenWithFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/OpenWithFSBFileHandler.java new file mode 100644 index 00000000000..7fd748d13ed --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/OpenWithFSBFileHandler.java @@ -0,0 +1,64 @@ +/* ### + * 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.plugins.fsbrowser.filehandlers; + +import java.util.*; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import ghidra.framework.model.DomainFile; +import ghidra.plugin.importer.ProjectIndexService; +import ghidra.plugins.fsbrowser.*; + +public class OpenWithFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List getPopupProviderActions() { + FileSystemBrowserPlugin plugin = context.plugin(); + List results = new ArrayList<>(); + for (OpenWithTarget target : OpenWithTarget.getAll()) { + DockingAction action = + new ActionBuilder("FSB Open With " + target.getName(), plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.hasSelectedLinkedNodes()) + .popupMenuIcon(target.getIcon()) + .popupMenuPath("Open With", target.getName()) + .popupMenuGroup(target.getPm() != null ? "A" : "B") // list running targets first + .onAction(ac -> { + FSBComponentProvider fsbComp = ac.getComponentProvider(); + ProjectIndexService projectIndex = fsbComp.getProjectIndex(); + List filesToOpen = ac.getSelectedNodes() + .stream() + .map(node -> projectIndex.findFirstByFSRL(node.getFSRL())) + .filter(Objects::nonNull) + .toList(); + target.open(filesToOpen); + }) + .build(); + action.getPopupMenuData().setParentMenuGroup("C"); + results.add(action); + } + return results; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/RefreshFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/RefreshFSBFileHandler.java new file mode 100644 index 00000000000..41e073abc78 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/RefreshFSBFileHandler.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.plugins.fsbrowser.filehandlers; + +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.widgets.tree.GTree; +import ghidra.plugins.fsbrowser.*; +import ghidra.util.Swing; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class RefreshFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder("FSB Refresh", context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.hasSelectedNodes()) + .popupMenuPath("Refresh") + .popupMenuGroup("Z", "Z") + .popupMenuIcon(FSBIcons.REFRESH) + .toolBarIcon(FSBIcons.REFRESH) + .description("Refresh file info") + .onAction(ac -> ac.getComponentProvider() + .runTask( + monitor -> doRefreshInfo(ac.getSelectedNodes(), ac.getTree(), monitor))) + .build()); + } + + void doRefreshInfo(List nodes, GTree gTree, TaskMonitor monitor) { + try { + for (FSBNode node : nodes) { + node.refreshNode(monitor); + } + + gTree.refilterLater(); // force the changed modelNodes to be recloned and displayed (if filter active) + } + catch (CancelledException e) { + // stop + } + Swing.runLater(() -> gTree.repaint()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/TextFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/TextFSBFileHandler.java new file mode 100644 index 00000000000..329a8255acf --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/TextFSBFileHandler.java @@ -0,0 +1,104 @@ +/* ### + * 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.plugins.fsbrowser.filehandlers; + +import java.awt.Component; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import ghidra.app.util.bin.ByteProvider; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.formats.gfilesystem.FSUtilities; +import ghidra.plugins.fsbrowser.*; +import ghidra.util.Msg; +import ghidra.util.Swing; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +public class TextFSBFileHandler implements FSBFileHandler { + public static final String FSB_VIEW_AS_TEXT = "FSB View As Text"; + + private static final int MAX_TEXT_FILE_LEN = 64 * 1024; + + FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public boolean fileDefaultAction(FSBFileNode fileNode) { + if (fileNode.getName().toLowerCase().endsWith(".txt")) { + FSBComponentProvider fsbComponent = context.fsbComponent(); + fsbComponent.runTask( + monitor -> doViewAsText(fileNode.getFSRL(), fsbComponent.getComponent(), monitor)); + return true; + } + return false; + } + + @Override + public List createActions() { + DockingAction action = new ActionBuilder(FSB_VIEW_AS_TEXT, context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getFileFSRL() != null) + .popupMenuIcon(FSBIcons.VIEW_AS_TEXT) + .popupMenuPath("View As", "Text") + .popupMenuGroup("G") + .onAction(ac -> { + if (ac.getSelectedNode() instanceof FSBFileNode fileNode && + fileNode.getFSRL() != null) { + ac.getTree() + .runTask(monitor -> doViewAsText(fileNode.getFSRL(), + ac.getSourceComponent(), monitor)); + } + }) + .build(); + + action.getPopupMenuData().setParentMenuGroup("C"); + return List.of(action); + + } + + void doViewAsText(FSRL fsrl, Component parent, TaskMonitor monitor) { + try (ByteProvider fileBP = context.fsService().getByteProvider(fsrl, false, monitor)) { + + if (fileBP.length() > MAX_TEXT_FILE_LEN) { + Msg.showInfo(this, context.fsbComponent().getComponent(), "View As Text Failed", + "File too large to view as text inside Ghidra. " + + "Please use the \"EXPORT\" action."); + return; + } + + try (InputStream is = fileBP.getInputStream(0)) { + String text = FileUtilities.getText(is); + Swing.runLater(() -> { + new TextEditorComponentProvider(context.plugin(), fsrl.getName(), text); + }); + } + } + catch (IOException | CancelledException e) { + FSUtilities.displayException(this, parent, "Error Viewing Text File", + "Error when trying to view text file %s".formatted(fsrl.getName()), e); + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/tasks/ImportBatchTask.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/tasks/ImportBatchTask.java index 4bfc342ab0c..c705a3e5544 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/tasks/ImportBatchTask.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/tasks/ImportBatchTask.java @@ -30,7 +30,6 @@ import ghidra.framework.main.AppInfo; import ghidra.framework.model.*; import ghidra.framework.store.local.LocalFileSystem; -import ghidra.plugin.importer.ProgramMappingService; import ghidra.plugins.importer.batch.*; import ghidra.plugins.importer.batch.BatchGroup.BatchLoadConfig; import ghidra.program.model.listing.Program; @@ -205,8 +204,6 @@ private void processImportResults(LoadResults loadResult totalObjsImported == 0 ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE); } - - ProgramMappingService.createAssociation(appInfo.getFSRL(), program); } totalObjsImported++; } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/plugins/fsbrowser/FileIconServiceTest.java b/Ghidra/Features/Base/src/test/java/ghidra/plugins/fsbrowser/FSBIconsTest.java similarity index 62% rename from Ghidra/Features/Base/src/test/java/ghidra/plugins/fsbrowser/FileIconServiceTest.java rename to Ghidra/Features/Base/src/test/java/ghidra/plugins/fsbrowser/FSBIconsTest.java index 7756fdf6547..1e1820c913b 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/plugins/fsbrowser/FileIconServiceTest.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/plugins/fsbrowser/FSBIconsTest.java @@ -17,9 +17,12 @@ import static org.junit.Assert.*; -import java.util.List; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; import javax.swing.Icon; +import javax.swing.ImageIcon; import org.junit.Assert; import org.junit.Test; @@ -27,12 +30,14 @@ import docking.test.AbstractDockingTest; import generic.theme.GIcon; import resources.MultiIcon; +import resources.ResourceManager; -public class FileIconServiceTest extends AbstractDockingTest { +public class FSBIconsTest extends AbstractDockingTest { + + FSBIcons fis = FSBIcons.getInstance(); @Test public void testGetIcon() { - FileIconService fis = FileIconService.getInstance(); Icon icon = fis.getIcon("blah.txt", null); Assert.assertNotNull(icon); assertTrue(icon instanceof GIcon); @@ -42,8 +47,7 @@ public void testGetIcon() { @Test public void testGetOverlayIcon() { - FileIconService fis = FileIconService.getInstance(); - Icon icon = fis.getIcon("blah.txt", List.of(FileIconService.FILESYSTEM_OVERLAY_ICON)); + Icon icon = fis.getIcon("blah.txt", List.of(FSBIcons.FILESYSTEM_OVERLAY_ICON)); Assert.assertNotNull(icon); assertTrue(icon instanceof MultiIcon); MultiIcon multiIcon = (MultiIcon) icon; @@ -54,7 +58,6 @@ public void testGetOverlayIcon() { @Test public void testGetSubstringIcon() { - FileIconService fis = FileIconService.getInstance(); Icon icon = fis.getIcon("blah.release.abcx.123", null); Assert.assertNotNull(icon); assertTrue(icon instanceof GIcon); @@ -64,8 +67,27 @@ public void testGetSubstringIcon() { @Test public void testNoMatch() { - FileIconService fis = FileIconService.getInstance(); Icon icon = fis.getIcon("aaaaaaaa.bbbbbbbb.cccccccc", null); - assertEquals(FileIconService.DEFAULT_ICON, icon); + assertEquals(FSBIcons.DEFAULT_ICON, icon); + } + + @Test + public void testImageManagerLoadedIconResources() + throws IllegalArgumentException, IllegalAccessException { + + ImageIcon defaultIcon = ResourceManager.getDefaultIcon(); + + Set failedIcons = new HashSet<>(); + for (Field field : FSBIcons.class.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) && + field.getType().equals(ImageIcon.class)) { + Object fieldValue = field.get(null); + if (fieldValue == null || fieldValue == defaultIcon) { + failedIcons.add(field.getName()); + } + } + } + Assert.assertTrue("Some icons failed to load or misconfigured: " + failedIcons.toString(), + failedIcons.isEmpty()); } } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/plugins/fsbrowser/ImageManagerTest.java b/Ghidra/Features/Base/src/test/java/ghidra/plugins/fsbrowser/ImageManagerTest.java deleted file mode 100644 index 7210051922d..00000000000 --- a/Ghidra/Features/Base/src/test/java/ghidra/plugins/fsbrowser/ImageManagerTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* ### - * 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.plugins.fsbrowser; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.HashSet; -import java.util.Set; - -import javax.swing.ImageIcon; - -import org.junit.Assert; -import org.junit.Test; - -import generic.test.AbstractGenericTest; -import resources.ResourceManager; - -public class ImageManagerTest extends AbstractGenericTest { - - - @Test - public void testImageManagerLoadedIconResources() - throws IllegalArgumentException, IllegalAccessException { - - ImageIcon defaultIcon = ResourceManager.getDefaultIcon(); - - Set failedIcons = new HashSet<>(); - for (Field field : ImageManager.class.getDeclaredFields()) { - if (Modifier.isStatic(field.getModifiers()) && - field.getType().equals(ImageIcon.class)) { - Object fieldValue = field.get(null); - if (fieldValue == null || fieldValue == defaultIcon) { - failedIcons.add(field.getName()); - } - } - } - Assert.assertTrue("Some icons failed to load or misconfigured: " + failedIcons.toString(), - failedIcons.isEmpty()); - } -} diff --git a/Ghidra/Features/FileFormats/data/ExtensionPoint.manifest b/Ghidra/Features/FileFormats/data/ExtensionPoint.manifest index 0a3943ce798..aebb112e355 100644 --- a/Ghidra/Features/FileFormats/data/ExtensionPoint.manifest +++ b/Ghidra/Features/FileFormats/data/ExtensionPoint.manifest @@ -1,3 +1,4 @@ Decryptor FileSystem FileSystemModel +FSBFileHandler diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/crypto/CryptoKeysFSBFileHandler.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/crypto/CryptoKeysFSBFileHandler.java new file mode 100644 index 00000000000..d52949a994a --- /dev/null +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/crypto/CryptoKeysFSBFileHandler.java @@ -0,0 +1,109 @@ +/* ### + * 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.file.crypto; + +import java.io.IOException; +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.widgets.OptionDialog; +import docking.widgets.tree.GTreeNode; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.formats.gfilesystem.FSUtilities; +import ghidra.plugins.fsbrowser.*; + +public class CryptoKeysFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List + .of(new ActionBuilder("FSB Create Crypto Key Template", context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && + ac.getSelectedNode() instanceof FSBRootNode && ac.getFSRL(true) != null) + .popupMenuPath("Create Crypto Key Template...") + .popupMenuGroup("Z", "B") + .onAction(ac -> { + FSRL fsrl = ac.getFSRL(true); + if (ac.getSelectedNode() instanceof FSBRootNode rootNode && + fsrl != null) { + createCryptoTemplate(fsrl, rootNode); + } + }) + .build()); + } + + /** + * Creates a crypto key file template based on the specified files under the GTree node. + * + * @param fsrl FSRL of a child file of the container that the crypto will be associated with + * @param node GTree node with children that will be iterated + */ + private void createCryptoTemplate(FSRL fsrl, FSBRootNode node) { + try { + String fsContainerName = fsrl.getFS().getContainer().getName(); + CryptoKeyFileTemplateWriter writer = new CryptoKeyFileTemplateWriter(fsContainerName); + if (writer.exists()) { + int answer = + OptionDialog.showYesNoDialog(null, "WARNING!! Crypto Key File Already Exists", + "WARNING!!" + "\n" + "The crypto key file already exists. " + + "Are you really sure that you want to overwrite it?"); + if (answer == OptionDialog.NO_OPTION) { + return; + } + } + writer.open(); + try { + // gTree.expandAll( node ); + writeFile(writer, node.getChildren()); + } + finally { + writer.close(); + } + } + catch (IOException e) { + FSUtilities.displayException(this, null, "Error writing crypt key file", e.getMessage(), + e); + } + + } + + private void writeFile(CryptoKeyFileTemplateWriter writer, List children) + throws IOException { + + if (children == null || children.isEmpty()) { + return; + } + for (GTreeNode child : children) { + if (child instanceof FSBFileNode fileNode) { + FSRL childFSRL = fileNode.getFSRL(); + writer.write(childFSRL.getName()); + } + else { + writeFile(writer, child.getChildren()); + } + } + } + +} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/apk/ApkFSBFileHandler.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/apk/ApkFSBFileHandler.java new file mode 100644 index 00000000000..5be4c3a3ec6 --- /dev/null +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/apk/ApkFSBFileHandler.java @@ -0,0 +1,106 @@ +/* ### + * 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.file.formats.android.apk; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.apache.commons.io.FilenameUtils; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import ghidra.file.eclipse.AndroidProjectCreator; +import ghidra.file.jad.JadProcessWrapper; +import ghidra.formats.gfilesystem.*; +import ghidra.plugins.fsbrowser.*; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class ApkFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + private File lastDirectory; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + private static boolean isAPK(FSRL fsrl) { + return (fsrl != null) && (fsrl.getName() != null) && + "apk".equalsIgnoreCase(FilenameUtils.getExtension(fsrl.getName())); + } + + @Override + public List createActions() { + return List.of(new ActionBuilder("FSB Export Eclipse Project", context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && JadProcessWrapper.isJadPresent() && + isAPK(ac.getFileFSRL())) + .popupMenuPath("Export Eclipse Project") + .popupMenuIcon(FSBIcons.ECLIPSE) + .popupMenuGroup("H") + .onAction(ac -> { + FSRL fsrl = ac.getFileFSRL(); + if (fsrl == null) { + Msg.info(this, "Unable to export eclipse project"); + return; + } + + lastDirectory = lastDirectory == null + ? new File(System.getProperty("user.home")) + : lastDirectory; + + GhidraFileChooser chooser = new GhidraFileChooser(ac.getSourceComponent()); + chooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); + chooser.setTitle("Select Eclipse Project Directory"); + chooser.setApproveButtonText("SELECT"); + chooser.setCurrentDirectory(context.plugin().getLastExportDirectory()); + File selectedFile = chooser.getSelectedFile(); + chooser.dispose(); + if (selectedFile == null) { + return; + } + lastDirectory = selectedFile; + + ac.getComponentProvider() + .runTask(monitor -> doExportToEclipse(fsrl, lastDirectory, monitor)); + + }) + .build()); + } + + private void doExportToEclipse(FSRL fsrl, File outputDirectory, TaskMonitor monitor) { + try (RefdFile refdFile = FileSystemService.getInstance().getRefdFile(fsrl, monitor)) { + AndroidProjectCreator creator = + new AndroidProjectCreator(refdFile.file.getFSRL(), outputDirectory); + creator.create(monitor); + + if (creator.getLog().hasMessages()) { + Msg.showInfo(this, null, "Export to Eclipse Project", creator.getLog().toString()); + } + } + catch (IOException | CancelledException e) { + FSUtilities.displayException(this, null, "Error Exporting to Eclipse", e.getMessage(), + e); + } + } + +} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageFileSystem.java index c57d46e461a..870b185eff4 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageFileSystem.java @@ -106,7 +106,7 @@ else if (file == secondStageFile) { FileAttribute.create(FileAttributeType.COMMENT_ATTR, "This is a second stage loader file. It appears unused at this time.")); } - return null; + return FileAttributes.EMPTY; } @Override diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/VendorBootImageFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/VendorBootImageFileSystem.java index e7dd171dbd6..1374c894a99 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/VendorBootImageFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/VendorBootImageFileSystem.java @@ -16,21 +16,14 @@ package ghidra.file.formats.android.bootimg; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.bin.ByteProviderWrapper; -import ghidra.formats.gfilesystem.GFile; -import ghidra.formats.gfilesystem.GFileImpl; -import ghidra.formats.gfilesystem.GFileSystemBase; +import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; -import ghidra.formats.gfilesystem.fileinfo.FileAttribute; -import ghidra.formats.gfilesystem.fileinfo.FileAttributeType; -import ghidra.formats.gfilesystem.fileinfo.FileAttributes; +import ghidra.formats.gfilesystem.fileinfo.*; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; @@ -121,7 +114,7 @@ public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { FileAttribute.create(FileAttributeType.COMMENT_ATTR, "This is a DTB file. It appears unused at this time.")); } - return null; + return FileAttributes.EMPTY; } @Override diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/prof/ProfileFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/prof/ProfileFileSystem.java index 5b9367affc5..1e09615c69f 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/prof/ProfileFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/prof/ProfileFileSystem.java @@ -74,7 +74,7 @@ public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { if (file == dataFile) { return FileAttributes.of(FileAttribute.create("Magic", header.getMagic())); } - return null; + return FileAttributes.EMPTY; } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/tasks/GFileSystemLoadKernelTask.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java similarity index 86% rename from Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/tasks/GFileSystemLoadKernelTask.java rename to Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java index fab65a844e1..c2ab384bc83 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/tasks/GFileSystemLoadKernelTask.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/GFileSystemLoadKernelTask.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.plugins.fsbrowser.tasks; +package ghidra.file.formats.ios.prelink; import java.io.IOException; import java.util.List; @@ -21,10 +21,9 @@ import ghidra.app.services.ProgramManager; import ghidra.formats.gfilesystem.*; import ghidra.framework.main.AppInfo; -import ghidra.framework.model.DomainFolder; -import ghidra.framework.model.ProjectDataUtils; +import ghidra.framework.model.*; import ghidra.framework.plugintool.Plugin; -import ghidra.plugin.importer.ProgramMappingService; +import ghidra.plugin.importer.ProjectIndexService; import ghidra.program.model.lang.LanguageService; import ghidra.program.model.listing.Program; import ghidra.program.util.DefaultLanguageService; @@ -115,21 +114,20 @@ private void loadKext(GFile file, TaskMonitor monitor) throws Exception { } monitor.setMessage("Opening " + file.getName()); - Program program = ProgramMappingService.findMatchingProgramOpenIfNeeded(file.getFSRL(), - this, programManager, ProgramManager.OPEN_VISIBLE); - if (program != null) { - program.release(this); + ProjectIndexService projectIndex = ProjectIndexService.getInstance(); + DomainFile existingDF = projectIndex.findFirstByFSRL(file.getFSRL()); + if ( existingDF != null && programManager != null ) { + programManager.openProgram(existingDF); return; } //File cacheFile = FileSystemService.getInstance().getFile(file.getFSRL(), monitor); - if (file.getFilesystem() instanceof GFileSystemProgramProvider) { + Program program = null; + if (file.getFilesystem() instanceof GFileSystemProgramProvider programProviderFS) { LanguageService languageService = DefaultLanguageService.getLanguageService(); - GFileSystemProgramProvider fileSystem = - (GFileSystemProgramProvider) file.getFilesystem(); - program = fileSystem.getProgram(file, languageService, monitor, this); + program = programProviderFS.getProgram(file, languageService, monitor, this); } if (program != null) { @@ -144,7 +142,6 @@ private void loadKext(GFile file, TaskMonitor monitor) throws Exception { folder.createFile(fileName, program, monitor); programManager.openProgram(program); - ProgramMappingService.createAssociation(file.getFSRL(), program); } finally { program.release(this); diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/MachoPrelinkFSBFileHandler.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/MachoPrelinkFSBFileHandler.java new file mode 100644 index 00000000000..e4b4c71e884 --- /dev/null +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/MachoPrelinkFSBFileHandler.java @@ -0,0 +1,100 @@ +/* ### + * 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.file.formats.ios.prelink; + +import java.util.ArrayList; +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.widgets.OptionDialog; +import docking.widgets.tree.GTreeNode; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.plugins.fsbrowser.*; +import ghidra.util.task.TaskLauncher; + +public class MachoPrelinkFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder("FSB Load iOS Kernel", context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> { + if (ac.isBusy() || ac.getSelectedNode() == null) { + return false; + } + FSBRootNode rootNode = ac.getSelectedNode().getFSBRootNode(); + return rootNode != null && rootNode.getFSRef() != null && + rootNode.getFSRef().getFilesystem() instanceof MachoPrelinkFileSystem; + }) + .popupMenuPath("Load iOS Kernel") + .popupMenuIcon(FSBIcons.iOS) + .popupMenuGroup("I") + .onAction(ac -> { + FSRL fsrl = ac.getFSRL(true); + List fileList = new ArrayList<>(); + + if (fsrl != null) { + FSBNode selectedNode = ac.getSelectedNode(); + if (selectedNode instanceof FSBRootNode) { + for (GTreeNode childNode : ac.getSelectedNode().getChildren()) { + if (childNode instanceof FSBNode baseNode) { + fileList.add(baseNode.getFSRL()); + } + } + } + else if (selectedNode instanceof FSBFileNode || + selectedNode instanceof FSBDirNode) { + fileList.add(fsrl); + } + } + + if (!fileList.isEmpty()) { + if (OptionDialog.showYesNoDialog(null, "Load iOS Kernel?", + "Performing this action will load the entire kernel and all KEXT files.\n" + + "Do you want to continue?") == OptionDialog.YES_OPTION) { + loadIOSKernel(fileList); + } + } + else { + ac.getComponentProvider() + .getPlugin() + .getTool() + .setStatusInfo("Load iOS kernel -- nothing to do."); + } + }) + .build() + + ); + } + + private void loadIOSKernel(List fileList) { + FileSystemBrowserPlugin fsbPlugin = context.plugin(); + OpenWithTarget openWith = OpenWithTarget.getRunningProgramManager(fsbPlugin.getTool()); + if (openWith.getPm() != null) { + TaskLauncher + .launch(new GFileSystemLoadKernelTask(fsbPlugin, openWith.getPm(), fileList)); + } + } + +} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/squashfs/SquashFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/squashfs/SquashFileSystemFactory.java index 73e5273ff1b..d558caa8cc0 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/squashfs/SquashFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/squashfs/SquashFileSystemFactory.java @@ -35,8 +35,14 @@ public SquashFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, throws IOException, CancelledException { SquashFileSystem fs = new SquashFileSystem(targetFSRL, byteProvider, fsService); - fs.mount(monitor); - return fs; + try { + fs.mount(monitor); + return fs; + } + catch (IOException e) { + FSUtilities.uncheckedClose(fs, null); + throw e; + } } @Override diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystem.java index 7a5544cbe53..448bfec7a07 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystem.java @@ -109,7 +109,7 @@ public boolean isClosed() { public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { TarMetadata tmd = fsIndex.getMetadata(file); if (tmd == null) { - return null; + return FileAttributes.EMPTY; } TarArchiveEntry blob = tmd.tarArchiveEntry; return FileAttributes.of( diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/zip/ZipFileSystemBuiltin.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/zip/ZipFileSystemBuiltin.java index aea0ea2737e..d7d46ba87fe 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/zip/ZipFileSystemBuiltin.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/zip/ZipFileSystemBuiltin.java @@ -95,7 +95,7 @@ public void mount(File f, boolean deleteFileWhenDone, TaskMonitor monitor) public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { ZipEntry zipEntry = fsIndex.getMetadata(file); if (zipEntry == null) { - return null; + return FileAttributes.EMPTY; } FileAttributes result = new FileAttributes(); diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/jad/JadFSBFileHandler.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/jad/JadFSBFileHandler.java new file mode 100644 index 00000000000..bd5aeed0b83 --- /dev/null +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/jad/JadFSBFileHandler.java @@ -0,0 +1,92 @@ +/* ### + * 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.file.jad; + +import java.io.File; +import java.util.List; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.formats.gfilesystem.FSUtilities; +import ghidra.plugins.fsbrowser.*; +import ghidra.util.Msg; + +public class JadFSBFileHandler implements FSBFileHandler { + + private FSBFileHandlerContext context; + private File lastDirectory; + + @Override + public void init(FSBFileHandlerContext context) { + this.context = context; + } + + @Override + public List createActions() { + return List.of(new ActionBuilder("FSB Decompile JAR", context.plugin().getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && JadProcessWrapper.isJadPresent() && + ac.getFileFSRL() != null) + .popupMenuPath("Decompile JAR") + .popupMenuIcon(FSBIcons.JAR) + .popupMenuGroup("J") + .onAction(ac -> { + FSRL jarFSRL = ac.getFileFSRL(); + if (jarFSRL == null) { + return; + } + + lastDirectory = lastDirectory == null + ? new File(System.getProperty("user.home")) + : lastDirectory; + + GhidraFileChooser chooser = new GhidraFileChooser(ac.getSourceComponent()); + chooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); + chooser.setTitle("Select JAR Output Directory"); + chooser.setApproveButtonText("SELECT"); + chooser.setCurrentDirectory(context.plugin().getLastExportDirectory()); + File selectedFile = chooser.getSelectedFile(); + chooser.dispose(); + if (selectedFile == null) { + return; + } + lastDirectory = selectedFile; + + context.fsbComponent().runTask(monitor -> { + try { + JarDecompiler decompiler = new JarDecompiler(jarFSRL, selectedFile); + decompiler.decompile(monitor); + + if (decompiler.getLog().hasMessages()) { + Msg.showInfo(this, null, "Decompiling Jar " + jarFSRL.getName(), + decompiler.getLog().toString()); + } + } + catch (Exception e) { + FSUtilities.displayException(this, null, "Error Decompiling Jar", + e.getMessage(), e); + } + }); + }) + .build() + + ); + } + +} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/plugins/fileformats/FileFormatsPlugin.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/plugins/fileformats/FileFormatsPlugin.java deleted file mode 100644 index 81b9e0f33f5..00000000000 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/plugins/fileformats/FileFormatsPlugin.java +++ /dev/null @@ -1,335 +0,0 @@ -/* ### - * 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.plugins.fileformats; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.io.FilenameUtils; - -import docking.action.DockingAction; -import docking.action.builder.ActionBuilder; -import docking.widgets.OptionDialog; -import docking.widgets.filechooser.GhidraFileChooser; -import docking.widgets.filechooser.GhidraFileChooserMode; -import docking.widgets.tree.GTree; -import docking.widgets.tree.GTreeNode; -import ghidra.app.CorePluginPackage; -import ghidra.app.plugin.PluginCategoryNames; -import ghidra.app.services.ProgramManager; -import ghidra.file.crypto.CryptoKeyFileTemplateWriter; -import ghidra.file.eclipse.AndroidProjectCreator; -import ghidra.file.formats.ios.prelink.MachoPrelinkFileSystem; -import ghidra.file.jad.JadProcessWrapper; -import ghidra.file.jad.JarDecompiler; -import ghidra.formats.gfilesystem.*; -import ghidra.framework.main.ApplicationLevelPlugin; -import ghidra.framework.plugintool.*; -import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.plugins.fsbrowser.*; -import ghidra.plugins.fsbrowser.tasks.GFileSystemLoadKernelTask; -import ghidra.util.Msg; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskLauncher; -import ghidra.util.task.TaskMonitor; - -/** - * A plugin that adds file format related actions to the file system browser. - */ -//@formatter:off -@PluginInfo( - status = PluginStatus.RELEASED, - packageName = CorePluginPackage.NAME, - category = PluginCategoryNames.COMMON, - shortDescription = "File format actions", - description = "This plugin provides file format related actions to the File System Browser." -) -//@formatter:on -public class FileFormatsPlugin extends Plugin implements ApplicationLevelPlugin { - - private GhidraFileChooser chooserEclipse; - private GhidraFileChooser chooserJarFolder; - - private List actions = new ArrayList<>(); - - public FileFormatsPlugin(PluginTool tool) { - super(tool); - } - - @Override - protected void init() { - super.init(); - - actions.add(createEclipseProjectAction()); - actions.add(createDecompileJarAction()); - actions.add(createCryptoTemplateAction()); - actions.add(createLoadKernelAction()); - - actions.forEach(action -> getTool().addAction(action)); - } - - @Override - protected void dispose() { - super.dispose(); - - if (chooserJarFolder != null) { - chooserJarFolder.dispose(); - } - - if (chooserEclipse != null) { - chooserEclipse.dispose(); - } - - actions.forEach(action -> getTool().removeAction(action)); - } - - private boolean isAPK(FSRL fsrl) { - return (fsrl != null) && (fsrl.getName() != null) && - "apk".equalsIgnoreCase(FilenameUtils.getExtension(fsrl.getName())); - } - - private void doExportToEclipse(FSRL fsrl, File outputDirectory, TaskMonitor monitor) { - try (RefdFile refdFile = - FileSystemService.getInstance().getRefdFile(fsrl, monitor)) { - AndroidProjectCreator creator = - new AndroidProjectCreator(refdFile.file.getFSRL(), outputDirectory); - creator.create(monitor); - - if (creator.getLog().hasMessages()) { - Msg.showInfo(this, getTool().getActiveWindow(), "Export to Eclipse Project", - creator.getLog().toString()); - } - } - catch (IOException | CancelledException e) { - FSUtilities.displayException(this, getTool().getActiveWindow(), - "Error Exporting to Eclipse", e.getMessage(), e); - } - } - - private DockingAction createEclipseProjectAction() { - return new ActionBuilder("FSB Export Eclipse Project", this.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && JadProcessWrapper.isJadPresent() && - isAPK(ac.getFileFSRL())) - .popupMenuPath("Export Eclipse Project") - .popupMenuIcon(ImageManager.ECLIPSE) - .popupMenuGroup("H") - .onAction( - ac -> { - FSRL fsrl = ac.getFileFSRL(); - if (fsrl == null) { - Msg.info(this, "Unable to export eclipse project"); - return; - } - - if (chooserEclipse == null) { - chooserEclipse = new GhidraFileChooser(null); - } - chooserEclipse.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); - chooserEclipse.setTitle("Select Eclipe Project Directory"); - chooserEclipse.setApproveButtonText("SELECT"); - chooserEclipse.setSelectedFile(null); - File outputDirectory = chooserEclipse.getSelectedFile(); - if (outputDirectory == null) { - return; - } - GTree gTree = ac.getTree(); - gTree.runTask(monitor -> doExportToEclipse(fsrl, outputDirectory, monitor)); - }) - .build(); - } - - private DockingAction createDecompileJarAction() { - return new ActionBuilder("FSB Decompile JAR", this.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && JadProcessWrapper.isJadPresent() && - ac.getFileFSRL() != null) - .popupMenuPath("Decompile JAR") - .popupMenuIcon(ImageManager.JAR) - .popupMenuGroup("J") - .onAction( - ac -> { - FSRL jarFSRL = ac.getFileFSRL(); - if (jarFSRL == null) { - return; - } - - if (chooserJarFolder == null) { - chooserJarFolder = new GhidraFileChooser(null); - } - chooserJarFolder.setFileSelectionMode( - GhidraFileChooserMode.DIRECTORIES_ONLY); - chooserJarFolder.setTitle("Select JAR Output Directory"); - chooserJarFolder.setApproveButtonText("SELECT"); - chooserJarFolder.setSelectedFile(null); - File outputDirectory = chooserJarFolder.getSelectedFile(); - if (outputDirectory == null) { - return; - } - GTree gTree = ac.getTree(); - gTree.runTask(monitor -> { - try { - JarDecompiler decompiler = - new JarDecompiler(jarFSRL, outputDirectory); - decompiler.decompile(monitor); - - if (decompiler.getLog().hasMessages()) { - Msg.showInfo(this, gTree, - "Decompiling Jar " + jarFSRL.getName(), - decompiler.getLog().toString()); - } - } - catch (Exception e) { - FSUtilities.displayException(this, gTree, "Error Decompiling Jar", - e.getMessage(), e); - } - }); - }) - .build(); - } - - private DockingAction createCryptoTemplateAction() { - return new ActionBuilder("FSB Create Crypto Key Template", this.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> ac.notBusy() && ac.getSelectedNode() instanceof FSBRootNode && - ac.getFSRL(true) != null) - .popupMenuPath("Create Crypto Key Template...") - .popupMenuGroup("Z", "B") - .onAction( - ac -> { - FSRL fsrl = ac.getFSRL(true); - if (ac.getSelectedNode() instanceof FSBRootNode && fsrl != null) { - createCryptoTemplate(fsrl, (FSBRootNode) ac.getSelectedNode()); - } - }) - .build(); - } - - /** - * Creates a crypto key file template based on the specified files under the GTree node. - * - * @param fsrl FSRL of a child file of the container that the crypto will be associated with - * @param node GTree node with children that will be iterated - */ - private void createCryptoTemplate(FSRL fsrl, FSBRootNode node) { - try { - String fsContainerName = fsrl.getFS().getContainer().getName(); - CryptoKeyFileTemplateWriter writer = new CryptoKeyFileTemplateWriter(fsContainerName); - if (writer.exists()) { - int answer = OptionDialog.showYesNoDialog(getTool().getActiveWindow(), - "WARNING!! Crypto Key File Already Exists", - "WARNING!!" + "\n" + "The crypto key file already exists. " + - "Are you really sure that you want to overwrite it?"); - if (answer == OptionDialog.NO_OPTION) { - return; - } - } - writer.open(); - try { - // gTree.expandAll( node ); - writeFile(writer, node.getChildren()); - } - finally { - writer.close(); - } - } - catch (IOException e) { - FSUtilities.displayException(this, getTool().getActiveWindow(), - "Error writing crypt key file", e.getMessage(), e); - } - - } - - private void writeFile(CryptoKeyFileTemplateWriter writer, List children) - throws IOException { - - if (children == null || children.isEmpty()) { - return; - } - for (GTreeNode child : children) { - if (child instanceof FSBFileNode) { - FSRL childFSRL = ((FSBFileNode) child).getFSRL(); - writer.write(childFSRL.getName()); - } - else { - writeFile(writer, child.getChildren()); - } - } - } - - private DockingAction createLoadKernelAction() { - return new ActionBuilder("FSB Load iOS Kernel", this.getName()) - .withContext(FSBActionContext.class) - .enabledWhen(ac -> { - if (ac.isBusy()) { - return false; - } - FSBRootNode rootNode = ac.getRootOfSelectedNode(); - return rootNode != null && rootNode.getFSRef() != null && - rootNode.getFSRef().getFilesystem() instanceof MachoPrelinkFileSystem; - }) - .popupMenuPath("Load iOS Kernel") - .popupMenuIcon(ImageManager.iOS) - .popupMenuGroup("I") - .onAction( - ac -> { - FSRL fsrl = ac.getFSRL(true); - List fileList = new ArrayList<>(); - - if (fsrl != null) { - FSBNode selectedNode = ac.getSelectedNode(); - if (selectedNode instanceof FSBRootNode) { - for (GTreeNode childNode : ac.getSelectedNode().getChildren()) { - if (childNode instanceof FSBNode) { - FSBNode baseNode = (FSBNode) childNode; - fileList.add(baseNode.getFSRL()); - } - } - } - else if (selectedNode instanceof FSBFileNode || - selectedNode instanceof FSBDirNode) { - fileList.add(fsrl); - } - } - - if (!fileList.isEmpty()) { - if (OptionDialog.showYesNoDialog(null, "Load iOS Kernel?", - "Performing this action will load the entire kernel and all KEXT files." + - "\n" + "Do you want to continue?") == OptionDialog.YES_OPTION) { - loadIOSKernel(fileList); - } - } - else { - getTool().setStatusInfo("Load iOS kernel -- nothing to do."); - } - }) - .build(); - } - - /** - * Loads or imports iOS kernel files. - * - * @param fileList List of {@link FSRL}s of the iOS kernel files. - */ - private void loadIOSKernel(List fileList) { - ProgramManager pm = FSBUtils.getProgramManager(getTool(), true); - if (pm != null) { - TaskLauncher.launch(new GFileSystemLoadKernelTask(this, pm, fileList)); - } - } -}