Merge remote-tracking branch 'origin/GP-4640_dev747368_fsb_symlink_and_actions--SQUASHED'

This commit is contained in:
Ryan Kurtz 2024-07-16 11:41:39 -04:00
commit e2e6215982
64 changed files with 3593 additions and 3054 deletions

View File

@ -21,3 +21,4 @@ ChecksumAlgorithm
OverviewColorService
DWARFFunctionFixup
ElfInfoProducer
FSBFileHandler

View File

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

View File

@ -28,7 +28,6 @@ import ghidra.app.util.importer.MessageLog;
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 abstract class AbstractProgramLoader implements Loader {
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);

View File

@ -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 abstract class AbstractFileSystem<METADATATYPE> implements GFileSystem {
return fsIndex.lookup(null, path, getFilenameComparator());
}
@Override
public GFile getRootDir() {
return fsIndex.getRootDir();
}
@Override
public List<GFile> getListing(GFile directory) {
return fsIndex.getListing(directory);
@ -75,4 +81,9 @@ public abstract class AbstractFileSystem<METADATATYPE> implements GFileSystem {
return fsIndex.getFileCount();
}
@Override
public GFile resolveSymlinks(GFile file) throws IOException {
return fsIndex.resolveSymlinks(file);
}
}

View File

@ -19,7 +19,6 @@ import java.io.IOException;
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 @@ import ghidra.util.SystemUtilities;
*/
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 class FSRL {
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.

View File

@ -20,6 +20,8 @@ import java.io.*;
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 org.apache.commons.io.FilenameUtils;
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 class FSUtilities {
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 class FSUtilities {
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 class FSUtilities {
* 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 class FSUtilities {
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;
}
}

View File

@ -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.
* <p>
* @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 @@ public interface GFile {
* @return {@link List} of {@link GFile} instances.
* @throws IOException if not a directory or error when accessing files.
*/
default public List<GFile> getListing() throws IOException {
default List<GFile> getListing() throws IOException {
return getFilesystem().getListing(this);
}

View File

@ -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 class GFileLocal implements GFile {
return f.length();
}
@Override
public long getLastModified() {
return f.lastModified();
}
public File getLocalFile() {
return f;
}
@ -99,4 +95,23 @@ public class GFileLocal implements GFile {
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);
}
}

View File

@ -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 @@ public interface GFileSystem extends Closeable, ExtensionPoint {
*
* @return description string
*/
default public String getDescription() {
default String getDescription() {
return FSUtilities.getFilesystemDescriptionFromClass(this.getClass());
}
@ -83,21 +83,21 @@ public interface GFileSystem extends Closeable, ExtensionPoint {
*
* @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 @@ public interface GFileSystem extends Closeable, ExtensionPoint {
* <p>
* @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 @@ public interface GFileSystem extends Closeable, ExtensionPoint {
* @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.
* <p>
* 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 @@ public interface GFileSystem extends Closeable, ExtensionPoint {
* @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 @@ public interface GFileSystem extends Closeable, ExtensionPoint {
* @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 interface GFileSystem extends Closeable, ExtensionPoint {
* @return {@link List} of {@link GFile} instances of file in the requested directory.
* @throws IOException if IO problem.
*/
public List<GFile> getListing(GFile directory) throws IOException;
List<GFile> getListing(GFile directory) throws IOException;
/**
* Returns a container of {@link FileAttribute} values.
@ -181,10 +197,23 @@ public interface GFileSystem extends Closeable, ExtensionPoint {
* @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 @@ public interface GFileSystem extends Closeable, ExtensionPoint {
* @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;

View File

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

View File

@ -18,7 +18,7 @@ package ghidra.formats.gfilesystem;
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.annotations.FileSystemInfo;
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 class LocalFileSystem implements GFileSystem, GFileHashProvider {
return new LocalFileSystem(FSRLRoot.makeRoot(FSTYPE));
}
private final List<GFile> emptyDir = List.of();
private final FSRLRoot fsFSRL;
private final GFile rootDir;
private final FileSystemRefManager refManager = new FileSystemRefManager(this);
private final ReferenceMap<FileFingerprintRec, String> 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 class LocalFileSystem implements GFileSystem, GFileHashProvider {
@Override
public List<GFile> getListing(GFile directory) {
List<GFile> 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 class LocalFileSystem implements GFileSystem, GFileHashProvider {
}
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 class LocalFileSystem implements GFileSystem, GFileHashProvider {
* @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 class LocalFileSystem implements GFileSystem, GFileHashProvider {
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 @@ public class LocalFileSystem implements GFileSystem, GFileHashProvider {
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 class LocalFileSystem implements GFileSystem, GFileHashProvider {
// 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,

View File

@ -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 class LocalFileSystemSub implements GFileSystem, GFileHashProvider {
return List.of();
}
File localDir = getFileFromGFile(directory);
if (Files.isSymbolicLink(localDir.toPath())) {
if (FSUtilities.isSymlink(localDir)) {
return List.of();
}
@ -112,7 +111,8 @@ public class LocalFileSystemSub implements GFileSystem, GFileHashProvider {
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 class LocalFileSystemSub implements GFileSystem, GFileHashProvider {
return rootFS.getFileAttributes(localFile);
}
catch (IOException e) {
// fail and return null
// fail and return empty
}
return FileAttributes.EMPTY;
}
@ -148,6 +148,11 @@ public class LocalFileSystemSub implements GFileSystem, GFileHashProvider {
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 class LocalFileSystemSub implements GFileSystem, GFileHashProvider {
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);
}
}

View File

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

View File

@ -301,7 +301,6 @@ public class ImporterPlugin extends Plugin
if (addToProgramAction != null) {
addToProgramAction.setEnabled(false);
}
ProgramMappingService.clear();
}
@Override
@ -315,8 +314,6 @@ public class ImporterPlugin extends Plugin
if (addToProgramAction != null) {
addToProgramAction.setEnabled(false);
}
ProgramMappingService.clear();
}
private void setupImportAction() {

View File

@ -151,8 +151,8 @@ public class ImporterUtilities {
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 @@ public class ImporterUtilities {
}
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 class ImporterUtilities {
* 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 @@ public class ImporterUtilities {
monitor.checkCancelled();
if (loaded.getDomainObject() instanceof Program program) {
ProgramMappingService.createAssociation(fsrl, program);
if (programManager != null) {
int openState = firstProgram
? ProgramManager.OPEN_CURRENT

View File

@ -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<sup>[1]</sup> mapping / association between Ghidra Program/DomainFile
* objects and GFilesystem files (identified by their {@link FSRL}).
* <p>
* 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.
* <p>
* Threadsafe.
* <p>
* The current implementation searches current open Ghidra Programs and maintains a
* short-lived, in-memory only mapping of FSRL-&gt;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.)
* <p>
* [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.
* <p>
* Limited in size to {@value #FSRL_TO_PATH_MAP_SIZE}.
*/
private static Map<FSRL, String> fsrlToProjectPathMap =
new FixedSizeHashMap<>(FSRL_TO_PATH_MAP_SIZE);
private ProgramMappingService() {
// utils class; cannot instantiate
}
/**
* Clears {@link ProgramMappingService} data.
* <p>
* 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}.
* <p>
* (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<DomainFile> 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.
* <p>
* @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}.
* <p>
* This method only consults an internal fsrl-to-DomainFile mapping that is short-lived
* and not persisted.
* <p>
* @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}.
* <p>
* @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.
* <p>
* 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.
* <p>
* @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.
* <p>
* @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.
* <p>
* @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.
* <p>
* (ie. an open program has a MD5 or FSRL metadata value that matches the file)
* <p>
* See also {@link #isFileOpen(FSRL)}.
* <p>
* @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<DomainFile> 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.
* <p>
* Warning, this operation is expensive and should only be done in a Task thread.
* <p>
* @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<FSRL, DomainFile> searchProjectForMatchingFiles(List<FSRL> 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<String, FSRL> fsrlsToFindByMD5;
try {
fsrlsToFindByMD5 = buildFullyQualifiedFSRLMap(fsrls, monitor);
}
catch (CancelledException ce) {
Msg.info(ProgramMappingService.class, "Canceling project search");
return Collections.emptyMap();
}
Map<FSRL, DomainFile> results = new HashMap<>();
Iterable<DomainFile> files = ProjectDataUtils.descendantFiles(projectData.getRootFolder());
for (DomainFile domainFile : files) {
if (monitor.isCancelled() || fsrlsToFindByMD5.isEmpty()) {
break;
}
monitor.incrementProgress(1);
Map<String, String> 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<String, String> metadata) {
return metadata.get(PROGRAM_METADATA_MD5);
}
private static FSRL getFSRLFromMetadata(Map<String, String> 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<DomainFile> getOpenFiles() {
List<DomainFile> files = new ArrayList<>();
Project project = AppInfo.getActiveProject();
if (project != null) {
files = project.getOpenData();
}
return files;
}
private static List<DomainFile> findOpenFiles() {
List<DomainFile> 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<String, FSRL> buildFullyQualifiedFSRLMap(List<FSRL> fsrls,
TaskMonitor monitor) throws CancelledException {
Map<String, FSRL> 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;
}
}

View File

@ -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<DomainFile, Map<String, String>, Object> mappingFunc,
Map<Object, Object> indexedFiles) {
IndexInfo(IndexType indexType,
BiFunction<DomainFile, Map<String, String>, Object> mappingFunc) {
this(indexType, mappingFunc, new HashMap<>());
}
}
private Project project;
private List<IndexInfo> 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<String, String> metadata) {
return metadata.get(IndexType.MD5.metadataKey);
}
private FSRL getFSRL(DomainFile file, Map<String, String> 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<DomainFile> lookupFiles(IndexType keyType, Object keyValue) {
IndexInfo index = indexes.get(keyType.ordinal());
Object fileInfo = index.indexedFiles.get(keyValue);
List<String> 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<DomainFile> files = lookupFiles(IndexType.FSRL, fsrl);
return !files.isEmpty() ? files.get(0) : null;
}
private synchronized void indexFile(DomainFile file) {
Map<String, String> 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<String>) 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<String> 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<Object> 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();
}
}
}
}
}
}

View File

@ -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<FSBNode> selectedNodes, MouseEvent event, GTree gTree) {
super(provider, selectedNodes, gTree);
this.gTree = gTree;
}
@Override
public FSBComponentProvider getComponentProvider() {
return (FSBComponentProvider) super.getComponentProvider();
}
@Override
public List<FSBNode> 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 class FSBActionContext extends DefaultActionContext {
* @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 class FSBActionContext extends DefaultActionContext {
* @return list of currently selected tree nodes
*/
public List<FSBNode> getSelectedNodes() {
return List.of((FSBNode[]) getContextObject());
return (List<FSBNode>) super.getContextObject();
}
/**
@ -104,27 +115,19 @@ public class FSBActionContext extends DefaultActionContext {
* selected
*/
public FSRL getFSRL(boolean dirsOk) {
FSBNode[] selectedNodes = (FSBNode[]) getContextObject();
if (selectedNodes.length != 1) {
List<FSBNode> 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();
return fsRootNode.getContainer();
}
boolean isDir = (node instanceof FSBDirNode) || (node instanceof FSBRootNode);
if (isDir && !dirsOk) {
return null;
}
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 @@ public class FSBActionContext extends DefaultActionContext {
* @return boolean true if the currently selected items are all directory items
*/
public boolean isSelectedAllDirs() {
FSBNode[] selectedNodes = (FSBNode[]) getContextObject();
List<FSBNode> 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 class FSBActionContext extends DefaultActionContext {
* @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<FSBNode> selectedNodes = getSelectedNodes();
return selectedNodes.size() == 1 ? selectedNodes.get(0) : null;
}
/**
@ -175,11 +160,10 @@ public class FSBActionContext extends DefaultActionContext {
* @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<FSRL> getFSRLsFromNodes(FSBNode[] nodes, boolean dirsOk) {
private List<FSRL> getFSRLsFromNodes(List<FSBNode> nodes, boolean dirsOk) {
List<FSRL> fsrls = new ArrayList<>();
for (FSBNode node : nodes) {
FSRL fsrl = node.getFSRL();
@ -206,7 +190,7 @@ public class FSBActionContext extends DefaultActionContext {
* @return list of FSRLs of the currently selected items, maybe empty but never null
*/
public List<FSRL> getFSRLs(boolean dirsOk) {
FSBNode[] selectedNodes = (FSBNode[]) getContextObject();
List<FSBNode> selectedNodes = getSelectedNodes();
return getFSRLsFromNodes(selectedNodes, dirsOk);
}
@ -228,41 +212,6 @@ public class FSBActionContext extends DefaultActionContext {
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 class FSBActionContext extends DefaultActionContext {
*/
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<FSRL> getLoadableFSRLs() {
FSBNode[] selectedNodes = (FSBNode[]) getContextObject();
List<FSRL> 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;
}
}

View File

@ -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}.
* <p>
* An instance of this class is created for each file system browser window (w/tree).
* <p>
* 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<FSBFileHandler> 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<Icon> 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<DockingActionIf> getPopupActions(Tool tool, ActionContext context) {
List<DockingActionIf> results = new ArrayList<>();
for (FSBFileHandler fileHandler : fileHandlers) {
List<DockingAction> 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<FSBNode> getSelectedNodes(MouseEvent event) {
TreePath[] selectionPaths = gTree.getSelectionPaths();
List<FSBNode> 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<FSBNode> 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<DockingAction> 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);
}
}
}

View File

@ -19,6 +19,7 @@ import java.io.IOException;
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 class FSBDirNode extends FSBFileNode {
}
@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 class FSBDirNode extends FSBFileNode {
return false;
}
@Override
public FSRL getLoadableFSRL() {
return getFSBRootNode().getProgramProviderFSRL(getFSRL());
}
}

View File

@ -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<DockingAction> 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.
* <p>
* 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<DockingAction> getPopupProviderActions() {
return List.of();
}
}

View File

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

View File

@ -17,15 +17,16 @@ package ghidra.plugins.fsbrowser;
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 class FSBFileNode extends FSBNode {
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();
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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<String, Icon> substringToIconMap = createSubstringMap();
private FSBIcons() {
// don't create instances of this class, use getInstance() instead
}
private Map<String, Icon> createSubstringMap() {
Map<String, Icon> results = new HashMap<>();
GThemeValueMap values = ThemeManager.getInstance().getCurrentValues();
List<IconValue> 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<Icon> 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<Icon> 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;
}
}

View File

@ -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 abstract class FSBNode extends GTreeSlowLoadingNode {
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<FSRL, GFile> getListing(GFile f) {
try {
List<GFile> 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<FSRL, GFile> currentFiles = getListing(f);
int changeCount = 0;
boolean cryptoCausesFullRefresh = true;
boolean flagFSBRootNodeWithCryptoUpdate = false;
List<GTreeNode> newNodes = new ArrayList<>();
List<GTreeNode> 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 abstract class FSBNode extends GTreeSlowLoadingNode {
List<GTreeNode> 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 abstract class FSBNode extends GTreeSlowLoadingNode {
* @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<GTreeNode> 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;
};
}

View File

@ -38,8 +38,8 @@ public class FSBRootNode extends FSBNode {
private FileSystemRef fsRef;
private FSBFileNode prevNode;
private List<FSBRootNode> subRootNodes = new ArrayList<>();
private FSBRootNode modelNode;
private boolean cryptoStatusUpdated;
FSBRootNode(FileSystemRef fsRef) {
this(fsRef, null);
@ -60,11 +60,24 @@ public class FSBRootNode extends FSBNode {
@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 @@ public class FSBRootNode extends FSBNode {
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 class FSBRootNode extends FSBNode {
@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<GFile> 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<GFile> splitGFilePath(GFile f) {
List<GFile> 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();
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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<PluginTool> 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<PluginTool> getRunningProgramManagerTools(PluginTool tool) {
List<PluginTool> 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<PluginTool> 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;
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* Thread safe
* <p>
*/
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<String, Icon> substringToIconMap = new HashMap<>();
private FileIconService() {
createSubstringMap();
}
private void createSubstringMap() {
GThemeValueMap values = ThemeManager.getInstance().getCurrentValues();
List<IconValue> 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<Icon> 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<Icon> 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;
}
}

View File

@ -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}.
* <p>
* An instance of this class is created for each file system browser window (w/tree).
* <p>
* 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<Icon> 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<FSBNode> 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;
}
}

View File

@ -33,16 +33,14 @@ import ghidra.app.CorePluginPackage;
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<FSRL, FileSystemBrowserComponentProvider> currentBrowsers = new HashMap<>();
private Map<FSRL, FSBComponentProvider> 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 @@ public class FileSystemBrowserPlugin extends Plugin
if (frontEndService != null) {
frontEndService.addProjectListener(this);
}
else {
FSBUtils.getProgramManager(tool, false);
}
setupActions();
}
@ -122,7 +118,7 @@ public class FileSystemBrowserPlugin extends Plugin
chooserOpen.dispose();
}
for (FileSystemBrowserComponentProvider provider : currentBrowsers.values()) {
for (FSBComponentProvider provider : currentBrowsers.values()) {
provider.dispose();
}
currentBrowsers.clear();
@ -143,19 +139,19 @@ public class FileSystemBrowserPlugin extends Plugin
* @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 @@ public class FileSystemBrowserPlugin extends Plugin
}
}
void removeFileSystemBrowserComponent(FileSystemBrowserComponentProvider componentProvider) {
void removeFileSystemBrowserComponent(FSBComponentProvider componentProvider) {
if (componentProvider != null) {
Swing.runIfSwingOrRunLater(() -> currentBrowsers.remove(componentProvider.getFSRL()));
}
@ -179,7 +175,7 @@ public class FileSystemBrowserPlugin extends Plugin
*/
private void removeAllFileSystemBrowsers() {
Swing.runIfSwingOrRunLater(() -> {
for (FileSystemBrowserComponentProvider fsbcp : new ArrayList<>(
for (FSBComponentProvider fsbcp : new ArrayList<>(
currentBrowsers.values())) {
fsbcp.dispose();
}
@ -187,29 +183,6 @@ public class FileSystemBrowserPlugin extends Plugin
});
}
@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 @@ public class FileSystemBrowserPlugin extends Plugin
* 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 @@ public class FileSystemBrowserPlugin extends Plugin
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;
}
}

View File

@ -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.
* <p>
* 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
}

View File

@ -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<OpenWithTarget> getAll() {
List<OpenWithTarget> 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<OpenWithTarget> 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<OpenWithTarget> 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.
* <p>
* 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<DomainFile> files) {
Project project = AppInfo.getActiveProject();
if (project == null) {
return;
}
if (pm != null) {
openWithPM(files);
}
else {
openWithToolTemplate(project, files);
}
}
private Map<DomainFile, Program> openWithPM(List<DomainFile> files) {
Map<DomainFile, Program> 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<DomainFile, Program> openWithToolTemplate(Project project, List<DomainFile> files) {
Map<DomainFile, Program> results = new HashMap<>();
PluginTool newTool = project.getToolServices().launchTool(name, files);
ProgramManager newToolPM;
if (newTool != null && (newToolPM = newTool.getService(ProgramManager.class)) != null) {
Set<DomainFile> fileSet = new HashSet<>(files);
for (Program openProgram : newToolPM.getAllOpenPrograms()) {
if (fileSet.contains(openProgram.getDomainFile())) {
results.put(openProgram.getDomainFile(), openProgram);
}
}
}
return results;
}
//----------------------------------------------------------------------------------------------
private static List<OpenWithTarget> getRunningTargets(Project project) {
List<OpenWithTarget> 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;
}
}

View File

@ -33,7 +33,6 @@ import docking.options.editor.FontEditor;
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<UndoableEdit> undoStack = new FixedSizeStack<>(MAX_UNDO_REDO_SIZE);
private FixedSizeStack<UndoableEdit> 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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<DockingAction> 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<FSRL> 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<FileAttributes> 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<FileAttributes> fileAttributesList) {
StringBuilder sb = new StringBuilder("<html>\n<table>\n");
sb.append("<tr><th>Property</th><th>Value</th></tr>\n");
for (FileAttributes fattrs : fileAttributesList) {
if (fattrs != fileAttributesList.get(0)) {
// not first element, put a visual divider line
sb.append("<tr><td colspan=2><hr></td></tr>");
}
List<FileAttribute<?>> 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("<tr><td><b>")
.append(group.getDescriptiveName())
.append("</b></td><td><hr></td></tr>\n");
}
}
String valStr =
FAT_TOSTRING_FUNCS.getOrDefault(attr.getAttributeType(), PLAIN_TOSTRING)
.apply(attr.getAttributeValue());
String html = HTMLUtilities.escapeHTML(valStr);
html = html.replace("\n", "<br>\n");
sb.append("<tr><td>")
.append(attr.getAttributeDisplayName())
.append(":</td><td>")
.append(html)
.append("</td></tr>\n");
}
}
sb.append("</table>");
return sb.toString();
}
//---------------------------------------------------------------------------------------------
// static lookup tables for rendering file attributes
//---------------------------------------------------------------------------------------------
private static final Function<Object, String> PLAIN_TOSTRING = o -> o.toString();
private static final Function<Object, String> SIZE_TOSTRING =
o -> (o instanceof Long) ? FSUtilities.formatSize((Long) o) : o.toString();
private static final Function<Object, String> UNIX_ACL_TOSTRING =
o -> (o instanceof Number) ? String.format("%05o", (Number) o) : o.toString();
private static final Function<Object, String> DATE_TOSTRING =
o -> (o instanceof Date) ? FSUtilities.formatFSTimestamp((Date) o) : o.toString();
private static final Function<Object, String> FSRL_TOSTRING =
o -> (o instanceof FSRL) ? ((FSRL) o).toPrettyString().replace("|", "|\n\t") : o.toString();
private static final Map<FileAttributeType, Function<Object, String>> 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));
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<DockingAction> getPopupProviderActions() {
FileSystemBrowserPlugin plugin = context.plugin();
List<DockingAction> 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<DomainFile> 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;
}
}

View File

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

View File

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

View File

@ -30,7 +30,6 @@ import ghidra.formats.gfilesystem.*;
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 @@ public class ImportBatchTask extends Task {
totalObjsImported == 0 ? ProgramManager.OPEN_CURRENT
: ProgramManager.OPEN_VISIBLE);
}
ProgramMappingService.createAssociation(appInfo.getFSRL(), program);
}
totalObjsImported++;
}

View File

@ -17,9 +17,12 @@ package ghidra.plugins.fsbrowser;
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 org.junit.Test;
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 class FileIconServiceTest extends AbstractDockingTest {
@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 class FileIconServiceTest extends AbstractDockingTest {
@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 class FileIconServiceTest extends AbstractDockingTest {
@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<String> 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());
}
}

View File

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

View File

@ -1,3 +1,4 @@
Decryptor
FileSystem
FileSystemModel
FSBFileHandler

View File

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

View File

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

View File

@ -106,7 +106,7 @@ public class BootImageFileSystem extends GFileSystemBase {
FileAttribute.create(FileAttributeType.COMMENT_ATTR,
"This is a second stage loader file. It appears unused at this time."));
}
return null;
return FileAttributes.EMPTY;
}
@Override

View File

@ -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 class VendorBootImageFileSystem extends GFileSystemBase {
FileAttribute.create(FileAttributeType.COMMENT_ATTR,
"This is a DTB file. It appears unused at this time."));
}
return null;
return FileAttributes.EMPTY;
}
@Override

View File

@ -74,7 +74,7 @@ public class ProfileFileSystem extends GFileSystemBase {
if (file == dataFile) {
return FileAttributes.of(FileAttribute.create("Magic", header.getMagic()));
}
return null;
return FileAttributes.EMPTY;
}
@Override

View File

@ -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 java.util.List;
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 @@ public class GFileSystemLoadKernelTask extends Task {
}
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 @@ public class GFileSystemLoadKernelTask extends Task {
folder.createFile(fileName, program, monitor);
programManager.openProgram(program);
ProgramMappingService.createAssociation(file.getFSRL(), program);
}
finally {
program.release(this);

View File

@ -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<DockingAction> 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<FSRL> 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<FSRL> fileList) {
FileSystemBrowserPlugin fsbPlugin = context.plugin();
OpenWithTarget openWith = OpenWithTarget.getRunningProgramManager(fsbPlugin.getTool());
if (openWith.getPm() != null) {
TaskLauncher
.launch(new GFileSystemLoadKernelTask(fsbPlugin, openWith.getPm(), fileList));
}
}
}

View File

@ -35,8 +35,14 @@ public class SquashFileSystemFactory
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

View File

@ -109,7 +109,7 @@ public class TarFileSystem extends AbstractFileSystem<TarMetadata> {
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(

View File

@ -95,7 +95,7 @@ public class ZipFileSystemBuiltin extends AbstractFileSystem<ZipEntry> {
public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) {
ZipEntry zipEntry = fsIndex.getMetadata(file);
if (zipEntry == null) {
return null;
return FileAttributes.EMPTY;
}
FileAttributes result = new FileAttributes();

View File

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

View File

@ -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<DockingAction> 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<GTreeNode> 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<FSRL> 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<FSRL> fileList) {
ProgramManager pm = FSBUtils.getProgramManager(getTool(), true);
if (pm != null) {
TaskLauncher.launch(new GFileSystemLoadKernelTask(this, pm, fileList));
}
}
}