From 72fe7b89d206732e2880eebfa075ac6b975a6699 Mon Sep 17 00:00:00 2001 From: dev747368 <48332326+dev747368@users.noreply.github.com> Date: Fri, 1 Oct 2021 10:09:38 -0400 Subject: [PATCH] GP-253, GP-725 Gfilesystem ByteProviders, obfuscated temp files, passwords Migrate GFilesystem methods to use ByteProviders instead of java Files for their source, and to produce ByteProviders instead of InputStreams. Refactor file info query method to return a structured collection of enum specified meta data instead of a free-form multiline string. Add locked icon badge to files in the file system browser that are password protected. Reduces the number of temp files created on disk, and obfuscates files that are created to avoid the wrath of virus scanners (in the same manner that ghidra db files are obfuscated). Add support for filesystems to query for passwords to decrypt files. Refactor the SevenZipFileSystem implementation to decrypt files embedded inside zips that were created with passwords. Fix Ext4 to support 128 byte inodes. --- .../Base/data/file_extension_icons.xml | 1 + .../FileSystemBrowserPlugin.html | 27 + .../app/util/bin/ByteArrayProvider.java | 16 +- .../ghidra/app/util/bin/ByteProvider.java | 15 +- .../app/util/bin/ByteProviderInputStream.java | 119 +- .../app/util/bin/ByteProviderWrapper.java | 53 +- .../app/util/bin/EmptyByteProvider.java | 103 ++ .../ghidra/app/util/bin/FileByteProvider.java | 323 ++++ .../app/util/bin/MemBufferByteProvider.java | 49 - .../util/bin/ObfuscatedFileByteProvider.java | 97 + .../app/util/bin/ObfuscatedInputStream.java | 99 ++ .../app/util/bin/ObfuscatedOutputStream.java | 69 + .../util/bin/RandomAccessByteProvider.java | 3 + .../app/util/bin/RangeMappedByteProvider.java | 5 - .../util/bin/SynchronizedByteProvider.java | 7 +- .../coff/archive/CoffArchiveMemberHeader.java | 23 +- .../coff/archive/FirstLinkerMember.java | 25 +- .../sectionprovider/NullSectionProvider.java | 7 +- .../java/ghidra/app/util/opinion/Loader.java | 5 +- .../AbstractFileExtractorTask.java | 9 +- .../gfilesystem/DerivedFileProducer.java | 44 - .../gfilesystem/DerivedFilePushProducer.java | 41 - .../java/ghidra/formats/gfilesystem/FSRL.java | 35 +- .../formats/gfilesystem/FSUtilities.java | 173 +- .../ghidra/formats/gfilesystem/FileCache.java | 668 ++++--- .../gfilesystem/FileFingerprintCache.java | 131 -- .../gfilesystem/FileSystemIndexHelper.java | 270 ++- ...he.java => FileSystemInstanceManager.java} | 25 +- .../gfilesystem/FileSystemRefManager.java | 9 +- .../gfilesystem/FileSystemService.java | 914 +++++----- .../gfilesystem/GFileHashProvider.java | 40 + .../ghidra/formats/gfilesystem/GFileImpl.java | 46 +- .../formats/gfilesystem/GFileSystem.java | 69 +- .../formats/gfilesystem/GFileSystemBase.java | 20 - .../formats/gfilesystem/GIconProvider.java | 15 +- .../formats/gfilesystem/LocalFileSystem.java | 197 ++- .../gfilesystem/LocalFileSystemSub.java | 52 +- .../formats/gfilesystem/RefdByteProvider.java | 98 ++ .../ghidra/formats/gfilesystem/RefdFile.java | 6 + .../gfilesystem/SelectFromListDialog.java | 9 +- .../SingleFileSystemIndexHelper.java | 2 +- .../crypto/CachedPasswordProvider.java | 143 ++ .../crypto/CmdLinePasswordProvider.java | 115 ++ .../gfilesystem/crypto/CryptoProvider.java | 55 + .../CryptoProviderSessionChildImpl.java | 53 + .../gfilesystem/crypto/CryptoProviders.java | 205 +++ .../gfilesystem/crypto/CryptoSession.java | 68 + .../gfilesystem/crypto/PasswordDialog.java | 135 ++ .../gfilesystem/crypto/PasswordProvider.java | 51 + .../gfilesystem/crypto/PasswordValue.java | 108 ++ .../crypto/PopupGUIPasswordProvider.java | 101 ++ .../factory/FileSystemFactoryMgr.java | 148 +- .../factory/GFileSystemBaseFactory.java | 21 +- ...va => GFileSystemFactoryByteProvider.java} | 20 +- .../factory/GFileSystemFactoryWithFile.java | 50 - .../gfilesystem/factory/GFileSystemProbe.java | 3 +- ...java => GFileSystemProbeByteProvider.java} | 22 +- .../factory/GFileSystemProbeWithFile.java | 48 - .../gfilesystem/fileinfo/FileAttribute.java | 133 ++ .../fileinfo/FileAttributeType.java | 99 ++ .../fileinfo/FileAttributeTypeGroup.java | 46 + .../gfilesystem/fileinfo/FileAttributes.java | 153 ++ .../FileType.java} | 21 +- .../GhidraApplicationConfiguration.java | 3 + .../plugin/importer/AddToProgramDialog.java | 2 + .../plugin/importer/ImporterDialog.java | 20 +- .../plugin/importer/ImporterPlugin.java | 6 +- .../plugin/importer/ImporterUtilities.java | 21 +- .../plugins/fsbrowser/FSBActionContext.java | 312 +++- .../plugins/fsbrowser/FSBActionManager.java | 1556 ++++++++--------- .../ghidra/plugins/fsbrowser/FSBDirNode.java | 55 +- .../ghidra/plugins/fsbrowser/FSBFileNode.java | 86 +- .../ghidra/plugins/fsbrowser/FSBNode.java | 52 +- .../ghidra/plugins/fsbrowser/FSBRootNode.java | 109 +- .../ghidra/plugins/fsbrowser/FSBUtils.java | 39 +- .../plugins/fsbrowser/FileIconService.java | 1 + .../FileSystemBrowserComponentProvider.java | 174 +- .../fsbrowser/FileSystemBrowserPlugin.java | 95 +- .../tasks/GFileSystemExtractAllTask.java | 21 +- .../importer/batch/BatchImportDialog.java | 18 +- .../plugins/importer/batch/BatchInfo.java | 86 +- .../importer/tasks/ImportBatchTask.java | 5 +- .../gfilesystem/FileSystemServiceTest.java | 33 +- .../app/util/bin/FileByteProviderTest.java | 110 ++ .../formats/gfilesystem/FileCacheTest.java | 163 +- .../crypto/CachedPasswordProviderTest.java | 71 + .../crypto/CmdLinePasswordProviderTest.java | 105 ++ .../file/eclipse/AndroidProjectCreator.java | 58 +- .../formats/android/apk/ApkFileSystem.java | 4 +- .../android/bootimg/BootImageFileSystem.java | 55 +- .../android/bootimg/BootImageHeader.java | 11 + .../android/bootimg/BootImageHeaderV0.java | 13 +- .../android/bootimg/BootImageHeaderV1.java | 6 +- .../android/bootimg/BootImageHeaderV2.java | 6 +- .../android/bootimg/BootImageHeaderV3.java | 14 +- .../bootimg/VendorBootImageFileSystem.java | 36 +- .../bootldr/AndroidBootLoaderFileSystem.java | 28 +- .../android/dex/DexToJarFileSystem.java | 35 +- .../android/dex/DexToSmaliFileSystem.java | 11 +- .../formats/android/fbpk/FBPK_FileSystem.java | 31 +- .../android/kernel/KernelFileSystem.java | 11 +- .../android/lz4/LZ4ArchiveFileSystem.java | 85 +- .../android/lz4/LZ4FrameFileSystem.java | 59 +- .../formats/android/oat/OatFileSystem.java | 55 +- .../formats/android/odex/OdexFileSystem.java | 39 +- .../android/prof/ProfileFileSystem.java | 79 +- .../formats/android/vdex/VdexFileSystem.java | 34 +- .../android/xml/AndroidXmlFileSystem.java | 17 +- .../formats/coff/CoffArchiveFileSystem.java | 40 +- .../coff/CoffArchiveFileSystemFactory.java | 13 +- .../formats/complzss/CompLzssFileSystem.java | 38 +- .../complzss/CompLzssFileSystemFactory.java | 15 +- .../file/formats/cpio/CpioFileSystem.java | 95 +- .../formats/cpio/CpioFileSystemFactory.java | 14 +- .../file/formats/ext4/Ext4FileSystem.java | 63 +- .../formats/ext4/Ext4FileSystemFactory.java | 31 +- .../ghidra/file/formats/ext4/Ext4Inode.java | 30 +- .../file/formats/gzip/GZipFileSystem.java | 117 +- .../formats/gzip/GZipFileSystemFactory.java | 17 +- .../ios/apple8900/Apple8900FileSystem.java | 14 +- .../formats/ios/dmg/DmgClientFileSystem.java | 49 +- .../ios/dmg/DmgClientFileSystemFactory.java | 54 +- .../dyldcache/DyldCacheDylibExtractor.java | 27 +- .../ios/dyldcache/DyldCacheFileSystem.java | 5 +- .../ios/ibootim/iBootImFileSystem.java | 21 +- .../ghidra/file/formats/ios/img2/Img2.java | 9 +- .../file/formats/ios/img2/Img2FileSystem.java | 91 +- .../ios/img2/Img2FileSystemFactory.java | 48 + .../file/formats/ios/img3/Img3FileSystem.java | 125 +- .../ios/img3/Img3FileSystemFactory.java | 48 + .../file/formats/ios/img4/Img4FileSystem.java | 15 +- .../file/formats/ios/ipsw/IpswFileSystem.java | 5 +- .../formats/ios/png/CrushedPNGFileSystem.java | 32 +- .../file/formats/ios/png/CrushedPNGUtil.java | 23 +- .../ios/prelink/PrelinkFileSystem.java | 12 +- .../formats/iso9660/ISO9660FileSystem.java | 28 +- .../java/JavaClassDecompilerFileSystem.java | 83 +- .../JavaClassDecompilerFileSystemFactory.java | 12 +- .../formats/omf/OmfArchiveFileSystem.java | 32 +- .../omf/OmfArchiveFileSystemFactory.java | 23 +- .../sevenzip/SZByteProviderStream.java | 93 + .../formats/sevenzip/SevenZipFileSystem.java | 693 +++++--- .../sevenzip/SevenZipFileSystemFactory.java | 12 +- .../sparseimage/SparseImageFileSystem.java | 102 +- .../SparseImageFileSystemFactory.java | 22 +- .../file/formats/tar/TarFileSystem.java | 133 +- .../formats/tar/TarFileSystemFactory.java | 88 +- .../ubi/UniversalBinaryFileSystem.java | 22 +- .../file/formats/yaffs2/YAFFS2FileSystem.java | 23 +- .../formats/yaffs2/YAFFS2InputStream.java | 9 +- .../file/formats/zip/ZipFileSystem.java | 116 +- .../formats/zip/ZipFileSystemFactory.java | 40 +- .../java/ghidra/file/jad/JarDecompiler.java | 79 +- .../fileformats/FileFormatsPlugin.java | 379 ++-- .../ghidra/util/MonitoredOutputStream.java | 21 +- .../java/skeleton/SkeletonFileSystem.java | 54 +- 156 files changed, 7948 insertions(+), 4714 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/EmptyByteProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/FileByteProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedFileByteProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedInputStream.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedOutputStream.java delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/DerivedFileProducer.java delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/DerivedFilePushProducer.java delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileFingerprintCache.java rename Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/{FileSystemCache.java => FileSystemInstanceManager.java} (91%) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileHashProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdByteProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CachedPasswordProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CmdLinePasswordProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProviderSessionChildImpl.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProviders.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoSession.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordValue.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PopupGUIPasswordProvider.java rename Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/{GFileSystemFactoryFull.java => GFileSystemFactoryByteProvider.java} (74%) delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemFactoryWithFile.java rename Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/{GFileSystemProbeFull.java => GFileSystemProbeByteProvider.java} (70%) delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbeWithFile.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttribute.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributeType.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributeTypeGroup.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributes.java rename Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/{FileCacheEntry.java => fileinfo/FileType.java} (67%) create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/FileByteProviderTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/crypto/CachedPasswordProviderTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/crypto/CmdLinePasswordProviderTest.java create mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2FileSystemFactory.java create mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img3/Img3FileSystemFactory.java create mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SZByteProviderStream.java diff --git a/Ghidra/Features/Base/data/file_extension_icons.xml b/Ghidra/Features/Base/data/file_extension_icons.xml index 52a3bdd107..50b56ac3ea 100644 --- a/Ghidra/Features/Base/data/file_extension_icons.xml +++ b/Ghidra/Features/Base/data/file_extension_icons.xml @@ -38,6 +38,7 @@ + diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FileSystemBrowserPlugin/FileSystemBrowserPlugin.html b/Ghidra/Features/Base/src/main/help/help/topics/FileSystemBrowserPlugin/FileSystemBrowserPlugin.html index 3019e8dbe3..ca03dfbab1 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/FileSystemBrowserPlugin/FileSystemBrowserPlugin.html +++ b/Ghidra/Features/Base/src/main/help/help/topics/FileSystemBrowserPlugin/FileSystemBrowserPlugin.html @@ -123,6 +123,19 @@ of the file systems will display that file system's browser tree.

+

Clear Cached Passwords

+ +
+

Clears any cached passwords that previously entered when accessing a password + protected container file.

+
+ +

Refresh

+ +
+

Refreshes the status of the selected items.

+
+

Close

@@ -150,6 +163,20 @@
+

Password Dialog

+ +
+

This dialog is used to prompt for a password when opening a password protected container.

+ +

If the password isn't known, the Cancel button will stop Ghidra from re-prompting + for the password for the current file during the current operation (but the user may be + re-prompted again later depending on the logic of the operation), and Cancel All + will stop Ghidra from prompting for passwords for any file during the current operation.

+ +

Passwords can also be provided via a text file specified on the java command line, useful + for headless operations. See ghidra.formats.gfilesystem.crypto.CmdLinePasswordProvider

+
+

How To Handle Unsupported File Systems

diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteArrayProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteArrayProvider.java index 37abf30fa2..f12f16a5c3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteArrayProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteArrayProvider.java @@ -20,7 +20,7 @@ import java.io.*; import ghidra.formats.gfilesystem.FSRL; /** - * An implementation of {@link ByteProvider} where the underlying bytes are supplied by a static + * An implementation of {@link ByteProvider} where the underlying bytes are supplied by a * byte array. *

* NOTE: Use of this class is discouraged when the byte array could be large. @@ -66,6 +66,16 @@ public class ByteArrayProvider implements ByteProvider { // don't do anything for now } + /** + * Releases the byte storage of this instance. + *

+ * This is separate from the normal close() to avoid changing existing + * behavior of this class. + */ + public void hardClose() { + srcBytes = new byte[0]; + } + @Override public FSRL getFSRL() { return fsrl; @@ -78,12 +88,12 @@ public class ByteArrayProvider implements ByteProvider { @Override public String getName() { - return name; + return fsrl != null ? fsrl.getName() : name; } @Override public String getAbsolutePath() { - return ""; + return fsrl != null ? fsrl.getPath() : ""; } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProvider.java index c0c0a80ad4..b871e35dd9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProvider.java @@ -25,6 +25,11 @@ import ghidra.formats.gfilesystem.FileSystemService; */ public interface ByteProvider extends Closeable { + /** + * A static re-usable empty {@link ByteProvider} instance. + */ + public static final ByteProvider EMPTY_BYTEPROVIDER = new EmptyByteProvider(); + /** * Returns the {@link FSRL} of the underlying file for this byte provider, * or null if this byte provider is not associated with a file. @@ -109,9 +114,17 @@ public interface ByteProvider extends Closeable { *

* The caller is responsible for closing the returned {@link InputStream} instance. *

+ * If you need to override this default implementation, please document why your inputstream + * is needed. + * * @param index where in the {@link ByteProvider} to start the {@link InputStream} * @return the {@link InputStream} * @throws IOException if an I/O error occurs */ - public InputStream getInputStream(long index) throws IOException; + default public InputStream getInputStream(long index) throws IOException { + if (index < 0 || index > length()) { + throw new IOException("Invalid start position: " + index); + } + return new ByteProviderInputStream(this, index); + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProviderInputStream.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProviderInputStream.java index 4b0f420050..32b9f25bf1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProviderInputStream.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProviderInputStream.java @@ -18,25 +18,122 @@ package ghidra.app.util.bin; import java.io.IOException; import java.io.InputStream; +/** + * An {@link InputStream} that reads from a {@link ByteProvider}. + *

+ * Does not close the underlying ByteProvider when closed itself. + * + */ public class ByteProviderInputStream extends InputStream { - private ByteProvider provider; - private long offset; - private long length; - private long nextOffset; - public ByteProviderInputStream( ByteProvider provider, long offset, long length ) { + /** + * An {@link InputStream} that reads from a {@link ByteProvider}, and DOES + * {@link ByteProvider#close() close()} the underlying ByteProvider when + * closed itself. + *

+ */ + public static class ClosingInputStream extends ByteProviderInputStream { + /** + * Creates an {@link InputStream} that reads from a {@link ByteProvider}, that + * DOES {@link ByteProvider#close() close()} the underlying ByteProvider when + * closed itself. + *

+ * @param provider the {@link ByteProvider} to read from (and close) + */ + public ClosingInputStream(ByteProvider provider) { + super(provider); + } + + @Override + public void close() throws IOException { + super.close(); + if (provider != null) { + provider.close(); + provider = null; + } + } + } + + protected ByteProvider provider; + private long currentPosition; + private long markPosition; + + /** + * Creates an InputStream that uses a ByteProvider as its source of bytes. + * + * @param provider the {@link ByteProvider} to wrap + */ + public ByteProviderInputStream(ByteProvider provider) { + this(provider, 0); + } + + /** + * Creates an InputStream that uses a ByteProvider as its source of bytes. + * + * @param provider the {@link ByteProvider} to wrap + * @param startPosition starting position in the provider + */ + public ByteProviderInputStream(ByteProvider provider, long startPosition) { this.provider = provider; - this.offset = offset; - this.length = length; - this.nextOffset = offset; + this.markPosition = startPosition; + this.currentPosition = startPosition; + } + + @Override + public void close() throws IOException { + // nothing to do here + } + + @Override + public int available() throws IOException { + return (int) Math.min(provider.length() - currentPosition, Integer.MAX_VALUE); + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public synchronized void mark(int readlimit) { + this.markPosition = currentPosition; + } + + @Override + public synchronized void reset() throws IOException { + // synchronized because the base class's method is synchronized. + this.currentPosition = markPosition; + } + + @Override + public long skip(long n) throws IOException { + if (n <= 0) { + return 0; + } + long newPosition = Math.min(provider.length(), currentPosition + n); + long skipped = newPosition - currentPosition; + currentPosition = newPosition; + return skipped; } @Override public int read() throws IOException { - if ( nextOffset < offset + length ) { - return provider.readByte( nextOffset++ ) & 0xff; + return (currentPosition < provider.length()) + ? Byte.toUnsignedInt(provider.readByte(currentPosition++)) + : -1; + } + + @Override + public int read(byte[] b, int bufferOffset, int len) throws IOException { + long eof = provider.length(); + if (currentPosition >= eof) { + return -1; } - return -1; + len = (int) Math.min(len, eof - currentPosition); + byte[] bytes = provider.readBytes(currentPosition, len); + System.arraycopy(bytes, 0, b, bufferOffset, len); + currentPosition += len; + return len; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProviderWrapper.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProviderWrapper.java index 0358c096b5..b608c8b7f4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProviderWrapper.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ByteProviderWrapper.java @@ -15,12 +15,13 @@ */ package ghidra.app.util.bin; -import java.io.*; +import java.io.File; +import java.io.IOException; import ghidra.formats.gfilesystem.FSRL; /** - * Creates a {@link ByteProvider} constrained to a sub-section of an existing {@link ByteProvider}. + * A {@link ByteProvider} constrained to a sub-section of an existing {@link ByteProvider}. */ public class ByteProviderWrapper implements ByteProvider { private ByteProvider provider; @@ -29,7 +30,21 @@ public class ByteProviderWrapper implements ByteProvider { private FSRL fsrl; /** - * Constructs a {@link ByteProviderWrapper} around the specified {@link ByteProvider} + * Creates a wrapper around a {@link ByteProvider} that contains the same bytes as the specified + * provider, but with a new {@link FSRL} identity. + *

+ * + * @param provider {@link ByteProvider} to wrap + * @param fsrl {@link FSRL} identity for the instance + * @throws IOException if error + */ + public ByteProviderWrapper(ByteProvider provider, FSRL fsrl) throws IOException { + this(provider, 0, provider.length(), fsrl); + } + + /** + * Constructs a {@link ByteProviderWrapper} around the specified {@link ByteProvider}, + * constrained to a subsection of the provider. * * @param provider the {@link ByteProvider} to wrap * @param subOffset the offset in the {@link ByteProvider} of where to start the new @@ -41,13 +56,14 @@ public class ByteProviderWrapper implements ByteProvider { } /** - * Constructs a {@link ByteProviderWrapper} around the specified {@link ByteProvider} + * Constructs a {@link ByteProviderWrapper} around the specified {@link ByteProvider}, + * constrained to a subsection of the provider. * * @param provider the {@link ByteProvider} to wrap * @param subOffset the offset in the {@link ByteProvider} of where to start the new * {@link ByteProviderWrapper} * @param subLength the length of the new {@link ByteProviderWrapper} - * @param fsrl FSRL identity of the file this ByteProvider represents + * @param fsrl {@link FSRL} identity of the file this ByteProvider represents */ public ByteProviderWrapper(ByteProvider provider, long subOffset, long subLength, FSRL fsrl) { this.provider = provider; @@ -57,8 +73,8 @@ public class ByteProviderWrapper implements ByteProvider { } @Override - public void close() { - // don't do anything for now + public void close() throws IOException { + // do not close the wrapped provider } @Override @@ -68,24 +84,22 @@ public class ByteProviderWrapper implements ByteProvider { @Override public File getFile() { - return provider.getFile(); - } - - @Override - public InputStream getInputStream(long index) throws IOException { - return new ByteProviderInputStream(this, index, subLength - index); + // there is no file that represents the actual contents of the subrange, so return null + return null; } @Override public String getName() { - return provider.getName() + "[0x" + Long.toHexString(subOffset) + ",0x" + - Long.toHexString(subLength) + "]"; + return (fsrl != null) + ? fsrl.getName() + : String.format("%s[0x%x,0x%x]", provider.getName(), subOffset, subLength); } @Override public String getAbsolutePath() { - return provider.getAbsolutePath() + "[0x" + Long.toHexString(subOffset) + ",0x" + - Long.toHexString(subLength) + "]"; + return (fsrl != null) + ? fsrl.getPath() + : String.format("%s[0x%x,0x%x]", provider.getAbsolutePath(), subOffset, subLength); } @Override @@ -95,10 +109,7 @@ public class ByteProviderWrapper implements ByteProvider { @Override public boolean isValidIndex(long index) { - if (provider.isValidIndex(index)) { - return index >= subOffset && index < subLength; - } - return false; + return (0 <= index && index < subLength) && provider.isValidIndex(subOffset + index); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/EmptyByteProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/EmptyByteProvider.java new file mode 100644 index 0000000000..a45fbd58a5 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/EmptyByteProvider.java @@ -0,0 +1,103 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin; + +import java.io.*; + +import ghidra.formats.gfilesystem.FSRL; + +/** + * A {@link ByteProvider} that has no contents. + * + */ +public class EmptyByteProvider implements ByteProvider { + + private final FSRL fsrl; + + /** + * Create an instance with a null identity + */ + public EmptyByteProvider() { + this(null); + } + + /** + * Create an instance with the specified {@link FSRL} identity. + * + * @param fsrl {@link FSRL} identity for this instance + */ + public EmptyByteProvider(FSRL fsrl) { + this.fsrl = fsrl; + } + + @Override + public FSRL getFSRL() { + return fsrl; + } + + @Override + public File getFile() { + return null; + } + + @Override + public String getName() { + return fsrl != null ? fsrl.getName() : null; + } + + @Override + public String getAbsolutePath() { + return fsrl != null ? fsrl.getPath() : null; + } + + @Override + public byte readByte(long index) throws IOException { + throw new IOException("Not supported"); + } + + @Override + public byte[] readBytes(long index, long length) throws IOException { + if (index != 0 || length != 0) { + throw new IOException("Not supported"); + } + return new byte[0]; + } + + @Override + public long length() { + return 0; + } + + @Override + public boolean isValidIndex(long index) { + return false; + } + + @Override + public void close() throws IOException { + // do nothing + } + + @Override + public InputStream getInputStream(long index) throws IOException { + if (index != 0) { + throw new IOException("Invalid offset"); + } + return InputStream.nullInputStream(); + } + + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/FileByteProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/FileByteProvider.java new file mode 100644 index 0000000000..8462ce0b60 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/FileByteProvider.java @@ -0,0 +1,323 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin; + +import java.io.*; +import java.nio.file.AccessMode; + +import org.apache.commons.collections4.map.ReferenceMap; + +import ghidra.formats.gfilesystem.FSRL; +import ghidra.util.Msg; +import ghidra.util.datastruct.LRUMap; + +/** + * A {@link ByteProvider} that reads its bytes from a file. + * + */ +public class FileByteProvider implements ByteProvider, MutableByteProvider { + + static final int BUFFER_SIZE = 64 * 1024; + private static final int BUFFERS_TO_PIN = 4; + + private FSRL fsrl; + private File file; + private RandomAccessFile raf; + private ReferenceMap buffers = new ReferenceMap<>(); + private LRUMap lruBuffers = new LRUMap<>(BUFFERS_TO_PIN); // only used to pin a small set of recently used buffers in memory. not used for lookup + private long currentLength; + private AccessMode accessMode; // probably wrong Enum class, but works for now + + /** + * Creates a new instance. + * + * @param file {@link File} to open + * @param fsrl {@link FSRL} identity of the file + * @param accessMode {@link AccessMode#READ} or {@link AccessMode#WRITE} + * @throws IOException if error + */ + public FileByteProvider(File file, FSRL fsrl, AccessMode accessMode) + throws IOException { + this.file = file; + this.fsrl = fsrl; + this.accessMode = accessMode; + this.raf = new RandomAccessFile(file, accessModeToString(accessMode)); + this.currentLength = raf.length(); + } + + /** + * Returns the access mode the file was opened with. + * + * @return {@link AccessMode} used to open file + */ + public AccessMode getAccessMode() { + return accessMode; + } + + @Override + public void close() throws IOException { + if (raf != null) { + raf.close(); + raf = null; + } + buffers.clear(); + lruBuffers.clear(); + } + + @Override + public File getFile() { + return file; + } + + @Override + public String getName() { + return fsrl != null ? fsrl.getName() : file.getName(); + } + + @Override + public String getAbsolutePath() { + return fsrl != null ? fsrl.getPath() : file.getAbsolutePath(); + } + + @Override + public FSRL getFSRL() { + return fsrl; + } + + @Override + public long length() throws IOException { + return currentLength; + } + + @Override + public boolean isValidIndex(long index) { + return 0 <= index && index < currentLength; + } + + @Override + public byte readByte(long index) throws IOException { + ensureBounds(index, 1); + Buffer fileBuffer = getBufferFor(index); + int ofs = fileBuffer.getBufferOffset(index); + + return fileBuffer.bytes[ofs]; + } + + @Override + public byte[] readBytes(long index, long length) throws IOException { + ensureBounds(index, length); + if (length > Integer.MAX_VALUE) { + throw new IllegalArgumentException(); + } + int len = (int) length; + byte[] result = new byte[len]; + int bytesRead = readBytes(index, result, 0, len); + if (bytesRead != len) { + throw new IOException("Unable to read " + len + " bytes at " + index); + } + return result; + } + + /** + * Read bytes at the specified index into the given byte array. + *

+ * See {@link InputStream#read(byte[], int, int)}. + *

+ * + * @param index file offset to start reading + * @param buffer byte array that will receive the bytes + * @param offset offset inside the byte array to place the bytes + * @param length number of bytes to read + * @return number of actual bytes read + * @throws IOException if error + */ + public int readBytes(long index, byte[] buffer, int offset, int length) throws IOException { + ensureBounds(index, 0); + length = (int) Math.min(currentLength - index, length); + + int totalBytesRead = 0; + while (length > 0) { + Buffer fileBuffer = getBufferFor(index); + int ofs = fileBuffer.getBufferOffset(index); + int bytesToReadFromThisBuffer = Math.min(fileBuffer.len - ofs, length); + System.arraycopy(fileBuffer.bytes, ofs, buffer, totalBytesRead, + bytesToReadFromThisBuffer); + + length -= bytesToReadFromThisBuffer; + index += bytesToReadFromThisBuffer; + totalBytesRead += bytesToReadFromThisBuffer; + } + return totalBytesRead; + } + + @Override + protected void finalize() { + if (raf != null) { + Msg.warn(this, "FAIL TO CLOSE " + file); + } + } + + /** + * Writes bytes to the specified offset in the file. + * + * @param index the location in the file to starting writing + * @param buffer bytes to write + * @param offset offset in the buffer byte array to start + * @param length number of bytes to write + * @throws IOException if bad {@link AccessMode} or other io error + */ + public synchronized void writeBytes(long index, byte[] buffer, int offset, int length) + throws IOException { + if (accessMode != AccessMode.WRITE) { + throw new IOException("Not write mode"); + } + + doWriteBytes(index, buffer, offset, length); + long writeEnd = index + length; + currentLength = Math.max(currentLength, writeEnd); + + // after writing new bytes to the file, update + // any buffers that we can completely fill with the contents of + // this write buffer, and invalidate any buffers that we can't + // completely fill (they can be re-read in a normal fashion later when needed) + while (length > 0) { + long bufferPos = getBufferPos(index); + int bufferOfs = (int) (index - bufferPos); + int bytesAvailForThisBuffer = Math.min(length, BUFFER_SIZE - bufferOfs); + + Buffer fileBuffer = buffers.get(bufferPos); + if (fileBuffer != null) { + if (bufferOfs == 0 && length >= BUFFER_SIZE) { + System.arraycopy(buffer, offset, fileBuffer.bytes, 0, BUFFER_SIZE); + fileBuffer.len = BUFFER_SIZE; + } + else { + buffers.remove(bufferPos); + lruBuffers.remove(bufferPos); + } + } + index += bytesAvailForThisBuffer; + offset += bytesAvailForThisBuffer; + length -= bytesAvailForThisBuffer; + } + } + + @Override + public void writeByte(long index, byte value) throws IOException { + writeBytes(index, new byte[] { value }, 0, 1); + } + + @Override + public void writeBytes(long index, byte[] values) throws IOException { + writeBytes(index, values, 0, values.length); + } + + //------------------------------------------------------------------------------------ + /** + * Reads bytes from the file. + *

+ * Protected by synchronized lock. (See {@link #getBufferFor(long)}). + * + * @param index file position of where to read + * @param buffer byte array that will receive bytes + * @return actual number of byte read + * @throws IOException if error + */ + protected int doReadBytes(long index, byte[] buffer) throws IOException { + raf.seek(index); + return raf.read(buffer, 0, buffer.length); + } + + /** + * Writes the specified bytes to the file. + *

+ * Protected by synchronized lock (See {@link #writeBytes(long, byte[], int, int)}) + * + * @param index file position of where to write + * @param buffer byte array containing bytes to write + * @param offset offset inside of byte array to start + * @param length number of bytes from buffer to write + * @throws IOException if error + */ + protected void doWriteBytes(long index, byte[] buffer, int offset, int length) + throws IOException { + raf.seek(index); + raf.write(buffer, offset, length); + } + + //------------------------------------------------------------------------------------ + private void ensureBounds(long index, long length) throws IOException { + if (index < 0 || index > currentLength) { + throw new IOException("Invalid index: " + index); + } + if (index + length > currentLength) { + throw new IOException("Unable to read past EOF: " + index + ", " + length); + } + } + + private long getBufferPos(long index) { + return (index / BUFFER_SIZE) * BUFFER_SIZE; + } + + private synchronized Buffer getBufferFor(long pos) throws IOException { + long bufferPos = getBufferPos(pos); + if (bufferPos >= currentLength) { + throw new EOFException(); + } + Buffer buffer = buffers.get(bufferPos); + if (buffer == null) { + buffer = new Buffer(bufferPos, (int) Math.min(currentLength - bufferPos, BUFFER_SIZE)); + int bytesRead = doReadBytes(bufferPos, buffer.bytes); + if (bytesRead != buffer.len) { + buffer.len = bytesRead; + // warn? + } + buffers.put(bufferPos, buffer); + } + lruBuffers.put(bufferPos, buffer); + return buffer; + } + + private static class Buffer { + long pos; // absolute position in file of this buffer + int len; // number of valid bytes in buffer + byte[] bytes; + + Buffer(long pos, int len) { + this.pos = pos; + this.len = len; + this.bytes = new byte[len]; + } + + int getBufferOffset(long filePos) throws EOFException { + int ofs = (int) (filePos - pos); + if (ofs >= len) { + throw new EOFException(); + } + return ofs; + } + } + + private String accessModeToString(AccessMode mode) { + switch (mode) { + default: + case READ: + return "r"; + case WRITE: + return "rw"; + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/MemBufferByteProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/MemBufferByteProvider.java index c8f50af18e..497efbde89 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/MemBufferByteProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/MemBufferByteProvider.java @@ -105,53 +105,4 @@ public class MemBufferByteProvider implements ByteProvider { return bytes; } - @Override - public InputStream getInputStream(long index) throws IOException { - if (index < 0 || index > Integer.MAX_VALUE) { - throw new IOException("index out of range"); - } - return new MemBufferProviderInputStream((int) index); - } - - private class MemBufferProviderInputStream extends InputStream { - - private int initialOffset; - private int offset; - - MemBufferProviderInputStream(int offset) { - this.offset = offset; - this.initialOffset = offset; - } - - @Override - public int read() throws IOException { - byte b = readByte(offset++); - return b & 0xff; - } - - @Override - public int read(byte[] b, int off, int len) { - byte[] bytes = new byte[len]; - int count = buffer.getBytes(bytes, offset); - System.arraycopy(bytes, 0, b, off, count); - offset += count; - return count; - } - - @Override - public int available() { - return (int) length() - offset; - } - - @Override - public synchronized void reset() throws IOException { - offset = initialOffset; - } - - @Override - public void close() throws IOException { - // not applicable - } - } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedFileByteProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedFileByteProvider.java new file mode 100644 index 0000000000..45d5dc6f62 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedFileByteProvider.java @@ -0,0 +1,97 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin; + +import java.io.File; +import java.io.IOException; +import java.nio.file.AccessMode; + +import ghidra.formats.gfilesystem.FSRL; + +/** + * A {@link ByteProvider} that reads from an on-disk file, but obfuscates / de-obfuscates the + * contents of the file when reading / writing. + */ +public class ObfuscatedFileByteProvider extends FileByteProvider { + + // @formatter:off + // copied from ChainedBuffer + static final byte[] XOR_MASK_BYTES = new byte[] { + (byte)0x59, (byte)0xea, (byte)0x67, (byte)0x23, (byte)0xda, (byte)0xb8, (byte)0x00, (byte)0xb8, + (byte)0xc3, (byte)0x48, (byte)0xdd, (byte)0x8b, (byte)0x21, (byte)0xd6, (byte)0x94, (byte)0x78, + (byte)0x35, (byte)0xab, (byte)0x2b, (byte)0x7e, (byte)0xb2, (byte)0x4f, (byte)0x82, (byte)0x4e, + (byte)0x0e, (byte)0x16, (byte)0xc4, (byte)0x57, (byte)0x12, (byte)0x8e, (byte)0x7e, (byte)0xe6, + (byte)0xb6, (byte)0xbd, (byte)0x56, (byte)0x91, (byte)0x57, (byte)0x72, (byte)0xe6, (byte)0x91, + (byte)0xdc, (byte)0x52, (byte)0x2e, (byte)0xf2, (byte)0x1a, (byte)0xb7, (byte)0xd6, (byte)0x6f, + (byte)0xda, (byte)0xde, (byte)0xe8, (byte)0x48, (byte)0xb1, (byte)0xbb, (byte)0x50, (byte)0x6f, + (byte)0xf4, (byte)0xdd, (byte)0x11, (byte)0xee, (byte)0xf2, (byte)0x67, (byte)0xfe, (byte)0x48, + (byte)0x8d, (byte)0xae, (byte)0x69, (byte)0x1a, (byte)0xe0, (byte)0x26, (byte)0x8c, (byte)0x24, + (byte)0x8e, (byte)0x17, (byte)0x76, (byte)0x51, (byte)0xe2, (byte)0x60, (byte)0xd7, (byte)0xe6, + (byte)0x83, (byte)0x65, (byte)0xd5, (byte)0xf0, (byte)0x7f, (byte)0xf2, (byte)0xa0, (byte)0xd6, + (byte)0x4b, (byte)0xbd, (byte)0x24, (byte)0xd8, (byte)0xab, (byte)0xea, (byte)0x9e, (byte)0xa6, + (byte)0x48, (byte)0x94, (byte)0x3e, (byte)0x7b, (byte)0x2c, (byte)0xf4, (byte)0xce, (byte)0xdc, + (byte)0x69, (byte)0x11, (byte)0xf8, (byte)0x3c, (byte)0xa7, (byte)0x3f, (byte)0x5d, (byte)0x77, + (byte)0x94, (byte)0x3f, (byte)0xe4, (byte)0x8e, (byte)0x48, (byte)0x20, (byte)0xdb, (byte)0x56, + (byte)0x32, (byte)0xc1, (byte)0x87, (byte)0x01, (byte)0x2e, (byte)0xe3, (byte)0x7f, (byte)0x40, + + }; + // @formatter:on + + /** + * Creates an instance of {@link ObfuscatedFileByteProvider}. + * + * @param file {@link File} to read from / write to + * @param fsrl {@link FSRL} identity of this file + * @param accessMode {@link AccessMode#READ} or {@link AccessMode#WRITE} + * @throws IOException if error + */ + public ObfuscatedFileByteProvider(File file, FSRL fsrl, AccessMode accessMode) + throws IOException { + super(file, fsrl, accessMode); + } + + @Override + public File getFile() { + // obfuscated file isn't readable, so force null + return null; + } + + @Override + protected int doReadBytes(long index, byte[] buffer) throws IOException { + int bytesRead = super.doReadBytes(index, buffer); + for (int i = 0; i < bytesRead; i++) { + long byteIndex = index + i; + int xorMaskIndex = (int) (byteIndex % XOR_MASK_BYTES.length); + byte xorMask = XOR_MASK_BYTES[xorMaskIndex]; + buffer[i] ^= xorMask; + } + return bytesRead; + } + + @Override + protected void doWriteBytes(long index, byte[] buffer, int offset, int length) + throws IOException { + byte[] tmpBuffer = new byte[length]; + for (int i = 0; i < length; i++) { + long byteIndex = index + i; + int xorMaskIndex = (int) (byteIndex % XOR_MASK_BYTES.length); + byte xorMask = XOR_MASK_BYTES[xorMaskIndex]; + tmpBuffer[i] = (byte) (buffer[i + offset] ^ xorMask); + } + super.doWriteBytes(index, tmpBuffer, 0, length); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedInputStream.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedInputStream.java new file mode 100644 index 0000000000..76d75ac436 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedInputStream.java @@ -0,0 +1,99 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin; + +import java.io.*; + + +/** + * An {@link InputStream} wrapper that de-obfuscates the bytes being read from the underlying + * stream. + */ +public class ObfuscatedInputStream extends InputStream { + + private InputStream delegate; + private long currentPosition; + + /** + * Creates instance. + * + * @param delegate {@link InputStream} to wrap + */ + public ObfuscatedInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public void close() throws IOException { + delegate.close(); + super.close(); + } + + @Override + public int read() throws IOException { + byte[] buffer = new byte[1]; + int bytesRead = read(buffer, 0, 1); + return bytesRead == 1 ? Byte.toUnsignedInt(buffer[0]) : -1; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int bytesRead = delegate.read(b, off, len); + + for (int i = 0; i < bytesRead; i++, currentPosition++) { + int xorMaskIndex = + (int) (currentPosition % ObfuscatedFileByteProvider.XOR_MASK_BYTES.length); + byte xorMask = ObfuscatedFileByteProvider.XOR_MASK_BYTES[xorMaskIndex]; + b[off + i] ^= xorMask; + + } + return bytesRead; + } + + /** + * Entry point to enable command line users to retrieve the contents of an obfuscated + * file. + * + * @param args either ["--help"], or [ "input_filename", "output_filename" ] + * @throws IOException if error + */ + public static void main(String[] args) throws IOException { + if (args.length != 2 || (args.length > 1 && args[0].equals("--help"))) { + System.err.println("De-Obfuscator Usage:"); + System.err.println("\t" + ObfuscatedInputStream.class.getName() + + " obfuscated_input_filename_path plain_dest_output_filename_path"); + System.err.println(""); + System.err.println("\tExample:"); + System.err.println("\t\t" + ObfuscatedInputStream.class.getName() + + " /tmp/myuserid-Ghidra/fscache2/aa/bb/aabbccddeeff00112233445566778899 /tmp/aabbccddeeff00112233445566778899.plaintext"); + System.err.println(""); + return; + } + File obfuscatedInputFile = new File(args[0]); + File plainTextOutputFile = new File(args[1]); + + try (InputStream is = new ObfuscatedInputStream(new FileInputStream(obfuscatedInputFile)); + OutputStream os = new FileOutputStream(plainTextOutputFile)) { + + byte buffer[] = new byte[4096]; + int bytesRead; + while ((bytesRead = is.read(buffer)) > 0) { + os.write(buffer, 0, bytesRead); + } + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedOutputStream.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedOutputStream.java new file mode 100644 index 0000000000..32103895d2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/ObfuscatedOutputStream.java @@ -0,0 +1,69 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * An {@link OutputStream} wrapper that obfuscates the bytes being written to the underlying + * stream. + */ +public class ObfuscatedOutputStream extends OutputStream { + + private OutputStream delegate; + private long currentPosition; + + /** + * Creates instance. + * + * @param delegate {@link OutputStream} to wrap + */ + public ObfuscatedOutputStream(OutputStream delegate) { + this.delegate = delegate; + } + + @Override + public void close() throws IOException { + delegate.close(); + super.close(); + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + byte[] tmpBuffer = new byte[len]; + for (int i = 0; i < len; i++) { + long byteIndex = currentPosition + i; + int xorMaskIndex = + (int) (byteIndex % ObfuscatedFileByteProvider.XOR_MASK_BYTES.length); + byte xorMask = ObfuscatedFileByteProvider.XOR_MASK_BYTES[xorMaskIndex]; + tmpBuffer[i] = (byte) (b[i + off] ^ xorMask); + } + delegate.write(tmpBuffer, 0, tmpBuffer.length); + currentPosition += len; + } + + @Override + public void write(int b) throws IOException { + write(new byte[] { (byte) b }, 0, 1); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/RandomAccessByteProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/RandomAccessByteProvider.java index 403b27d0ec..3160ec342c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/RandomAccessByteProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/RandomAccessByteProvider.java @@ -29,7 +29,10 @@ import ghidra.util.Msg; * {@link ArrayIndexOutOfBoundsException}s. *

* See {@link SynchronizedByteProvider} as a solution. + *

+ * @deprecated See {@link FileByteProvider} as replacement ByteProvider. */ +@Deprecated(since = "10.1", forRemoval = true) public class RandomAccessByteProvider implements ByteProvider { protected File file; protected GhidraRandomAccessFile randomAccessFile; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/RangeMappedByteProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/RangeMappedByteProvider.java index e30f49129c..c48f7915bc 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/RangeMappedByteProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/RangeMappedByteProvider.java @@ -220,11 +220,6 @@ public class RangeMappedByteProvider implements ByteProvider { return totalBytesRead; } - @Override - public InputStream getInputStream(long index) throws IOException { - return new ByteProviderInputStream(this, 0, length); - } - private void ensureBounds(long index, long count) throws IOException { if (index < 0 || index > length) { throw new IOException("Invalid index: " + index); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/SynchronizedByteProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/SynchronizedByteProvider.java index 3d36e431c4..8f67f7b198 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/SynchronizedByteProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/SynchronizedByteProvider.java @@ -78,9 +78,12 @@ public class SynchronizedByteProvider implements ByteProvider { public synchronized byte[] readBytes(long index, long length) throws IOException { return provider.readBytes(index, length); } - + @Override public synchronized InputStream getInputStream(long index) throws IOException { - return provider.getInputStream(index); + // Return a ByteProviderInputStream that reads its bytes via this wrapper so that it is completely + // synchronized. Returning the delegate provider's getInputStream() would subvert + // synchronization and allow direct access to the underlying delegate provider. + return ByteProvider.super.getInputStream(index); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/coff/archive/CoffArchiveMemberHeader.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/coff/archive/CoffArchiveMemberHeader.java index 08b4e3a7d1..5fc6063a00 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/coff/archive/CoffArchiveMemberHeader.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/coff/archive/CoffArchiveMemberHeader.java @@ -15,14 +15,14 @@ */ package ghidra.app.util.bin.format.coff.archive; +import java.io.IOException; + import ghidra.app.util.bin.*; import ghidra.program.model.data.*; import ghidra.util.Msg; import ghidra.util.StringUtilities; import ghidra.util.exception.DuplicateNameException; -import java.io.IOException; - public class CoffArchiveMemberHeader implements StructConverter { public static final String SLASH = "/"; public static final String SLASH_SLASH = "//"; @@ -249,6 +249,24 @@ public class CoffArchiveMemberHeader implements StructConverter { return groupId; } + public int getUserIdInt() { + try { + return Integer.parseInt(userId); + } + catch (NumberFormatException nfe) { + return 0; + } + } + + public int getGroupIdInt() { + try { + return Integer.parseInt(groupId); + } + catch (NumberFormatException nfe) { + return 0; + } + } + public String getMode() { return mode; } @@ -274,6 +292,7 @@ public class CoffArchiveMemberHeader implements StructConverter { !name.equals(CoffArchiveMemberHeader.SLASH_SLASH); } + @Override public DataType toDataType() throws DuplicateNameException, IOException { String camh_name = StructConverterUtil.parseName(CoffArchiveMemberHeader.class); Structure struct = new StructureDataType(camh_name, 0); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/coff/archive/FirstLinkerMember.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/coff/archive/FirstLinkerMember.java index b63623481a..38c32f0fa2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/coff/archive/FirstLinkerMember.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/coff/archive/FirstLinkerMember.java @@ -15,31 +15,30 @@ */ package ghidra.app.util.bin.format.coff.archive; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + import ghidra.app.util.bin.*; import ghidra.program.model.data.*; import ghidra.util.BigEndianDataConverter; import ghidra.util.DataConverter; import ghidra.util.exception.DuplicateNameException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - public final class FirstLinkerMember implements StructConverter { private int numberOfSymbols; private int [] offsets; - private List stringTable = new ArrayList(); + private List stringTable = new ArrayList<>(); private long _fileOffset; - private List stringLengths = new ArrayList(); + private List stringLengths = new ArrayList<>(); public FirstLinkerMember(BinaryReader reader, CoffArchiveMemberHeader header, boolean skip) throws IOException { _fileOffset = reader.getPointerIndex(); - - boolean isLittleEndian = reader.isLittleEndian(); - reader.setLittleEndian(false);//this entire structure is stored as big-endian.. + BinaryReader origReader = reader; + reader = reader.asBigEndian(); //this entire structure is stored as big-endian.. numberOfSymbols = readNumberOfSymbols(reader); @@ -57,7 +56,7 @@ public final class FirstLinkerMember implements StructConverter { } } else { - stringTable = new ArrayList(numberOfSymbols); + stringTable = new ArrayList<>(numberOfSymbols); for (int i = 0 ; i < numberOfSymbols ; ++i) { String string = reader.readNextAsciiString(); stringTable.add( string ); @@ -65,8 +64,7 @@ public final class FirstLinkerMember implements StructConverter { } } - reader.setLittleEndian(isLittleEndian); - reader.setPointerIndex(_fileOffset + header.getSize()); + origReader.setPointerIndex(_fileOffset + header.getSize()); } /** @@ -100,9 +98,10 @@ public final class FirstLinkerMember implements StructConverter { if (stringTable.isEmpty()) { throw new RuntimeException("FirstLinkerMember::getStringTable() has been skipped."); } - return new ArrayList(stringTable); + return new ArrayList<>(stringTable); } + @Override public DataType toDataType() throws DuplicateNameException, IOException { String name = StructConverterUtil.parseName(FirstLinkerMember.class); Structure struct = new StructureDataType(name + "_" + numberOfSymbols, 0); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf4/next/sectionprovider/NullSectionProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf4/next/sectionprovider/NullSectionProvider.java index fb40b19466..783d7cfebc 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf4/next/sectionprovider/NullSectionProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf4/next/sectionprovider/NullSectionProvider.java @@ -15,11 +15,10 @@ */ package ghidra.app.util.bin.format.dwarf4.next.sectionprovider; -import ghidra.app.util.bin.ByteArrayProvider; -import ghidra.app.util.bin.ByteProvider; - import java.io.IOException; +import ghidra.app.util.bin.ByteProvider; + public class NullSectionProvider implements DWARFSectionProvider { public NullSectionProvider() { @@ -28,7 +27,7 @@ public class NullSectionProvider implements DWARFSectionProvider { @Override public ByteProvider getSectionAsByteProvider(String sectionName) throws IOException { - return new ByteArrayProvider(new byte[] {}); + return ByteProvider.EMPTY_BYTEPROVIDER; } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/Loader.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/Loader.java index adf23d7161..fb76e3716e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/Loader.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/Loader.java @@ -22,6 +22,7 @@ import java.util.List; import ghidra.app.util.Option; import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.importer.MessageLog; +import ghidra.formats.gfilesystem.FSRL; import ghidra.framework.model.DomainFolder; import ghidra.framework.model.DomainObject; import ghidra.program.model.listing.Program; @@ -169,7 +170,9 @@ public interface Loader extends ExtensionPoint, Comparable { * @return The preferred file name to use when loading. */ public default String getPreferredFileName(ByteProvider provider) { - return provider.getName().replaceAll("[\\\\:|]+", "/"); + FSRL fsrl = provider.getFSRL(); + String name = (fsrl != null) ? fsrl.getName() : provider.getName(); + return name.replaceAll("[\\\\:|]+", "/"); } /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/AbstractFileExtractorTask.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/AbstractFileExtractorTask.java index 6c45e5026d..b4dabb78c3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/AbstractFileExtractorTask.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/AbstractFileExtractorTask.java @@ -15,15 +15,10 @@ */ package ghidra.formats.gfilesystem; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; -import ghidra.util.exception.CryptoException; import ghidra.util.exception.IOCancelledException; import ghidra.util.task.Task; import ghidra.util.task.TaskMonitor; @@ -190,7 +185,7 @@ public abstract class AbstractFileExtractorTask extends Task { } protected void extractFile(GFile srcFile, File outputFile, TaskMonitor monitor) - throws CancelledException, CryptoException { + throws CancelledException { monitor.setMessage(srcFile.getName()); try (InputStream in = getSourceFileInputStream(srcFile, monitor)) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/DerivedFileProducer.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/DerivedFileProducer.java deleted file mode 100644 index 4f5cab2650..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/DerivedFileProducer.java +++ /dev/null @@ -1,44 +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.formats.gfilesystem; - -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - -import java.io.*; - -/** - * Used by {@link FileSystemService#getDerivedFile(FSRL, String, DerivedFileProducer, TaskMonitor)} - * to produce a derived file from a source file. - *

- * The {@link InputStream} returned from the method will be closed by the caller. - */ -public interface DerivedFileProducer { - - /** - * Callback method intended to be implemented by the caller to - * {@link FileSystemService#getDerivedFile(FSRL, String, DerivedFileProducer, TaskMonitor)}. - *

- * The implementation needs to return an {@link InputStream} that contains the bytes - * of the derived file. - *

- * @param srcFile {@link File} location of the source file (usually in the file cache) - * @return a new {@link InputStream} that will produce all the bytes of the derived file. - * @throws IOException if there is a problem while producing the InputStream. - * @throws CancelledException if the user canceled. - */ - public InputStream produceDerivedStream(File srcFile) throws IOException, CancelledException; -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/DerivedFilePushProducer.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/DerivedFilePushProducer.java deleted file mode 100644 index e02095300d..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/DerivedFilePushProducer.java +++ /dev/null @@ -1,41 +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.formats.gfilesystem; - -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * Used by {@link FileSystemService#getDerivedFilePush(FSRL, String, DerivedFilePushProducer, TaskMonitor)} - * to produce a derived file from a source file. - */ -public interface DerivedFilePushProducer { - /** - * Callback method intended to be implemented by the caller to - * {@link FileSystemService#getDerivedFilePush(FSRL, String, DerivedFilePushProducer, TaskMonitor)}. - *

- * The implementation needs to write bytes to the supplied {@link OutputStream}. - *

- * @param os {@link OutputStream} that the implementor should write the bytes to. Do - * not close the stream when done. - * @throws IOException if there is a problem while writing to the OutputStream. - * @throws CancelledException if the user canceled. - */ - public void push(OutputStream os) throws IOException, CancelledException; -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java index a01458e1cd..74dc1726a1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java @@ -96,6 +96,19 @@ public class FSRL { return parent; } + /** + * Ensures that a FSRL instance is a file type reference by converting any FSRLRoots + * into the container file that hosts the FSRLRoot. + * + * @param fsrl FSRL or FSRLRoot instance to possibly convert + * @return original FSRL if already a normal FSRL, or the container if it was a FSRLRoot + */ + public static FSRL convertRootToContainer(FSRL fsrl) { + return (fsrl instanceof FSRLRoot && fsrl.getFS().hasContainer()) + ? fsrl.getFS().getContainer() + : fsrl; + } + /** * Creates a single {@link FSRL} from a FSRL-part string. *

@@ -282,15 +295,29 @@ public class FSRL { return md5; } + /** + * Tests specified MD5 value against MD5 in this FSRL. + * + * @param otherMD5 md5 in a hex string + * @return boolean true if equal, or that both are null, false otherwise + */ + public boolean isMD5Equal(String otherMD5) { + if (this.md5 == null) { + return otherMD5 == null; + } + return this.md5.equalsIgnoreCase(otherMD5); + } + /** * Creates a new {@link FSRL} instance, using the same information as this instance, * but with a new {@link #getMD5() MD5} value. * * @param newMD5 string md5 - * @return new {@link FSRL} instance with the same path and the specified md5 value. + * @return new {@link FSRL} instance with the same path and the specified md5 value, + * or if newMD5 is same as existing, returns this */ public FSRL withMD5(String newMD5) { - return new FSRL(getFS(), path, newMD5); + return Objects.equals(md5, newMD5) ? this : new FSRL(getFS(), path, newMD5); } /** @@ -312,7 +339,7 @@ public class FSRL { *

* Used when re-root'ing a FSRL path onto another parent object (usually during intern()'ing) * - * @param copyPath + * @param copyPath another FSRL to copy path and md5 from * @return new FSRL instance */ public FSRL withPath(FSRL copyPath) { @@ -323,7 +350,7 @@ public class FSRL { * Creates a new {@link FSRL} instance, using the same {@link FSRLRoot} as this instance, * combining the current {@link #getPath() path} with the {@code relPath} value. *

- * @param relPath + * @param relPath relative path string to append, '/'s will be automatically added * @return new {@link FSRL} instance with additional path appended. */ public FSRL appendPath(String relPath) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSUtilities.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSUtilities.java index e29902b1cc..89850e5f00 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSUtilities.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSUtilities.java @@ -20,6 +20,7 @@ import java.io.*; import java.net.MalformedURLException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.*; @@ -27,9 +28,8 @@ import java.util.Map.Entry; import org.apache.commons.io.FilenameUtils; -import com.google.common.io.ByteStreams; - import docking.widgets.OptionDialog; +import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.util.*; import ghidra.util.exception.CancelledException; @@ -45,16 +45,6 @@ public class FSUtilities { private static final char DOT = '.'; private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); - public static class StreamCopyResult { - public long bytesCopied; - public byte[] md5; - - public StreamCopyResult(long bytesCopied, byte[] md5) { - this.bytesCopied = bytesCopied; - this.md5 = md5; - } - } - private static char[] hexdigit = "0123456789abcdef".toCharArray(); /** @@ -62,25 +52,17 @@ public class FSUtilities { * case-insensitive. */ public static final Comparator GFILE_NAME_TYPE_COMPARATOR = (o1, o2) -> { - String n1 = o1.getName(); - String n2 = o2.getName(); - if (n1 == null) { - return -1; + int result = Boolean.compare(!o1.isDirectory(), !o2.isDirectory()); + if (result == 0) { + String n1 = Objects.requireNonNullElse(o1.getName(), ""); + String n2 = Objects.requireNonNullElse(o2.getName(), ""); + result = n1.compareToIgnoreCase(n2); } - if (o1.isDirectory()) { - if (o2.isDirectory()) { - return n1.compareToIgnoreCase(n2); - } - return -1; - } - else if (o2.isDirectory()) { - return 1; - } - return n1.compareToIgnoreCase(n2); + return result; }; /** - * Converts a string -> string mapping into a "key: value" multi-line string. + * Converts a string-to-string mapping into a "key: value\n" multi-line string. * * @param info map of string key to string value. * @return Multi-line string "key: value" string. @@ -349,55 +331,61 @@ public class FSUtilities { } /** - * Copies a stream and calculates the md5 at the same time. - *

- * Does not close the passed-in InputStream or OutputStream. - * - * @param is {@link InputStream} to copy. NOTE: not closed by this method. - * @param os {@link OutputStream} to write to. NOTE: not closed by this method. - * @return {@link StreamCopyResult} with md5 and bytes copied count, never null. + * Copy the contents of a {@link ByteProvider} to a file. + * + * @param provider {@link ByteProvider} source of bytes + * @param destFile {@link File} destination file + * @param monitor {@link TaskMonitor} to update + * @return number of bytes copied * @throws IOException if error - * @throws CancelledException if canceled + * @throws CancelledException if cancelled */ - @SuppressWarnings("resource") - public static StreamCopyResult streamCopy(InputStream is, OutputStream os, TaskMonitor monitor) - throws IOException, CancelledException { - HashingOutputStream hos; - try { - // This wrapping outputstream is not closed on purpose. - hos = new HashingOutputStream(os, "MD5"); - } - catch (NoSuchAlgorithmException e) { - throw new IOException("Could not get MD5 hash algo", e); + public static long copyByteProviderToFile(ByteProvider provider, File destFile, + TaskMonitor monitor) throws IOException, CancelledException { + try (InputStream is = provider.getInputStream(0); + FileOutputStream fos = new FileOutputStream(destFile)) { + return streamCopy(is, fos, monitor); } + } - // TODO: use FileUtilities.copyStreamToStream() + /** + * Copy a stream while updating a TaskMonitor. + * + * @param is {@link InputStream} source of bytes + * @param os {@link OutputStream} destination of bytes + * @param monitor {@link TaskMonitor} to update + * @return number of bytes copied + * @throws IOException if error + * @throws CancelledException if cancelled + */ + public static long streamCopy(InputStream is, OutputStream os, TaskMonitor monitor) + throws IOException, CancelledException { byte buffer[] = new byte[FileUtilities.IO_BUFFER_SIZE]; int bytesRead; long totalBytesCopied = 0; while ((bytesRead = is.read(buffer)) > 0) { - hos.write(buffer, 0, bytesRead); + os.write(buffer, 0, bytesRead); totalBytesCopied += bytesRead; monitor.setProgress(totalBytesCopied); monitor.checkCanceled(); } - hos.flush(); - return new StreamCopyResult(totalBytesCopied, hos.getDigest()); + os.flush(); + return totalBytesCopied; } /** - * Calculate the MD5 of a stream. - * - * @param is {@link InputStream} to read - * @param monitor {@link TaskMonitor} to watch for cancel - * @return md5 as a hex encoded string, never null. + * Returns the text lines in the specified ByteProvider. + *

+ * See {@link FileUtilities#getLines(InputStream)} + * + * @param byteProvider {@link ByteProvider} to read + * @return list of text lines * @throws IOException if error - * @throws CancelledException if cancelled */ - public static String getStreamMD5(InputStream is, TaskMonitor monitor) - throws IOException, CancelledException { - StreamCopyResult results = streamCopy(is, ByteStreams.nullOutputStream(), monitor); - return NumericUtilities.convertBytesToString(results.md5); + public static List getLines(ByteProvider byteProvider) throws IOException { + try (InputStream is = byteProvider.getInputStream(0)) { + return FileUtilities.getLines(is); + } } /** @@ -412,7 +400,54 @@ public class FSUtilities { public static String getFileMD5(File f, TaskMonitor monitor) throws IOException, CancelledException { try (FileInputStream fis = new FileInputStream(f)) { - return getStreamMD5(fis, monitor); + monitor.initialize(f.length()); + monitor.setMessage("Hashing file: " + f.getName()); + return getMD5(fis, monitor); + } + } + + /** + * Calculate the MD5 of a file. + * + * @param provider {@link ByteProvider} + * @param monitor {@link TaskMonitor} to watch for cancel + * @return md5 as a hex encoded string, never null. + * @throws IOException if error + * @throws CancelledException if cancelled + */ + 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); + } + } + + /** + * Calculate the hash of an {@link InputStream}. + * + * @param is {@link 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 { + try { + MessageDigest messageDigest = MessageDigest.getInstance(HashUtilities.MD5_ALGORITHM); + byte[] buf = new byte[16 * 1024]; + int bytesRead; + while ((bytesRead = is.read(buf)) >= 0) { + messageDigest.update(buf, 0, bytesRead); + monitor.incrementProgress(bytesRead); + monitor.checkCanceled(); + } + return NumericUtilities.convertBytesToString(messageDigest.digest()); + } + catch (NoSuchAlgorithmException e) { + throw new IOException(e); } } @@ -534,4 +569,22 @@ public class FSUtilities { : "NA"; } + /** + * Helper method to invoke close() on a Closeable without having to catch + * an IOException. + * + * @param c {@link Closeable} to close + * @param msg optional msg to log if exception is thrown, null is okay + */ + public static void uncheckedClose(Closeable c, String msg) { + try { + if (c != null) { + c.close(); + } + } + catch (IOException e) { + Msg.warn(FSUtilities.class, Objects.requireNonNullElse(msg, "Problem closing object"), + e); + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileCache.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileCache.java index c72c4ccccc..dc1951938b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileCache.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileCache.java @@ -16,12 +16,14 @@ package ghidra.formats.gfilesystem; import java.io.*; +import java.nio.file.*; import java.security.NoSuchAlgorithmException; -import java.util.Date; -import java.util.UUID; +import java.util.*; import java.util.regex.Pattern; -import ghidra.formats.gfilesystem.FSUtilities.StreamCopyResult; +import org.apache.commons.collections4.map.ReferenceMap; + +import ghidra.app.util.bin.*; import ghidra.util.*; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -31,44 +33,58 @@ import utilities.util.FileUtilities; * File caching implementation. *

* Caches files based on a hash of the contents of the file.
- * Files are retrieved using the hash string.

- * Cached files are stored in a file with a name that is the hex encoded value of the hash. + * Files are retrieved using the hash string.
+ * Cached files are stored in a file with a name that is the hex encoded value of the hash.
+ * Cached files are obfuscated/de-obfuscated when written/read to/from disk. See + * {@link ObfuscatedFileByteProvider}, {@link ObfuscatedInputStream}, + * {@link ObfuscatedOutputStream}.
* Cached files are organized into a nested directory structure to prevent * overwhelming a single directory with thousands of files. *

- * Nested directory structure is based on the file's name: - * File: AABBCCDDEEFF... - * Directory (2 level nesting): AA/BB/AABBCCDDEEFF... + * Nested directory structure is based on the file's name:
+ *

   File: AABBCCDDEEFF... → AA/AABBCCDDEEFF...
*

* Cache size is not bounded. *

- * Cache maint is done during startup if interval since last maint has been exceeded + * Cache maintenance is done during startup if interval since last maintenance has been exceeded. *

- * No file data is maintained in memory. - *

- * No file is moved or removed from the cache after being added (except during startup) - * as there is no use count or reference tracking of the files. + * Files are not removed from the cache after being added, except during startup maintenance. * */ public class FileCache { - + /** + * Max size of a file that will be kept in {@link #memCache} (2Mb) + */ + public static final int MAX_INMEM_FILESIZE = 2 * 1024 * 1024; // 2mb + private static final long FREESPACE_RESERVE_BYTES = 50 * 1024 * 1024; // 50mb private static final Pattern NESTING_DIR_NAME_REGEX = Pattern.compile("[0-9a-fA-F][0-9a-fA-F]"); + private static final Pattern FILENAME_REGEX = Pattern.compile("[0-9a-fA-F]{32}"); private static final int MD5_BYTE_LEN = 16; public static final int MD5_HEXSTR_LEN = MD5_BYTE_LEN * 2; - private static final int NESTING_LEVEL = 2; private static final long MAX_FILE_AGE_MS = DateUtils.MS_PER_DAY; private static final long MAINT_INTERVAL_MS = DateUtils.MS_PER_DAY * 2; private final File cacheDir; + private final FileStore cacheDirFileStore; private final File newDir; - private final File lastMaintFile; private FileCacheMaintenanceDaemon cleanDaemon; + private ReferenceMap memCache = new ReferenceMap<>(); - private int fileAddCount; - private int fileReUseCount; - private long storageEstimateBytes; - private long lastMaintTS; + /** + * Backwards compatible with previous cache directories to age off the files located + * therein. + * + * @param oldCacheDir the old 2-level cache directory + * @deprecated Marked as deprecated to ensure this is removed in a few versions after most + * user's old-style cache dirs have been cleaned up. + */ + @Deprecated(forRemoval = true, since = "10.1") + public static void performCacheMaintOnOldDirIfNeeded(File oldCacheDir) { + if (oldCacheDir.isDirectory()) { + performCacheMaintIfNeeded(oldCacheDir, 2 /* old nesting level */); + } + } /** * Creates a new {@link FileCache} instance where files are stored under the specified @@ -81,12 +97,13 @@ public class FileCache { public FileCache(File cacheDir) throws IOException { this.cacheDir = cacheDir; this.newDir = new File(cacheDir, "new"); - this.lastMaintFile = new File(cacheDir, ".lastmaint"); if ((!cacheDir.exists() && !cacheDir.mkdirs()) || (!newDir.exists() && !newDir.mkdirs())) { throw new IOException("Unable to initialize cache dir " + cacheDir); } - performCacheMaintIfNeeded(); + + cacheDirFileStore = Files.getFileStore(cacheDir.toPath()); + cleanDaemon = performCacheMaintIfNeeded(cacheDir, 1 /* current nesting level */); } /** @@ -102,22 +119,26 @@ public class FileCache { FileUtilities.deleteDir(f); } } + memCache.clear(); } - /** - * Adds a {@link File} to the cache, returning a {@link FileCacheEntry}. - * - * @param f {@link File} to add to cache. - * @param monitor {@link TaskMonitor} to monitor for cancel and to update progress. - * @return {@link FileCacheEntry} with new File and md5. - * @throws IOException if error - * @throws CancelledException if canceled - */ - public FileCacheEntry addFile(File f, TaskMonitor monitor) - throws IOException, CancelledException { - try (FileInputStream fis = new FileInputStream(f)) { - return addStream(fis, monitor); + synchronized boolean hasEntry(String md5) { + FileCacheEntry fce = memCache.get(md5); + if (fce == null) { + fce = getFileByMD5(md5); } + return fce != null; + } + + private void ensureAvailableSpace(long sizeHint) throws IOException { + if ( sizeHint > MAX_INMEM_FILESIZE ) { + long usableSpace = cacheDirFileStore.getUsableSpace(); + if (usableSpace >= 0 && usableSpace < sizeHint + FREESPACE_RESERVE_BYTES) { + throw new IOException("Not enough storage available in " + cacheDir + + " to store file sized: " + sizeHint); + } + } + } /** @@ -130,12 +151,26 @@ public class FileCache { * @return {@link FileCacheEntry} with a File and it's md5 string or {@code null} if no * matching file exists in cache. */ - public FileCacheEntry getFile(String md5) { - FileCacheEntry cfi = getFileByMD5(md5); - if (cfi != null) { - cfi.file.setLastModified(System.currentTimeMillis()); + synchronized FileCacheEntry getFileCacheEntry(String md5) { + if (md5 == null) { + return null; + } + FileCacheEntry fce = memCache.get(md5); + if (fce == null) { + fce = getFileByMD5(md5); + if (fce != null) { + fce.file.setLastModified(System.currentTimeMillis()); + } + } + return fce; + } + + synchronized void releaseFileCacheEntry(String md5) { + FileCacheEntry fce = memCache.get(md5); + if (fce != null) { + memCache.remove(md5); + Msg.debug(this, "Releasing memCache entry: " + fce.md5 + ", " + fce.bytes.length); } - return cfi; } /** @@ -150,165 +185,53 @@ public class FileCache { } /** - * Prunes cache if interval since last maintenance exceeds {@link #MAINT_INTERVAL_MS} - *

- * Only called during construction, and the only known multi-process conflict that can occur - * is when re-writing the "lastMaint" timestamp file, which isn't a problem as its the - * approximate timestamp of that file that is important, not the contents. - * - * @throws IOException if error when writing metadata file. + * Creates a randomly generated file name in the temp directory. + * + * @return randomly generated file name in the cache's temp directory */ - private void performCacheMaintIfNeeded() throws IOException { - lastMaintTS = (lastMaintTS == 0) ? lastMaintFile.lastModified() : lastMaintTS; - if (lastMaintTS + MAINT_INTERVAL_MS > System.currentTimeMillis()) { - return; - } - - cleanDaemon = new FileCacheMaintenanceDaemon(); - cleanDaemon.start(); + private File createTempFile() { + return new File(newDir, UUID.randomUUID().toString()); } /** - * Prunes files in cache if they are old, calculates space used by cache. - */ - private void performCacheMaint() { - storageEstimateBytes = 0; - Msg.info(this, "Starting cache cleanup: " + cacheDir); - // TODO: add check for orphan files in ./new - cacheMaintForDir(cacheDir, 0); - Msg.info(this, "Finished cache cleanup, estimated storage used: " + storageEstimateBytes); - } - - private void cacheMaintForDir(File dir, int nestingLevel) { - if (nestingLevel < NESTING_LEVEL) { - for (File f : dir.listFiles()) { - String name = f.getName(); - if (f.isDirectory() && NESTING_DIR_NAME_REGEX.matcher(name).matches()) { - cacheMaintForDir(f, nestingLevel + 1); - } - } - } - else if (nestingLevel == NESTING_LEVEL) { - cacheMaintForLeafDir(dir); - } - } - - private void cacheMaintForLeafDir(File dir) { - long cutoffMS = System.currentTimeMillis() - MAX_FILE_AGE_MS; - - for (File f : dir.listFiles()) { - if (f.isFile() && isCacheFileName(f.getName())) { - if (f.lastModified() < cutoffMS) { - if (!f.delete()) { - Msg.error(this, "Failed to delete cache file " + f); - } - else { - Msg.info(this, "Expired cache file " + f); - continue; - } - } - storageEstimateBytes += f.length(); - } - } - } - - private boolean isCacheFileName(String s) { - try { - byte[] bytes = NumericUtilities.convertStringToBytes(s); - return (bytes != null) && bytes.length == MD5_BYTE_LEN; - } - catch (IllegalArgumentException e) { - return false; - } - } - - /** - * Adds a contents of a stream to the cache, returning the md5 identifier of the stream. - *

- * The stream is copied into a temp file in the cacheDir/new directory while its md5 - * is calculated. The temp file is then moved into its final location - * based on the md5 of the stream: AA/BB/AABBCCDDEEFF.... - *

- * The monitor progress is updated with the number of bytes that are being copied. No - * message or maximum is set. - *

- * @param is {@link InputStream} to add to the cache. Not closed when done. - * @param monitor {@link TaskMonitor} that will be checked for canceling and updating progress. - * @return {@link FileCacheEntry} with file info and md5, never null. + * Creates a new {@link FileCacheEntryBuilder} that will accept bytes written to it + * (via its {@link OutputStream} methods). When finished writing, the {@link FileCacheEntryBuilder} + * will give the caller a {@link FileCacheEntry}. + * + * @param sizeHint a hint about the size of the file being added. Use -1 if unsure or unknown + * @return new {@link FileCacheEntryBuilder} * @throws IOException if error - * @throws CancelledException if canceled */ - public FileCacheEntry addStream(InputStream is, TaskMonitor monitor) - throws IOException, CancelledException { - File tmpFile = new File(newDir, UUID.randomUUID().toString()); - try (FileOutputStream fos = new FileOutputStream(tmpFile)) { - StreamCopyResult copyResults = FSUtilities.streamCopy(is, fos, monitor); - - // Close the fos so the tmpFile can be moved even though - // the try(){} will attempt to close it as well. - fos.close(); - - String md5 = NumericUtilities.convertBytesToString(copyResults.md5); - - return addTmpFileToCache(tmpFile, md5, copyResults.bytesCopied); - } - finally { - if (tmpFile.exists()) { - Msg.debug(this, "Removing left-over temp file " + tmpFile); - tmpFile.delete(); - } - } + FileCacheEntryBuilder createCacheEntryBuilder(long sizeHint) throws IOException { + ensureAvailableSpace(sizeHint); + return new FileCacheEntryBuilder(sizeHint); } + /** - * Adds a file to the cache, using a 'pusher' strategy where the producer is given a - * {@link OutputStream} to write to. + * Adds a plaintext file to this cache, consuming it. *

- * Unbeknownst to the producer, but knownst to us, the outputstream is really a - * {@link HashingOutputStream} that will allow us to get the MD5 hash when the producer - * is finished pushing. - * - * @param pusher functional callback that will accept an {@link OutputStream} and write - * to it. - *

 (os) -> { os.write(.....); }
- * @param monitor {@link TaskMonitor} that will be checked for cancel and updated with - * file io progress. - * @return a new {@link FileCacheEntry} with the newly added cache file's File and MD5, - * never null. - * @throws IOException if an IO error - * @throws CancelledException if the user cancels + * @param file plaintext file + * @param monitor {@link TaskMonitor} + * @return a {@link FileCacheEntry} that controls the contents of the newly added file + * @throws IOException if error + * @throws CancelledException if cancelled */ - public FileCacheEntry pushStream(DerivedFilePushProducer pusher, TaskMonitor monitor) - throws IOException, CancelledException { - File tmpFile = new File(newDir, UUID.randomUUID().toString()); - try (HashingOutputStream hos = - new HashingOutputStream(new FileOutputStream(tmpFile), "MD5")) { - pusher.push(hos); - // early hos.close() so it can be renamed/moved on the filesystem - hos.close(); - - String md5 = NumericUtilities.convertBytesToString(hos.getDigest()); - long fileSize = tmpFile.length(); - - return addTmpFileToCache(tmpFile, md5, fileSize); - } - catch (NoSuchAlgorithmException e) { - throw new IOException("Error getting MD5 algo", e); - } - catch (Throwable th) { - throw new IOException("Error while pushing stream into cache", th); + FileCacheEntry giveFile(File file, TaskMonitor monitor) throws IOException, CancelledException { + try (InputStream fis = new FileInputStream(file); + FileCacheEntryBuilder fceBuilder = createCacheEntryBuilder(file.length())) { + FSUtilities.streamCopy(fis, fceBuilder, monitor); + return fceBuilder.finish(); } finally { - if (tmpFile.exists()) { - Msg.debug(this, "Removing left-over temp file " + tmpFile); - tmpFile.delete(); + if (!file.delete()) { + Msg.warn(this, "Failed to delete temporary file: " + file); } } - } /** - * Adds a File to this cache, consuming the file. + * Adds an already obfuscated File to this cache, consuming the file. *

* This method makes some assumptions: *

@@ -318,16 +241,14 @@ public class FileCache { * existence and the attempt to place the file into the directory. Solution: no * process may remove a nested directory after it has been created. * 2) The source file is co-located with the cache directory to ensure its on the - * same physical filesystem volume. + * same physical filesystem volume, and is already obfuscated. *

* @param tmpFile the File to add to the cache * @param md5 hex string md5 of the file - * @param fileLen the length in bytes of the file being added * @return a new {@link FileCacheEntry} with the File's location and its md5 * @throws IOException if an file error occurs */ - private FileCacheEntry addTmpFileToCache(File tmpFile, String md5, long fileLen) - throws IOException { + private FileCacheEntry addTmpFileToCache(File tmpFile, String md5) throws IOException { String relPath = getCacheRelPath(md5); File destCacheFile = new File(cacheDir, relPath); @@ -337,91 +258,28 @@ public class FileCache { throw new IOException("Failed to create cache dir " + destCacheFileDir); } - boolean moved = false; - boolean reused = false; - if (destCacheFile.exists()) { - reused = true; + try { + tmpFile.renameTo(destCacheFile); } - else { - moved = tmpFile.renameTo(destCacheFile); - - // test again to see if another process was racing us if the rename failed - reused = !moved && destCacheFile.exists(); - } - if (!moved && reused) { - //Msg.info(this, "File already exists in cache, reusing: " + destCacheFile); + finally { tmpFile.delete(); - } - else if (!moved) { - throw new IOException("Failed to move " + tmpFile + " to " + destCacheFile); - } - - synchronized (this) { - fileAddCount++; - if (reused) { - fileReUseCount++; - destCacheFile.setLastModified(System.currentTimeMillis()); - } - else { - storageEstimateBytes += fileLen; + if (!destCacheFile.exists()) { + throw new IOException("Failed to move " + tmpFile + " to " + destCacheFile); } } - + destCacheFile.setLastModified(System.currentTimeMillis()); return new FileCacheEntry(destCacheFile, md5); } private String getCacheRelPath(String md5) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < NESTING_LEVEL; i++) { - sb.append(md5.substring(i * 2, (i + 1) * 2)); - sb.append('/'); - } - sb.append(md5); - return sb.toString(); + return String.format("%s/%s", + md5.substring(0, 2), + md5); } @Override public String toString() { - return "FileCache [cacheDir=" + cacheDir + ", fileAddCount=" + fileAddCount + - ", storageEstimateBytes=" + storageEstimateBytes + ", lastMaintTS=" + lastMaintTS + "]"; - } - - /** - * Number of files added to this cache. - * - * @return Number of files added to this cache - */ - public int getFileAddCount() { - return fileAddCount; - } - - /** - * Number of times a file-add was a no-op and the contents were already present - * in the cache. - * - * @return Number of times a file-add was a no-op and the contents were already present - * in the cache. - */ - public int getFileReUseCount() { - return fileReUseCount; - } - - /** - * Estimate of the number of bytes in the cache. - * - * @return estimate of the number of bytes in the cache - could be very wrong - */ - public long getStorageEstimateBytes() { - return storageEstimateBytes; - } - - /** - * How old (in milliseconds) files must be before being aged-off during cache maintenance. - * - * @return Max cache file age in milliseconds. - */ - public long getMaxFileAgeMS() { - return MAX_FILE_AGE_MS; + return "FileCache [cacheDir=" + cacheDir + "]"; } /** @@ -433,19 +291,53 @@ public class FileCache { return cleanDaemon != null && cleanDaemon.isAlive(); } - private class FileCacheMaintenanceDaemon extends Thread { + /** + * Prunes cache if interval since last maintenance exceeds {@link #MAINT_INTERVAL_MS} + *

+ * Only called during construction, and the only known multi-process conflict that can occur + * is when re-writing the "lastMaint" timestamp file, which isn't a problem as its the + * approximate timestamp of that file that is important, not the contents. + * + * @param cacheDir cache directory location + * @param nestingLevel the depth of directory nesting, 2 for old style, 1 for newer style + * @return {@link FileCacheMaintenanceDaemon} instance if started, null otherwise + */ + private static FileCacheMaintenanceDaemon performCacheMaintIfNeeded(File cacheDir, + int nestingLevel) { + File lastMaintFile = new File(cacheDir, ".lastmaint"); + long lastMaintTS = lastMaintFile.isFile() ? lastMaintFile.lastModified() : 0; + if (lastMaintTS + MAINT_INTERVAL_MS > System.currentTimeMillis()) { + return null; + } - FileCacheMaintenanceDaemon() { + FileCacheMaintenanceDaemon cleanDaemon = + new FileCacheMaintenanceDaemon(cacheDir, lastMaintFile, nestingLevel); + cleanDaemon.start(); + return cleanDaemon; + } + + private static class FileCacheMaintenanceDaemon extends Thread { + private File lastMaintFile; + private File cacheDir; + private long storageEstimateBytes; + private int nestingLevel; + + FileCacheMaintenanceDaemon(File cacheDir, File lastMaintFile, int nestingLevel) { setDaemon(true); + setName("FileCacheMaintenanceDaemon for " + cacheDir.getName()); + this.cacheDir = cacheDir; + this.lastMaintFile = lastMaintFile; + this.nestingLevel = nestingLevel; } @Override public void run() { - - performCacheMaint(); + Msg.info(this, "Starting cache cleanup: " + cacheDir); + cacheMaintForDir(cacheDir, 0); + Msg.info(this, + "Finished cache cleanup, estimated storage used: " + storageEstimateBytes); // stamp the file after we finish, in case the VM stopped this daemon thread - lastMaintTS = System.currentTimeMillis(); try { FileUtilities.writeStringToFile(lastMaintFile, "Last maint run at " + (new Date())); } @@ -453,5 +345,255 @@ public class FileCache { Msg.error(this, "Unable to write file cache maintenance file: " + lastMaintFile, e); } } + + private void cacheMaintForDir(File dir, int dirLevel) { + if (dirLevel < nestingLevel) { + for (File f : dir.listFiles()) { + String name = f.getName(); + if (f.isDirectory() && NESTING_DIR_NAME_REGEX.matcher(name).matches()) { + cacheMaintForDir(f, dirLevel + 1); + } + } + } + else if (dirLevel == nestingLevel) { + cacheMaintForLeafDir(dir); + } + } + + private void cacheMaintForLeafDir(File dir) { + long cutoffMS = System.currentTimeMillis() - MAX_FILE_AGE_MS; + + for (File f : dir.listFiles()) { + if (f.isFile() && isCacheFileName(f.getName())) { + if (f.lastModified() < cutoffMS) { + if (f.delete()) { + Msg.debug(this, "Expired cache file " + f); + continue; + } + Msg.error(this, "Failed to delete cache file " + f); + } + storageEstimateBytes += f.length(); + } + } + } + + private boolean isCacheFileName(String s) { + return FILENAME_REGEX.matcher(s).matches(); + } + + } + + /** + * Helper class, keeps a FileCacheEntry pinned while the ByteProvider is alive. When + * the ByteProvider is closed, the FileCacheEntry is allowed to be garbage collected + * if there is enough memory pressure to also remove its entry from the {@link FileCache#memCache} + * map. + */ + private static class RefPinningByteArrayProvider extends ByteArrayProvider { + @SuppressWarnings("unused") + private FileCacheEntry fce; // its just here to be pinned in memory + + public RefPinningByteArrayProvider(FileCacheEntry fce, FSRL fsrl) { + super(fce.bytes, fsrl); + + this.fce = fce; + } + + @Override + public void close() { + fce = null; + super.hardClose(); + } + } + + /** + * Allows creating {@link FileCacheEntry file cache entries} at the caller's convenience. + *

+ */ + public class FileCacheEntryBuilder extends OutputStream { + + private OutputStream delegate; + private HashingOutputStream hos; + private FileCacheEntry fce; + private long delegateLength; + private File tmpFile; + + private FileCacheEntryBuilder(long sizeHint) throws IOException { + sizeHint = sizeHint <= 0 ? 512 : sizeHint; + if (sizeHint < MAX_INMEM_FILESIZE) { + delegate = new ByteArrayOutputStream((int) sizeHint); + } + else { + tmpFile = createTempFile(); + delegate = new ObfuscatedOutputStream(new FileOutputStream(tmpFile)); + } + initHashingOutputStream(); + } + + @Override + protected void finalize() throws Throwable { + if (hos != null) { + Msg.warn(this, "FAIL TO CLOSE FileCacheEntryBuilder, currentSize=" + + delegateLength + ", file=" + (tmpFile != null ? tmpFile : "not set")); + } + } + + @Override + public void write(int b) throws IOException { + switchToTempFileIfNecessary(1); + hos.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + switchToTempFileIfNecessary(b.length); + hos.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + switchToTempFileIfNecessary(len); + hos.write(b, off, len); + } + + @Override + public void flush() throws IOException { + hos.flush(); + } + + @Override + public void close() throws IOException { + finish(); + } + + private void initHashingOutputStream() throws IOException { + try { + hos = new HashingOutputStream(delegate, HashUtilities.MD5_ALGORITHM); + } + catch (NoSuchAlgorithmException e) { + throw new IOException("Error getting MD5 algo", e); + } + } + + private void switchToTempFileIfNecessary(int bytesToAdd) throws IOException { + delegateLength += bytesToAdd; + if (tmpFile == null && delegateLength > MAX_INMEM_FILESIZE) { + tmpFile = createTempFile(); + byte[] bytes = ((ByteArrayOutputStream) delegate).toByteArray(); + delegate = new ObfuscatedOutputStream(new FileOutputStream(tmpFile)); + initHashingOutputStream(); + // send the old bytes through the new hasher and to the tmp file + hos.write(bytes); + } + } + + /** + * Finalizes this builder, pushing the bytes that have been written to it into + * the FileCache. + *

+ * @return new {@link FileCacheEntry} + * @throws IOException if error + */ + public FileCacheEntry finish() throws IOException { + if (hos != null) { + hos.close(); + String md5 = NumericUtilities.convertBytesToString(hos.getDigest()); + if (tmpFile != null) { + fce = addTmpFileToCache(tmpFile, md5); + } + else { + ByteArrayOutputStream baos = (ByteArrayOutputStream) delegate; + byte[] bytes = baos.toByteArray(); + fce = new FileCacheEntry(bytes, md5); + synchronized (FileCache.this) { + memCache.put(md5, fce); + } + } + hos = null; + delegate = null; + } + return fce; + } + + } + + /** + * Represents a cached file. It may be an actual file if {@link FileCacheEntry#file file} + * is set, or if smaller than {@link FileCache#MAX_INMEM_FILESIZE 2Mb'ish} just an + * in-memory byte array that is weakly pinned in the {@link FileCache#memCache} map. + */ + public static class FileCacheEntry { + + final String md5; + final File file; + final byte[] bytes; + + private FileCacheEntry(File file, String md5) { + this.file = file; + this.bytes = null; + this.md5 = md5; + } + + private FileCacheEntry(byte[] bytes, String md5) { + this.file = null; + this.bytes = bytes; + this.md5 = md5; + } + + /** + * Returns the contents of this cache entry as a {@link ByteProvider}, using the specified + * {@link FSRL}. + *

+ * @param fsrl {@link FSRL} that the returned {@link ByteProvider} should have as its + * identity + * @return new {@link ByteProvider} containing the contents of this cache entry, caller is + * responsible for {@link ByteProvider#close() closing} + * @throws IOException if error + */ + public ByteProvider asByteProvider(FSRL fsrl) throws IOException { + if (fsrl.getMD5() == null) { + fsrl = fsrl.withMD5(md5); + } + if (file != null) { + file.setLastModified(System.currentTimeMillis()); + } + return (bytes != null) + ? new RefPinningByteArrayProvider(this, fsrl) + : new ObfuscatedFileByteProvider(file, fsrl, AccessMode.READ); + } + + /** + * Returns the MD5 of this cache entry. + * + * @return the MD5 (as a string) of this cache entry + */ + public String getMD5() { + return md5; + } + + public long length() { + return bytes != null ? bytes.length : file.length(); + } + + @Override + public int hashCode() { + return Objects.hash(md5); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + FileCacheEntry other = (FileCacheEntry) obj; + return Objects.equals(md5, other.md5); + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileFingerprintCache.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileFingerprintCache.java deleted file mode 100644 index 64bed1ed41..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileFingerprintCache.java +++ /dev/null @@ -1,131 +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.formats.gfilesystem; - -import org.apache.commons.collections4.map.ReferenceMap; - -/** - * A best-effort cache of MD5 values of local files based on their {name,timestamp,length} fingerprint. - *

- * Used to quickly verify that a local file hasn't changed. - * - */ -public class FileFingerprintCache { - - private ReferenceMap fileFingerprintToMD5Map = new ReferenceMap<>(); - - /** - * Clears the cache. - */ - public synchronized void clear() { - fileFingerprintToMD5Map.clear(); - } - - /** - * Add a file's fingerprint to the cache. - * - * @param path String path to the file - * @param md5 hex-string md5 of the file - * @param timestamp long last modified timestamp of the file - * @param length long file size - */ - public synchronized void add(String path, String md5, long timestamp, long length) { - fileFingerprintToMD5Map.put(new FileFingerprintRec(path, timestamp, length), md5); - } - - /** - * Returns true if the specified file with the specified fingerprints (timestamp, length) - * was previously added to the cache with the specified md5. - * - * @param path String path to the file - * @param md5 hex-string md5 of the file - * @param timestamp long last modified timestamp of the file - * @param length long file size - * @return true if the fingerprint has previously been added to the cache. - */ - public synchronized boolean contains(String path, String md5, long timestamp, long length) { - String prevMD5 = - fileFingerprintToMD5Map.get(new FileFingerprintRec(path, timestamp, length)); - return prevMD5 != null && prevMD5.equals(md5); - } - - /** - * Retrieves the md5 for the specified file that has the specified fingerprint (timestamp, length). - * - * @param path String path to the file - * @param timestamp long last modified timestamp of the file - * @param length long file size - * @return hex-string md5 or null if not present in the cache. - */ - public synchronized String getMD5(String path, long timestamp, long length) { - String prevMD5 = - fileFingerprintToMD5Map.get(new FileFingerprintRec(path, timestamp, length)); - return prevMD5; - } - - //----------------------------------------------------------------------------------- - - static class FileFingerprintRec { - final String path; - final long timestamp; - final long length; - - FileFingerprintRec(String path, long timestamp, long length) { - this.path = path; - this.timestamp = timestamp; - this.length = length; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (int) (length ^ (length >>> 32)); - result = prime * result + ((path == null) ? 0 : path.hashCode()); - result = prime * result + (int) (timestamp ^ (timestamp >>> 32)); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof FileFingerprintRec)) { - return false; - } - FileFingerprintRec other = (FileFingerprintRec) obj; - if (length != other.length) { - return false; - } - if (path == null) { - if (other.path != null) { - return false; - } - } - else if (!path.equals(other.path)) { - return false; - } - if (timestamp != other.timestamp) { - return false; - } - return true; - } - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemIndexHelper.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemIndexHelper.java index 81009360e1..848eecf0fe 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemIndexHelper.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemIndexHelper.java @@ -15,14 +15,17 @@ */ package ghidra.formats.gfilesystem; +import java.io.IOException; import java.util.*; +import java.util.stream.Collectors; + +import ghidra.util.Msg; /** * A helper class used by GFilesystem implementors to track mappings between GFile * instances and the underlying container filesystem's native file objects. *

- * Threadsafe after initial use of {@link #storeFile(String, int, boolean, long, Object) storeFile()} - * by the owning filesystem. + * Threadsafe (methods are synchronized). *

* This class also provides filename 'unique-ifying' (per directory) where an auto-incrementing * number will be added to a file's filename if it is not unique in the directory. @@ -33,9 +36,16 @@ import java.util.*; public class FileSystemIndexHelper { private GFile rootDir; + + static class FileData { + GFile file; + METADATATYPE metaData; + long fileIndex; + } - protected Map fileToEntryMap = new HashMap<>(); - protected Map> directoryToListing = new HashMap<>(); + protected Map> fileToEntryMap = new HashMap<>(); + protected Map> fileIndexToEntryMap = new HashMap<>(); + protected Map>> directoryToListing = new HashMap<>(); /** * Creates a new {@link FileSystemIndexHelper} for the specified {@link GFileSystem}. @@ -49,6 +59,17 @@ public class FileSystemIndexHelper { */ public FileSystemIndexHelper(GFileSystem fs, FSRLRoot fsFSRL) { this.rootDir = GFileImpl.fromFSRL(fs, null, fsFSRL.withPath("/"), true, -1); + initRootDir(null); + } + + private void initRootDir(METADATATYPE metadata) { + FileData fileData = new FileData<>(); + fileData.file = rootDir; + fileData.fileIndex = -1; + fileData.metaData = metadata; + + fileToEntryMap.put(rootDir, fileData); + directoryToListing.put(rootDir, new HashMap<>()); } /** @@ -63,7 +84,7 @@ public class FileSystemIndexHelper { /** * Removes all file info from this index. */ - public void clear() { + public synchronized void clear() { fileToEntryMap.clear(); directoryToListing.clear(); } @@ -71,56 +92,84 @@ public class FileSystemIndexHelper { /** * Number of files in this index. * - * @return number of file in this index. + * @return number of file in this index */ - public int getFileCount() { + public synchronized int getFileCount() { return fileToEntryMap.size(); } /** * Gets the opaque filesystem specific blob that was associated with the specified file. * - * @param f {@link GFile} to look for. - * @return Filesystem specific blob associated with the specified file, or null if not found. + * @param f {@link GFile} to look for + * @return Filesystem specific blob associated with the specified file, or null if not found */ - public METADATATYPE getMetadata(GFile f) { - return fileToEntryMap.get(f); + public synchronized METADATATYPE getMetadata(GFile f) { + FileData fileData = fileToEntryMap.get(f); + return fileData != null ? fileData.metaData : null; + } + + /** + * Sets the associated metadata blob for the specified file. + * + * @param f GFile to update + * @param metaData new metdata blob + * @throws IOException if unknown file + */ + public synchronized void setMetadata(GFile f, METADATATYPE metaData) throws IOException { + FileData fileData = fileToEntryMap.get(f); + if ( fileData == null ) { + throw new IOException("Unknown file: " + f); + } + fileData.metaData = metaData; + } + + /** + * Gets the GFile instance that was associated with the filesystem file index. + * + * @param fileIndex index of the file in its filesystem + * @return the associated GFile instance, or null if not found + */ + public synchronized GFile getFileByIndex(long fileIndex) { + FileData fileData = fileIndexToEntryMap.get(fileIndex); + return (fileData != null) ? fileData.file : null; } /** * Mirror's {@link GFileSystem#getListing(GFile)} interface. * * @param directory {@link GFile} directory to get the list of child files that have been - * added to this index, null means root directory. - * @return {@link List} of GFile files that are in the specified directory, never null. + * added to this index, null means root directory + * @return {@link List} of GFile files that are in the specified directory, never null */ - public List getListing(GFile directory) { - Map dirListing = getDirectoryContents(directory, false); - List results = - (dirListing != null) ? new ArrayList<>(dirListing.values()) : Collections.emptyList(); - return results; + public synchronized List getListing(GFile directory) { + Map> dirListing = getDirectoryContents(directory, false); + if (dirListing == null) { + return List.of(); + } + return dirListing.values() + .stream() + .map(fd -> fd.file) + .collect(Collectors.toList()); } /** * Mirror's {@link GFileSystem#lookup(String)} interface. * - * @param path path and filename of a file to find. - * @return {@link GFile} instance or null if no file was added to the index at that path. + * @param path path and filename of a file to find + * @return {@link GFile} instance or null if no file was added to the index at that path */ - public GFile lookup(String path) { + public synchronized GFile lookup(String path) { String[] nameparts = (path != null ? path : "").split("/"); GFile parent = lookupParent(nameparts); - if (nameparts.length == 0) { - return parent; - } - - String name = nameparts[nameparts.length - 1]; + String name = (nameparts.length > 0) ? nameparts[nameparts.length - 1] : null; if (name == null || name.isEmpty()) { return parent; } - Map dirListing = getDirectoryContents(parent, false); - return (dirListing != null) ? dirListing.get(name) : null; + Map> dirListing = getDirectoryContents(parent, false); + FileData fileData = (dirListing != null) ? dirListing.get(name) : null; + return (fileData != null) ? fileData.file : null; } /** @@ -133,38 +182,27 @@ public class FileSystemIndexHelper { * suffix added to the resultant GFile name, where nnn is the file's * order of occurrence in the container file. *

+ * * @param path string path and filename of the file being added to the index. Back - * slashes are normalized to forward slashes. + * slashes are normalized to forward slashes * @param fileIndex the filesystem specific unique index for this file, or -1 - * if not available. + * if not available * @param isDirectory boolean true if the new file is a directory - * @param length number of bytes in the file or -1 if not known or directory. - * @param fileInfo opaque blob that will be stored and associated with the new - * GFile instance. - * @return new GFile instance. + * @param length number of bytes in the file or -1 if not known or directory + * @param metadata opaque blob that will be stored and associated with the new + * GFile instance + * @return new GFile instance */ - public GFileImpl storeFile(String path, int fileIndex, boolean isDirectory, long length, - METADATATYPE fileInfo) { + public synchronized GFile storeFile(String path, long fileIndex, boolean isDirectory, + long length, METADATATYPE metadata) { String[] nameparts = path.replaceAll("[\\\\]", "/").split("/"); GFile parent = lookupParent(nameparts); - int fileNum = (fileIndex != -1) ? fileIndex : fileToEntryMap.size(); String lastpart = nameparts[nameparts.length - 1]; - Map dirContents = getDirectoryContents(parent, true); - String uniqueName = dirContents.containsKey(lastpart) && !isDirectory - ? lastpart + "[" + Integer.toString(fileNum) + "]" - : lastpart; - - GFileImpl file = createNewFile(parent, uniqueName, isDirectory, length, fileInfo); - - dirContents.put(uniqueName, file); - if (file.isDirectory()) { - getDirectoryContents(file, true); - } - - fileToEntryMap.put(file, fileInfo); - return file; + FileData fileData = + doStoreFile(lastpart, parent, fileIndex, isDirectory, length, metadata); + return fileData.file; } /** @@ -176,47 +214,81 @@ public class FileSystemIndexHelper { * suffix added to the resultant GFile name, where nnn is the file's * order of occurrence in the container file. *

+ * * @param filename the new file's name * @param parent the new file's parent directory * @param fileIndex the filesystem specific unique index for this file, or -1 - * if not available. + * if not available * @param isDirectory boolean true if the new file is a directory - * @param length number of bytes in the file or -1 if not known or directory. - * @param fileInfo opaque blob that will be stored and associated with the new - * GFile instance. - * @return new GFile instance. + * @param length number of bytes in the file or -1 if not known or directory + * @param metadata opaque blob that will be stored and associated with the new + * GFile instance + * @return new GFile instance */ - public GFile storeFileWithParent(String filename, GFile parent, int fileIndex, - boolean isDirectory, long length, METADATATYPE fileInfo) { + public synchronized GFile storeFileWithParent(String filename, GFile parent, long fileIndex, + boolean isDirectory, long length, METADATATYPE metadata) { + FileData fileData = + doStoreFile(filename, parent, fileIndex, isDirectory, length, metadata); + return fileData.file; + } + + private FileData doStoreMissingDir(String filename, GFile parent) { parent = (parent == null) ? rootDir : parent; - int fileNum = (fileIndex != -1) ? fileIndex : fileToEntryMap.size(); - Map dirContents = getDirectoryContents(parent, true); - String uniqueName = dirContents.containsKey(filename) && !isDirectory - ? filename + "[" + Integer.toString(fileNum) + "]" - : filename; + Map> dirContents = getDirectoryContents(parent, true); + GFile file = createNewFile(parent, filename, true, -1, null); - GFile file = createNewFile(parent, uniqueName, isDirectory, length, fileInfo); + FileData fileData = new FileData<>(); + fileData.file = file; + fileData.fileIndex = -1; + fileToEntryMap.put(file, fileData); + dirContents.put(filename, fileData); + getDirectoryContents(file, true); - dirContents.put(uniqueName, file); - if (file.isDirectory()) { + return fileData; + } + + private FileData doStoreFile(String filename, GFile parent, long fileIndex, + boolean isDirectory, long length, METADATATYPE metadata) { + parent = (parent == null) ? rootDir : parent; + long fileNum = (fileIndex != -1) ? fileIndex : fileToEntryMap.size(); + if (fileIndexToEntryMap.containsKey(fileNum)) { + Msg.warn(this, "Duplicate fileNum for file " + parent.getPath() + "/" + filename); + } + + Map> dirContents = getDirectoryContents(parent, true); + String uniqueName = makeUniqueFilename(dirContents.containsKey(filename) && !isDirectory, + filename, fileNum); + + GFile file = createNewFile(parent, uniqueName, isDirectory, length, metadata); + + FileData fileData = new FileData<>(); + fileData.file = file; + fileData.fileIndex = fileNum; + fileData.metaData = metadata; + fileToEntryMap.put(file, fileData); + fileIndexToEntryMap.put(fileNum, fileData); + + dirContents.put(uniqueName, fileData); + if (isDirectory) { + // side-effect of get will eagerly create the directorylisting entry getDirectoryContents(file, true); } - fileToEntryMap.put(file, fileInfo); - return file; + return fileData; } - /** - * Returns a string->GFile map that holds the contents of a single directory. - * @param directoryFile - * @return - */ - protected Map getDirectoryContents(GFile directoryFile, + private String makeUniqueFilename(boolean wasNameCollision, String filename, long fileIndex) { + return wasNameCollision + ? filename + "[" + Long.toString(fileIndex) + "]" + : filename; + } + + private Map> getDirectoryContents(GFile directoryFile, boolean createIfMissing) { directoryFile = (directoryFile != null) ? directoryFile : rootDir; - Map dirContents = directoryToListing.get(directoryFile); + Map> dirContents = directoryToListing.get(directoryFile); if (dirContents == null && createIfMissing) { dirContents = new HashMap<>(); directoryToListing.put(directoryFile, dirContents); @@ -242,23 +314,21 @@ public class FileSystemIndexHelper { protected GFile lookupParent(String[] nameparts) { GFile currentDir = rootDir; - GFile currentFile = rootDir; for (int i = 0; i < nameparts.length - 1; i++) { - Map currentDirContents = getDirectoryContents(currentDir, true); + Map> currentDirContents = + getDirectoryContents(currentDir, true); String name = nameparts[i]; if (name.isEmpty()) { continue; } - currentFile = currentDirContents.get(name); - if (currentFile == null) { - currentFile = createNewFile(currentDir, name, true, -1, null); - currentDirContents.put(name, currentFile); - getDirectoryContents(currentFile, true); + FileData fileData = currentDirContents.get(name); + if (fileData == null) { + fileData = doStoreMissingDir(name, currentDir); } - currentDir = currentFile; + currentDir = fileData.file; } - return currentFile; + return currentDir; } /** @@ -282,6 +352,38 @@ public class FileSystemIndexHelper { size); } + /** + * Updates the FSRL of a file already in the index. + * + * @param file current {@link GFile} + * @param newFSRL the new FSRL the new file will be given + */ + public synchronized void updateFSRL(GFile file, FSRL newFSRL) { + GFileImpl newFile = GFileImpl.fromFSRL(rootDir.getFilesystem(), file.getParentFile(), + newFSRL, file.isDirectory(), file.getLength()); + + FileData fileData = fileToEntryMap.get(file); + if (fileData != null) { + fileToEntryMap.remove(file); + fileIndexToEntryMap.remove(fileData.fileIndex); + + fileData.file = newFile; + + fileToEntryMap.put(newFile, fileData); + if (fileData.fileIndex != -1) { + fileIndexToEntryMap.put(fileData.fileIndex, fileData); + } + } + + Map> dirListing = directoryToListing.get(file); + if ( dirListing != null) { + // typically this shouldn't ever happen as directory entries don't have MD5s and won't need to be updated + // after the fact + directoryToListing.remove(file); + directoryToListing.put(newFile, dirListing); + } + } + @Override public String toString() { return "FileSystemIndexHelper for " + rootDir.getFilesystem(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemCache.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemInstanceManager.java similarity index 91% rename from Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemCache.java rename to Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemInstanceManager.java index 0483c9a545..c922b4ab66 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemCache.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemInstanceManager.java @@ -27,7 +27,7 @@ import ghidra.util.Msg; * Any filesystems that are not referenced by outside users (via a {@link FileSystemRef}) will * be closed and removed from the cache when the next {@link #cacheMaint()} is performed. */ -public class FileSystemCache implements FileSystemEventListener { +class FileSystemInstanceManager implements FileSystemEventListener { private static class FSCacheInfo { FileSystemRef ref; @@ -47,7 +47,7 @@ public class FileSystemCache implements FileSystemEventListener { * @param rootFS reference to the global root file system, which is a special case * file system that is not subject to eviction. */ - public FileSystemCache(GFileSystem rootFS) { + public FileSystemInstanceManager(GFileSystem rootFS) { this.rootFS = rootFS; this.rootFSRL = rootFS.getFSRL(); } @@ -190,7 +190,7 @@ public class FileSystemCache implements FileSystemEventListener { continue; } - if (fsContainer.equals(containerFSRL)) { + if (containerFSRL.isEquivalent(fsContainer)) { return ref.dup(); } } @@ -267,4 +267,23 @@ public class FileSystemCache implements FileSystemEventListener { Msg.error(this, "Error closing filesystem", e); } } + + /** + * Closes the specified ref, and if no other refs to the file system remain, closes the file system. + * + * @param ref {@link FileSystemRef} to close + */ + public synchronized void releaseImmediate(FileSystemRef ref) { + FSCacheInfo fsci = filesystems.get(ref.getFilesystem().getFSRL()); + ref.close(); + if (fsci == null) { + Msg.warn(this, "Unknown file system reference: " + ref.getFilesystem().getFSRL()); + return; + } + FileSystemRefManager refManager = fsci.ref.getFilesystem().getRefManager(); + if (refManager.canClose(fsci.ref)) { + release(fsci); + } + + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemRefManager.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemRefManager.java index cbe3a4f335..a07b782315 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemRefManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemRefManager.java @@ -15,13 +15,13 @@ */ package ghidra.formats.gfilesystem; +import java.util.ArrayList; +import java.util.List; + import ghidra.util.Msg; import ghidra.util.datastruct.WeakDataStructureFactory; import ghidra.util.datastruct.WeakSet; -import java.util.ArrayList; -import java.util.List; - /** * A threadsafe helper class that manages creating and releasing {@link FileSystemRef} instances * and broadcasting events to {@link FileSystemEventListener} listeners. @@ -166,7 +166,8 @@ public class FileSystemRefManager { // where instances are created and thrown away without a close() to probe // filesystem container files. if (fs != null && !(fs instanceof GFileSystemBase)) { - Msg.warn(this, "Unclosed FilesytemRefManager for filesystem: " + fs.getClass()); + Msg.warn(this, "Unclosed FilesytemRefManager for filesystem: " + fs.getClass() + ", " + + fs.getName()); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemService.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemService.java index 1ada2cecae..5a05e9bbf7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemService.java @@ -18,15 +18,14 @@ package ghidra.formats.gfilesystem; import java.io.*; import java.util.List; -import org.apache.commons.io.FilenameUtils; - -import ghidra.app.util.bin.ByteProvider; -import ghidra.app.util.bin.RandomAccessByteProvider; +import ghidra.app.util.bin.*; +import ghidra.formats.gfilesystem.FileCache.FileCacheEntry; +import ghidra.formats.gfilesystem.FileCache.FileCacheEntryBuilder; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; +import ghidra.formats.gfilesystem.crypto.*; import ghidra.formats.gfilesystem.factory.FileSystemFactoryMgr; import ghidra.framework.Application; import ghidra.util.Msg; -import ghidra.util.datastruct.FixedSizeHashMap; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; import ghidra.util.timer.GTimer; @@ -48,40 +47,14 @@ import ghidra.util.timer.GTimer; * If you are working with {@link GFile} instances, you should have a * {@link FileSystemRef fs ref} that you are using to pin the filesystem. *

+ * Files written to the {@code fscache} directory are obfuscated to prevent interference from + * virus scanners. See {@link ObfuscatedInputStream} or {@link ObfuscatedOutputStream} or + * {@link ObfuscatedFileByteProvider}. + *

* Thread-safe. *

- * - *

{@literal
- * TODO list:
- *
- * Refactor fileInfo -> needs dialog to show properties
- * Refactor GFile.getInfo() to return Map<> instead of String.
- * Persistent filesystem - when reopen tool, filesystems should auto-reopen.
- * Unify GhidraFileChooser with GFileSystem.
- * Add "Mounted Filesystems" button to show currently opened GFilesystems?
- * Dockable filesystem browser in FrontEnd.
- * Reorg filesystem browser right-click popup menu to be more Eclipse action-like
- * 	Show In -> Project tree
- *             Tool [CodeBrowser name]
- *  Import
- *  Open With -> Text Viewer
- *               Image Viewer
- *  Export -> To Project dir
- *            To Home dir
- *            To Dir
- *            To Eclipse Project
- *            Decompiled source
- * ProgramMappingService - more robust, precache when open project.
- * Make BatchImportDialog modeless, drag-and-drop to src list
- *
- * Testing:
- *
- * More format tests
- * Large test binary support
- * }
*/ public class FileSystemService { - private static int FSRL_INTERN_SIZE = 1000; private static FileSystemService instance; @@ -100,20 +73,82 @@ public class FileSystemService { return instance != null; } - private final LocalFileSystem localFS = LocalFileSystem.makeGlobalRootFS(); - private final FSRLRoot localFSRL = localFS.getFSRL(); - private final FileSystemFactoryMgr fsFactoryMgr = FileSystemFactoryMgr.getInstance(); - private FileCache fileCache; - private FileSystemCache filesystemCache = new FileSystemCache(localFS); - private FileCacheNameIndex fileCacheNameIndex = new FileCacheNameIndex(); - private FileFingerprintCache fileFingerprintCache = new FileFingerprintCache(); - private long fsCacheMaintIntervalMS = 10 * 1000; + /** + * Used by {@link FileSystemService#getDerivedByteProvider(FSRL, FSRL, String, long, DerivedStreamProducer, TaskMonitor) getDerivedByteProvider()} + * to produce a derivative stream from a source file. + *

+ * The {@link InputStream} returned from the method needs to supply the bytes of the derived file + * and will be closed by the caller. + *

+ * Example: + *

+ *

fsService.getDerivedByteProvider(
+	 *     containerFSRL, 
+	 *     null,
+	 *     "the_derived_file",
+	 *     -1,
+	 *     () -> new MySpecialtyInputstream(),
+	 *     monitor);
+ *

+ * See {@link #produceDerivedStream()}. + */ + public interface DerivedStreamProducer { + + /** + * Callback method intended to be implemented by the caller to + * {@link FileSystemService#getDerivedByteProvider(FSRL, FSRL, String, long, DerivedStreamProducer, TaskMonitor)} + *

+ * The implementation needs to return an {@link InputStream} that contains the bytes + * of the derived file. + *

+ * @return a new {@link InputStream} that will produce all the bytes of the derived file + * @throws IOException if there is a problem while producing the InputStream + * @throws CancelledException if the user canceled + */ + InputStream produceDerivedStream() throws IOException, CancelledException; + } /** - * LRU hashmap, limited in size to FSRL_INTERN_SIZE. + * Used by {@link FileSystemService#getDerivedByteProviderPush(FSRL, FSRL, String, long, DerivedStreamPushProducer, TaskMonitor) getDerivedByteProviderPush()} + * to produce a derivative stream from a source file. + *

+ * The implementation needs to write bytes to the supplied {@link OutputStream}. + *

+ * Example: + *

+ *

fsService.getDerivedByteProviderPush(
+	 *     containerFSRL, 
+	 *     null,
+	 *     "the_derived_file",
+	 *     -1,
+	 *     os -> FileUtilities.copyStream(my_input_stream, os),
+	 *     monitor);
+ *

+ * See {@link #push(OutputStream)}. + * */ - private FixedSizeHashMap fsrlInternMap = - new FixedSizeHashMap<>(FSRL_INTERN_SIZE, FSRL_INTERN_SIZE); + public interface DerivedStreamPushProducer { + /** + * Callback method intended to be implemented by the caller to + * {@link FileSystemService#getDerivedByteProviderPush(FSRL, FSRL, String, long, DerivedStreamPushProducer, TaskMonitor) getDerivedByteProviderPush()} + *

+ * @param os {@link OutputStream} that the implementor should write the bytes to. Do + * not close the stream when done + * @throws IOException if there is a problem while writing to the OutputStream + * @throws CancelledException if the user canceled + */ + void push(OutputStream os) throws IOException, CancelledException; + } + + private final LocalFileSystem localFS = LocalFileSystem.makeGlobalRootFS(); + private final FileSystemFactoryMgr fsFactoryMgr = FileSystemFactoryMgr.getInstance(); + private final FSRLRoot cacheFSRL = FSRLRoot.makeRoot("cache"); + private final FileCache fileCache; + private final FileSystemInstanceManager fsInstanceManager = + new FileSystemInstanceManager(localFS); + private final FileCacheNameIndex fileCacheNameIndex = new FileCacheNameIndex(); + private long fsCacheMaintIntervalMS = 10 * 1000; + private CryptoSession currentCryptoSession; /** * Creates a FilesystemService instance, using the {@link Application}'s default value @@ -121,7 +156,11 @@ public class FileSystemService { * cache directory. */ public FileSystemService() { - this(new File(Application.getUserCacheDirectory(), "fscache")); + this(new File(Application.getUserCacheDirectory(), "fscache2")); + + // age off files in old cache dir. Remove this after a few versions + FileCache.performCacheMaintOnOldDirIfNeeded( + new File(Application.getUserCacheDirectory(), "fscache")); } /** @@ -134,7 +173,7 @@ public class FileSystemService { try { fileCache = new FileCache(fscacheDir); GTimer.scheduleRepeatingRunnable(fsCacheMaintIntervalMS, fsCacheMaintIntervalMS, - () -> filesystemCache.cacheMaint()); + () -> fsInstanceManager.cacheMaint()); } catch (IOException e) { throw new RuntimeException("Failed to init global cache " + fscacheDir, e); @@ -145,9 +184,8 @@ public class FileSystemService { * Forcefully closes all open filesystems and clears caches. */ public void clear() { - synchronized (filesystemCache) { - filesystemCache.clear(); - fsrlInternMap.clear(); + synchronized (fsInstanceManager) { + fsInstanceManager.clear(); fileCacheNameIndex.clear(); } } @@ -156,7 +194,19 @@ public class FileSystemService { * Close unused filesystems. */ public void closeUnusedFileSystems() { - filesystemCache.closeAllUnused(); + fsInstanceManager.closeAllUnused(); + } + + /** + * Releases the specified {@link FileSystemRef}, and if no other references remain, removes + * it from the shared cache of file system instances. + * + * @param fsRef the ref to release + */ + public void releaseFileSystemImmediate(FileSystemRef fsRef) { + if (fsRef != null && !fsRef.isClosed()) { + fsInstanceManager.releaseImmediate(fsRef); + } } /** @@ -168,6 +218,27 @@ public class FileSystemService { return localFS; } + /** + * Returns true if the specified location is a path on the local computer's + * filesystem. + * + * @param fsrl {@link FSRL} path to query + * @return true if local, false if the path points to an embedded file in a container. + */ + public boolean isLocal(FSRL fsrl) { + return fsrl.getFS().hasContainer() == false; + } + + /** + * Builds a {@link FSRL} of a {@link File file} located on the local filesystem. + * + * @param f {@link File} on the local filesystem + * @return {@link FSRL} pointing to the same file, never null + */ + public FSRL getLocalFSRL(File f) { + return localFS.getLocalFSRL(f); + } + /** * Returns true of there is a {@link GFileSystem filesystem} mounted at the requested * {@link FSRL} location. @@ -176,7 +247,7 @@ public class FileSystemService { * @return boolean true if filesystem mounted at location. */ public boolean isFilesystemMountedAt(FSRL fsrl) { - return filesystemCache.isFilesystemMountedAt(fsrl); + return fsInstanceManager.isFilesystemMountedAt(fsrl); } /** @@ -195,8 +266,7 @@ public class FileSystemService { */ public RefdFile getRefdFile(FSRL fsrl, TaskMonitor monitor) throws CancelledException, IOException { - FSRLRoot fsRoot = fsrl.getFS(); - FileSystemRef ref = getFilesystem(fsRoot, monitor); + FileSystemRef ref = getFilesystem(fsrl.getFS(), monitor); try { GFile gfile = ref.getFilesystem().lookup(fsrl.getPath()); if (gfile == null) { @@ -214,83 +284,6 @@ public class FileSystemService { } } - /** - * Return a {@link FileCacheEntry} with information about the requested file specified - * by the FSRL, forcing a read/cache add of the file is it is missing from the cache. - *

- * Never returns NULL, instead throws IOException. - * - * @param fsrl {@link FSRL} of the desired file. - * @param monitor {@link TaskMonitor} to watch and update with progress. - * @return new {@link FileCacheEntry} with info about the cached file. - * @throws IOException if IO error when getting file. - * @throws CancelledException if user canceled. - */ - private FileCacheEntry getCacheFile(FSRL fsrl, TaskMonitor monitor) - throws IOException, CancelledException { - - if (fsrl.getPath() == null) { - throw new IOException("Invalid FSRL specified: " + fsrl); - } - String md5 = fsrl.getMD5(); - if (md5 == null && fsrl.getNestingDepth() == 1) { - // if this is a real file on the local file system, and the FSRL doesn't specify - // its MD5, try to fetch the MD5 from the fingerprint cache based on its - // size and lastmod time, which will help us locate the file in the cache - File f = localFS.getLocalFile(fsrl); - if (f.isFile()) { - md5 = fileFingerprintCache.getMD5(f.getPath(), f.lastModified(), f.length()); - } - } - FSRLRoot fsRoot = fsrl.getFS(); - - FileCacheEntry result = (md5 != null) ? fileCache.getFile(md5) : null; - if (result == null) { - try (FileSystemRef ref = getFilesystem(fsRoot, monitor)) { - GFileSystem fs = ref.getFilesystem(); - GFile gfile = fs.lookup(fsrl.getPath()); - if (gfile == null) { - throw new IOException( - "File [" + fsrl + "] not found in filesystem [" + fs.getFSRL() + "]"); - } - - // Its possible the filesystem added the file to the cache when it was mounted, - // or that we now have a better FSRL with a MD5 value that we can use to - // search the file cache. - if (gfile.getFSRL().getMD5() != null) { - result = fileCache.getFile(gfile.getFSRL().getMD5()); - if (result != null) { - return result; - } - } - - try (InputStream dataStream = fs.getInputStream(gfile, monitor)) { - if (dataStream == null) { - throw new IOException("Unable to get datastream for " + fsrl); - } - monitor.setMessage("Caching " + gfile.getName()); - monitor.initialize(gfile.getLength()); - result = fileCache.addStream(dataStream, monitor); - if (md5 != null && !md5.equals(result.md5)) { - throw new IOException("Error reading file, MD5 has changed: " + fsrl + - ", md5 now " + result.md5); - } - } - if (fsrl.getNestingDepth() == 1) { - // if this is a real file on the local filesystem, now that we have its - // MD5, save it in the fingerprint cache so it can be found later - File f = localFS.getLocalFile(fsrl); - if (f.isFile()) { - fileFingerprintCache.add(f.getPath(), result.md5, f.lastModified(), - f.length()); - } - } - - } - } - - return result; - } /** * Returns a filesystem instance for the requested {@link FSRLRoot}, either from an already @@ -311,148 +304,25 @@ public class FileSystemService { */ public FileSystemRef getFilesystem(FSRLRoot fsFSRL, TaskMonitor monitor) throws IOException, CancelledException { - synchronized (filesystemCache) { - FileSystemRef ref = filesystemCache.getRef(fsFSRL); + synchronized (fsInstanceManager) { + FileSystemRef ref = fsInstanceManager.getRef(fsFSRL); if (ref == null) { if (!fsFSRL.hasContainer()) { throw new IOException("Bad FSRL " + fsFSRL); } - fsFSRL = intern(fsFSRL); - FSRL containerFSRL = fsFSRL.getContainer(); - FileCacheEntry cfi = getCacheFile(containerFSRL, monitor); - if (containerFSRL.getMD5() == null) { - containerFSRL = containerFSRL.withMD5(cfi.md5); - } - GFileSystem fs = FileSystemFactoryMgr.getInstance() - .mountFileSystem( - fsFSRL.getProtocol(), containerFSRL, cfi.file, this, monitor); + ByteProvider containerByteProvider = getByteProvider(fsFSRL.getContainer(), true, monitor); + GFileSystem fs = + fsFactoryMgr.mountFileSystem(fsFSRL.getProtocol(), containerByteProvider, this, monitor); ref = fs.getRefManager().create(); - filesystemCache.add(fs); + fsInstanceManager.add(fs); } return ref; } } /** - * Adds a {@link GFile file}'s stream's contents to the file cache, returning its MD5 hash. - * - * @param file {@link GFile} not really used currently - * @param is {@link InputStream} to add to the cache. - * @param monitor {@link TaskMonitor} to monitor and update. - * @return string with new file's md5. - * @throws IOException if IO error - * @throws CancelledException if user canceled. - */ - public FileCacheEntry addFileToCache(GFile file, InputStream is, TaskMonitor monitor) - throws IOException, CancelledException { - FileCacheEntry fce = fileCache.addStream(is, monitor); - return fce; - } - - /** - * Stores a stream in the file cache. - *

- * @param is {@link InputStream} to store in the cache. - * @param monitor {@link TaskMonitor} to watch and update. - * @return {@link File} location of the new file. - * @throws IOException if IO error - * @throws CancelledException if the user cancels. - */ - public FileCacheEntry addStreamToCache(InputStream is, TaskMonitor monitor) - throws IOException, CancelledException { - FileCacheEntry fce = fileCache.addStream(is, monitor); - return fce; - } - - /** - * Returns a {@link File java.io.file} with the data from the requested FSRL. - * Simple local files will be returned directly, and files nested in containers - * will be located in the file cache directory and have a 'random' name. - *

- * Never returns nulls, throws IOException if not found or error. - * - * @param fsrl {@link FSRL} of the desired file. - * @param monitor {@link TaskMonitor} to watch and update. - * @return {@link File} of the desired file in the cache, never null. - * @throws CancelledException if user cancels. - * @throws IOException if IO problem. - */ - public File getFile(FSRL fsrl, TaskMonitor monitor) throws CancelledException, IOException { - if (fsrl.getNestingDepth() == 1) { - // If this is a real files on the local filesystem, verify any - // MD5 embedded in the FSRL before returning the live local file - // as the result. - File f = localFS.getLocalFile(fsrl); - if (f.isFile() && fsrl.getMD5() != null) { - if (!fileFingerprintCache.contains(f.getPath(), fsrl.getMD5(), f.lastModified(), - f.length())) { - String fileMD5 = FSUtilities.getFileMD5(f, monitor); - if (!fsrl.getMD5().equals(fileMD5)) { - throw new IOException("Exact file no longer exists: " + f.getPath() + - " contents have changed, old md5: " + fsrl.getMD5() + ", new md5: " + - fileMD5); - } - fileFingerprintCache.add(f.getPath(), fileMD5, f.lastModified(), f.length()); - } - } - return f; - } - FileCacheEntry fce = getCacheFile(fsrl, monitor); - return fce.file; - } - - private String getMD5(FSRL fsrl, TaskMonitor monitor) throws CancelledException, IOException { - if (fsrl.getNestingDepth() == 1) { - File f = localFS.getLocalFile(fsrl); - if (!f.isFile()) { - return null; - } - String md5 = fileFingerprintCache.getMD5(f.getPath(), f.lastModified(), f.length()); - if (md5 == null) { - md5 = FSUtilities.getFileMD5(f, monitor); - fileFingerprintCache.add(f.getPath(), md5, f.lastModified(), f.length()); - } - return md5; - } - FileCacheEntry fce = getCacheFile(fsrl, monitor); - return fce.md5; - } - - /** - * Builds a {@link FSRL} of a {@link File file} located on the local filesystem. - * - * @param f {@link File} on the local filesystem - * @return {@link FSRL} pointing to the same file, never null - */ - public FSRL getLocalFSRL(File f) { - return localFS.getFSRL() - .withPath( - FSUtilities.appendPath("/", FilenameUtils.separatorsToUnix(f.getPath()))); - } - - /** - * Converts a java {@link File} instance into a GFilesystem {@link GFile} hosted on the - * {@link #getLocalFS() local filesystem}. - *

- * @param f {@link File} on the local filesystem - * @return {@link GFile} representing the same file or {@code null} if there was a problem - * with the file path. - */ - public GFile getLocalGFile(File f) { - try { - return localFS.lookup(f.getPath()); - } - catch (IOException e) { - // the LocalFileSystem impl doesn't check the validity of the path so this - // exception should never happen. If it does, fall thru and return null. - } - return null; - } - - /** - * Returns a {@link ByteProvider} with the contents of the requested {@link GFile file} - * (in the Global file cache directory). + * Returns a {@link ByteProvider} with the contents of the requested {@link GFile file}. *

* Never returns null, throws IOException if there was a problem. *

@@ -460,81 +330,142 @@ public class FileSystemService { * when finished. * * @param fsrl {@link FSRL} file to wrap - * @param monitor {@link TaskMonitor} to watch and update. + * @param fullyQualifiedFSRL if true, the returned ByteProvider's FSRL will always have a MD5 + * hash + * @param monitor {@link TaskMonitor} to watch and update * @return new {@link ByteProvider} * @throws CancelledException if user cancels - * @throws IOException if IO problem. + * @throws IOException if IO problem */ - public ByteProvider getByteProvider(FSRL fsrl, TaskMonitor monitor) - throws CancelledException, IOException { - File file = getFile(fsrl, monitor); - RandomAccessByteProvider rabp = new RandomAccessByteProvider(file, fsrl); - return rabp; - } - - /** - * Returns a reference to a file in the FileCache that contains the - * derived (ie. decompressed or decrypted) contents of a source file, as well as - * its md5. - *

- * If the file was not present in the cache, the {@link DerivedFileProducer producer} - * lambda will be called and it will be responsible for returning an {@link InputStream} - * which has the derived contents, which will be added to the file cache for next time. - *

- * @param fsrl {@link FSRL} of the source (or container) file that this derived file is based on - * @param derivedName a unique string identifying the derived file inside the source (or container) file - * @param producer a {@link DerivedFileProducer callback or lambda} that returns an - * {@link InputStream} that will be streamed into a file and placed into the file cache. - * Example:{@code (file) -> { return new XYZDecryptorInputStream(file); }} - * @param monitor {@link TaskMonitor} that will be monitor for cancel requests and updated - * with file io progress - * @return {@link FileCacheEntry} with file and md5 fields - * @throws CancelledException if the user cancels - * @throws IOException if there was an io error - */ - public FileCacheEntry getDerivedFile(FSRL fsrl, String derivedName, - DerivedFileProducer producer, TaskMonitor monitor) + public ByteProvider getByteProvider(FSRL fsrl, boolean fullyQualifiedFSRL, TaskMonitor monitor) throws CancelledException, IOException { - // fileCacheNameIndex is queried and updated in separate steps, - // which could be a race issue with another thread, but in this - // case should be okay as the only bad result will be extra - // work being performed recreating the contents of the same derived file a second - // time. - FileCacheEntry cacheEntry = getCacheFile(fsrl, monitor); - String derivedMD5 = fileCacheNameIndex.get(cacheEntry.md5, derivedName); - FileCacheEntry derivedFile = (derivedMD5 != null) ? fileCache.getFile(derivedMD5) : null; - if (derivedFile == null) { - monitor.setMessage(derivedName + " " + fsrl.getName()); - try (InputStream is = producer.produceDerivedStream(cacheEntry.file)) { - derivedFile = fileCache.addStream(is, monitor); - fileCacheNameIndex.add(cacheEntry.md5, derivedName, derivedFile.md5); + if ( fsrl.getMD5() != null ) { + FileCacheEntry fce = fileCache.getFileCacheEntry(fsrl.getMD5()); + if ( fce != null ) { + return fce.asByteProvider(fsrl); } } - return derivedFile; + + try ( FileSystemRef fsRef = getFilesystem(fsrl.getFS(), monitor) ) { + GFileSystem fs = fsRef.getFilesystem(); + GFile file = fs.lookup(fsrl.getPath()); + if (file == null) { + throw new IOException("File not found: " + fsrl); + } + if (file.getFSRL().getMD5() != null) { + fsrl = file.getFSRL(); + // try again to fetch cached file if we now have a md5 + FileCacheEntry fce = fileCache.getFileCacheEntry(fsrl.getMD5()); + if (fce != null) { + return fce.asByteProvider(fsrl); + } + } + ByteProvider provider = fs.getByteProvider(file, monitor); + if (provider == null) { + throw new IOException("Unable to get ByteProvider for " + fsrl); + } + + // use the returned provider's FSRL as it may have more info + FSRL resultFSRL = provider.getFSRL(); + if (resultFSRL.getMD5() == null && (fsrl.getMD5() != null || fullyQualifiedFSRL)) { + String md5 = (fs instanceof GFileHashProvider) + ? ((GFileHashProvider) fs).getMD5Hash(file, true, monitor) + : FSUtilities.getMD5(provider, monitor); + resultFSRL = resultFSRL.withMD5(md5); + } + if (fsrl.getMD5() != null) { + if (!fsrl.isMD5Equal(resultFSRL.getMD5())) { + throw new IOException("Unable to retrieve requested file, hash has changed: " + + fsrl + ", new hash: " + resultFSRL.getMD5()); + } + } + return new RefdByteProvider(fsRef.dup(), provider, resultFSRL); + } } /** - * Returns a reference to a file in the FileCache that contains the - * derived (ie. decompressed or decrypted) contents of a source file, as well as - * its md5. + * Returns a {@link ByteProvider} that contains the + * derived (ie. decompressed or decrypted) contents of the requested file. *

- * If the file was not present in the cache, the {@link DerivedFilePushProducer push producer} - * lambda will be called and it will be responsible for producing and writing the derived - * file's bytes to a {@link OutputStream}, which will be added to the file cache for next time. + * The resulting ByteProvider will be a cached file, either written to a + * temporary file, or a in-memory buffer if small enough (see {@link FileCache#MAX_INMEM_FILESIZE}). + *

+ * If the file was not present in the cache, the {@link DerivedStreamProducer producer} + * will be called and it will be responsible for returning an {@link InputStream} + * which has the derived contents, which will be added to the file cache for next time. *

- * @param fsrl {@link FSRL} of the source (or container) file that this derived file is based on + * @param containerFSRL {@link FSRL} w/hash of the source (or container) file that this + * derived file is based on + * @param derivedFSRL (optional) {@link FSRL} to assign to the resulting ByteProvider * @param derivedName a unique string identifying the derived file inside the source (or container) file - * @param pusher a {@link DerivedFilePushProducer callback or lambda} that recieves a {@link OutputStream}. - * Example:{@code (os) -> { ...write to outputstream os here...; }} + * @param sizeHint the expected size of the resulting ByteProvider, or -1 if unknown + * @param producer supplies an InputStream if needed. See {@link DerivedStreamProducer} * @param monitor {@link TaskMonitor} that will be monitor for cancel requests and updated * with file io progress - * @return {@link FileCacheEntry} with file and md5 fields + * @return a {@link ByteProvider} containing the bytes of the requested file, that has the + * specified derivedFSRL, or a pseudo FSRL if not specified. Never null * @throws CancelledException if the user cancels * @throws IOException if there was an io error */ - public FileCacheEntry getDerivedFilePush(FSRL fsrl, String derivedName, - DerivedFilePushProducer pusher, TaskMonitor monitor) + public ByteProvider getDerivedByteProvider(FSRL containerFSRL, FSRL derivedFSRL, + String derivedName, long sizeHint, DerivedStreamProducer producer, + TaskMonitor monitor) throws CancelledException, IOException { + + // fileCacheNameIndex is queried and updated in separate steps, + // which could be a race issue with another thread, but in this + // case should be okay as the only bad result will be extra + // work being performed recreating the contents of the same derived file a second + // time. + assertFullyQualifiedFSRL(containerFSRL); + String containerMD5 = containerFSRL.getMD5(); + String derivedMD5 = fileCacheNameIndex.get(containerMD5, derivedName); + FileCacheEntry derivedFile = fileCache.getFileCacheEntry(derivedMD5); + if (derivedFile == null) { + monitor.setMessage("Caching " + containerFSRL.getName() + " " + derivedName); + if (sizeHint > 0) { + monitor.initialize(sizeHint); + } + try (InputStream is = producer.produceDerivedStream(); + FileCacheEntryBuilder fceBuilder = + fileCache.createCacheEntryBuilder(sizeHint)) { + FSUtilities.streamCopy(is, fceBuilder, monitor); + derivedFile = fceBuilder.finish(); + fileCacheNameIndex.add(containerMD5, derivedName, derivedFile.getMD5()); + } + } + derivedFSRL = (derivedFSRL != null) + ? derivedFSRL.withMD5(derivedFile.getMD5()) + : createCachedFileFSRL(derivedFile.getMD5()); + return derivedFile.asByteProvider(derivedFSRL); + } + + /** + * Returns a {@link ByteProvider} that contains the + * derived (ie. decompressed or decrypted) contents of the requested file. + *

+ * The resulting ByteProvider will be a cached file, either written to a + * temporary file, or a in-memory buffer if small enough (see {@link FileCache#MAX_INMEM_FILESIZE}). + *

+ * If the file was not present in the cache, the {@link DerivedStreamPushProducer pusher} + * will be called and it will be responsible for producing and writing the derived + * file's bytes to a {@link OutputStream}, which will be added to the file cache for next time. + *

+ * @param containerFSRL {@link FSRL} w/hash of the source (or container) file that this + * derived file is based on + * @param derivedFSRL (optional) {@link FSRL} to assign to the resulting ByteProvider + * @param derivedName a unique string identifying the derived file inside the source (or container) file + * @param sizeHint the expected size of the resulting ByteProvider, or -1 if unknown + * @param pusher writes bytes to the supplied OutputStream. See {@link DerivedStreamPushProducer} + * @param monitor {@link TaskMonitor} that will be monitor for cancel requests and updated + * with file io progress + * @return a {@link ByteProvider} containing the bytes of the requested file, that has the + * specified derivedFSRL, or a pseudo FSRL if not specified. Never null + * @throws CancelledException if the user cancels + * @throws IOException if there was an io error + */ + public ByteProvider getDerivedByteProviderPush(FSRL containerFSRL, FSRL derivedFSRL, + String derivedName, long sizeHint, DerivedStreamPushProducer pusher, TaskMonitor monitor) throws CancelledException, IOException { // fileCacheNameIndex is queried and updated in separate steps, @@ -542,32 +473,100 @@ public class FileSystemService { // case should be okay as the only bad result will be extra // work being performed recreating the contents of the same derived file a second // time. - FileCacheEntry cacheEntry = getCacheFile(fsrl, monitor); - String derivedMD5 = fileCacheNameIndex.get(cacheEntry.md5, derivedName); - FileCacheEntry derivedFile = (derivedMD5 != null) ? fileCache.getFile(derivedMD5) : null; + assertFullyQualifiedFSRL(containerFSRL); + String containerMD5 = containerFSRL.getMD5(); + String derivedMD5 = fileCacheNameIndex.get(containerMD5, derivedName); + FileCacheEntry derivedFile = fileCache.getFileCacheEntry(derivedMD5); if (derivedFile == null) { - monitor.setMessage("Caching " + fsrl.getName() + " " + derivedName); - derivedFile = fileCache.pushStream(pusher, monitor); - fileCacheNameIndex.add(cacheEntry.md5, derivedName, derivedFile.md5); + monitor.setMessage("Caching " + containerFSRL.getName() + " " + derivedName); + if (sizeHint > 0) { + monitor.initialize(sizeHint); + } + try (FileCacheEntryBuilder fceBuilder = fileCache.createCacheEntryBuilder(sizeHint)) { + pusher.push(fceBuilder); + derivedFile = fceBuilder.finish(); + } + fileCacheNameIndex.add(containerMD5, derivedName, derivedFile.getMD5()); } - return derivedFile; + derivedFSRL = (derivedFSRL != null) + ? derivedFSRL.withMD5(derivedFile.getMD5()) + : createCachedFileFSRL(derivedFile.getMD5()); + return derivedFile.asByteProvider(derivedFSRL); + } + + private FSRL createCachedFileFSRL(String md5) { + return cacheFSRL.withPathMD5("/" + md5, md5); + } + + /** + * Returns a {@link FileCacheEntryBuilder} that will allow the caller to + * write bytes to it. + *

+ * After calling {@link FileCacheEntryBuilder#finish() finish()}, + * the caller will have a {@link FileCacheEntry} that can provide access to a + * {@link ByteProvider}. + *

+ * Temporary files that are written to disk are obfuscated to avoid interference from + * overzealous virus scanners. See {@link ObfuscatedInputStream} / + * {@link ObfuscatedOutputStream}. + *

+ * @param sizeHint the expected size of the file, or -1 if unknown + * @return {@link FileCacheEntryBuilder} that must be finalized by calling + * {@link FileCacheEntryBuilder#finish() finish()} + * @throws IOException if error + */ + public FileCacheEntryBuilder createTempFile(long sizeHint) throws IOException { + return fileCache.createCacheEntryBuilder(sizeHint); + } + + /** + * Allows the resources used by caching the specified file to be released. + * + * @param fsrl {@link FSRL} file to release cache resources for + */ + public void releaseFileCache(FSRL fsrl) { + if (fsrl.getMD5() != null) { + fileCache.releaseFileCacheEntry(fsrl.getMD5()); + } + } + + /** + * Adds a plaintext (non-obfuscated) file to the cache, consuming it in the process, and returns + * a {@link ByteProvider} that contains the contents of the file. + *

+ * NOTE: only use this if you have no other choice and are forced to deal with already + * existing files in the local filesystem. + * + * @param file {@link File} to add + * @param fsrl {@link FSRL} of the file that is being added + * @param monitor {@link TaskMonitor} + * @return {@link ByteProvider} (hosted in the FileCache) that contains the bytes of the + * specified file + * @throws CancelledException if cancelled + * @throws IOException if error + */ + public ByteProvider pushFileToCache(File file, FSRL fsrl, TaskMonitor monitor) + throws CancelledException, IOException { + FileCacheEntry fce = fileCache.giveFile(file, monitor); + return fce.asByteProvider(fsrl); } /** * Returns true if the specified derived file exists in the file cache. * - * @param fsrl {@link FSRL} of the container + * @param containerFSRL {@link FSRL} w/hash of the container * @param derivedName name of the derived file inside of the container * @param monitor {@link TaskMonitor} * @return boolean true if file exists at time of query, false if file is not in cache * @throws CancelledException if user cancels * @throws IOException if other IO error */ - public boolean hasDerivedFile(FSRL fsrl, String derivedName, TaskMonitor monitor) + public boolean hasDerivedFile(FSRL containerFSRL, String derivedName, TaskMonitor monitor) throws CancelledException, IOException { - FileCacheEntry cacheEntry = getCacheFile(fsrl, monitor); - String derivedMD5 = fileCacheNameIndex.get(cacheEntry.md5, derivedName); - return derivedMD5 != null; + assertFullyQualifiedFSRL(containerFSRL); + String containerMD5 = containerFSRL.getMD5(); + String derivedMD5 = fileCacheNameIndex.get(containerMD5, derivedName); + return derivedMD5 != null && fileCache.hasEntry(derivedMD5); } /** @@ -582,8 +581,9 @@ public class FileSystemService { */ public boolean isFileFilesystemContainer(FSRL containerFSRL, TaskMonitor monitor) throws CancelledException, IOException { - File containerFile = getFile(containerFSRL, monitor); - return fsFactoryMgr.test(containerFSRL, containerFile, this, monitor); + try (ByteProvider byteProvider = getByteProvider(containerFSRL, false, monitor)) { + return fsFactoryMgr.test(byteProvider, this, monitor); + } } /** @@ -637,43 +637,30 @@ public class FileSystemService { FileSystemProbeConflictResolver conflictResolver, int priorityFilter) throws CancelledException, IOException { - // Fix up FSRL first before querying - containerFSRL = getFullyQualifiedFSRL(containerFSRL, monitor); - - synchronized (filesystemCache) { - containerFSRL = intern(containerFSRL); - FileSystemRef ref = filesystemCache.getFilesystemRefMountedAt(containerFSRL); + synchronized (fsInstanceManager) { + FileSystemRef ref = fsInstanceManager.getFilesystemRefMountedAt(containerFSRL); if (ref != null) { return ref; } - // Special case when the container is really a local filesystem directory. - // Return a LocalFilesystem subfs instance. - if (localFS.isLocalSubdir(containerFSRL)) { - try { - File localDir = new File(containerFSRL.getPath()); - GFileSystem fs = new LocalFileSystemSub(localDir, getLocalFS()); - ref = fs.getRefManager().create(); - filesystemCache.add(fs); - return ref; - } - catch (IOException e) { - Msg.error(this, "Problem when probing for local directory: ", e); - } - return null; + GFileSystem subdirFS = probeForLocalSubDirFilesystem(containerFSRL); + if (subdirFS != null) { + ref = subdirFS.getRefManager().create(); + fsInstanceManager.add(subdirFS); + return ref; } } // Normal case, probe the container file and create a filesystem instance. // Do this outside of the sync lock so if any swing stuff happens in the conflictResolver // it doesn't deadlock us. - File containerFile = getFile(containerFSRL, monitor); try { - GFileSystem fs = fsFactoryMgr.probe(containerFSRL, containerFile, this, - conflictResolver, priorityFilter, monitor); + ByteProvider byteProvider = getByteProvider(containerFSRL, true, monitor); + GFileSystem fs = + fsFactoryMgr.probe(byteProvider, this, conflictResolver, priorityFilter, monitor); if (fs != null) { - synchronized (filesystemCache) { - FileSystemRef fsRef = filesystemCache.getFilesystemRefMountedAt(fs.getFSRL()); + synchronized (fsInstanceManager) { + FileSystemRef fsRef = fsInstanceManager.getFilesystemRefMountedAt(fs.getFSRL()); if (fsRef != null) { // race condition between sync block at top of this func and here. // Throw away our new FS instance and use instance already in @@ -682,7 +669,7 @@ public class FileSystemService { return fsRef; } - filesystemCache.add(fs); + fsInstanceManager.add(fs); return fs.getRefManager().create(); } } @@ -694,6 +681,18 @@ public class FileSystemService { return null; } + private GFileSystem probeForLocalSubDirFilesystem(FSRL containerFSRL) { + if (localFS.isLocalSubdir(containerFSRL)) { + try { + return localFS.getSubFileSystem(containerFSRL); + } + catch (IOException e) { + Msg.error(this, "Problem when probing for local directory: ", e); + } + } + return null; + } + /** * Mount a specific file system (by class) using a specified container file. *

@@ -713,16 +712,15 @@ public class FileSystemService { public FSTYPE mountSpecificFileSystem(FSRL containerFSRL, Class fsClass, TaskMonitor monitor) throws CancelledException, IOException { - containerFSRL = getFullyQualifiedFSRL(containerFSRL, monitor); - File containerFile = getFile(containerFSRL, monitor); String fsType = fsFactoryMgr.getFileSystemType(fsClass); if (fsType == null) { Msg.error(this, "Specific file system implemention " + fsClass.getName() + " not registered correctly in file system factory."); return null; } + ByteProvider byteProvider = getByteProvider(containerFSRL, true, monitor); GFileSystem fs = - fsFactoryMgr.mountFileSystem(fsType, containerFSRL, containerFile, this, monitor); + fsFactoryMgr.mountFileSystem(fsType, byteProvider, this, monitor); Class producedClass = fs.getClass(); if (!fsClass.isAssignableFrom(fs.getClass())) { fs.close(); @@ -750,24 +748,20 @@ public class FileSystemService { public GFileSystem openFileSystemContainer(FSRL containerFSRL, TaskMonitor monitor) throws CancelledException, IOException { - if (localFS.isLocalSubdir(containerFSRL)) { - File localDir = localFS.getLocalFile(containerFSRL); - return new LocalFileSystemSub(localDir, localFS); + GFileSystem subdirFS = probeForLocalSubDirFilesystem(containerFSRL); + if (subdirFS != null) { + return subdirFS; } - File containerFile = getFile(containerFSRL, monitor); - return fsFactoryMgr.probe(containerFSRL, containerFile, this, null, - FileSystemInfo.PRIORITY_LOWEST, monitor); + ByteProvider byteProvider = getByteProvider(containerFSRL, true, monitor); + return fsFactoryMgr.probe(byteProvider, this, null, FileSystemInfo.PRIORITY_LOWEST, + monitor); } /** * Returns a cloned copy of the {@code FSRL} that should have MD5 values specified. * (excluding GFile objects that don't have data streams) *

- * Also implements a best-effort caching of non-root filesystem FSRL's MD5 values. - * (ie. the md5 values of files inside of containers are cached. The md5 value of - * files on the real OS filesystem are not cached) - *

* @param fsrl {@link FSRL} of the file that should be forced to have a MD5 * @param monitor {@link TaskMonitor} to watch and update with progress. * @return possibly new {@link FSRL} instance with a MD5 value. @@ -776,83 +770,63 @@ public class FileSystemService { */ public FSRL getFullyQualifiedFSRL(FSRL fsrl, TaskMonitor monitor) throws CancelledException, IOException { - if (fsrl == null) { - return null; + if (fsrl == null || fsrl.getMD5() != null) { + return fsrl; } - - FSRL fqParentContainer = getFullyQualifiedFSRL(fsrl.getFS().getContainer(), monitor); - - FSRL resultFSRL = (fqParentContainer != fsrl.getFS().getContainer()) - ? FSRLRoot.nestedFS(fqParentContainer, fsrl.getFS()).withPath(fsrl) - : fsrl; - - if (resultFSRL.getMD5() == null) { - String md5 = null; - if (fqParentContainer != null) { - md5 = fileCacheNameIndex.get(fqParentContainer.getMD5(), resultFSRL.getPath()); - } - if (md5 == null) { - try { - md5 = getMD5(resultFSRL, monitor); - if (fqParentContainer != null) { - fileCacheNameIndex.add(fqParentContainer.getMD5(), resultFSRL.getPath(), - md5); - } - } - catch (IOException ioe) { - // ignore, default to no MD5 value - } - } - if (md5 != null) { - resultFSRL = resultFSRL.withMD5(md5); - } + try (FileSystemRef fsRef = getFilesystem(fsrl.getFS(), monitor)) { + return getFullyQualifiedFSRL(fsRef.getFilesystem(), fsrl, monitor); } - - return resultFSRL; } - /** - * Returns true if the specified file is on the local computer's - * filesystem. - * - * @param gfile file to query - * @return true if local, false if the path points to an embedded file in a container. - */ - public boolean isLocal(GFile gfile) { - return gfile.getFSRL().getFS().hasContainer() == false; + private void assertFullyQualifiedFSRL(FSRL fsrl) throws IOException { + if (fsrl.getMD5() == null) { + throw new IOException("Bad FSRL, expected fully qualified: " + fsrl); + } } - /** - * Returns true if the specified location is a path on the local computer's - * filesystem. - * - * @param fsrl {@link FSRL} path to query - * @return true if local, false if the path points to an embedded file in a container. - */ - public boolean isLocal(FSRL fsrl) { - return fsrl.getFS().hasContainer() == false; - } - - public String getFileHash(GFile gfile, TaskMonitor monitor) + private FSRL getFullyQualifiedFSRL(GFileSystem fs, FSRL fsrl, TaskMonitor monitor) throws CancelledException, IOException { - if (isLocal(gfile)) { - File f = localFS.getLocalFile(gfile.getFSRL()); - if (f.isFile()) { - return FSUtilities.getFileMD5(f, monitor); - } + if (fsrl.getMD5() != null) { + return fsrl; } - else { - try (InputStream dataStream = gfile.getFilesystem().getInputStream(gfile, monitor)) { - if (dataStream == null) { - throw new IOException("Unable to get datastream for " + gfile.getFSRL()); + GFile file = fs.lookup(fsrl.getPath()); + if (file == null) { + throw new IOException("File not found: " + fsrl); + } + if (file.getFSRL().getMD5() != null || file.isDirectory()) { + return file.getFSRL(); + } + + FSRL containerFSRL = fsrl.getFS().getContainer(); + if (containerFSRL != null && containerFSRL.getMD5() == null) { + // re-home the fsrl to the parent container's fsrl since + // filesystems will always have fully qualified fsrl + containerFSRL = fs.getFSRL().getContainer(); + fsrl = FSRLRoot.nestedFS(containerFSRL, fsrl.getFS()).withPath(fsrl); + } + + if (fs instanceof GFileHashProvider) { + GFileHashProvider hashProvider = (GFileHashProvider) fs; + return fsrl.withMD5(hashProvider.getMD5Hash(file, true, monitor)); + } + + String md5 = (containerFSRL != null) + ? fileCacheNameIndex.get(containerFSRL.getMD5(), fsrl.getPath()) + : null; + if (md5 == null) { + try (ByteProvider bp = fs.getByteProvider(file, monitor)) { + if (bp == null) { + throw new IOException("Unable to get bytes for " + fsrl); } - monitor.setMessage("Caching " + gfile.getName()); - monitor.initialize(gfile.getLength()); - FileCacheEntry cfi = fileCache.addStream(dataStream, monitor); - return cfi.md5; + md5 = (bp.getFSRL().getMD5() != null) + ? bp.getFSRL().getMD5() + : FSUtilities.getMD5(bp, monitor); } } - return null; + if (containerFSRL != null && fs.isStatic()) { + fileCacheNameIndex.add(containerFSRL.getMD5(), fsrl.getPath(), md5); + } + return fsrl.withMD5(md5); } /** @@ -875,8 +849,8 @@ public class FileSystemService { * @return {@link List} of {@link FSRLRoot} of currently mounted filesystems. */ public List getMountedFilesystems() { - synchronized (filesystemCache) { - return filesystemCache.getMountedFilesystems(); + synchronized (fsInstanceManager) { + return fsInstanceManager.getMountedFilesystems(); } } @@ -891,58 +865,30 @@ public class FileSystemService { * @return new {@link FileSystemRef} or null if requested file system not mounted. */ public FileSystemRef getMountedFilesystem(FSRLRoot fsFSRL) { - synchronized (filesystemCache) { - return filesystemCache.getRef(fsFSRL); + synchronized (fsInstanceManager) { + return fsInstanceManager.getRef(fsFSRL); } - } /** - * Interns the FSRLRoot so that its parent parts are shared with already interned instances. + * Returns a new {@link CryptoSession} that the caller can use to query for + * passwords and such. Caller is responsible for closing the instance when done. *

- * Caller needs to hold sync mutex - * - * @param fsrl {@link FSRLRoot} to intern-alize. - * @return possibly different {@link FSRLRoot} instance that has shared parent references - * instead of unique bespoke instances. + * Later callers to this method will receive a nested CryptoSession that shares it's + * state with the initial CryptoSession, until the initial CryptoSession is closed(). + * + * @return new {@link CryptoSession} instance, never null */ - private FSRLRoot intern(FSRLRoot fsrl) { - if (localFSRL.equals(fsrl)) { - return localFSRL; + public synchronized CryptoSession newCryptoSession() { + if (currentCryptoSession == null || currentCryptoSession.isClosed()) { + // If no this no current open cryptosession, return a new full/independent + // cryptosession, and use it as the parent for any subsequent sessions + currentCryptoSession = CryptoProviders.getInstance().newSession(); + return currentCryptoSession; } - FSRL container = fsrl.getContainer(); - if (container != null) { - FSRLRoot parentFSRL = intern(container.getFS()); - if (parentFSRL != container.getFS()) { - FSRL internedContainer = parentFSRL.withPath(container); - fsrl = FSRLRoot.nestedFS(internedContainer, fsrl); - } - } - FSRLRoot existing = fsrlInternMap.get(fsrl); - if (existing == null) { - fsrlInternMap.put(fsrl, fsrl); - existing = fsrl; - } - - return existing; - } - - /** - * Interns the FSRL so that its parent parts are shared with already interned instances. - *

- * Caller needs to hold sync mutex. - *

- * Only {@link FSRLRoot} instances are cached in the intern map, {@link FSRL} instances - * are not. - * - * @param fsrl {@link FSRL} to intern-alize. - * @return possibly different {@link FSRL} instance that has shared parent references - * instead of unique bespoke instances. - */ - private FSRL intern(FSRL fsrl) { - FSRLRoot internedRoot = intern(fsrl.getFS()); - return internedRoot == fsrl.getFS() ? fsrl : internedRoot.withPath(fsrl); + // return a nested / dependent cryptosession + return new CryptoProviderSessionChildImpl(currentCryptoSession); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileHashProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileHashProvider.java new file mode 100644 index 0000000000..dd139a0b23 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileHashProvider.java @@ -0,0 +1,40 @@ +/* ### + * 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.formats.gfilesystem; + +import java.io.IOException; + +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * GFileSystem add-on interface that provides MD5 hashing for file located within the filesystem + */ +public interface GFileHashProvider { + /** + * Returns the MD5 hash of the specified file. + * + * @param file the {@link GFile} + * @param required boolean flag, if true the hash will always be returned, even if it has to + * be calculated. If false, the hash will be returned if easily available + * @param monitor {@link TaskMonitor} + * @return MD5 hash as a string + * @throws CancelledException if cancelled + * @throws IOException if error + */ + String getMD5Hash(GFile file, boolean required, TaskMonitor monitor) + throws CancelledException, IOException; +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileImpl.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileImpl.java index 09b43fa5bc..b369796fa9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileImpl.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileImpl.java @@ -15,7 +15,7 @@ */ package ghidra.formats.gfilesystem; -import ghidra.util.SystemUtilities; +import java.util.Objects; /** * Base implementation of file in a {@link GFileSystem filesystem}. @@ -140,11 +140,11 @@ public class GFileImpl implements GFile { return new GFileImpl(fileSystem, parent, isDirectory, length, fsrl); } - private GFileSystem fileSystem; - private GFile parentFile; - private boolean isDirectory = false; - private long length = -1; - private FSRL fsrl; + private final GFileSystem fileSystem; + private final GFile parentFile; + private final boolean isDirectory; + private long length; + private final FSRL fsrl; /** * Protected constructor, use static helper methods to create new instances. @@ -197,21 +197,6 @@ public class GFileImpl implements GFile { return getPath(); } - @Override - public boolean equals(Object obj) { - if (!(obj instanceof GFile)) { - return false; - } - - GFile other = (GFile) obj; - return SystemUtilities.isEqual(fsrl, other.getFSRL()) && isDirectory == other.isDirectory(); - } - - @Override - public int hashCode() { - return fsrl.hashCode() ^ Boolean.hashCode(isDirectory()); - } - @Override public String getPath() { return fsrl.getPath(); @@ -226,7 +211,22 @@ public class GFileImpl implements GFile { return fsrl; } - public void setFSRL(FSRL fsrl) { - this.fsrl = fsrl; + @Override + public int hashCode() { + return Objects.hash(fileSystem, fsrl.getPath(), isDirectory); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof GFile)) { + return false; + } + GFile other = (GFile) obj; + return Objects.equals(fileSystem, other.getFilesystem()) && + Objects.equals(fsrl.getPath(), other.getFSRL().getPath()) && + isDirectory == other.isDirectory(); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystem.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystem.java index ed7143acb5..975367dab3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystem.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystem.java @@ -15,12 +15,14 @@ */ package ghidra.formats.gfilesystem; -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.util.List; +import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.ByteProviderInputStream; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; +import ghidra.formats.gfilesystem.fileinfo.FileAttribute; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.classfinder.ExtensionPoint; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -30,8 +32,9 @@ import ghidra.util.task.TaskMonitor; *

* Operations take a {@link TaskMonitor} if they need to be cancel-able. *

- * Use {@link FileSystemService} to discover and open instances of filesystems in files or - * to open a known {@link FSRL} path. + * Use a {@link FileSystemService FileSystemService instance} to discover and + * open instances of filesystems in files or to open a known {@link FSRL} path or to + * deal with creating {@link FileSystemService#createTempFile(long) temp files}. *

* NOTE:

* ALL GFileSystem sub-CLASSES MUST END IN "FileSystem". If not, the ClassSearcher @@ -137,7 +140,24 @@ public interface GFileSystem extends Closeable, ExtensionPoint { * @throws IOException if IO problem * @throws CancelledException if user cancels. */ - public InputStream getInputStream(GFile file, TaskMonitor monitor) + default public InputStream getInputStream(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { + return getInputStreamHelper(file, this, monitor); + } + + /** + * Returns a {@link ByteProvider} that contains the contents of the specified {@link GFile}. + *

+ * The caller is responsible for closing the provider. + * + * @param file {@link GFile} to get bytes for + * @param monitor {@link TaskMonitor} to watch and update progress + * @return new {@link ByteProvider} that contains the contents of the file, or NULL if file + * doesn't have data + * @throws IOException if error + * @throws CancelledException if user cancels + */ + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException; /** @@ -151,17 +171,36 @@ public interface GFileSystem extends Closeable, ExtensionPoint { public List getListing(GFile directory) throws IOException; /** - * Returns a multi-line string with information about the specified {@link GFile file}. + * Returns a container of {@link FileAttribute} values. *

- * TODO:{@literal this method needs to be refactored to return a Map; instead of} - * a pre-formatted multi-line string. - *

- * @param file {@link GFile} to get info message for. - * @param monitor {@link TaskMonitor} to watch and update progress. - * @return multi-line formatted string with info about the file, or null. + * Implementors of this method are not required to add FSRL, NAME, or PATH values unless + * the values are non-standard. + * + * @param file {@link GFile} to get the attributes for + * @param monitor {@link TaskMonitor} + * @return {@link FileAttributes} instance (possibly read-only), maybe empty but never null */ - default public String getInfo(GFile file, TaskMonitor monitor) { - return null; + default public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + return FileAttributes.EMPTY; + } + + /** + * Default implementation of getting an {@link InputStream} from a {@link GFile}'s + * {@link ByteProvider}. + *

+ * + * @param file {@link GFile} + * @param fs the {@link GFileSystem filesystem} containing the file + * @param monitor {@link TaskMonitor} to allow canceling + * @return new {@link InputStream} containing bytes of the file + * @throws CancelledException if canceled + * @throws IOException if error + */ + public static InputStream getInputStreamHelper(GFile file, GFileSystem fs, TaskMonitor monitor) + throws CancelledException, IOException { + ByteProvider bp = fs.getByteProvider(file, monitor); + return (bp != null) ? new ByteProviderInputStream.ClosingInputStream(bp) : null; + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystemBase.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystemBase.java index 21ab576ccb..d860246646 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystemBase.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GFileSystemBase.java @@ -129,26 +129,6 @@ public abstract class GFileSystemBase implements GFileSystem { @Override abstract public List getListing(GFile directory) throws IOException; - /** - * Legacy implementation of {@link #getInputStream(GFile, TaskMonitor)}. - * - * @param file {@link GFile} to get an InputStream for - * @param monitor {@link TaskMonitor} to watch and update progress - * @return new {@link InputStream} contains the contents of the file or NULL if the - * file doesn't have data. - * @throws IOException if IO problem - * @throws CancelledException if user cancels. - * @throws CryptoException if crypto problem. - */ - abstract protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException; - - @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) - throws CancelledException, IOException { - return getData(file, monitor); - } - /** * Writes the given bytes to a tempfile in the temp directory. * @param bytes the bytes to write diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GIconProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GIconProvider.java index 468e42962c..200547b953 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GIconProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/GIconProvider.java @@ -15,18 +15,18 @@ */ package ghidra.formats.gfilesystem; -import ghidra.util.Msg; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - import java.awt.Image; -import java.io.File; import java.io.IOException; +import java.io.InputStream; import javax.imageio.ImageIO; import javax.swing.Icon; import javax.swing.ImageIcon; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + /** * {@link GFileSystem} add-on interface to allow filesystems to override how image files * are converted into viewable {@link Icon} instances. @@ -61,9 +61,8 @@ public interface GIconProvider { return ((GIconProvider) fs).getIcon(file, monitor); } - File data = FileSystemService.getInstance().getFile(file.getFSRL(), monitor); - try { - Image image = ImageIO.read(data); + try (InputStream is = file.getFilesystem().getInputStream(file, monitor)) { + Image image = ImageIO.read(is); if (image == null) { return null; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystem.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystem.java index 01b6ad37c0..313f407ec7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystem.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystem.java @@ -15,15 +15,22 @@ */ package ghidra.formats.gfilesystem; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + import java.io.*; -import java.nio.file.Files; +import java.nio.file.*; import java.util.*; +import org.apache.commons.collections4.map.ReferenceMap; import org.apache.commons.io.FilenameUtils; +import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.FileByteProvider; 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.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; /** @@ -36,7 +43,7 @@ import ghidra.util.task.TaskMonitor; * Closing() this filesystem does nothing. */ @FileSystemInfo(type = LocalFileSystem.FSTYPE, description = "Local filesystem", factory = GFileSystemFactoryIgnore.class) -public class LocalFileSystem implements GFileSystem { +public class LocalFileSystem implements GFileSystem, GFileHashProvider { public static final String FSTYPE = "file"; /** @@ -48,9 +55,11 @@ public class LocalFileSystem implements GFileSystem { return new LocalFileSystem(FSRLRoot.makeRoot(FSTYPE)); } - private final List emptyDir = Collections.emptyList(); + private final List emptyDir = List.of(); private final FSRLRoot fsFSRL; private final FileSystemRefManager refManager = new FileSystemRefManager(this); + private final ReferenceMap fileFingerprintToMD5Map = + new ReferenceMap<>(); private LocalFileSystem(FSRLRoot fsrl) { this.fsFSRL = fsrl; @@ -60,6 +69,21 @@ public class LocalFileSystem implements GFileSystem { return fsFSRL.equals(fsrl.getFS()); } + /** + * Creates a new file system instance that is a sub-view limited to the specified directory. + * + * @param fsrl {@link FSRL} that must be a directory in this local filesystem + * @return new {@link LocalFileSystemSub} instance + * @throws IOException if bad FSRL + */ + public LocalFileSystemSub getSubFileSystem(FSRL fsrl) throws IOException { + if (isLocalSubdir(fsrl)) { + File localDir = getLocalFile(fsrl); + return new LocalFileSystemSub(localDir, this); + } + return null; + } + /** * Returns true if the {@link FSRL} is a local filesystem subdirectory. * @@ -74,6 +98,13 @@ public class LocalFileSystem implements GFileSystem { return localFile.isDirectory(); } + /** + * Convert a FSRL that points to this file system into a java {@link File}. + * + * @param fsrl {@link FSRL} + * @return {@link File} + * @throws IOException if FSRL does not point to this file system + */ public File getLocalFile(FSRL fsrl) throws IOException { if (!isSameFS(fsrl)) { throw new IOException("FSRL does not specify local file: " + fsrl); @@ -82,6 +113,17 @@ public class LocalFileSystem implements GFileSystem { return localFile; } + /** + * Converts a {@link File} into a {@link FSRL}. + * + * @param f {@link File} + * @return {@link FSRL} + */ + public FSRL getLocalFSRL(File f) { + return fsFSRL + .withPath(FSUtilities.appendPath("/", FilenameUtils.separatorsToUnix(f.getPath()))); + } + @Override public String getName() { return "Root Filesystem"; @@ -130,14 +172,48 @@ public class LocalFileSystem implements GFileSystem { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { - File localFile = new File(file.getPath()); + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + File f = new File(file.getPath()); + return getFileAttributes(f); + } - StringBuilder buffer = new StringBuilder(); - buffer.append("Name: " + localFile.getName() + "\n"); - buffer.append("Size: " + localFile.length() + "\n"); - buffer.append("Date: " + new Date(localFile.lastModified()).toString() + "\n"); - return buffer.toString(); + /** + * Create a {@link FileAttributes} container with info about the specified local file. + * + * @param f {@link File} to query + * @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 + } + 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; } @Override @@ -153,12 +229,6 @@ public class LocalFileSystem implements GFileSystem { return gf; } - @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) throws IOException { - File f = new File(file.getPath()); - return new FileInputStream(f); - } - @Override public boolean isClosed() { return false; @@ -169,8 +239,103 @@ public class LocalFileSystem implements GFileSystem { return refManager; } + @Override + public InputStream getInputStream(GFile file, TaskMonitor monitor) throws IOException { + return getInputStream(file.getFSRL(), monitor); + } + + InputStream getInputStream(FSRL fsrl, TaskMonitor monitor) throws IOException { + return new FileInputStream(getLocalFile(fsrl)); + } + + @Override + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException { + return getByteProvider(file.getFSRL(), monitor); + } + + ByteProvider getByteProvider(FSRL fsrl, TaskMonitor monitor) throws IOException { + File f = getLocalFile(fsrl); + return new FileByteProvider(f, fsrl, AccessMode.READ); + } + @Override public String toString() { return "Local file system " + fsFSRL; } + + @Override + public String getMD5Hash(GFile file, boolean required, TaskMonitor monitor) + throws CancelledException, IOException { + return getMD5Hash(file.getFSRL(), required, monitor); + } + + synchronized String getMD5Hash(FSRL fsrl, boolean required, TaskMonitor monitor) + throws CancelledException, IOException { + File f = getLocalFile(fsrl); + if ( !f.isFile() ) { + return null; + } + + FileFingerprintRec fileFingerprintRec = new FileFingerprintRec(f.getPath(), f.lastModified(), f.length()); + String md5 = fileFingerprintToMD5Map.get(fileFingerprintRec); + if (md5 == null && required) { + md5 = FSUtilities.getFileMD5(f, monitor); + fileFingerprintToMD5Map.put(fileFingerprintRec, md5); + } + + return md5; + } + + //----------------------------------------------------------------------------------- + + private static class FileFingerprintRec { + final String path; + final long timestamp; + final long length; + + FileFingerprintRec(String path, long timestamp, long length) { + this.path = path; + this.timestamp = timestamp; + this.length = length; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (length ^ (length >>> 32)); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + result = prime * result + (int) (timestamp ^ (timestamp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof FileFingerprintRec)) { + return false; + } + FileFingerprintRec other = (FileFingerprintRec) obj; + if (length != other.length) { + return false; + } + if (path == null) { + if (other.path != null) { + return false; + } + } + else if (!path.equals(other.path)) { + return false; + } + if (timestamp != other.timestamp) { + return false; + } + return true; + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystemSub.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystemSub.java index 94701c4a91..41a5505984 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystemSub.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/LocalFileSystemSub.java @@ -17,10 +17,13 @@ package ghidra.formats.gfilesystem; import java.io.*; import java.nio.file.Files; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import org.apache.commons.lang3.StringUtils; +import ghidra.app.util.bin.ByteProvider; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -35,24 +38,20 @@ import ghidra.util.task.TaskMonitor; * by the FileSystemFactoryMgr. * */ -public class LocalFileSystemSub implements GFileSystem { +public class LocalFileSystemSub implements GFileSystem, GFileHashProvider { private final FSRLRoot fsFSRL; - private final GFileSystem rootFS; - private final List emptyDir = Collections.emptyList(); + private final LocalFileSystem rootFS; private File localfsRootDir; private FileSystemRefManager refManager = new FileSystemRefManager(this); private GFileLocal rootGFile; - public LocalFileSystemSub(File rootDir, GFileSystem rootFS) throws IOException { + public LocalFileSystemSub(File rootDir, LocalFileSystem rootFS) throws IOException { this.rootFS = rootFS; this.localfsRootDir = rootDir.getCanonicalFile(); - GFile containerDir = rootFS.lookup(localfsRootDir.getPath()); - if (containerDir == null) { - throw new IOException("Bad root dir: " + rootDir); - } - this.fsFSRL = FSRLRoot.nestedFS(containerDir.getFSRL(), rootFS.getFSRL().getProtocol()); - this.rootGFile = new GFileLocal(localfsRootDir, "/", containerDir.getFSRL(), this, null); + FSRL containerFSRL = rootFS.getLocalFSRL(localfsRootDir); + this.fsFSRL = FSRLRoot.nestedFS(containerFSRL, rootFS.getFSRL().getProtocol()); + this.rootGFile = new GFileLocal(localfsRootDir, "/", containerFSRL, this, null); } @Override @@ -97,17 +96,17 @@ public class LocalFileSystemSub implements GFileSystem { directory = rootGFile; } if (!directory.isDirectory()) { - return emptyDir; + return List.of(); } File localDir = getFileFromGFile(directory); if (Files.isSymbolicLink(localDir.toPath())) { - return emptyDir; + return List.of(); } File[] localFiles = localDir.listFiles(); if (localFiles == null) { - return emptyDir; + return List.of(); } List tmp = new ArrayList<>(localFiles.length); @@ -130,19 +129,15 @@ public class LocalFileSystemSub implements GFileSystem { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { try { File localFile = getFileFromGFile(file); - StringBuilder buffer = new StringBuilder(); - buffer.append("Name: " + localFile.getName() + "\n"); - buffer.append("Size: " + localFile.length() + "\n"); - buffer.append("Date: " + new Date(localFile.lastModified()).toString() + "\n"); - return buffer.toString(); + return rootFS.getFileAttributes(localFile); } catch (IOException e) { // fail and return null } - return null; + return FileAttributes.EMPTY; } @Override @@ -179,8 +174,7 @@ public class LocalFileSystemSub implements GFileSystem { @Override public InputStream getInputStream(GFile file, TaskMonitor monitor) throws IOException, CancelledException { - - return new FileInputStream(getFileFromGFile(file)); + return rootFS.getInputStream(file.getFSRL(), monitor); } @Override @@ -192,4 +186,16 @@ public class LocalFileSystemSub implements GFileSystem { public String toString() { return getName(); } + + @Override + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { + return rootFS.getByteProvider(file.getFSRL(), monitor); + } + + @Override + public String getMD5Hash(GFile file, boolean required, TaskMonitor monitor) + throws CancelledException, IOException { + return rootFS.getMD5Hash(file.getFSRL(), required, monitor); + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdByteProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdByteProvider.java new file mode 100644 index 0000000000..bb481b2cc8 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdByteProvider.java @@ -0,0 +1,98 @@ +/* ### + * 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.formats.gfilesystem; + +import java.io.File; +import java.io.IOException; + +import ghidra.app.util.bin.ByteProvider; + +/** + * A {@link ByteProvider} along with a {@link FileSystemRef} to keep the filesystem pinned + * in memory. + *

+ * The caller is responsible for {@link #close() closing} this object, which releases + * the FilesystemRef. + */ +public class RefdByteProvider implements ByteProvider { + private final FileSystemRef fsRef; + private final ByteProvider provider; + private final FSRL fsrl; + + /** + * Creates a RefdByteProvider instance, taking ownership of the supplied FileSystemRef. + * + * @param fsRef {@link FileSystemRef} that contains the specified ByteProvider + * @param provider {@link ByteProvider} inside the filesystem held open by the ref + * @param fsrl {@link FSRL} identity of this new ByteProvider + */ + public RefdByteProvider(FileSystemRef fsRef, ByteProvider provider, FSRL fsrl) { + this.fsRef = fsRef; + this.provider = provider; + this.fsrl = fsrl; + } + + @Override + public void close() throws IOException { + provider.close(); + fsRef.close(); + } + + @Override + public FSRL getFSRL() { + return fsrl; + } + + @Override + public File getFile() { + return provider.getFile(); + } + + @Override + public String getName() { + return fsrl != null ? fsrl.getName() : provider.getName(); + } + + @Override + public String getAbsolutePath() { + return fsrl != null ? fsrl.getPath() : provider.getAbsolutePath(); + } + + @Override + public long length() throws IOException { + return provider.length(); + } + + @Override + public boolean isValidIndex(long index) { + return provider.isValidIndex(index); + } + + @Override + public byte readByte(long index) throws IOException { + return provider.readByte(index); + } + + @Override + public byte[] readBytes(long index, long length) throws IOException { + return provider.readBytes(index, length); + } + + @Override + public String toString() { + return "ByteProvider " + provider.getFSRL() + " in file system " + fsRef.getFilesystem(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdFile.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdFile.java index 6ce850dac5..112a4951d8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdFile.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdFile.java @@ -29,6 +29,12 @@ public class RefdFile implements Closeable { public final FileSystemRef fsRef; public final GFile file; + /** + * Creates a RefdFile instance, taking ownership of the supplied fsRef. + * + * @param fsRef {@link FileSystemRef} that pins the filesystem open + * @param file GFile file inside the specified filesystem + */ public RefdFile(FileSystemRef fsRef, GFile file) { this.fsRef = fsRef; this.file = file; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SelectFromListDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SelectFromListDialog.java index 60869e5594..b1236c7a6f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SelectFromListDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SelectFromListDialog.java @@ -15,8 +15,6 @@ */ package ghidra.formats.gfilesystem; -import ghidra.util.SystemUtilities; - import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -29,6 +27,7 @@ import docking.DialogComponentProvider; import docking.DockingWindowManager; import docking.widgets.MultiLineLabel; import docking.widgets.list.ListPanel; +import ghidra.util.SystemUtilities; /** * Dialog that presents the user with a list of strings and returns the object @@ -96,17 +95,17 @@ public class SelectFromListDialog extends DialogComponentProvider { private void doSelect() { selectedObject = null; actionComplete = false; - DockingWindowManager activeInstance = DockingWindowManager.getActiveInstance(); - activeInstance.showDialog(this); + DockingWindowManager.showDialog(this); if (actionComplete) { selectedObject = list.get(listPanel.getSelectedIndex()); } } private JPanel buildWorkPanel(String prompt) { - DefaultListModel listModel = new DefaultListModel() { + DefaultListModel listModel = new DefaultListModel<>() { @Override public String getElementAt(int index) { + @SuppressWarnings("unchecked") T t = (T) super.getElementAt(index); return toStringFunc.apply(t); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SingleFileSystemIndexHelper.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SingleFileSystemIndexHelper.java index ef7c5b413e..2195a2bab7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SingleFileSystemIndexHelper.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SingleFileSystemIndexHelper.java @@ -141,7 +141,7 @@ public class SingleFileSystemIndexHelper { * the payload file. */ public GFile lookup(String path) { - if (path.equals("/")) { + if (path == null || path.equals("/")) { return rootDir; } else if (path.equals(payloadFile.getFSRL().getPath())) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CachedPasswordProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CachedPasswordProvider.java new file mode 100644 index 0000000000..7ce6e04778 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CachedPasswordProvider.java @@ -0,0 +1,143 @@ +/* ### + * 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.formats.gfilesystem.crypto; + +import java.util.*; + +import ghidra.formats.gfilesystem.FSRL; + +/** + * Caches passwords used to unlock a file. + *

+ * Threadsafe. + */ +public class CachedPasswordProvider implements PasswordProvider { + + + private Map> values = new HashMap<>(); + private int count; + + /** + * Adds a password / file combo to the cache. + * + * @param fsrl {@link FSRL} file + * @param password password to unlock the file. Specified PasswordValue is + * only copied, clearing is still callers responsibility + */ + public synchronized void addPassword(FSRL fsrl, PasswordValue password) { + CryptoRec rec = new CryptoRec(); + rec.fsrl = fsrl; + rec.value = password.clone(); + addRec(rec); + } + + + private void addRec(CryptoRec rec) { + // index the record by its full FSRL, a simplified FSRL, its plain filename, and any MD5 + String fsrlStr = rec.fsrl.toString(); + boolean isNewValue = + addIfUnique(values.computeIfAbsent(fsrlStr, x -> new ArrayList<>()), rec); + + String fsrlStr2 = rec.fsrl.toPrettyString(); + if (!fsrlStr2.equals(fsrlStr)) { + addIfUnique(values.computeIfAbsent(fsrlStr2, x -> new ArrayList<>()), rec); + } + + addIfUnique(values.computeIfAbsent(rec.fsrl.getName(), x -> new ArrayList<>()), rec); + + if (rec.fsrl.getMD5() != null) { + addIfUnique(values.computeIfAbsent(rec.fsrl.getMD5(), x -> new ArrayList<>()), rec); + } + + if (isNewValue) { + count++; + } + } + + private boolean addIfUnique(List recs, CryptoRec newRec) { + for (CryptoRec rec : recs) { + if (rec.value.equals(newRec.value)) { + return false; + } + } + recs.add(newRec); + return true; + } + + /** + * Remove all cached information. + */ + public synchronized void clearCache() { + values.clear(); + count = 0; + } + + /** + * Returns the number of items in cache + * + * @return number of items in cache + */ + public synchronized int getCount() { + return count; + } + + @Override + public synchronized Iterator getPasswordsFor(FSRL fsrl, String prompt, + Session session) { + Set uniqueFoundRecs = new LinkedHashSet<>(); + uniqueFoundRecs.addAll(values.getOrDefault(fsrl.toString(), Collections.emptyList())); + uniqueFoundRecs.addAll(values.getOrDefault(fsrl.toPrettyString(), Collections.emptyList())); + uniqueFoundRecs.addAll(values.getOrDefault(fsrl.getName(), Collections.emptyList())); + if (fsrl.getMD5() != null) { + uniqueFoundRecs.addAll(values.getOrDefault(fsrl.getMD5(), Collections.emptyList())); + } + + List results = new ArrayList<>(); + for (CryptoRec rec : uniqueFoundRecs) { + results.add(rec.value); + } + + // Use an iterator that clones the values before giving them to the caller + // so our internal values don't get cleared + return new CloningPasswordIterator(results.iterator()); + } + + private static class CryptoRec { + FSRL fsrl; + PasswordValue value; + } + + private class CloningPasswordIterator implements Iterator { + Iterator delegate; + + CloningPasswordIterator(Iterator it) { + this.delegate = it; + } + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public PasswordValue next() { + PasswordValue result = delegate.next(); + return result.clone(); + } + + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CmdLinePasswordProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CmdLinePasswordProvider.java new file mode 100644 index 0000000000..87e65872c8 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CmdLinePasswordProvider.java @@ -0,0 +1,115 @@ +/* ### + * 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.formats.gfilesystem.crypto; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.*; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.formats.gfilesystem.FSRL; +import ghidra.util.Msg; +import utilities.util.FileUtilities; + +/** + * A {@link PasswordProvider} that supplies passwords to decrypt files via the java jvm invocation. + *

+ * Example:

java -Dfilesystem.passwords=/fullpath/to/textfile
+ *

+ * The password file is a plain text tabbed-csv file, where each line + * specifies a password and an optional file identifier. + *

+ * Example file contents, where each line is divided into fields by a tab + * character where the first field is the password and the second optional field + * is the file's identifying information (name, path, etc): + *

+ *

+ * password1   [tab]   myfirstzipfile.zip ← supplies a password for the named file located in any directory
+ * someOtherPassword   [tab]   /full/path/tozipfile.zip ← supplies password for file at specified location 
+ * anotherPassword [tab]   file:///full/path/tozipfile.zip|zip:///subdir/in/zip/somefile.txt ← supplies password for file embedded inside a zip
+ * yetAnotherPassword ← a password to try for any file that needs a password
+ * 
+ * + * + */ +public class CmdLinePasswordProvider implements PasswordProvider { + public static final String CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME = "filesystem.passwords"; + + @Override + public Iterator getPasswordsFor(FSRL fsrl, String prompt, Session session) { + String propertyValue = System.getProperty(CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME); + if (propertyValue == null) { + return Collections.emptyIterator(); + } + File passwordFile = new File(propertyValue); + return load(passwordFile, fsrl).iterator(); + } + + private List load(File f, FSRL fsrl) { + List result = new ArrayList<>(); + try { + for (String s : FileUtilities.getLines(f)) { + String[] fields = s.split("\t"); + String password = fields[0]; + if (password.isBlank()) { + continue; + } + String fileIdStr = fields.length > 1 ? fields[1] : null; + + if (fileIdStr == null) { + // no file identifier string, always matches + result.add(PasswordValue.wrap(password.toCharArray())); + continue; + } + + // try to match the name string as a FSRL, a path or a plain name. + try { + FSRL currentFSRL = FSRL.fromString(fileIdStr); + // was a fsrl string, only test as fsrl + if (currentFSRL.isEquivalent(fsrl)) { + result.add(PasswordValue.wrap(password.toCharArray())); + } + continue; + } + catch (MalformedURLException e) { + // ignore + } + String nameOnly = FilenameUtils.getName(fileIdStr); + if (!nameOnly.equals(fileIdStr)) { + // was a path str, only test against path component + if (fileIdStr.equals(fsrl.getPath())) { + result.add(PasswordValue.wrap(password.toCharArray())); + } + continue; + } + + // was a plain name, only test against name component + if (nameOnly.equals(fsrl.getName())) { + result.add(PasswordValue.wrap(password.toCharArray())); + continue; + } + // no matches, try next line + } + } + catch (IOException e) { + Msg.warn(this, "Error reading passwords from file: " + f, e); + } + + return result; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProvider.java new file mode 100644 index 0000000000..235bdb8286 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProvider.java @@ -0,0 +1,55 @@ +/* ### + * 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.formats.gfilesystem.crypto; + +import java.util.function.Supplier; + +/** + * Common interface for provider interfaces that provide crypto information. + *

+ * TODO: add CryptoKeyProvider. + */ +public interface CryptoProvider { + + interface Session { + /** + * Saves a state object into the session using the cryptoprovider's identity as the key + * + * @param cryptoProvider the instance storing the value + * @param value the value to store + */ + void setStateValue(CryptoProvider cryptoProvider, Object value); + + /** + * Retrieves a state object from the session + * + * @param the type of the state object + * @param cryptoProvider the CryptoProvider instance + * @param stateFactory supplier that will create a new instance of the requested + * state object if not present in the session + * @return state object (either previously saved or newly created by the factory supplier) + */ + T getStateValue(CryptoProvider cryptoProvider, Supplier stateFactory); + + /** + * Returns the {@link CryptoProviders} instance that created this session. + * + * @return the {@link CryptoProviders} instance that created this session + */ + CryptoProviders getCryptoProviders(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProviderSessionChildImpl.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProviderSessionChildImpl.java new file mode 100644 index 0000000000..13a55dea4c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProviderSessionChildImpl.java @@ -0,0 +1,53 @@ +/* ### + * 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.formats.gfilesystem.crypto; + +import java.util.Iterator; + +import ghidra.formats.gfilesystem.FSRL; + +/** + * A stub implementation of CryptoSession that relies on a parent instance. + */ +public class CryptoProviderSessionChildImpl implements CryptoSession { + + private CryptoSession parentSession; + + public CryptoProviderSessionChildImpl(CryptoSession parentSession) { + this.parentSession = parentSession; + } + + @Override + public void close() { + // don't close parent + } + + @Override + public boolean isClosed() { + return parentSession.isClosed(); + } + + @Override + public Iterator getPasswordsFor(FSRL fsrl, String prompt) { + return parentSession.getPasswordsFor(fsrl, prompt); + } + + @Override + public void addSuccessfulPassword(FSRL fsrl, PasswordValue password) { + parentSession.addSuccessfulPassword(fsrl, password); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProviders.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProviders.java new file mode 100644 index 0000000000..345510d300 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoProviders.java @@ -0,0 +1,205 @@ +/* ### + * 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.formats.gfilesystem.crypto; + +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import ghidra.formats.gfilesystem.FSRL; + +/** + * Registry of {@link CryptoProvider crypto providers} and {@link #newSession() session creator}. + */ +public class CryptoProviders { + private static final CryptoProviders singletonInstance = new CryptoProviders(); + + /** + * Fetch the global {@link CryptoProviders} singleton instance. + * + * @return shared {@link CryptoProviders} singleton instance + */ + public static CryptoProviders getInstance() { + return singletonInstance; + } + + private CachedPasswordProvider cachedCryptoProvider; + private List cryptoProviders = new CopyOnWriteArrayList<>(); + + CryptoProviders() { + initPasswordCryptoProviders(); + } + + private void initPasswordCryptoProviders() { + cachedCryptoProvider = new CachedPasswordProvider(); + CmdLinePasswordProvider runtimePasswords = new CmdLinePasswordProvider(); + + registerCryptoProvider(runtimePasswords); + registerCryptoProvider(cachedCryptoProvider); + } + + /** + * Adds a {@link CryptoProvider} to this registry. + *

+ * TODO: do we need provider priority ordering? + * + * @param provider {@link CryptoProvider} + */ + public void registerCryptoProvider(CryptoProvider provider) { + cryptoProviders.add(provider); + } + + /** + * Removes a {@link CryptoProvider} from this registry. + * + * @param provider {@link CryptoProvider} to remove + */ + public void unregisterCryptoProvider(CryptoProvider provider) { + cryptoProviders.remove(provider); + } + + /** + * Returns the {@link CachedPasswordProvider}. + *

+ * (Used by GUI actions to manage the cache) + * + * @return cached crypto provider instance + */ + public CachedPasswordProvider getCachedCryptoProvider() { + return cachedCryptoProvider; + } + + /** + * Returns the previously registered matching {@link CryptoProvider} instance. + * + * @param CryptoProvider type + * @param providerClass {@link CryptoProvider} class + * @return previously registered CryptoProvider instance, or null if not found + */ + public T getCryptoProviderInstance(Class providerClass) { + return cryptoProviders.stream() + .filter(providerClass::isInstance) + .map(providerClass::cast) + .findFirst() + .orElse(null); + } + + /** + * Creates a new {@link CryptoSession}. + *

+ * TODO: to truly be effective when multiple files + * are being opened (ie. batch import), nested sessions + * need to be implemented. + * + * @return new {@link CryptoSession} instance + */ + public CryptoSession newSession() { + return new CryptoProviderSessionImpl(cryptoProviders); + } + + private class CryptoProviderSessionImpl + implements CryptoProvider.Session, CryptoSession { + private List providers; + private Map sessionStateValues = new IdentityHashMap<>(); + private boolean closed; + + public CryptoProviderSessionImpl(List providers) { + this.providers = new ArrayList<>(providers); + } + + @Override + public void addSuccessfulPassword(FSRL fsrl, PasswordValue password) { + cachedCryptoProvider.addPassword(fsrl, password); + } + + @Override + public void close() { + closed = true; + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void setStateValue(CryptoProvider cryptoProvider, Object value) { + sessionStateValues.put(cryptoProvider, value); + } + + @Override + public T getStateValue(CryptoProvider cryptoProvider, + Supplier stateFactory) { + Object val = sessionStateValues.get(cryptoProvider); + if (val == null) { + T newVal = stateFactory.get(); + sessionStateValues.put(cryptoProvider, newVal); + return newVal; + } + return (T) val; + } + + @Override + public CryptoProviders getCryptoProviders() { + return CryptoProviders.this; + } + + @Override + public Iterator getPasswordsFor(FSRL fsrl, String prompt) { + return new PasswordIterator(providers, fsrl, prompt); + } + + /** + * Union iterator of all password providers + */ + class PasswordIterator implements Iterator { + private List providers; + private Iterator currentIt; + private String prompt; + private FSRL fsrl; + + PasswordIterator(List providers, FSRL fsrl, String prompt) { + this.providers = providers.stream() + .filter(PasswordProvider.class::isInstance) + .map(PasswordProvider.class::cast) + .collect(Collectors.toList()); + this.fsrl = fsrl; + this.prompt = prompt; + } + + @Override + public boolean hasNext() { + while (currentIt == null || !currentIt.hasNext()) { + if (providers.isEmpty()) { + return false; + } + PasswordProvider provider = providers.remove(0); + currentIt = provider.getPasswordsFor(fsrl, prompt, CryptoProviderSessionImpl.this); + } + return true; + } + + @Override + public PasswordValue next() { + return currentIt.next(); + } + + } + + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoSession.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoSession.java new file mode 100644 index 0000000000..c5ca55c1fa --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/CryptoSession.java @@ -0,0 +1,68 @@ +/* ### + * 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.formats.gfilesystem.crypto; + +import java.io.Closeable; +import java.util.Iterator; + +import ghidra.formats.gfilesystem.FSRL; + +/** + * Provides the caller with the ability to perform crypto querying operations + * for a group of related files. + *

+ * Typically used to query passwords and to add known good passwords + * to caches for later re-retrieval. + *

+ * Closing a CryptoSession instance does not invalidate the instance, instead is is a suggestion + * that the instance should not be used for any further nested sessions. + *

+ * See {@link CryptoProviders#newSession()}. + */ +public interface CryptoSession extends Closeable { + + /** + * Returns a sequence of passwords (sorted by quality) that may apply to + * the specified file. + * + * @param fsrl {@link FSRL} path to the password protected file + * @param prompt optional prompt that may be displayed to a user + * @return {@link Iterator} of possible passwords + */ + Iterator getPasswordsFor(FSRL fsrl, String prompt); + + /** + * Pushes a known good password into a cache for later re-retrieval. + * + * @param fsrl {@link FSRL} path to the file that was unlocked by the password + * @param password the good password + */ + void addSuccessfulPassword(FSRL fsrl, PasswordValue password); + + /** + * Returns true if this session has been closed. + * + * @return boolean true if closed + */ + boolean isClosed(); + + /** + * Closes this session. + */ + @Override + void close(); + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordDialog.java new file mode 100644 index 0000000000..6aa83b4138 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordDialog.java @@ -0,0 +1,135 @@ +/* ### + * 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.formats.gfilesystem.crypto; + +import java.awt.Toolkit; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +import javax.swing.*; + +import docking.DialogComponentProvider; +import docking.widgets.label.GLabel; +import ghidra.util.HelpLocation; +import ghidra.util.MessageType; +import ghidra.util.layout.PairLayout; + +/** + * Simple dialog with single input field to prompt user for password. + *

+ * User can cancel, or cancel-all, which can be determined by inspecting + * the value of the semi-visible member variables. + *

+ * Treat this as an internal detail of PopupGUIPasswordProvider. + */ +class PasswordDialog extends DialogComponentProvider { + enum RESULT_STATE { + OK, CANCELED + } + + private JPanel workPanel; + JPasswordField passwordField; + RESULT_STATE resultState; + boolean cancelledAll; + + PasswordDialog(String title, String prompt) { + super(title, true, true, true, false); + setRememberSize(false); + setStatusJustification(SwingConstants.CENTER); + setMinimumSize(300, 100); + + passwordField = new JPasswordField(16); + passwordField.addKeyListener(new KeyListener() { + @Override + public void keyTyped(KeyEvent e) { + if (e.getModifiersEx() == 0 && e.getKeyChar() == KeyEvent.VK_ENTER) { + e.consume(); + okCallback(); + } + } + + @Override + public void keyReleased(KeyEvent e) { + updateCapLockWarning(); + } + + @Override + public void keyPressed(KeyEvent e) { + updateCapLockWarning(); + } + }); + + workPanel = new JPanel(new PairLayout(5, 5)); + workPanel.setBorder(BorderFactory.createEmptyBorder(5, 10, 0, 10)); + + workPanel.add(new GLabel(prompt != null ? prompt : "Password:")); + workPanel.add(passwordField); + + addWorkPanel(workPanel); + addOKButton(); + addCancelButton(); + JButton cancelAllButton = new JButton("Cancel All"); + cancelAllButton.addActionListener(e -> { + cancelledAll = true; + cancelButton.doClick(); + }); + addButton(cancelAllButton); + updateCapLockWarning(); + + setFocusComponent(passwordField); + + setHelpLocation( + new HelpLocation("FileSystemBrowserPlugin", "PasswordDialog")); + } + + private void updateCapLockWarning() { + try { + boolean capsLockOn = + Toolkit.getDefaultToolkit().getLockingKeyState(KeyEvent.VK_CAPS_LOCK); + if (capsLockOn) { + setStatusText("Warning! Caps-Lock is on", MessageType.WARNING); + } + else { + clearStatusText(); + } + } + catch (UnsupportedOperationException e) { + // unable to detect caps-lock + } + } + + @Override + protected void okCallback() { + resultState = RESULT_STATE.OK; + close(); + } + + @Override + protected void cancelCallback() { + resultState = RESULT_STATE.CANCELED; + super.cancelCallback(); + } + + @Override + public void dispose() { + if (passwordField != null) { + passwordField.setText(""); + workPanel.remove(passwordField); + passwordField = null; + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordProvider.java new file mode 100644 index 0000000000..ec08de8dc4 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordProvider.java @@ -0,0 +1,51 @@ +/* ### + * 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.formats.gfilesystem.crypto; + +import java.util.Iterator; + +import ghidra.formats.gfilesystem.FSRL; + +/** + * Instances of this interface provide passwords to decrypt files. + *

+ * Instances are typically not called directly, instead are used + * by a {@link CryptoSession} along with other provider instances to provide + * a balanced breakfast. + *

+ * Multiple passwords can be returned for each request with the + * assumption that the consumer of the values can test and validate each one + * to find the correct value. Conversely, it would not be appropriate to use this to get + * a password for a login service that may lock the requester out after a small number + * of failed attempts. + *

+ * TODO: add negative password result that can be persisted / cached so + * user isn't spammed with requests for an unknown password during batch / recursive + * operations. + */ +public interface PasswordProvider extends CryptoProvider { + /** + * Returns a sequence of passwords (ordered by quality) that may apply to + * the specified file. + * + * @param fsrl {@link FSRL} path to the password protected file + * @param prompt optional prompt that may be displayed to a user + * @param session a place to hold state values that persist across + * related queries + * @return {@link Iterator} of possible passwords + */ + Iterator getPasswordsFor(FSRL fsrl, String prompt, Session session); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordValue.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordValue.java new file mode 100644 index 0000000000..a1cfadce88 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PasswordValue.java @@ -0,0 +1,108 @@ +/* ### + * 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.formats.gfilesystem.crypto; + +import java.io.Closeable; +import java.util.Arrays; + +/** + * Wrapper for a password, held in a char[] array. + *

+ * {@link #close() Closing} an instance will clear the characters of the char array. + */ +public class PasswordValue implements Closeable { + + /** + * Creates a new PasswordValue using a copy the specified characters. + * + * @param password password characters + * @return new PasswordValue instance + */ + public static PasswordValue copyOf(char[] password) { + PasswordValue result = new PasswordValue(); + result.password = new char[password.length]; + System.arraycopy(password, 0, result.password, 0, password.length); + return result; + } + + /** + * Creates a new PasswordValue by wrapping the specified character array. + *

+ * The new instance will take ownership of the char array, and + * clear it when the instance is {@link #close() closed}. + * + * @param password password characters + * @return new PasswordValue instance + */ + public static PasswordValue wrap(char[] password) { + PasswordValue result = new PasswordValue(); + result.password = password; + return result; + } + + private char[] password; + + private PasswordValue() { + // empty + } + + @Override + public PasswordValue clone() { + return copyOf(password); + } + + /** + * Clears the password characters by overwriting them with '\0's. + */ + @Override + public void close() { + Arrays.fill(password, '\0'); + password = null; + } + + /** + * Returns a reference to the current password characters. + * + * @return reference to the current password characters + */ + public char[] getPasswordChars() { + return password; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(password); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PasswordValue other = (PasswordValue) obj; + return Arrays.equals(password, other.password); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PopupGUIPasswordProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PopupGUIPasswordProvider.java new file mode 100644 index 0000000000..a8939b6e08 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/crypto/PopupGUIPasswordProvider.java @@ -0,0 +1,101 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.formats.gfilesystem.crypto; + +import java.util.Iterator; + +import java.awt.Component; + +import docking.DockingWindowManager; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.formats.gfilesystem.crypto.PasswordDialog.RESULT_STATE; + +/** + * Pops up up a GUI dialog prompting the user to enter a password for the specified file. + *

+ * The dialog is presented to the user when the iterator's hasNext() is called. + *

+ * Repeated requests to the same iterator will adjust the dialog's title with a "try count" to + * help the user understand the previous password was unsuccessful. + *

+ * Iterator's hasNext() will return false if the user has previously canceled the dialog, + */ +public class PopupGUIPasswordProvider implements PasswordProvider { + + @Override + public Iterator getPasswordsFor(FSRL fsrl, String prompt, Session session) { + return new PasswordIterator(session, fsrl, prompt); + } + + static class SessionState { + boolean cancelAll; + } + + class PasswordIterator implements Iterator { + private SessionState sessionState; + private FSRL fsrl; + private boolean cancelled; + private PasswordValue password; + private String prompt; + private int tryCount; + + PasswordIterator(Session session, FSRL fsrl, String prompt) { + this.sessionState = + session.getStateValue(PopupGUIPasswordProvider.this, SessionState::new); + this.fsrl = fsrl; + this.prompt = prompt; + } + + private void showDlg() { + String dlgPrompt = (prompt != null && !prompt.isBlank()) ? prompt : fsrl.getName(); + if (!dlgPrompt.endsWith(":")) { + dlgPrompt += ":"; + } + tryCount++; + String dlgTitle = + "Enter Password" + (tryCount > 1 ? " (Try " + tryCount + ")" : ""); + PasswordDialog pwdDialog = new PasswordDialog(dlgTitle, dlgPrompt); + DockingWindowManager winMgr = DockingWindowManager.getActiveInstance(); + Component rootFrame = winMgr != null ? winMgr.getRootFrame() : null; + DockingWindowManager.showDialog(rootFrame, pwdDialog); + + cancelled = pwdDialog.resultState == RESULT_STATE.CANCELED; + password = cancelled ? null : PasswordValue.wrap(pwdDialog.passwordField.getPassword()); + sessionState.cancelAll |= cancelled && pwdDialog.cancelledAll; + pwdDialog.dispose(); + } + + @Override + public boolean hasNext() { + if (cancelled || sessionState.cancelAll) { + return false; + } + if (password == null) { + showDlg(); + } + return !cancelled; + } + + @Override + public PasswordValue next() { + PasswordValue result = password; + password = null; + return result; + } + + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/FileSystemFactoryMgr.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/FileSystemFactoryMgr.java index e9ca6e4f7c..031a46b9bc 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/FileSystemFactoryMgr.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/FileSystemFactoryMgr.java @@ -20,7 +20,7 @@ import java.io.IOException; import java.util.*; import java.util.stream.Collectors; -import ghidra.app.util.bin.*; +import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.util.Msg; @@ -135,53 +135,53 @@ public class FileSystemFactoryMgr { /** * Creates a new {@link GFileSystem} instance when the filesystem type is already - * known. + * known, consuming the specified ByteProvider. *

* * @param fsType filesystem type string, ie. "file", "zip". - * @param containerFSRL {@link FSRL} of the containing file. - * @param containerFile {@link File} the containing file. + * @param byteProvider {@link ByteProvider}, will be owned by the new file system * @param fsService reference to the {@link FileSystemService} instance. * @param monitor {@link TaskMonitor} to use for canceling and updating progress. * @return new {@link GFileSystem} instance. * @throws IOException if error when opening the filesystem or unknown fsType. * @throws CancelledException if the user canceled the operation. */ - public GFileSystem mountFileSystem(String fsType, FSRL containerFSRL, File containerFile, + public GFileSystem mountFileSystem(String fsType, ByteProvider byteProvider, FileSystemService fsService, TaskMonitor monitor) throws IOException, CancelledException { FileSystemInfoRec fsir = fsByType.get(fsType); if (fsir == null) { + byteProvider.close(); throw new IOException("Unknown file system type " + fsType); } - GFileSystem result = mountUsingFactory(fsir, containerFSRL, containerFile, null, - containerFSRL.makeNested(fsType), fsService, monitor); + GFileSystem result = mountUsingFactory(fsir, byteProvider, + byteProvider.getFSRL().makeNested(fsType), fsService, monitor); return result; } - private GFileSystem mountUsingFactory(FileSystemInfoRec fsir, FSRL containerFSRL, - File containerFile, ByteProvider byteProvider, FSRLRoot targetFSRL, - FileSystemService fsService, TaskMonitor monitor) + private GFileSystem mountUsingFactory(FileSystemInfoRec fsir, ByteProvider byteProvider, + FSRLRoot targetFSRL, FileSystemService fsService, TaskMonitor monitor) throws IOException, CancelledException { GFileSystem result = null; boolean bpTaken = false; try { - if (fsir.getFactory() instanceof GFileSystemFactoryFull) { - byteProvider = - (byteProvider == null) ? makeBP(containerFile, containerFSRL) : byteProvider; + GFileSystemFactory factory = fsir.getFactory(); + if (factory instanceof GFileSystemFactoryByteProvider) { + GFileSystemFactoryByteProvider bpFactory = + (GFileSystemFactoryByteProvider) factory; bpTaken = true; - result = ((GFileSystemFactoryFull) fsir.getFactory()).create(containerFSRL, - targetFSRL, byteProvider, containerFile, fsService, monitor); - } - else if (fsir.getFactory() instanceof GFileSystemFactoryWithFile) { - result = ((GFileSystemFactoryWithFile) fsir.getFactory()).create(containerFSRL, - targetFSRL, containerFile, fsService, monitor); + result = bpFactory.create(targetFSRL, byteProvider, fsService, monitor); } // add additional GFileSystemFactoryXYZ support blocks here } + catch (IOException | CancelledException e) { + Msg.warn(this, + "Error during fs factory create: " + fsir.getType() + ", " + fsir.getFSClass(), e); + throw e; + } finally { if (byteProvider != null && !bpTaken) { byteProvider.close(); @@ -194,55 +194,45 @@ public class FileSystemFactoryMgr { /** * Returns true if the specified file contains a supported {@link GFileSystem}. *

- * @param containerFSRL {@link FSRL} of the containing file. - * @param containerFile {@link File} the containing file. + * @param byteProvider * @param fsService reference to the {@link FileSystemService} instance. * @param monitor {@link TaskMonitor} to use for canceling and updating progress. * @return {@code true} if the file seems to contain a filesystem, {@code false} if it does not. * @throws IOException if error when accessing the containing file * @throws CancelledException if the user canceled the operation */ - public boolean test(FSRL containerFSRL, File containerFile, FileSystemService fsService, + public boolean test(ByteProvider byteProvider, FileSystemService fsService, TaskMonitor monitor) throws IOException, CancelledException { int pboByteCount = Math.min( - (int) Math.min(containerFile.length(), GFileSystemProbeBytesOnly.MAX_BYTESREQUIRED), + (int) Math.min(byteProvider.length(), GFileSystemProbeBytesOnly.MAX_BYTESREQUIRED), largestBytesRequired); - try (ByteProvider bp = new RandomAccessByteProvider(containerFile, containerFSRL)) { - byte[] startBytes = bp.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 GFileSystemProbeWithFile) { - GFileSystemProbeWithFile factoryProbe = - (GFileSystemProbeWithFile) fsir.getFactory(); - if (factoryProbe.probe(containerFSRL, containerFile, fsService, monitor)) { - return true; - } - } - if (fsir.getFactory() instanceof GFileSystemProbeFull) { - GFileSystemProbeFull factoryProbe = - (GFileSystemProbeFull) fsir.getFactory(); - if (factoryProbe.probe(containerFSRL, bp, containerFile, fsService, - monitor)) { + FSRL containerFSRL = byteProvider.getFSRL(); + 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; } } } - catch (IOException e) { - Msg.trace(this, "File system probe error for " + fsir.getDescription() + - " with " + containerFSRL, e); + if (fsir.getFactory() instanceof GFileSystemProbeByteProvider) { + GFileSystemProbeByteProvider factoryProbe = + (GFileSystemProbeByteProvider) fsir.getFactory(); + if (factoryProbe.probe(byteProvider, fsService, monitor)) { + return true; + } } } + catch (IOException e) { + Msg.trace(this, "File system probe error for " + fsir.getDescription() + + " with " + containerFSRL, e); + } } return false; } @@ -263,26 +253,20 @@ public class FileSystemFactoryMgr { * @throws IOException if error accessing the containing file * @throws CancelledException if the user cancels the operation */ - public GFileSystem probe(FSRL containerFSRL, File containerFile, FileSystemService fsService, + public GFileSystem probe(ByteProvider byteProvider, FileSystemService fsService, FileSystemProbeConflictResolver conflictResolver, TaskMonitor monitor) throws IOException, CancelledException { - return probe(containerFSRL, containerFile, fsService, conflictResolver, - FileSystemInfo.PRIORITY_LOWEST, monitor); - } - - private ByteProvider makeBP(File containerFile, FSRL containerFSRL) throws IOException { - return new SynchronizedByteProvider( - new RandomAccessByteProvider(containerFile, containerFSRL)); + return probe(byteProvider, fsService, conflictResolver, FileSystemInfo.PRIORITY_LOWEST, + monitor); } /** * Probes the specified file for a supported {@link GFileSystem} implementation, and - * if found, creates a new filesystem instance. + * if found, creates a new filesystem instance. The ByteProvider is owned by the new + * file system. *

- * - * @param containerFSRL {@link FSRL} of the containing file. - * @param containerFile {@link File} the containing file. + * @param byteProvider container {@link ByteProvider}, will be owned by the new filesystem * @param fsService reference to the {@link FileSystemService} instance. * @param conflictResolver {@link FileSystemProbeConflictResolver conflict resolver} to * use when more than one {@link GFileSystem} implementation can handle the specified @@ -295,20 +279,20 @@ public class FileSystemFactoryMgr { * @throws IOException if error accessing the containing file * @throws CancelledException if the user cancels the operation */ - public GFileSystem probe(FSRL containerFSRL, File containerFile, FileSystemService fsService, + public GFileSystem probe(ByteProvider byteProvider, FileSystemService fsService, FileSystemProbeConflictResolver conflictResolver, int priorityFilter, TaskMonitor monitor) throws IOException, CancelledException { conflictResolver = (conflictResolver == null) ? FileSystemProbeConflictResolver.CHOOSEFIRST : conflictResolver; - ByteProvider probeBP = makeBP(containerFile, containerFSRL); + FSRL containerFSRL = byteProvider.getFSRL(); try { int pboByteCount = Math.min( - (int) Math.min(containerFile.length(), GFileSystemProbeBytesOnly.MAX_BYTESREQUIRED), + (int) Math.min(byteProvider.length(), GFileSystemProbeBytesOnly.MAX_BYTESREQUIRED), largestBytesRequired); - byte[] startBytes = probeBP.readBytes(0, pboByteCount); + byte[] startBytes = byteProvider.readBytes(0, pboByteCount); List probeMatches = new ArrayList<>(); for (FileSystemInfoRec fsir : sortedFactories) { try { @@ -325,19 +309,10 @@ public class FileSystemFactoryMgr { } } } - if (fsir.getFactory() instanceof GFileSystemProbeWithFile) { - GFileSystemProbeWithFile factoryProbe = - (GFileSystemProbeWithFile) fsir.getFactory(); - if (factoryProbe.probe(containerFSRL, containerFile, fsService, monitor)) { - probeMatches.add(fsir); - continue; - } - } - if (fsir.getFactory() instanceof GFileSystemProbeFull) { - GFileSystemProbeFull factoryProbe = - (GFileSystemProbeFull) fsir.getFactory(); - if (factoryProbe.probe(containerFSRL, probeBP, containerFile, fsService, - monitor)) { + if (fsir.getFactory() instanceof GFileSystemProbeByteProvider) { + GFileSystemProbeByteProvider factoryProbe = + (GFileSystemProbeByteProvider) fsir.getFactory(); + if (factoryProbe.probe(byteProvider, fsService, monitor)) { probeMatches.add(fsir); continue; } @@ -355,16 +330,19 @@ public class FileSystemFactoryMgr { return null; } - ByteProvider mountBP = probeBP; - probeBP = null; - GFileSystem fs = mountUsingFactory(fsir, containerFSRL, containerFile, mountBP, + // After this point, the byteProvider will be closed by the new filesystem, + // or by the factory method if there is an error during mount + ByteProvider mountBP = byteProvider; + byteProvider = null; + + GFileSystem fs = mountUsingFactory(fsir, mountBP, containerFSRL.makeNested(fsir.getType()), fsService, monitor); monitor.setMessage("Found file system " + fs.getDescription()); return fs; } finally { - if (probeBP != null) { - probeBP.close(); + if (byteProvider != null) { + byteProvider.close(); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemBaseFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemBaseFactory.java index e18e7fff9b..71a552ead0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemBaseFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemBaseFactory.java @@ -15,7 +15,6 @@ */ package ghidra.formats.gfilesystem.factory; -import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -39,7 +38,7 @@ import ghidra.util.task.TaskMonitor; * */ public class GFileSystemBaseFactory - implements GFileSystemFactoryFull, GFileSystemProbeFull { + implements GFileSystemFactoryByteProvider, GFileSystemProbeByteProvider { private Class fsClass; private static final Class[] FS_CTOR_PARAM_TYPES = @@ -54,14 +53,15 @@ public class GFileSystemBaseFactory } @Override - public boolean probe(FSRL containerFSRL, ByteProvider byteProvider, File containerFile, - FileSystemService fsService, TaskMonitor monitor) - throws IOException, CancelledException { + public boolean probe(ByteProvider byteProvider, FileSystemService fsService, + TaskMonitor monitor) throws IOException, CancelledException { try { + FSRL containerFSRL = byteProvider.getFSRL(); Constructor ctor = fsClass.getConstructor(FS_CTOR_PARAM_TYPES); GFileSystemBase fs = ctor.newInstance(containerFSRL.getName(), byteProvider); + fs.setFilesystemService(fsService); // do NOT close fs here because that would close the byteProvider return fs.isValid(monitor); } @@ -72,21 +72,22 @@ public class GFileSystemBaseFactory } @Override - public GFileSystemBase create(FSRL containerFSRL, FSRLRoot targetFSRL, - ByteProvider byteProvider, File containerFile, FileSystemService fsService, - TaskMonitor monitor) throws IOException, CancelledException { + public GFileSystemBase create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { try { + FSRL containerFSRL = byteProvider.getFSRL(); Constructor ctor = fsClass.getConstructor(FS_CTOR_PARAM_TYPES); GFileSystemBase fs = ctor.newInstance(containerFSRL.getName(), byteProvider); + fs.setFilesystemService(fsService); + fs.setFSRL(targetFSRL); try { if (!fs.isValid(monitor)) { throw new IOException("Error when creating new filesystem " + fsClass.getName() + ", isvalid failed"); } - fs.setFilesystemService(fsService); - fs.setFSRL(targetFSRL); fs.open(monitor); GFileSystemBase successFS = fs; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemFactoryFull.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemFactoryByteProvider.java similarity index 74% rename from Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemFactoryFull.java rename to Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemFactoryByteProvider.java index 00a8e4a801..61a0dfaa97 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemFactoryFull.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemFactoryByteProvider.java @@ -15,32 +15,28 @@ */ package ghidra.formats.gfilesystem.factory; +import java.io.IOException; + import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.*; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -import java.io.File; -import java.io.IOException; - /** - * A {@link GFileSystemFactory} interface for filesystem implementations that need all available - * references to the source file, including a {@link ByteProvider}. + * A {@link GFileSystemFactory} interface for filesystem implementations + * that use a {@link ByteProvider}. *

* @param */ -public interface GFileSystemFactoryFull +public interface GFileSystemFactoryByteProvider extends GFileSystemFactory { /** * Constructs a new {@link GFileSystem} instance that handles the specified file. *

- * @param containerFSRL the {@link FSRL} of the file being opened. * @param targetFSRL the {@link FSRLRoot} of the filesystem being created. * @param byteProvider a {@link ByteProvider} containing the contents of the file being probed. * This method is responsible for closing this byte provider instance. - * @param containerFile the {@link File} (probably in the filecache with non-useful filename) - * being opened. * @param fsService a reference to the {@link FileSystemService} object * @param monitor a {@link TaskMonitor} that should be polled to see if the user has * requested to cancel the operation, and updated with progress information. @@ -48,7 +44,7 @@ public interface GFileSystemFactoryFull * @throws IOException if there is an error reading files. * @throws CancelledException if the user cancels */ - public FSTYPE create(FSRL containerFSRL, FSRLRoot targetFSRL, ByteProvider byteProvider, - File containerFile, FileSystemService fsService, TaskMonitor monitor) - throws IOException, CancelledException; + public FSTYPE create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemFactoryWithFile.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemFactoryWithFile.java deleted file mode 100644 index cd2c9d7eb5..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemFactoryWithFile.java +++ /dev/null @@ -1,50 +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.formats.gfilesystem.factory; - -import ghidra.formats.gfilesystem.*; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - -import java.io.File; -import java.io.IOException; - -/** - * A {@link GFileSystemFactory} interface for filesystem implementations that can - * be constructed using just a reference to the source {@link File}. - *

- * @param - */ -public interface GFileSystemFactoryWithFile - extends GFileSystemFactory { - /** - * Constructs a new {@link GFileSystem} instance that handles the specified File. - *

- * @param containerFSRL the {@link FSRL} of the file being opened. - * @param targetFSRL the {@link FSRLRoot} of the filesystem being created. - * @param containerFile the {@link File} (probably in the filecache with non-useful filename) - * being opened. - * @param fsService a reference to the {@link FileSystemService} object - * @param monitor a {@link TaskMonitor} that should be polled to see if the user has - * requested to cancel the operation, and updated with progress information. - * @return a new {@link GFileSystem} derived instance. - * @throws IOException if there is an error reading files. - * @throws CancelledException if the user cancels - */ - public FSTYPE create(FSRL containerFSRL, FSRLRoot targetFSRL, File containerFile, - FileSystemService fsService, TaskMonitor monitor) - throws IOException, CancelledException; -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbe.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbe.java index 5eb7b617fe..3de23741bd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbe.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbe.java @@ -18,8 +18,7 @@ package ghidra.formats.gfilesystem.factory; /** * An empty interface that is a common type for the real probe interfaces to derive from. *

- * See {@link GFileSystemProbeBytesOnly}, {@link GFileSystemProbeFull}, or - * {@link GFileSystemProbeWithFile} + * See {@link GFileSystemProbeBytesOnly}, {@link GFileSystemProbeByteProvider} */ public interface GFileSystemProbe { // empty interface diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbeFull.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbeByteProvider.java similarity index 70% rename from Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbeFull.java rename to Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbeByteProvider.java index 1bef1614ea..be9134b9f2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbeFull.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbeByteProvider.java @@ -15,30 +15,25 @@ */ package ghidra.formats.gfilesystem.factory; +import java.io.IOException; + import ghidra.app.util.bin.ByteProvider; -import ghidra.formats.gfilesystem.FSRL; import ghidra.formats.gfilesystem.FileSystemService; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -import java.io.File; -import java.io.IOException; - /** - * A {@link GFileSystemProbe} interface for filesystems that need all available - * references to the source file, including a {@link ByteProvider}. + * A {@link GFileSystemProbe} interface for filesystems that need to examine + * a {@link ByteProvider}. */ -public interface GFileSystemProbeFull extends GFileSystemProbe { +public interface GFileSystemProbeByteProvider extends GFileSystemProbe { /** - * Probes the specified {@code containerFile} to determine if this filesystem implementation + * Probes the specified {@code ByteProvider} to determine if this filesystem implementation * can handle the file. * - * @param containerFSRL the {@link FSRL} of the file being probed * @param byteProvider a {@link ByteProvider} containing the contents of the file being probed. * Implementors of this method should NOT {@link ByteProvider#close() close()} this * object. - * @param containerFile the {@link File} (probably in the filecache with non-useful filename) - * being probed. * @param fsService a reference to the {@link FileSystemService} object * @param monitor a {@link TaskMonitor} that should be polled to see if the user has * requested to cancel the operation, and updated with progress information. @@ -47,7 +42,6 @@ public interface GFileSystemProbeFull extends GFileSystemProbe { * @throws IOException if there is an error reading files. * @throws CancelledException if the user cancels */ - public boolean probe(FSRL containerFSRL, ByteProvider byteProvider, File containerFile, - FileSystemService fsService, TaskMonitor monitor) - throws IOException, CancelledException; + public boolean probe(ByteProvider byteProvider, FileSystemService fsService, + TaskMonitor monitor) throws IOException, CancelledException; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbeWithFile.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbeWithFile.java deleted file mode 100644 index 11502942e0..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/factory/GFileSystemProbeWithFile.java +++ /dev/null @@ -1,48 +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.formats.gfilesystem.factory; - -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.FileSystemService; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - -import java.io.File; -import java.io.IOException; - -/** - * A {@link GFileSystemProbe} interface for filesystems that only need a {@link File} - * reference to the source file. - */ -public interface GFileSystemProbeWithFile extends GFileSystemProbe { - /** - * Probes the specified {@code containerFile} to determine if this filesystem implementation - * can handle the file. - * - * @param containerFSRL the {@link FSRL} of the file being probed - * @param containerFile the {@link File} (probably in the filecache with non-useful filename) - * being probed. - * @param fsService a reference to the {@link FileSystemService} object - * @param monitor a {@link TaskMonitor} that should be polled to see if the user has - * requested to cancel the operation, and updated with progress information. - * @return {@code true} if the specified file is handled by this filesystem implementation, - * {@code false} if not. - * @throws IOException if there is an error reading files. - * @throws CancelledException if the user cancels - */ - public boolean probe(FSRL containerFSRL, File containerFile, FileSystemService fsService, - TaskMonitor monitor) throws IOException, CancelledException; -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttribute.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttribute.java new file mode 100644 index 0000000000..96a4e80732 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttribute.java @@ -0,0 +1,133 @@ +/* ### + * 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.formats.gfilesystem.fileinfo; + +import java.util.Objects; + +/** + * A (type, type_display_string, value) tuple. + * + * @param type of the value + */ +public class FileAttribute { + private final FileAttributeType attributeType; + private final String attributeDisplayName; + private final T attributeValue; + + /** + * Creates a new {@link FileAttribute} instance with an + * {@link FileAttributeType#UNKNOWN_ATTRIBUTE} type and the specified display name. + * + * @param type of the value + * @param name custom display name for the value + * @param attributeValue value (should be .toString()'able) + * @return new FileAttribute instance + */ + public static FileAttribute create(String name, T attributeValue) { + return create(FileAttributeType.UNKNOWN_ATTRIBUTE, name, attributeValue); + } + + /** + * Creates a new {@link FileAttribute} instance with the specified type and value. + * + * @param type of the value + * @param attributeType {@link FileAttributeType} type + * @param attributeValue value (should match the + * type specified in {@link FileAttributeType#getValueType()}) + * @return new FileAttribute instance + */ + public static FileAttribute create(FileAttributeType attributeType, + T attributeValue) { + return create(attributeType, attributeType.getDisplayName(), attributeValue); + } + + /** + * Creates a new {@link FileAttribute} instance with the specified type, display name and + * value. + * + * @param type of the value + * @param attributeType {@link FileAttributeType} type + * @param attributeDisplayName display name of the type + * @param attributeValue value (should match the + * type specified in {@link FileAttributeType#getValueType()}) + * @return new FileAttribute instance + */ + public static FileAttribute create(FileAttributeType attributeType, + String attributeDisplayName, T attributeValue) { + if (!attributeType.getValueType().isInstance(attributeValue)) { + throw new IllegalArgumentException("FileAttribute type " + attributeType + + " does not match value: " + attributeValue.getClass()); + } + return new FileAttribute<>(attributeType, attributeDisplayName, attributeValue); + } + + private FileAttribute(FileAttributeType attributeType, String attributeDisplayName, + T attributeValue) { + this.attributeType = attributeType; + this.attributeDisplayName = attributeDisplayName; + this.attributeValue = attributeValue; + } + + /** + * Returns the {@link FileAttributeType} of this instance. + * + * @return {@link FileAttributeType} + */ + public FileAttributeType getAttributeType() { + return attributeType; + } + + /** + * Returns the display name of this instance. This is usually derived from + * the {@link FileAttributeType#getDisplayName()}. + * + * @return string display name + */ + public String getAttributeDisplayName() { + return attributeDisplayName; + } + + /** + * Return the value. + * + * @return value + */ + public T getAttributeValue() { + return attributeValue; + } + + @Override + public int hashCode() { + return Objects.hash(attributeDisplayName, attributeType, attributeValue); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + FileAttribute other = (FileAttribute) obj; + return Objects.equals(attributeDisplayName, other.attributeDisplayName) && + attributeType == other.attributeType && + Objects.equals(attributeValue, other.attributeValue); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributeType.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributeType.java new file mode 100644 index 0000000000..a7bffc7d57 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributeType.java @@ -0,0 +1,99 @@ +/* ### + * 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.formats.gfilesystem.fileinfo; + +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeTypeGroup.*; + +import java.util.Date; + +import ghidra.formats.gfilesystem.FSRL; + +/** + * Well known types of file attributes. + *

+ * Uncommon information about a file should be added to the {@link FileAttributes} collection + * as an {@link #UNKNOWN_ATTRIBUTE} with a custom display name. + *

+ * When adding new attribute types to this enum, add them adjacent to other types of the same + * {@link FileAttributeTypeGroup category}. The enum ordinal controls display ordering. + */ +public enum FileAttributeType { + FSRL_ATTR("FSRL", GENERAL_INFO, FSRL.class), + NAME_ATTR("Name", GENERAL_INFO, String.class), + PATH_ATTR("Path", GENERAL_INFO, String.class), + FILE_TYPE_ATTR("File type", GENERAL_INFO, FileType.class), + PROJECT_FILE_ATTR("Project file", GENERAL_INFO, String.class), + + SIZE_ATTR("Size", SIZE_INFO, Long.class), + COMPRESSED_SIZE_ATTR("Compressed size", SIZE_INFO, Long.class), + + CREATE_DATE_ATTR("Create date", DATE_INFO, Date.class), + MODIFIED_DATE_ATTR("Last modified date", DATE_INFO, Date.class), + ACCESSED_DATE_ATTR("Last accessed date", DATE_INFO, Date.class), + + USER_NAME_ATTR("User", OWNERSHIP_INFO, String.class), + USER_ID_ATTR("UserId", OWNERSHIP_INFO, Long.class), + GROUP_NAME_ATTR("Group", OWNERSHIP_INFO, String.class), + GROUP_ID_ATTR("GroupId", OWNERSHIP_INFO, Long.class), + + UNIX_ACL_ATTR("Unix acl", PERMISSION_INFO, Long.class), + + IS_ENCRYPTED_ATTR("Is encrypted?", ENCRYPTION_INFO, Boolean.class), + HAS_GOOD_PASSWORD_ATTR("Password available?", ENCRYPTION_INFO, Boolean.class), + + SYMLINK_DEST_ATTR("Symbolic link destination", MISC_INFO, String.class), + COMMENT_ATTR("Comment", MISC_INFO, String.class), + + UNKNOWN_ATTRIBUTE("Other attribute", ADDITIONAL_INFO, Object.class); + + private final String displayName; + private final FileAttributeTypeGroup group; + private final Class valueType; + + private FileAttributeType(String displayName, FileAttributeTypeGroup group, + Class valueType) { + this.displayName = displayName; + this.group = group; + this.valueType = valueType; + } + + /** + * Returns the display name of this attribute type. + * + * @return string display name + */ + public String getDisplayName() { + return displayName; + } + + /** + * Returns the {@link FileAttributeTypeGroup group} this attribute belongs in. + * + * @return {@link FileAttributeTypeGroup} + */ + public FileAttributeTypeGroup getGroup() { + return group; + } + + /** + * Returns the class the value should match. + * + * @return expected class of the value + */ + public Class getValueType() { + return valueType; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributeTypeGroup.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributeTypeGroup.java new file mode 100644 index 0000000000..f3f7d2648e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributeTypeGroup.java @@ -0,0 +1,46 @@ +/* ### + * 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.formats.gfilesystem.fileinfo; + +/** + * Categories of file attributes. + */ +public enum FileAttributeTypeGroup { + GENERAL_INFO("General"), + SIZE_INFO("Size Info"), + DATE_INFO("Date Info"), + OWNERSHIP_INFO("Ownership Info"), + PERMISSION_INFO("Permission Info"), + ENCRYPTION_INFO("Encryption Info"), + MISC_INFO("Misc"), + ADDITIONAL_INFO("Addional Info"); + + private final String descriptiveName; + + private FileAttributeTypeGroup(String descriptiveName) { + this.descriptiveName = descriptiveName; + } + + /** + * Returns the descriptive name of the group. + * + * @return string descriptive name + */ + public String getDescriptiveName() { + return descriptiveName; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributes.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributes.java new file mode 100644 index 0000000000..62dbbbe752 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileAttributes.java @@ -0,0 +1,153 @@ +/* ### + * 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.formats.gfilesystem.fileinfo; + +import java.util.ArrayList; +import java.util.List; + +/** + * A collection of {@link FileAttribute} values that describe a file. + */ +public class FileAttributes { + /** + * Read-only empty instance. + */ + public static FileAttributes EMPTY = new FileAttributes(List.of()); // read-only because of List.of() + + /** + * Creates a {@link FileAttributes} instance containing the specified attribute values. + * + * @param attribs var-arg list of {@link FileAttribute} values, null values are ignored and + * skipped + * @return a new {@link FileAttributes} instance + */ + public static FileAttributes of(FileAttribute... attribs) { + FileAttributes result = new FileAttributes(); + for (FileAttribute fa : attribs) { + if (fa != null) { + result.attributes.add(fa); + } + } + return result; + } + + private List> attributes; + + /** + * Creates a new / empty {@link FileAttributes} instance. + */ + public FileAttributes() { + this.attributes = new ArrayList<>(); + } + + private FileAttributes(List> attributes) { + this.attributes = attributes; + } + + @Override + public FileAttributes clone() { + return new FileAttributes(new ArrayList<>(attributes)); + } + + /** + * Adds a custom named file attribute. + *

+ * The value class should have a reasonable toString() that converts the value to something + * that is presentable to the user. + * + * @param name name of the attribute + * @param attributeValue value of the attribute + */ + public void add(String name, Object attributeValue) { + add(FileAttributeType.UNKNOWN_ATTRIBUTE, name, attributeValue); + } + + /** + * Adds a typed file attribute value. + *

+ * The value class needs to match {@link FileAttributeType#getValueType()}. + * + * @param attributeType {@link FileAttributeType} type of this value + * @param attributeValue value of attribute + */ + public void add(FileAttributeType attributeType, Object attributeValue) { + add(attributeType, attributeType.getDisplayName(), attributeValue); + } + + /** + * Adds a typed file attribute value. + *

+ * The value class needs to match {@link FileAttributeType#getValueType()}. + * + * @param attributeType {@link FileAttributeType} type of this value + * @param displayName string used to label the value when displayed to the user + * @param attributeValue value of attribute + * @throws IllegalArgumentException if attributeValue does not match attributeType's + * {@link FileAttributeType#getValueType()}. + */ + public void add(FileAttributeType attributeType, String displayName, Object attributeValue) { + if (attributeValue != null) { + attributes.add(FileAttribute.create(attributeType, displayName, attributeValue)); + } + } + + /** + * Gets the value of the specified attribute. + * + * @param expected class of the attribute value + * @param attributeType {@link FileAttributeType} enum type of attribute to search for + * @param valueClass java class of the value + * @param defaultValue value to return if attribute is not present + * @return value of requested attribute, or defaultValue if not present + */ + public T get(FileAttributeType attributeType, Class valueClass, T defaultValue) { + for (FileAttribute attr : attributes) { + if (attr.getAttributeType() == attributeType) { + Object val = attr.getAttributeValue(); + if (valueClass.isAssignableFrom(val.getClass())) { + return valueClass.cast(val); + } + break; + } + } + return defaultValue; + } + + /** + * Return a list of all the attributes added to this instance. + * + * @return list of {@link FileAttribute} + */ + public List> getAttributes() { + return new ArrayList<>(attributes); + } + + /** + * Returns true if the specified attribute is present. + * + * @param attributeType attribute to query + * @return boolean true if present + */ + public boolean contains(FileAttributeType attributeType) { + for (FileAttribute attr : attributes) { + if (attr.getAttributeType() == attributeType) { + return true; + } + } + return false; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileCacheEntry.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileType.java similarity index 67% rename from Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileCacheEntry.java rename to Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileType.java index 4a7355db9b..b01909f93c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileCacheEntry.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/fileinfo/FileType.java @@ -13,20 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.formats.gfilesystem; - -import java.io.File; +package ghidra.formats.gfilesystem.fileinfo; /** - * Simple class that contains a {@link File} and its MD5 string. + * Enumeration of file types */ -public class FileCacheEntry { - - public String md5; - public File file; - - public FileCacheEntry(File file, String md5) { - this.file = file; - this.md5 = md5; - } +public enum FileType { + FILE, + DIRECTORY, + SYMBOLIC_LINK, + OTHER, + UNKNOWN } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/GhidraApplicationConfiguration.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/GhidraApplicationConfiguration.java index 5108d2523a..25aa525ef2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/GhidraApplicationConfiguration.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/GhidraApplicationConfiguration.java @@ -25,6 +25,8 @@ import docking.framework.ApplicationInformationDisplayFactory; import docking.framework.SplashScreen; import docking.widgets.PopupKeyStorePasswordProvider; import ghidra.docking.util.DockingWindowsLookAndFeelUtils; +import ghidra.formats.gfilesystem.crypto.CryptoProviders; +import ghidra.formats.gfilesystem.crypto.PopupGUIPasswordProvider; import ghidra.framework.main.GhidraApplicationInformationDisplayFactory; import ghidra.framework.main.UserAgreementDialog; import ghidra.framework.preferences.Preferences; @@ -58,6 +60,7 @@ public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationCon ApplicationKeyManagerFactory.setKeyStorePasswordProvider( new PopupKeyStorePasswordProvider()); + CryptoProviders.getInstance().registerCryptoProvider(new PopupGUIPasswordProvider()); } private static void platformSpecificFixups() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/AddToProgramDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/AddToProgramDialog.java index 4c13ea7bc1..8c60a1425d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/AddToProgramDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/AddToProgramDialog.java @@ -44,6 +44,8 @@ public class AddToProgramDialog extends ImporterDialog { * @param fsrl the FileSystemURL for where the imported data can be read. * @param loaderMap the loaders and their corresponding load specifications * @param byteProvider the ByteProvider from which the bytes from the source can be read. + * The dialog takes ownership of the ByteProvider and it will be closed when + * the dialog is closed * @param addToProgram the program to which the newly imported data will be added */ protected AddToProgramDialog(PluginTool tool, FSRL fsrl, LoaderMap loaderMap, diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java index b863bd48a0..c49de70c56 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterDialog.java @@ -89,7 +89,8 @@ public class ImporterDialog extends DialogComponentProvider { * @param tool the active tool that spawned this dialog. * @param programManager program manager to open imported file with or null * @param loaderMap the loaders and their corresponding load specifications - * @param byteProvider the ByteProvider for getting the bytes from the file to be imported. + * @param byteProvider the ByteProvider for getting the bytes from the file to be imported. The + * dialog takes ownership of the ByteProvider and it will be closed when the dialog is closed * @param suggestedDestinationPath optional string path that will be pre-pended to the destination * filename. Any path specified in the destination filename field will be created when * the user performs the import (as opposed to the {@link #setDestinationFolder(DomainFolder) destination folder} @@ -166,8 +167,10 @@ public class ImporterDialog extends DialogComponentProvider { } private Component buildFilenameTextField() { - filenameTextField = new JTextField(); - filenameTextField.setText(getSuggestedFilename()); + String initalSuggestedFilename = + FSUtilities.appendPath(suggestedDestinationPath, getSuggestedFilename()); + int columns = (initalSuggestedFilename.length() > 50) ? 50 : 0; + filenameTextField = new JTextField(initalSuggestedFilename, columns); // Use a key listener to track users edits. We can't use the document listener, as // we change the name field ourselves when other fields are changed. @@ -302,13 +305,9 @@ public class ImporterDialog extends DialogComponentProvider { if (loader != null) { languageNeeded = isLanguageNeeded(loader); setSelectedLanguage(getPreferredLanguage(loader)); - if (suggestedDestinationPath != null) { - setFilename( - FSUtilities.appendPath(suggestedDestinationPath, getSuggestedFilename())); - } - else { - setFilename(getSuggestedFilename()); - } + String newSuggestedFilename = + FSUtilities.appendPath(suggestedDestinationPath, getSuggestedFilename()); + setFilename(newSuggestedFilename); } else { languageNeeded = true; @@ -556,6 +555,7 @@ public class ImporterDialog extends DialogComponentProvider { } filenameTextField.setText(s); + filenameTextField.setCaretPosition(s.length()); } protected void setSelectedLanguage(LanguageCompilerSpecPair lcsPair) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java index 78dd310c37..a5b5c70c22 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterPlugin.java @@ -30,7 +30,8 @@ import ghidra.app.events.ProgramActivatedPluginEvent; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.services.*; import ghidra.app.util.importer.LibrarySearchPathManager; -import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.formats.gfilesystem.FileSystemService; import ghidra.framework.main.*; import ghidra.framework.main.datatree.DomainFolderNode; import ghidra.framework.model.*; @@ -350,8 +351,7 @@ public class ImporterPlugin extends Plugin } private void addToProgram(File file) { - GFile gFile = FileSystemService.getInstance().getLocalGFile(file); - if (gFile.getLength() == 0) { + if (file.length() == 0) { Msg.showInfo(this, null, "Import File Failed", "File " + file.getName() + " is empty (0 bytes)."); return; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterUtilities.java b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterUtilities.java index bff004d36b..b511e2a692 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterUtilities.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugin/importer/ImporterUtilities.java @@ -72,6 +72,8 @@ public class ImporterUtilities { ExtensionFileFilter.forExtensions("Container files", "zip", "tar", "tgz", "jar", "gz", "ipsw", "img3", "dmg", "apk", "cpio", "rpm", "lib"); + private static final FileSystemService fsService = FileSystemService.getInstance(); + static List getPairs(Collection loadSpecs) { Set pairs = new HashSet<>(); for (LoadSpec loadSpec : loadSpecs) { @@ -99,7 +101,7 @@ public class ImporterUtilities { int id = program.startTransaction("setImportProperties"); try { - fsrl = FileSystemService.getInstance().getFullyQualifiedFSRL(fsrl, monitor); + fsrl = fsService.getFullyQualifiedFSRL(fsrl, monitor); Options propertyList = program.getOptions(Program.PROGRAM_INFO); propertyList.setString(ProgramMappingService.PROGRAM_SOURCE_FSRL, fsrl.toString()); @@ -165,11 +167,10 @@ public class ImporterUtilities { RefdFile referencedFile = null; try { - FileSystemService service = FileSystemService.getInstance(); - referencedFile = service.getRefdFile(fsrl, monitor); + referencedFile = fsService.getRefdFile(fsrl, monitor); - FSRL fullFsrl = service.getFullyQualifiedFSRL(fsrl, monitor); - boolean isFSContainer = service.isFileFilesystemContainer(fullFsrl, monitor); + FSRL fullFsrl = fsService.getFullyQualifiedFSRL(fsrl, monitor); + boolean isFSContainer = fsService.isFileFilesystemContainer(fullFsrl, monitor); if (referencedFile.file.getLength() == 0) { Msg.showError(ImporterUtilities.class, null, "File is empty", "File " + fsrl.getPath() + " is empty, nothing to import"); @@ -264,10 +265,11 @@ public class ImporterUtilities { Objects.requireNonNull(monitor); try { - ByteProvider provider = FileSystemService.getInstance().getByteProvider(fsrl, monitor); + ByteProvider provider = fsService.getByteProvider(fsrl, false, monitor); if (provider.length() == 0) { Msg.showWarn(null, null, "Error opening " + fsrl.getName(), "The item does not correspond to a valid file."); + provider.close(); return; } @@ -307,8 +309,7 @@ public class ImporterUtilities { TaskMonitor monitor) { try { - - ByteProvider provider = FileSystemService.getInstance().getByteProvider(fsrl, monitor); + ByteProvider provider = fsService.getByteProvider(fsrl, true, monitor); LoaderMap loaderMap = LoaderService.getAllSupportedLoadSpecs(provider); SystemUtilities.runSwingLater(() -> { @@ -393,7 +394,7 @@ public class ImporterUtilities { Objects.requireNonNull(monitor); - try (ByteProvider bp = FileSystemService.getInstance().getByteProvider(fsrl, monitor)) { + try (ByteProvider bp = fsService.getByteProvider(fsrl, false, monitor)) { Object consumer = new Object(); MessageLog messageLog = new MessageLog(); @@ -464,7 +465,7 @@ public class ImporterUtilities { Objects.requireNonNull(monitor); MessageLog messageLog = new MessageLog(); - try (ByteProvider bp = FileSystemService.getInstance().getByteProvider(fsrl, monitor)) { + try (ByteProvider bp = fsService.getByteProvider(fsrl, false, monitor)) { loadSpec.getLoader().loadInto(bp, loadSpec, options, messageLog, program, monitor); displayResults(tool, program, program.getDomainFile(), messageLog.toString()); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionContext.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionContext.java index 9091ea9ae5..f22ef712ad 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionContext.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionContext.java @@ -15,9 +15,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.ActionContext; -import docking.ComponentProvider; import docking.widgets.tree.GTree; +import docking.widgets.tree.GTreeNode; +import ghidra.formats.gfilesystem.*; /** * {@link FileSystemBrowserPlugin}-specific action. @@ -30,15 +38,32 @@ public class FSBActionContext extends ActionContext { * Creates a new {@link FileSystemBrowserPlugin}-specific action context. * * @param provider the ComponentProvider that generated this context. - * @param contextObject an optional contextObject that the ComponentProvider can provide to the - * action. + * @param selectedNodes selected nodes in the tree + * @param event MouseEvent that caused the update, or null * @param gTree {@link FileSystemBrowserPlugin} provider tree. */ - public FSBActionContext(ComponentProvider provider, Object contextObject, GTree gTree) { - super(provider, contextObject, gTree); + public FSBActionContext(FileSystemBrowserComponentProvider provider, FSBNode[] selectedNodes, + MouseEvent event, GTree gTree) { + super(provider, selectedNodes, gTree); this.gTree = gTree; } + /** + * Returns true if the GTree is not busy + * @return boolean true if GTree is not busy + */ + public boolean notBusy() { + return !gTree.isBusy(); + } + + /** + * Returns true if the GTree is busy + * @return boolean true if the GTree is busy + */ + public boolean isBusy() { + return gTree.isBusy(); + } + /** * Gets the {@link FileSystemBrowserPlugin} provider's tree. * @@ -47,4 +72,281 @@ public class FSBActionContext extends ActionContext { public GTree getTree() { return gTree; } + + /** + * Returns true if there are selected nodes in the browser tree. + * + * @return boolean true if there are selected nodes in the browser tree + */ + public boolean hasSelectedNodes() { + FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); + + return selectedNodes.length > 0; + } + + /** + * Returns a list of the currently selected tree nodes. + * + * @return list of currently selected tree nodes + */ + public List getSelectedNodes() { + return List.of((FSBNode[]) getContextObject()); + } + + /** + * Returns the {@link FSRL} of the currently selected item, as long as it conforms to + * the dirsOk requirement. + * + * @param dirsOk boolean flag, if true the selected item can be either a file or directory + * element, if false, it must be a file or the root of a file system that has a container + * file + * @return FSRL of the single selected item, null if no items selected or more than 1 item + * selected + */ + public FSRL getFSRL(boolean dirsOk) { + FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); + if (selectedNodes.length != 1) { + return null; + } + FSBNode node = selectedNodes[0]; + FSRL fsrl = node.getFSRL(); + if (!dirsOk && node instanceof FSBRootNode && fsrlHasContainer(fsrl.getFS())) { + // 'convert' a file system root node back into its container file + return fsrl.getFS().getContainer(); + } + + boolean isDir = (node instanceof FSBDirNode) || (node instanceof FSBRootNode); + if (isDir && !dirsOk) { + return null; + } + + return fsrl; + } + + private boolean fsrlHasContainer(FSRLRoot fsFSRL) { + return fsFSRL.hasContainer() && !fsFSRL.getProtocol().equals(LocalFileSystem.FSTYPE); + } + + /** + * Returns true if the currently selected items are all directory items + * @return boolean true if the currently selected items are all directory items + */ + public boolean isSelectedAllDirs() { + FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); + for (FSBNode node : selectedNodes) { + boolean isDir = (node instanceof FSBDirNode) || (node instanceof FSBRootNode); + if (!isDir) { + return false; + } + } + return true; + } + + /** + * Returns the currently selected tree node + * + * @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; + } + + /** + * Returns the number of selected nodes in the tree. + * + * @return returns the number of selected nodes in the tree. + */ + public int getSelectedCount() { + FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); + return selectedNodes.length; + } + + private List getFSRLsFromNodes(FSBNode[] nodes, boolean dirsOk) { + List fsrls = new ArrayList<>(); + for (FSBNode node : nodes) { + FSRL fsrl = node.getFSRL(); + if (!node.isLeaf() && !dirsOk) { + boolean canConvertToContainerNode = + node instanceof FSBRootNode && fsrl.getFS().hasContainer(); + if (!canConvertToContainerNode) { + continue; // skip this node + } + // 'convert' a file system root node back into its container file node + fsrl = fsrl.getFS().getContainer(); + } + fsrls.add(fsrl); + } + return fsrls; + } + + /** + * Returns a list of FSRLs of the currently selected nodes in the tree. + * + * @param dirsOk boolean flag, if true the selected items can be either a file or directory + * element, if false, it must be a file or the root of a file system that has a container + * file before being included in the resulting list + * @return list of FSRLs of the currently selected items, maybe empty but never null + */ + public List getFSRLs(boolean dirsOk) { + FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); + return getFSRLsFromNodes(selectedNodes, dirsOk); + } + + /** + * Returns a list of FSRLs of the currently selected file nodes in the tree. + * + * @return list of FSRLs of the currently selected file items, maybe empty but never null + */ + public List getFileFSRLs() { + return getFSRLs(false); + } + + /** + * Returns the FSRL of the currently selected file node + * + * @return FSRL of the currently selected file, or null if not file or more than 1 selected + */ + public FSRL getFileFSRL() { + return getFSRL(false); + } + + /** + * Converts the tree-node hierarchy of the currently selected item into a string path using + * "/" separators. + * + * @return string path of the currently selected tree item + */ + public String getFormattedTreePath() { + FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); + if (selectedNodes.length != 1) { + return null; + } + TreePath treePath = selectedNodes[0].getTreePath(); + StringBuilder path = new StringBuilder(); + for (Object pathElement : treePath.getPath()) { + if (pathElement instanceof FSBNode) { + FSBNode node = (FSBNode) pathElement; + FSRL fsrl = node.getFSRL(); + if (path.length() != 0) { + path.append("/"); + } + String s; + if (fsrl instanceof FSRLRoot) { + s = fsrl.getFS().hasContainer() ? fsrl.getFS().getContainer().getName() + : "/"; + } + else { + s = fsrl.getName(); + } + path.append(s); + } + } + + return path.toString(); + } + + /** + * Returns the FSRL of the currently selected item, if it is a 'loadable' item. + * + * @return FSRL of the currently selected loadable item, or null if nothing selected or + * more than 1 selected + */ + 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; + } + + /** + * Returns a list of FSRLs of the currently selected loadable items. + * + * @return list of FSRLs of currently selected loadable items, maybe empty but never null + */ + public List getLoadableFSRLs() { + FSBNode[] selectedNodes = (FSBNode[]) getContextObject(); + + List fsrls = new ArrayList<>(); + for (FSBNode node : selectedNodes) { + FSRL fsrl = node.getFSRL(); + + FSRL validated = vaildateFsrl(fsrl, node); + if (validated != null) { + fsrls.add(validated); + continue; + } + else if (node instanceof FSBRootNode && fsrl.getFS().hasContainer()) { + // 'convert' a file system root node back into its container file + fsrls.add(fsrl.getFS().getContainer()); + } + else if (node instanceof FSBFileNode) { + fsrls.add(fsrl); + } + } + 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; + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java index e57ea2fafd..b3e7c5f711 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java @@ -15,17 +15,20 @@ */ package ghidra.plugins.fsbrowser; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; +import static java.util.Map.*; + import java.awt.Component; import java.io.*; import java.util.*; +import java.util.function.Function; import javax.swing.*; -import javax.swing.tree.TreePath; import org.apache.commons.io.FilenameUtils; -import docking.ActionContext; -import docking.action.*; +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; import docking.widgets.OptionDialog; import docking.widgets.dialogs.MultiLineMessageDialog; import docking.widgets.filechooser.GhidraFileChooser; @@ -35,8 +38,12 @@ import docking.widgets.tree.GTree; import docking.widgets.tree.GTreeNode; import ghidra.app.services.ProgramManager; import ghidra.app.services.TextEditorService; +import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.opinion.LoaderService; import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.crypto.CachedPasswordProvider; +import ghidra.formats.gfilesystem.crypto.CryptoProviders; +import ghidra.formats.gfilesystem.fileinfo.*; import ghidra.framework.main.AppInfo; import ghidra.framework.model.DomainFile; import ghidra.framework.plugintool.PluginTool; @@ -45,12 +52,11 @@ import ghidra.plugin.importer.ProgramMappingService; import ghidra.plugins.fsbrowser.tasks.GFileSystemExtractAllTask; import ghidra.plugins.importer.batch.BatchImportDialog; import ghidra.program.model.listing.Program; -import ghidra.util.Msg; -import ghidra.util.SystemUtilities; +import ghidra.util.*; import ghidra.util.exception.CancelledException; +import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskLauncher; import ghidra.util.task.TaskMonitor; -import utilities.util.FileUtilities; /** * Handles the menu actions for the {@link FileSystemBrowserComponentProvider}. @@ -78,6 +84,7 @@ class FSBActionManager { DockingAction actionCollapse; DockingAction actionImportBatch; DockingAction actionCloseFileSystem; + DockingAction actionClearCachedPasswords; /* end package visibility */ protected FileSystemBrowserPlugin plugin; @@ -91,6 +98,7 @@ class FSBActionManager { private GhidraFileChooser chooserExportAll; private List actions = new ArrayList<>(); + private FileSystemService fsService = FileSystemService.getInstance(); FSBActionManager(FileSystemBrowserPlugin plugin, FileSystemBrowserComponentProvider provider, TextEditorService textEditorService, GTree gTree) { @@ -109,11 +117,11 @@ class FSBActionManager { private void createActions() { actions.add((actionCloseFileSystem = createCloseAction())); - actions.add((actionOpenPrograms = createOpenAllProgramsAction())); + actions.add((actionOpenPrograms = createOpenProgramsAction())); actions.add((actionImport = createImportAction())); actions.add((actionImportBatch = createBatchImportAction())); - actions.add((actionOpenFileSystemNewWindow = createOpenFileSystemActionNewWindow())); - actions.add((actionOpenFileSystemNested = createOpenNestedFileSystemAction())); + actions.add((actionOpenFileSystemNewWindow = createOpenFileSystemNewWindowAction())); + actions.add((actionOpenFileSystemNested = createOpenFileSystemNestedAction())); actions.add((actionOpenFileSystemChooser = createOpenNewFileSystemAction())); actions.add((actionExpand = createExpandAllAction())); actions.add((actionCollapse = createCollapseAllAction())); @@ -125,6 +133,8 @@ class FSBActionManager { actions.add( (actionShowSupportedFileSystemsAndLoaders = createSupportedFileSystemsAction())); actions.add((actionListMountedFileSystems = createListMountedFilesystemsAction())); + actions.add((actionClearCachedPasswords = createClearCachedPasswordsAction())); + actions.add(createRefreshAction()); } private void removeActions() { @@ -143,178 +153,18 @@ class FSBActionManager { removeActions(); } - private List getLoadableFSRLsFromContext(ActionContext context) { - Object contextObject = context.getContextObject(); - if (contextObject instanceof FSBNode) { - contextObject = new FSBNode[] { (FSBNode) contextObject }; - } - if (!(contextObject instanceof FSBNode[])) { - return Collections.emptyList(); - } - - List fsrls = new ArrayList<>(); - for (FSBNode node : (FSBNode[]) contextObject) { - 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); - } - } - return fsrls; - } - - private FSRL vaildateFsrl(FSRL fsrl, FSBNode node) { - if ((node instanceof FSBDirNode) || (node instanceof FSBRootNode)) { - FSBRootNode rootNode = FSBUtils.getNodesRoot(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; - } - - private FSRL getLoadableFSRLFromContext(ActionContext context) { - if (context == null || !(context.getContextObject() instanceof FSBNode)) { - return null; - } - - FSBNode node = (FSBNode) context.getContextObject(); - FSRL fsrl = node.getFSRL(); - if ((node instanceof FSBDirNode) || (node instanceof FSBRootNode)) { - FSBRootNode rootNode = FSBUtils.getNodesRoot(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 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; - } - - /* - * Transforms the GTree path specified by the context parameter into a - * folder-like path, suitable for use in a filepath. - */ - private String getFormattedTreePath(ActionContext context) { - if (context != null && context.getContextObject() instanceof FSBNode) { - TreePath treePath = ((FSBNode) context.getContextObject()).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(); - } - - return null; - } - - private boolean isSelectedContextAllDirs(ActionContext context) { - Object contextObject = context.getContextObject(); - if (contextObject instanceof FSBNode[]) { - for (FSBNode node : (FSBNode[]) context.getContextObject()) { - boolean isDir = (node instanceof FSBDirNode) || (node instanceof FSBRootNode); - if (!isDir) { - return false; - } - } - return true; - } - return (contextObject instanceof FSBDirNode) || (contextObject instanceof FSBRootNode); - } - - private List getFSRLsFromNodes(FSBNode[] nodes, boolean dirsOk) { - List fsrls = new ArrayList<>(); - for (FSBNode node : nodes) { - FSRL fsrl = node.getFSRL(); - if (!dirsOk && node instanceof FSBRootNode && fsrl.getFS().hasContainer()) { - // 'convert' a file system root node back into its container file node - fsrl = fsrl.getFS().getContainer(); - } - else { - boolean isDir = (node instanceof FSBDirNode) || (node instanceof FSBRootNode); - if (isDir && !dirsOk) { - continue; - } - } - fsrls.add(fsrl); - } - return fsrls; - } - - private List getFSRLsFromContext(ActionContext context, boolean dirsOk) { - Object contextObject = context.getContextObject(); - if (contextObject instanceof FSBNode) { - contextObject = new FSBNode[] { (FSBNode) contextObject }; - } - if (contextObject instanceof FSBNode[]) { - return getFSRLsFromNodes((FSBNode[]) contextObject, dirsOk); - } - return Collections.emptyList(); - } - - private List getFileFSRLsFromContext(ActionContext context) { - return getFSRLsFromContext(context, false); - } - - private void openProgramFromFile(FSRL file, String suggestedDestinationPath) { + private void openProgramFromFile(FSRL file, FSBNode node, String suggestedDestinationPath) { ProgramManager pm = FSBUtils.getProgramManager(plugin.getTool(), false); if (pm == null) { return; } - TaskLauncher.launchModal("Open Programs", monitor -> { + gTree.runTask(monitor -> { boolean success = doOpenProgramFromFile(file, suggestedDestinationPath, pm, monitor); if (!success) { + if (!ensureFileAccessable(file, node, monitor)) { + return; + } ImporterUtilities.showImportDialog(plugin.getTool(), pm, file, null, suggestedDestinationPath, monitor); } @@ -328,13 +178,13 @@ class FSBActionManager { Program program = ProgramMappingService.findMatchingProgramOpenIfNeeded(fsrl, consumer, programManager, ProgramManager.OPEN_CURRENT); - if (program == null) { - return searchProjectForMatchingFileOrFail(fsrl, suggestedDestinationPath, - programManager, monitor); + if (program != null) { + program.release(consumer); + return true; } - program.release(consumer); - return true; + return searchProjectForMatchingFileOrFail(fsrl, suggestedDestinationPath, programManager, + monitor); } private boolean searchProjectForMatchingFileOrFail(FSRL fsrl, String suggestedDestinationPath, @@ -344,8 +194,8 @@ class FSBActionManager { "Search entire Project for matching program? (WARNING, could take large amount of time)") == OptionDialog.YES_OPTION; Map matchedFSRLs = doSearch - ? ProgramMappingService.searchProjectForMatchingFiles(Arrays.asList(fsrl), monitor) - : Collections.emptyMap(); + ? ProgramMappingService.searchProjectForMatchingFiles(List.of(fsrl), monitor) + : Map.of(); DomainFile domainFile = matchedFSRLs.get(fsrl); if (domainFile != null) { @@ -372,7 +222,7 @@ class FSBActionManager { return; } - TaskLauncher.launchModal("Open Programs", monitor -> { + gTree.runTask(monitor -> { List unmatchedFiles = doOpenProgramsFromFiles(files, pm, monitor); if (unmatchedFiles.size() == 1) { @@ -435,9 +285,9 @@ class FSBActionManager { "Search entire Project for matching programs? " + "(WARNING, could take large amount of time)") == OptionDialog.YES_OPTION; - Map matchedFSRLs = - doSearch ? ProgramMappingService.searchProjectForMatchingFiles(fsrlList, monitor) - : Collections.emptyMap(); + Map matchedFSRLs = doSearch + ? ProgramMappingService.searchProjectForMatchingFiles(fsrlList, monitor) + : Map.of(); List unmatchedFSRLs = new ArrayList<>(); for (FSRL fsrl : fsrlList) { @@ -480,57 +330,6 @@ class FSBActionManager { } - /** - * Shows a dialog with information about the specified file. - * - * @param fsrl {@link FSRL} of the file to display info about. - * @param monitor {@link TaskMonitor} to monitor and update when accessing the filesystems. - */ - private void showInfoForFile(FSRL fsrl, TaskMonitor monitor) { - String title; - String info; - - if (fsrl != null) { - info = ""; - title = "Info about " + fsrl.getName(); - if (fsrl instanceof FSRLRoot && ((FSRLRoot) fsrl).hasContainer()) { - FSRL containerFSRL = ((FSRLRoot) fsrl).getContainer(); - title = containerFSRL.getName(); - info = getInfoStringFor(containerFSRL, monitor); - info += "------------------------------------\n"; - } - info += getInfoStringFor(fsrl, monitor); - } - else { - title = "Missing File"; - info = "Unable to retrieve information"; - } - - MultiLineMessageDialog.showMessageDialog(plugin.getTool().getActiveWindow(), title, null, - info, MultiLineMessageDialog.INFORMATION_MESSAGE); - - } - - private String getInfoStringFor(FSRL fsrl, TaskMonitor monitor) { - try (RefdFile refdFile = FileSystemService.getInstance().getRefdFile(fsrl, monitor)) { - GFileSystem fs = refdFile.fsRef.getFilesystem(); - String result = "File system: " + fs.getDescription() + "\n"; - result += "FSRL: " + fsrl + "\n"; - DomainFile associatedDomainFile = ProgramMappingService.getCachedDomainFileFor(fsrl); - if (associatedDomainFile != null) { - result += "Project file: " + associatedDomainFile.getPathname() + "\n"; - } - String nodeInfo = fs.getInfo(refdFile.file, monitor); - if (nodeInfo != null) { - result += nodeInfo; - } - return result; - } - catch (IOException | CancelledException e) { - return "Error retrieving information: " + e.getMessage() + "\n"; - } - } - /** * Shows a list of supported file system types and loaders. */ @@ -540,7 +339,7 @@ class FSBActionManager { sb.append( "\n"); sb.append("
Supported File SystemsSupported Loaders
    "); - for (String fileSystemName : FileSystemService.getInstance().getAllFilesystemNames()) { + for (String fileSystemName : fsService.getAllFilesystemNames()) { sb.append("
  • " + fileSystemName + "\n"); } @@ -559,671 +358,742 @@ class FSBActionManager { // DockingActions //---------------------------------------------------------------------------------- private DockingAction createSupportedFileSystemsAction() { - - FSBAction action = new FSBAction("Display Supported File Systems and Loaders", plugin) { - @Override - public void actionPerformed(ActionContext context) { - showSupportedFileSystems(); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return true; - } - }; - action.setToolBarData(new ToolBarData(ImageManager.INFO)); - action.setDescription(action.getMenuText()); - action.setEnabled(true); - return action; + return new ActionBuilder("FSB Display Supported File Systems and Loaders", plugin.getName()) + .description("Display Supported File Systems and Loaders") + .withContext(FSBActionContext.class) + .enabledWhen(ac -> true) + .toolBarIcon(ImageManager.INFO) + .onAction(ac -> showSupportedFileSystems()) + .build(); } private DockingAction createExportAction() { - - FSBAction action = new FSBAction("Export", plugin) { - @Override - public void actionPerformed(ActionContext context) { - FSRL fsrl = FSBUtils.getFileFSRLFromContext(context); - if (fsrl != null) { - File selectedFile = - new File(chooserExport.getCurrentDirectory(), fsrl.getName()); - chooserExport.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY); - chooserExport.setTitle("Select Where To Export File"); - chooserExport.setApproveButtonText(getMenuText()); - chooserExport.setSelectedFile(selectedFile); - File outputFile = chooserExport.getSelectedFile(); - if (outputFile == null) { - return; - } - if (outputFile.exists()) { - int answer = OptionDialog.showYesNoDialog(provider.getComponent(), - "Confirm Overwrite", outputFile.getAbsolutePath() + "\n" + - "The file already exists." + "\n" + "Do you want to overwrite it?"); - if (answer == OptionDialog.NO_OPTION) { + return new ActionBuilder("FSB Export", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getFileFSRL() != null) + .popupMenuIcon(ImageManager.EXTRACT) + .popupMenuPath("Export...") + .popupMenuGroup("F", "B") + .onAction( + ac -> { + FSRL fsrl = ac.getFileFSRL(); + if (fsrl == null) { return; } - } - gTree.runTask(monitor -> doExtractFile(fsrl, outputFile, monitor)); - } - } - - private void doExtractFile(FSRL fsrl, File outputFile, TaskMonitor monitor) { - monitor.setMessage("Exporting..."); - try { - File cacheFile = FileSystemService.getInstance().getFile(fsrl, monitor); - long totalBytesCopied = - FileUtilities.copyFile(cacheFile, outputFile, false, monitor); - Msg.info(this, "Exported " + fsrl.getName() + " to " + outputFile + ", " + - totalBytesCopied + " bytes copied."); - } - catch (IOException | CancelledException | UnsupportedOperationException e) { - FSUtilities.displayException(this, plugin.getTool().getActiveWindow(), - "Error Exporting File", e.getMessage(), e); - } - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - // allow files or the root node of nested filesystems - FSRL fsrl = FSBUtils.getFileFSRLFromContext(context); - return !gTree.isBusy() && (fsrl != null); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() + "..." }, - ImageManager.EXTRACT, "F", MenuData.NO_MNEMONIC, "B")); - action.setEnabled(true); - return action; + File selectedFile = + new File(chooserExport.getCurrentDirectory(), fsrl.getName()); + chooserExport.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY); + chooserExport.setTitle("Select Where To Export File"); + chooserExport.setApproveButtonText("Export"); + chooserExport.setSelectedFile(selectedFile); + File outputFile = chooserExport.getSelectedFile(); + if (outputFile == null) { + return; + } + if (outputFile.exists()) { + int answer = OptionDialog.showYesNoDialog(provider.getComponent(), + "Confirm Overwrite", outputFile.getAbsolutePath() + "\n" + + "The file already exists.\n" + + "Do you want to overwrite it?"); + if (answer == OptionDialog.NO_OPTION) { + return; + } + } + gTree.runTask(monitor -> doExtractFile(fsrl, outputFile, + ac.getSelectedNode(), monitor)); + }) + .build(); } private DockingAction createExportAllAction() { + return new ActionBuilder("FSB Export All", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.isSelectedAllDirs()) + .popupMenuIcon(ImageManager.EXTRACT) + .popupMenuPath("Export All...") + .popupMenuGroup("F", "C") + .onAction( + ac -> { + FSRL fsrl = ac.getFSRL(true); + if (fsrl == null) { + return; + } + if (fsrl instanceof FSRLRoot) { + fsrl = fsrl.appendPath("/"); + } - FSBAction action = new FSBAction("Export All", plugin) { - @Override - public void actionPerformed(ActionContext context) { - FSRL fsrl = FSBUtils.getFSRLFromContext(context, true); - if (fsrl == null) { - return; - } - if (fsrl instanceof FSRLRoot) { - fsrl = fsrl.appendPath("/"); - } + chooserExportAll + .setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); + chooserExportAll.setTitle("Select Export Directory"); + chooserExportAll.setApproveButtonText("Export All"); + chooserExportAll.setSelectedFile(null); + File outputFile = chooserExportAll.getSelectedFile(); + if (outputFile == null) { + return; + } - chooserExportAll.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); - chooserExportAll.setTitle("Select Export Directory"); - chooserExportAll.setApproveButtonText(getMenuText()); - chooserExportAll.setSelectedFile(null); - File outputFile = chooserExportAll.getSelectedFile(); - if (outputFile == null) { - return; - } - - if (!outputFile.isDirectory()) { - Msg.showInfo(getClass(), provider.getComponent(), getMenuText(), - "Selected file is not a directory."); - return; - } - Component parentComp = plugin.getTool().getActiveWindow(); - TaskLauncher.launch(new GFileSystemExtractAllTask(fsrl, outputFile, parentComp)); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - FSRL fsrl = FSBUtils.getFSRLFromContext(context, true); - return !gTree.isBusy() && (fsrl != null) && isSelectedContextAllDirs(context); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() + "..." }, - ImageManager.EXTRACT, "F", MenuData.NO_MNEMONIC, "C")); - action.setEnabled(false); - return action; + if (!outputFile.isDirectory()) { + Msg.showInfo(this, provider.getComponent(), "Export All", + "Selected file is not a directory."); + return; + } + Component parentComp = plugin.getTool().getActiveWindow(); + TaskLauncher.launch( + new GFileSystemExtractAllTask(fsrl, outputFile, parentComp)); + }) + .build(); } private DockingAction createViewAsImageAction() { - - FSBAction action = new FSBAction("View As Image", plugin) { - @Override - public void actionPerformed(ActionContext context) { - FSRL fsrl = FSBUtils.getFSRLFromContext(context, false); - if (fsrl != null) { - gTree.runTask(monitor -> doViewAsImage(fsrl, monitor)); - } - } - - private void doViewAsImage(FSRL fsrl, TaskMonitor monitor) { - Component parent = plugin.getTool().getActiveWindow(); - try (RefdFile refdFile = - FileSystemService.getInstance().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."); - } - else { - SystemUtilities.runSwingLater(() -> { - 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); - } - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - FSRL fsrl = FSBUtils.getFSRLFromContext(context, false); - return !gTree.isBusy() && (fsrl != null); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - }; - action.setPopupMenuData( - new MenuData(new String[] { action.getMenuText() }, ImageManager.VIEW_AS_IMAGE, "G")); - action.setEnabled(true); - return action; + return new ActionBuilder("FSB View As Image", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getFileFSRL() != null) + .popupMenuIcon(ImageManager.VIEW_AS_IMAGE) + .popupMenuPath("View As Image") + .popupMenuGroup("G") + .onAction( + ac -> { + FSRL fsrl = ac.getFileFSRL(); + if (fsrl != null) { + gTree.runTask( + monitor -> doViewAsImage(fsrl, ac.getSelectedNode(), monitor)); + } + }) + .build(); } private DockingAction createViewAsTextAction() { - - FSBAction action = new FSBAction("View As Text", plugin) { - @Override - public void actionPerformed(ActionContext context) { - FSRL fsrl = FSBUtils.getFSRLFromContext(context, false); - if (fsrl != null) { - gTree.runTask(monitor -> doViewAsText(fsrl, monitor)); - } - } - - private void doViewAsText(FSRL fsrl, TaskMonitor monitor) { - Component parent = plugin.getTool().getActiveWindow(); - try { - File file = FileSystemService.getInstance().getFile(fsrl, monitor); - if (file.length() == -1 || file.length() > MAX_TEXT_FILE_LEN) { - Msg.showInfo(this, parent, "View As Text Failed", - "File too large to view as text inside Ghidra. " + - "Please use the \"EXPORT\" action."); - return; - } - if (file.length() == 0) { - Msg.showInfo(this, parent, "View As Text Failed", - "File " + fsrl.getName() + " is empty (0 bytes)."); - return; - } - try { - InputStream inputStream = new FileInputStream(file); - // textEditorService closes the inputStream, and must be - // called on the swing thread or you get concurrentmodification - // exceptions. - SystemUtilities.runSwingLater( - () -> textEditorService.edit(fsrl.getName(), inputStream)); - } - catch (IOException e) { - Msg.showError(this, parent, "View As Text Failed", - "Error when trying to view text file " + fsrl.getName(), e); - } - } - catch (IOException | CancelledException e) { - FSUtilities.displayException(this, parent, "Error viewing text file", - e.getMessage(), e); - } - - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - FSRL fsrl = FSBUtils.getFSRLFromContext(context, false); - return !gTree.isBusy() && (fsrl != null); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - }; - action.setPopupMenuData( - new MenuData(new String[] { action.getMenuText() }, ImageManager.VIEW_AS_TEXT, "G")); - action.setEnabled(true); - return action; + return new ActionBuilder("FSB View As Text", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getFileFSRL() != null) + .popupMenuIcon(ImageManager.VIEW_AS_TEXT) + .popupMenuPath("View As Text") + .popupMenuGroup("G") + .onAction( + ac -> { + FSRL fsrl = ac.getFileFSRL(); + if (fsrl != null) { + gTree.runTask( + monitor -> doViewAsText(fsrl, ac.getSelectedNode(), monitor)); + } + }) + .build(); } private DockingAction createListMountedFilesystemsAction() { + return new ActionBuilder("FSB List Mounted Filesystems", plugin.getName()) + .description("List Mounted Filesystems") + .withContext(FSBActionContext.class) + .enabledWhen(FSBActionContext::notBusy) + .toolBarIcon(ImageManager.LIST_MOUNTED) + .toolBarGroup("ZZZZ") + .popupMenuIcon(ImageManager.LIST_MOUNTED) + .popupMenuPath("List Mounted Filesystems") + .popupMenuGroup("L") + .onAction(ac -> { + FSRLRoot fsFSRL = SelectFromListDialog.selectFromList( + fsService.getMountedFilesystems(), + "Select filesystem", + "Choose filesystem to view", f -> f.toPrettyString()); - FSBAction action = new FSBAction("List Mounted Filesystems", plugin) { - @Override - public void actionPerformed(ActionContext context) { - FSRLRoot fsFSRL = SelectFromListDialog.selectFromList( - FileSystemService.getInstance().getMountedFilesystems(), "Select filesystem", - "Choose filesystem to view", f -> f.toPrettyString()); - - FileSystemRef fsRef; - if (fsFSRL != null && (fsRef = - FileSystemService.getInstance().getMountedFilesystem(fsFSRL)) != null) { - plugin.createNewFileSystemBrowser(fsRef, true); - } - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return !gTree.isBusy(); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - }; - action.setPopupMenuData( - new MenuData(new String[] { action.getMenuText() }, ImageManager.LIST_MOUNTED, "L")); - action.setToolBarData(new ToolBarData(ImageManager.LIST_MOUNTED, "ZZZZ")); - action.setDescription(action.getMenuText()); - action.setEnabled(true); - return action; + FileSystemRef fsRef; + if (fsFSRL != null && + (fsRef = fsService.getMountedFilesystem(fsFSRL)) != null) { + plugin.createNewFileSystemBrowser(fsRef, true); + } + }) + .build(); } private DockingAction createExpandAllAction() { - - FSBAction action = new FSBAction("Expand All", plugin) { - @Override - public void actionPerformed(ActionContext context) { - if (context.getContextObject() instanceof GTreeNode) { - GTreeNode node = (GTreeNode) context.getContextObject(); - gTree.expandTree(node); - } - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - Object co = context.getContextObject(); - return !gTree.isBusy() && - ((co instanceof FSBRootNode) || (co instanceof FSBDirNode)); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() }, - ImageManager.EXPAND_ALL, "B", MenuData.NO_MNEMONIC, "A")); - action.setEnabled(true); - return action; + return new ActionBuilder("FSB Expand All", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen( + ac -> ac.notBusy() && ac.getSelectedCount() == 1 && ac.isSelectedAllDirs()) + .popupMenuIcon(ImageManager.EXPAND_ALL) + .popupMenuPath("Expand All") + .popupMenuGroup("B", "A") + .onAction(ac -> { + FSBNode selectedNode = ac.getSelectedNode(); + if (selectedNode != null) { + gTree.expandTree(selectedNode); + } + }) + .build(); } private DockingAction createCollapseAllAction() { - - FSBAction action = new FSBAction("Collapse All", plugin) { - @Override - public void actionPerformed(ActionContext context) { - if (context.getContextObject() instanceof GTreeNode) { - GTreeNode node = (GTreeNode) context.getContextObject(); - gTree.collapseAll(node); - } - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - Object co = context.getContextObject(); - return !gTree.isBusy() && - ((co instanceof FSBRootNode) || (co instanceof FSBDirNode)); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() }, - ImageManager.COLLAPSE_ALL, "B", MenuData.NO_MNEMONIC, "B")); - action.setEnabled(true); - return action; + return new ActionBuilder("FSB Collapse All", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen( + ac -> ac.notBusy() && ac.getSelectedCount() == 1 && ac.isSelectedAllDirs()) + .popupMenuIcon(ImageManager.COLLAPSE_ALL) + .popupMenuPath("Collapse All") + .popupMenuGroup("B", "B") + .onAction(ac -> { + FSBNode selectedNode = ac.getSelectedNode(); + if (selectedNode != null) { + gTree.collapseAll(selectedNode); + } + }) + .build(); } private DockingAction createGetInfoAction() { - - FSBAction action = new FSBAction("Get Info", plugin) { - @Override - public void actionPerformed(ActionContext context) { - FSRL fsrl = FSBUtils.getFSRLFromContext(context, true); - gTree.runTask(monitor -> showInfoForFile(fsrl, monitor)); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - FSRL fsrl = FSBUtils.getFSRLFromContext(context, true); - return !gTree.isBusy() && fsrl != null; - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - }; - action.setPopupMenuData( - new MenuData(new String[] { action.getMenuText() }, ImageManager.INFO, "A")); - action.setEnabled(true); - return action; + return new ActionBuilder("Get Info", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getFSRL(true) != null) + .popupMenuPath("Get Info") + .popupMenuGroup("A") + .popupMenuIcon(ImageManager.INFO) + .description("Show information about a file") + .onAction( + ac -> { + FSRL fsrl = ac.getFSRL(true); + gTree.runTask(monitor -> showInfoForFile(fsrl, monitor)); + }) + .build(); } - private DockingAction createOpenFileSystemActionNewWindow() { - FSBAction action = new FSBAction("Open File System In New Window", - "Open File System in new window", plugin) { - @Override - public void actionPerformed(ActionContext context) { + private DockingAction createOpenFileSystemNewWindowAction() { + return new ActionBuilder("FSB Open File System In New Window", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getSelectedNode() instanceof FSBFileNode) + .popupMenuIcon(ImageManager.OPEN_FILE_SYSTEM) + .popupMenuPath("Open File System in new window") + .popupMenuGroup("C") + .onAction( + ac -> { + if (!(ac.getSelectedNode() instanceof FSBFileNode) || + ac.getSelectedNode().getFSRL() == null) { + return; + } + FSBFileNode selectedNode = (FSBFileNode) ac.getSelectedNode(); + FSRL containerFSRL = selectedNode.getFSRL(); + if (containerFSRL != null) { + gTree.runTask(monitor -> { + doOpenFileSystem(containerFSRL, selectedNode, false, monitor); + }); + } + }) + .build(); + } - FSRL containerFSRL = FSBUtils.getFileFSRLFromContext(context); - if (containerFSRL != null) { - gTree.runTask(monitor -> { - doOpenFileSystem(containerFSRL, monitor); - }); - } - } - - /* - * run on gTree task thread - */ - private void doOpenFileSystem(FSRL containerFSRL, TaskMonitor monitor) { - try { - monitor.setMessage("Probing " + containerFSRL.getName() + " for filesystems"); - FileSystemRef ref = FileSystemService.getInstance() - .probeFileForFilesystem( - containerFSRL, monitor, FileSystemProbeConflictResolver.GUI_PICKER); - if (ref == null) { - Msg.showWarn(this, plugin.getTool().getActiveWindow(), "Open Filesystem", - "No filesystem provider for " + containerFSRL.getName()); + private DockingAction createOpenFileSystemNestedAction() { + return new ActionBuilder("FSB Open File System Nested", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getSelectedNode() instanceof FSBFileNode) + .popupMenuIcon(ImageManager.OPEN_FILE_SYSTEM) + .popupMenuPath("Open File System") + .popupMenuGroup("C") + .onAction(ac -> { + if (!(ac.getSelectedNode() instanceof FSBFileNode) || + ac.getSelectedNode().getFSRL() == null) { return; } - - SystemUtilities.runSwingLater(() -> { - plugin.createNewFileSystemBrowser(ref, true); - }); - } - catch (IOException | CancelledException e) { - FSUtilities.displayException(this, plugin.getTool().getActiveWindow(), - "Open Filesystem", - "Error opening filesystem for " + containerFSRL.getName(), e); - } - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - FSRL containerFSRL = FSBUtils.getFSRLFromContext(context, false); - return !gTree.isBusy() && (containerFSRL != null); - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() }, - ImageManager.OPEN_FILE_SYSTEM, "C")); - action.setEnabled(true); - return action; + FSBFileNode selectedNode = (FSBFileNode) ac.getSelectedNode(); + FSRL containerFSRL = selectedNode.getFSRL(); + if (containerFSRL != null) { + gTree.runTask(monitor -> { + doOpenFileSystem(containerFSRL, selectedNode, true, monitor); + }); + } + }) + .build(); } private DockingAction createOpenNewFileSystemAction() { - FSBAction action = new FSBAction("Open File System Chooser", plugin) { - @Override - public void actionPerformed(ActionContext context) { - plugin.openFileSystem(); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return false; - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return true; - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() }, - ImageManager.OPEN_FILE_SYSTEM, "C")); - action.setDescription(action.getMenuText()); - action.setToolBarData(new ToolBarData(ImageManager.OPEN_FILE_SYSTEM, "B")); - action.setEnabled(true); - return action; + return new ActionBuilder("FSB Open File System Chooser", plugin.getName()) + .description("Open File System Chooser") + .withContext(FSBActionContext.class) + .enabledWhen(FSBActionContext::notBusy) + .toolBarIcon(ImageManager.OPEN_FILE_SYSTEM) + .toolBarGroup("B") + .onAction(ac -> plugin.openFileSystem()) + .build(); } - private DockingAction createOpenAllProgramsAction() { - - FSBAction action = new FSBAction("Open Programs", "Open Program(s)", plugin) { - @Override - public void actionPerformed(ActionContext context) { - if (!plugin.hasProgramManager()) { - Msg.showInfo(this, plugin.getTool().getActiveWindow(), "Open Program Error", - "There is no tool currently open that can be used to show a program."); - return; - } - List files = getLoadableFSRLsFromContext(context); - if (files.size() == 1) { - String treePath = - FilenameUtils.getFullPathNoEndSeparator(getFormattedTreePath(context)); - openProgramFromFile(files.get(0), treePath); - } - else if (files.size() > 1) { - openProgramsFromFiles(files); - } - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return !gTree.isBusy() && !getLoadableFSRLsFromContext(context).isEmpty(); - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() }, - ImageManager.OPEN_ALL, "D", MenuData.NO_MNEMONIC, "B")); - action.setEnabled(plugin.hasProgramManager()); - return action; + private DockingAction createOpenProgramsAction() { + return new ActionBuilder("FSB Open Programs", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && plugin.hasProgramManager() && + !ac.getLoadableFSRLs().isEmpty()) + .popupMenuIcon(ImageManager.OPEN_ALL) + .popupMenuPath("Open Program(s)") + .popupMenuGroup("D", "B") + .onAction(ac -> { + if (!plugin.hasProgramManager()) { + Msg.showInfo(this, plugin.getTool().getActiveWindow(), "Open Program Error", + "There is no tool currently open that can be used to show a program."); + return; + } + List files = ac.getLoadableFSRLs(); + if (files.size() == 1) { + String treePath = + FilenameUtils.getFullPathNoEndSeparator(ac.getFormattedTreePath()); + openProgramFromFile(files.get(0), ac.getSelectedNodes().get(0), treePath); + } + else if (files.size() > 1) { + openProgramsFromFiles(files); + } + }) + .build(); } private DockingAction createCloseAction() { - FSBAction action = new FSBAction("Close", plugin) { - @Override - public void actionPerformed(ActionContext context) { - - if (!(context.getContextObject() instanceof FSBRootNode)) { - return; - } - FSBRootNode node = (FSBRootNode) context.getContextObject(); - if (node.equals(gTree.getModelRoot())) { - // Close entire window - FileSystemRef fsRef = node.getFSRef(); - if (fsRef != null && !fsRef.isClosed() && - OptionDialog.showYesNoDialog(provider.getComponent(), "Close File System", - "Do you want to close the filesystem browser for " + - fsRef.getFilesystem().getName() + "?") == OptionDialog.YES_OPTION) { - plugin.removeFileSystemBrowser(fsRef.getFilesystem().getFSRL()); - this.setEnabled(false); + return new ActionBuilder("FSB Close", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getSelectedNode() instanceof FSBRootNode) + .description("Close") + .toolBarIcon(ImageManager.CLOSE) + .toolBarGroup("ZZZZ") + .popupMenuIcon(ImageManager.CLOSE) + .popupMenuPath("Close") + .popupMenuGroup("ZZZZ") + .onAction(ac -> { + FSBNode selectedNode = ac.getSelectedNode(); + if (!(selectedNode instanceof FSBRootNode)) { + return; } - } - else { - // Close file system that is nested in the container's tree. - gTree.runTask(monitor -> { - int indexInParent = node.getIndexInParent(); - GTreeNode parent = node.getParent(); - parent.removeNode(node); - GTreeNode prevNode = node.getPrevNode(); - parent.addNode(indexInParent, prevNode); - node.dispose(); - }); - } - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return !gTree.isBusy() && (context.getContextObject() instanceof FSBRootNode); - } - }; - action.setPopupMenuData( - new MenuData(new String[] { action.getMenuText() }, ImageManager.CLOSE, "ZZZZ")); - action.setDescription(action.getMenuText()); - action.setToolBarData(new ToolBarData(ImageManager.CLOSE, "ZZZZ")); - action.setEnabled(false); - return action; + FSBRootNode node = (FSBRootNode) selectedNode; + if (node.getParent() == null) { + // Close entire window + if (OptionDialog.showYesNoDialog(provider.getComponent(), + "Close File System", + "Do you want to close the filesystem browser for " + node.getName() + + "?") == OptionDialog.YES_OPTION) { + provider.componentHidden(); // cause component to close itself + } + } + else { + // Close file system that is nested in the container's tree and swap + // in the saved node that was the original container file + gTree.runTask(monitor -> node.swapBackPrevModelNodeAndDispose()); + } + }) + .build(); } private DockingAction createImportAction() { - FSBAction action = new FSBAction("Import Single", "Import", plugin) { + return new ActionBuilder("FSB Import Single", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getLoadableFSRL() != null) + .popupMenuIcon(ImageManager.IMPORT) + .popupMenuPath("Import") + .popupMenuGroup("F", "A") + .onAction(ac -> { + FSRL fsrl = ac.getLoadableFSRL(); + if (fsrl == null) { + return; + } - @Override - public void actionPerformed(ActionContext context) { - FSRL fsrl = getLoadableFSRLFromContext(context); - if (fsrl == null) { - return; - } + String treePath = ac.getFormattedTreePath(); + String suggestedPath = + FilenameUtils.getFullPathNoEndSeparator(treePath).replaceAll(":/", "/"); - String treePath = getFormattedTreePath(context); - String suggestedPath = - FilenameUtils.getFullPathNoEndSeparator(treePath).replaceAll(":/", "/"); + PluginTool tool = plugin.getTool(); + ProgramManager pm = FSBUtils.getProgramManager(tool, false); - PluginTool tool = plugin.getTool(); - ProgramManager pm = FSBUtils.getProgramManager(tool, false); - ImporterUtilities.showImportDialog(tool, pm, fsrl, null, suggestedPath); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return !gTree.isBusy() && getLoadableFSRLFromContext(context) != null; - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() }, - ImageManager.IMPORT, "F", MenuData.NO_MNEMONIC, "A")); - action.setEnabled(true); - - return action; + gTree.runTask(monitor -> { + if (!ensureFileAccessable(fsrl, ac.getSelectedNode(), monitor)) { + return; + } + ImporterUtilities.showImportDialog(tool, pm, fsrl, null, suggestedPath, + monitor); + }); + }) + .build(); } private DockingAction createBatchImportAction() { - FSBAction action = new FSBAction("Import Batch", "Batch Import", plugin) { + return new ActionBuilder("FSB Import Batch", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.getSelectedCount() > 0) + .popupMenuIcon(ImageManager.IMPORT) + .popupMenuPath("Batch Import") + .popupMenuGroup("F", "B") + .onAction(ac -> { + // Do some fancy selection logic. + // If the user selected a combination of files and folders, + // ignore the folders. + // If they only selected folders, leave them in the list. + List files = ac.getFSRLs(true); + if (files.isEmpty()) { + return; + } - @Override - public void actionPerformed(ActionContext context) { - // Do some fancy selection logic. - // If the user selected a combination of files and folders, - // ignore the folders. - // If they only selected folders, leave them in the list. - List files = getFSRLsFromContext(context, true); - if (files.isEmpty()) { - return; - } + boolean allDirs = ac.isSelectedAllDirs(); + if (files.size() > 1 && !allDirs) { + files = ac.getFileFSRLs(); + } - boolean allDirs = isSelectedContextAllDirs(context); - if (files.size() > 1 && !allDirs) { - files = getFileFSRLsFromContext(context); - } - - BatchImportDialog.showAndImport(plugin.getTool(), null, files, null, - FSBUtils.getProgramManager(plugin.getTool(), false)); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return !gTree.isBusy() && !getFSRLsFromContext(context, true).isEmpty(); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() }, - ImageManager.IMPORT, "F", MenuData.NO_MNEMONIC, "B")); - action.setEnabled(true); - - return action; + BatchImportDialog.showAndImport(plugin.getTool(), null, files, null, + FSBUtils.getProgramManager(plugin.getTool(), false)); + }) + .build(); } - private DockingAction createOpenNestedFileSystemAction() { - FSBAction action = new FSBAction("Open File System Nested", "Open File System", plugin) { - @Override - public void actionPerformed(ActionContext context) { - FSRL containerFSRL = FSBUtils.getFileFSRLFromContext(context); - if (containerFSRL != null && context.getContextObject() instanceof FSBFileNode) { - FSBFileNode xfileNode = (FSBFileNode) context.getContextObject(); - FSBFileNode modelFileNode = - (FSBFileNode) gTree.getModelNodeForPath(xfileNode.getTreePath()); - - gTree.runTask(monitor -> { - try { - FileSystemRef fsRef = - FileSystemService.getInstance() - .probeFileForFilesystem( - containerFSRL, monitor, - FileSystemProbeConflictResolver.GUI_PICKER); - if (fsRef == null) { - Msg.showWarn(this, gTree, "No File System Provider", - "No file system provider for " + containerFSRL.getName()); - return; - } - - FSBRootNode nestedRootNode = new FSBRootNode(fsRef, modelFileNode); - nestedRootNode.setChildren(nestedRootNode.generateChildren(monitor)); - - int indexInParent = modelFileNode.getIndexInParent(); - GTreeNode parent = modelFileNode.getParent(); - parent.removeNode(modelFileNode); - parent.addNode(indexInParent, nestedRootNode); - gTree.expandPath(nestedRootNode); - } - catch (CancelledException | IOException e) { - FSUtilities.displayException(this, gTree, "Error Opening FileSystem", - e.getMessage(), e); - } - }); - } - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return true; - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - FSRL containerFSRL = FSBUtils.getFileFSRLFromContext(context); - return !gTree.isBusy() && (containerFSRL != null) && - (context.getContextObject() instanceof FSBFileNode); - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() }, - ImageManager.OPEN_FILE_SYSTEM, "C")); - action.setEnabled(true); - return action; + private DockingAction createClearCachedPasswordsAction() { + return new ActionBuilder("FSB Clear Cached Passwords", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(FSBActionContext::notBusy) + .popupMenuPath("Clear Cached Passwords") + .popupMenuGroup("Z", "B") + .description("Clear cached container file passwords") + .onAction( + ac -> { + CachedPasswordProvider ccp = + CryptoProviders.getInstance().getCachedCryptoProvider(); + int preCount = ccp.getCount(); + ccp.clearCache(); + Msg.info(this, + "Cleared " + (preCount - ccp.getCount()) + " cached passwords."); + }) + .build(); } + + private DockingAction createRefreshAction() { + return new ActionBuilder("FSB Refresh", plugin.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> ac.notBusy() && ac.hasSelectedNodes()) + .popupMenuPath("Refresh") + .popupMenuGroup("Z", "Z") + .description("Refresh file info") + .onAction( + ac -> gTree.runTask(monitor -> doRefreshInfo(ac.getSelectedNodes(), monitor))) + .build(); + } + //---------------------------------------------------------------------------------- // end DockingActions //---------------------------------------------------------------------------------- + private void doExtractFile(FSRL fsrl, File outputFile, FSBNode node, TaskMonitor monitor) { + if (!ensureFileAccessable(fsrl, node, monitor)) { + return; + } + monitor.setMessage("Exporting..."); + try (ByteProvider fileBP = fsService.getByteProvider(fsrl, false, monitor)) { + long bytesCopied = + FSUtilities.copyByteProviderToFile(fileBP, outputFile, monitor); + Msg.info(this, "Exported " + fsrl.getName() + " to " + outputFile + ", " + + bytesCopied + " bytes copied."); + } + catch (IOException | CancelledException | UnsupportedOperationException e) { + FSUtilities.displayException(this, plugin.getTool().getActiveWindow(), + "Error Exporting File", e.getMessage(), e); + } + } + + /* + * run on gTree task thread + */ + private void doOpenFileSystem(FSRL containerFSRL, FSBFileNode node, boolean nested, + TaskMonitor monitor) { + try { + if (!ensureFileAccessable(containerFSRL, node, monitor)) { + return; + } + + monitor.setMessage("Probing " + containerFSRL.getName() + " for filesystems"); + FileSystemRef ref = fsService.probeFileForFilesystem(containerFSRL, monitor, + FileSystemProbeConflictResolver.GUI_PICKER); + if (ref == null) { + Msg.showWarn(this, plugin.getTool().getActiveWindow(), "Open Filesystem", + "No filesystem detected in " + containerFSRL.getName()); + return; + } + + Swing.runLater(() -> { + if (nested) { + FSBFileNode modelFileNode = + (FSBFileNode) gTree.getModelNodeForPath(node.getTreePath()); + + FSBRootNode nestedRootNode = new FSBRootNode(ref, modelFileNode); + try { + nestedRootNode.setChildren(nestedRootNode.generateChildren(monitor)); + } + catch (CancelledException e) { + Msg.warn(this, "Failed to populate FSB root node with children"); + } + + int indexInParent = modelFileNode.getIndexInParent(); + GTreeNode parent = modelFileNode.getParent(); + parent.removeNode(modelFileNode); + parent.addNode(indexInParent, nestedRootNode); + gTree.expandPath(nestedRootNode); + provider.contextChanged(); + } + else { + plugin.createNewFileSystemBrowser(ref, true); + } + }); + } + catch (IOException | CancelledException e) { + FSUtilities.displayException(this, plugin.getTool().getActiveWindow(), + "Open Filesystem", + "Error opening filesystem for " + containerFSRL.getName(), e); + } + } + + private void doViewAsImage(FSRL fsrl, FSBNode node, TaskMonitor monitor) { + if (!ensureFileAccessable(fsrl, node, monitor)) { + return; + } + + Component parent = plugin.getTool().getActiveWindow(); + try (RefdFile refdFile = fsService.getRefdFile(fsrl, monitor)) { + + Icon icon = GIconProvider.getIconForFile(refdFile.file, monitor); + if (icon == null) { + Msg.showError(this, parent, "Unable To View Image", + "Unable to view " + fsrl.getName() + " as an image."); + return; + } + Swing.runLater(() -> { + JLabel label = new GIconLabel(icon); + JOptionPane.showMessageDialog(null, label, + "Image Viewer: " + fsrl.getName(), JOptionPane.INFORMATION_MESSAGE); + }); + } + catch (IOException | CancelledException e) { + FSUtilities.displayException(this, parent, "Error Viewing Image File", + e.getMessage(), e); + } + } + + private void doViewAsText(FSRL fsrl, FSBNode node, TaskMonitor monitor) { + if (!ensureFileAccessable(fsrl, node, monitor)) { + return; + } + + Component parent = plugin.getTool().getActiveWindow(); + try (ByteProvider fileBP = fsService.getByteProvider(fsrl, false, monitor)) { + + if (fileBP.length() > MAX_TEXT_FILE_LEN) { + Msg.showInfo(this, parent, "View As Text Failed", + "File too large to view as text inside Ghidra. " + + "Please use the \"EXPORT\" action."); + return; + } + if (fileBP.length() == 0) { + Msg.showInfo(this, parent, "View As Text Failed", + "File " + fsrl.getName() + " is empty (0 bytes)."); + return; + } + try { + // textEditorService closes the inputStream, and must be + // called on the swing thread or you get concurrentmodification + // exceptions. + ByteArrayInputStream bais = + new ByteArrayInputStream(fileBP.readBytes(0, fileBP.length())); + Swing.runLater(() -> textEditorService.edit(fsrl.getName(), bais)); + } + catch (IOException e) { + Msg.showError(this, parent, "View As Text Failed", + "Error when trying to view text file " + fsrl.getName(), e); + } + } + catch (IOException | CancelledException e) { + FSUtilities.displayException(this, parent, "Error Viewing Text File", + e.getMessage(), e); + } + } + + void doRefreshInfo(List nodes, TaskMonitor monitor) { + Set rootNodes = new HashSet<>(); + for (FSBNode node : nodes) { + if (node instanceof FSBFileNode) { + // for each file node, if it's password attr info is out of date, build a unique + // list of the containing root nodes that will later be used to refresh the + // entire FS + if (((FSBFileNode) node).needsFileAttributesUpdate(monitor)) { + rootNodes.add(node.getFSBRootNode()); + } + } + else if (node instanceof FSBDirNode) { + // if the user selected a dir node, force the FS to be refreshed + rootNodes.add(node.getFSBRootNode()); + } + else if (node instanceof FSBRootNode) { + rootNodes.add((FSBRootNode) node); + } + } + try { + for (FSBRootNode rootNode : rootNodes) { + rootNode.updateFileAttributes(monitor); + } + gTree.refilterLater(); // force the changed modelNodes to be recloned and displayed (if filter active) + } + catch (CancelledException e) { + // stop + } + Swing.runLater(() -> gTree.repaint()); + } + + private boolean ensureFileAccessable(FSRL fsrl, FSBNode node, TaskMonitor monitor) { + + FSBFileNode fileNode = (node instanceof FSBFileNode) ? (FSBFileNode) node : null; + + monitor.initialize(0); + monitor.setMessage("Testing file access"); + boolean wasMissingPasword = (fileNode != null) ? fileNode.hasMissingPassword() : false; + try (ByteProvider bp = fsService.getByteProvider(fsrl, false, monitor)) { + // if we can get here and it used to have a missing password, update the node's status + if (fileNode != null && wasMissingPasword) { + doRefreshInfo(List.of(fileNode), monitor); + } + return true; + } + catch (CryptoException e) { + Msg.showWarn(this, gTree, "Crypto / Password Error", + "Unable to access the specified file.\n" + + "This could be caused by not entering the correct password or because of missing crypto information.\n\n" + + e.getMessage()); + return false; + } + catch (IOException e) { + Msg.showError(this, gTree, "File IO Error", + "Unable to access the specified file.\n\n" + e.getMessage(), e); + return false; + } + catch (CancelledException e) { + return false; + } + + } + + //--------------------------------------------------------------------------------------------- + // static lookup tables for rendering file attributes + //--------------------------------------------------------------------------------------------- + private static final Function PLAIN_TOSTRING = o -> o.toString(); + private static final Function SIZE_TOSTRING = o -> (o instanceof Long) + ? FSUtilities.formatSize((Long) o) + : o.toString(); + private static final Function UNIX_ACL_TOSTRING = o -> (o instanceof Number) + ? String.format("%05o", (Number) o) + : o.toString(); + private static final Function DATE_TOSTRING = o -> (o instanceof Date) + ? FSUtilities.formatFSTimestamp((Date) o) + : o.toString(); + private static final Function FSRL_TOSTRING = o -> (o instanceof FSRL) + ? ((FSRL) o).toPrettyString().replace("|", "|\n\t") + : o.toString(); + + private static final Map> FAT_TOSTRING_FUNCS = + Map.ofEntries( + entry(FSRL_ATTR, FSRL_TOSTRING), + entry(SIZE_ATTR, SIZE_TOSTRING), + entry(COMPRESSED_SIZE_ATTR, SIZE_TOSTRING), + entry(CREATE_DATE_ATTR, DATE_TOSTRING), + entry(MODIFIED_DATE_ATTR, DATE_TOSTRING), + entry(ACCESSED_DATE_ATTR, DATE_TOSTRING), + entry(UNIX_ACL_ATTR, UNIX_ACL_TOSTRING)); + + /** + * Shows a dialog with information about the specified file. + * + * @param fsrl {@link FSRL} of the file to display info about. + * @param monitor {@link TaskMonitor} to monitor and update when accessing the filesystems. + */ + private void showInfoForFile(FSRL fsrl, TaskMonitor monitor) { + if (fsrl == null) { + Msg.showError(this, null, "Missing File", "Unable to retrieve information"); + return; + } + + // if looking at the root of a nested file system, also include its parent container + List fsrls = (fsrl instanceof FSRLRoot && ((FSRLRoot) fsrl).hasContainer()) + ? List.of(((FSRLRoot) fsrl).getContainer(), fsrl) + : List.of(fsrl); + String title = "Info about " + fsrls.get(0).getName(); + List fattrs = new ArrayList<>(); + for (FSRL fsrl2 : fsrls) { + try { + fattrs.add(getAttrsFor(fsrl2, monitor)); + } + catch (IOException e) { + Msg.warn(this, "Failed to get info for file " + fsrl2, e); + } + catch (CancelledException e) { + return; + } + } + String html = getHTMLInfoStringForAttributes(fattrs); + + MultiLineMessageDialog.showMessageDialog(plugin.getTool().getActiveWindow(), title, null, + html, MultiLineMessageDialog.INFORMATION_MESSAGE); + } + + private FileAttributes getAttrsFor(FSRL fsrl, TaskMonitor monitor) + throws CancelledException, IOException { + try (RefdFile refdFile = fsService.getRefdFile(fsrl, monitor)) { + GFileSystem fs = refdFile.fsRef.getFilesystem(); + GFile file = refdFile.file; + FileAttributes fattrs = fs.getFileAttributes(file, monitor); + if (fattrs == null) { + fattrs = FileAttributes.EMPTY; + } + fattrs = fattrs.clone(); + DomainFile associatedDomainFile = ProgramMappingService.getCachedDomainFileFor(fsrl); + if (associatedDomainFile != null) { + fattrs.add(PROJECT_FILE_ATTR, associatedDomainFile.getPathname()); + } + + if (!fattrs.contains(NAME_ATTR)) { + fattrs.add(NAME_ATTR, file.getName()); + } + if (!fattrs.contains(PATH_ATTR)) { + fattrs.add(PATH_ATTR, FilenameUtils.getFullPath(file.getPath())); + } + if (!fattrs.contains(FSRL_ATTR)) { + fattrs.add(FSRL_ATTR, file.getFSRL()); + } + return fattrs; + } + } + + private String getHTMLInfoStringForAttributes(List fileAttributesList) { + StringBuilder sb = + new StringBuilder("\n\n"); + sb.append("\n"); + for (FileAttributes fattrs : fileAttributesList) { + if (fattrs != fileAttributesList.get(0)) { + // not first element, put a visual divider line + sb.append(""); + } + List> sortedAttribs = fattrs.getAttributes(); + Collections.sort(sortedAttribs, + (o1, o2) -> Integer.compare(o1.getAttributeType().ordinal(), + o2.getAttributeType().ordinal())); + + FileAttributeTypeGroup group = null; + for (FileAttribute attr : sortedAttribs) { + if (attr.getAttributeType().getGroup() != group) { + group = attr.getAttributeType().getGroup(); + if (group != FileAttributeTypeGroup.GENERAL_INFO) { + sb + .append("\n"); + } + } + String valStr = + FAT_TOSTRING_FUNCS.getOrDefault(attr.getAttributeType(), PLAIN_TOSTRING) + .apply(attr.getAttributeValue()); + + String html = HTMLUtilities.escapeHTML(valStr); + html = html.replace("\n", "
    \n"); + sb + .append("\n"); + } + } + sb.append("
    PropertyValue

    ") + .append(group.getDescriptiveName()) + .append("
    ") + .append(attr.getAttributeDisplayName()) + .append(":") + .append(html) + .append("
    "); + return sb.toString(); + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBDirNode.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBDirNode.java index 3fd891d507..092716faa0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBDirNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBDirNode.java @@ -16,58 +16,44 @@ package ghidra.plugins.fsbrowser; import java.io.IOException; -import java.util.Collections; import java.util.List; -import javax.swing.Icon; - import docking.widgets.tree.GTreeNode; -import ghidra.formats.gfilesystem.*; -import ghidra.util.Msg; +import ghidra.formats.gfilesystem.GFile; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; /** * GTreeNode that represents a directory on a filesystem. - *

    - * Visible to just this package. */ -public class FSBDirNode extends FSBNode { - private FSRL fsrl; +public class FSBDirNode extends FSBFileNode { - FSBDirNode(FSRL fsrl) { - this.fsrl = fsrl; + FSBDirNode(GFile dirFile) { + super(dirFile); } @Override public List generateChildren(TaskMonitor monitor) throws CancelledException { - try (RefdFile dir = FileSystemService.getInstance().getRefdFile(fsrl, monitor)) { - return FSBNode.getNodesFromFileList(dir.file.getListing()); + try { + return FSBNode.createNodesFromFileList(file.getListing(), monitor); } catch (IOException e) { - Msg.showError(this, null, "loadChildren", e); + // fall thru, return empty list } - return Collections.emptyList(); + return List.of(); } @Override - public Icon getIcon(boolean expanded) { - return null; - } - - @Override - public String getName() { - return fsrl.getName(); - } - - @Override - public FSRL getFSRL() { - return fsrl; - } - - @Override - public String getToolTip() { - return fsrl.getName(); + public void updateFileAttributes(TaskMonitor monitor) { + for (GTreeNode node : getChildren()) { + if (node instanceof FSBFileNode) { + ((FSBFileNode) node).updateFileAttributes(monitor); + } + if (monitor.isCancelled()) { + break; + } + } + super.updateFileAttributes(monitor); } @Override @@ -75,9 +61,4 @@ public class FSBDirNode extends FSBNode { return false; } - @Override - public int hashCode() { - return fsrl.hashCode(); - } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileNode.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileNode.java index 8ba8b0caeb..568c2f5515 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBFileNode.java @@ -15,46 +15,34 @@ */ package ghidra.plugins.fsbrowser; -import java.util.Collections; -import java.util.List; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; -import javax.swing.Icon; +import java.util.List; import docking.widgets.tree.GTreeNode; import ghidra.formats.gfilesystem.FSRL; +import ghidra.formats.gfilesystem.GFile; +import ghidra.formats.gfilesystem.fileinfo.FileAttributeType; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; /** * GTreeNode that represents a file on a filesystem. - *

    - * Visible to just this package. */ public class FSBFileNode extends FSBNode { - protected FSRL fsrl; - FSBFileNode(FSRL fsrl) { - this.fsrl = fsrl; - } + protected GFile file; + protected boolean isEncrypted; + protected boolean hasPassword; - @Override - public Icon getIcon(boolean expanded) { - return null; - } - - @Override - public String getName() { - return fsrl.getName(); + FSBFileNode(GFile file) { + this.file = file; } @Override public FSRL getFSRL() { - return fsrl; - } - - @Override - public String getToolTip() { - return fsrl.getName(); + return file.getFSRL(); } @Override @@ -64,12 +52,60 @@ public class FSBFileNode extends FSBNode { @Override public int hashCode() { - return fsrl.hashCode(); + return file.hashCode(); + } + + @Override + protected void updateFileAttributes(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); } @Override public List generateChildren(TaskMonitor monitor) throws CancelledException { - return Collections.emptyList(); + return List.of(); + } + + /** + * Local copy of the original GFile's {@link FileAttributeType#IS_ENCRYPTED_ATTR} attribute. + * + * @return boolean true if file needs a password to be read + */ + public boolean isEncrypted() { + return isEncrypted; + } + + /** + * Local copy of the original GFile's {@link FileAttributeType#HAS_GOOD_PASSWORD_ATTR} attribute. + * + * @return boolean true if a password for the file has been found, false if missing the password + */ + public boolean hasPassword() { + return hasPassword; + } + + /** + * Returns true if this file is missing its password + * @return boolean true if this file is missing its password + */ + public boolean hasMissingPassword() { + return isEncrypted && !hasPassword; + } + + /** + * Returns true if this node's password status has changed, calling for a complete refresh + * of the status of all files in the file system. + * + * @param monitor {@link TaskMonitor} + * @return boolean true if this nodes password status has changed + */ + public boolean needsFileAttributesUpdate(TaskMonitor monitor) { + if (hasMissingPassword()) { + updateFileAttributes(monitor); + return hasPassword; // if true then the attribute has changed and everything should be refreshed + } + return false; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBNode.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBNode.java index af6fc0458f..f07cf9d77b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBNode.java @@ -17,12 +17,16 @@ package ghidra.plugins.fsbrowser; import java.util.*; +import javax.swing.Icon; + import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeSlowLoadingNode; import ghidra.formats.gfilesystem.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; /** - * Base interface for all filesystem browser gtree nodes. + * Base class for all filesystem browser gtree nodes. */ public abstract class FSBNode extends GTreeSlowLoadingNode { @@ -35,6 +39,31 @@ public abstract class FSBNode extends GTreeSlowLoadingNode { */ public abstract FSRL getFSRL(); + @Override + public String getToolTip() { + return getName(); + } + + @Override + public Icon getIcon(boolean expanded) { + return null; + } + + @Override + public String getName() { + return getFSRL().getName(); + } + + public FSBRootNode getFSBRootNode() { + GTreeNode node = getParent(); + while (node != null && !(node instanceof FSBRootNode)) { + node = node.getParent(); + } + return (node instanceof FSBRootNode) ? (FSBRootNode) node : null; + } + + protected abstract void updateFileAttributes(TaskMonitor monitor) throws CancelledException; + /** * Returns the {@link FSBRootNode} that represents the root of the file system that * contains the specified file node. @@ -54,15 +83,21 @@ public abstract class FSBNode extends GTreeSlowLoadingNode { * Helper method to convert {@link GFile} objects to FSBNode objects. * * @param files {@link List} of {@link GFile} objects to convert + * @param monitor {@link TaskMonitor} * @return {@link List} of {@link FSBNode} instances (return typed as a GTreeNode list), * specific to each GFile instance's type. */ - public static List getNodesFromFileList(List files) { - List nodes = new ArrayList<>(files.size()); - + public static List createNodesFromFileList(List files, TaskMonitor monitor) { + files = new ArrayList<>(files); Collections.sort(files, FSUtilities.GFILE_NAME_TYPE_COMPARATOR); + + List nodes = new ArrayList<>(files.size()); for (GFile child : files) { - nodes.add((GTreeNode) getNodeFromFile(child)); + FSBFileNode node = createNodeFromFile(child); + if (node.isLeaf()) { + node.updateFileAttributes(monitor); + } + nodes.add(node); } return nodes; } @@ -71,11 +106,10 @@ public abstract class FSBNode extends GTreeSlowLoadingNode { * Helper method to convert a single {@link GFile} object into a FSBNode object. * * @param file {@link GFile} to convert - * @return a new {@link FSBNode} with type specific to the GFile's type. + * @return a new {@link FSBFileNode} with type specific to the GFile's type. */ - public static FSBNode getNodeFromFile(GFile file) { - return file.isDirectory() ? new FSBDirNode(file.getFSRL()) - : new FSBFileNode(file.getFSRL()); + public static FSBFileNode createNodeFromFile(GFile file) { + return file.isDirectory() ? new FSBDirNode(file) : new FSBFileNode(file); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBRootNode.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBRootNode.java index c7d7a4e2f3..ec5da01a38 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBRootNode.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBRootNode.java @@ -16,9 +16,8 @@ package ghidra.plugins.fsbrowser; import java.io.IOException; -import java.util.*; - -import javax.swing.Icon; +import java.util.ArrayList; +import java.util.List; import docking.widgets.tree.GTreeNode; import ghidra.formats.gfilesystem.*; @@ -31,46 +30,88 @@ import ghidra.util.task.TaskMonitor; *

    * The {@link FileSystemRef} is released when this node is {@link #dispose()}d. *

    - * Visible to just this package. + * Since GTreeNodes are cloned during GTree filtering, and this class has a reference to an external + * resource that needs managing, this class needs to keeps track of the original modelNode + * and does all state modification using the modelNode's context. */ public class FSBRootNode extends FSBNode { private FileSystemRef fsRef; private FSBFileNode prevNode; private List subRootNodes = new ArrayList<>(); + private FSBRootNode modelNode; FSBRootNode(FileSystemRef fsRef) { - this.fsRef = fsRef; + this(fsRef, null); } FSBRootNode(FileSystemRef fsRef, FSBFileNode prevNode) { this.fsRef = fsRef; this.prevNode = prevNode; - } - - public FileSystemRef getFSRef() { - return fsRef; - } - - public void releaseFSRefs() { - for (FSBRootNode subFSBRootNode : subRootNodes) { - subFSBRootNode.releaseFSRefs(); - } - subRootNodes.clear(); - if (fsRef != null) { - fsRef.close(); - fsRef = null; - } + this.modelNode = this; } @Override - public Icon getIcon(boolean expanded) { - return null; + public GTreeNode clone() throws CloneNotSupportedException { + FSBRootNode clone = (FSBRootNode) super.clone(); + clone.fsRef = null; // stomp on the clone's fsRef to force it to use modelNode's fsRef + return clone; + } + + @Override + public void dispose() { + releaseFSRefsIfModelNode(); + super.dispose(); + } + + void swapBackPrevModelNodeAndDispose() { + if (this != modelNode) { + modelNode.swapBackPrevModelNodeAndDispose(); + return; + } + int indexInParent = getIndexInParent(); + GTreeNode parent = getParent(); + parent.removeNode(this); + parent.addNode(indexInParent, prevNode); + dispose(); // releases the fsRef + } + + public FileSystemRef getFSRef() { + return modelNode.fsRef; + } + + private void releaseFSRefsIfModelNode() { + 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 { + if (this != modelNode) { + modelNode.updateFileAttributes(monitor); + return; + } + for (GTreeNode node : getChildren()) { + monitor.checkCanceled(); + if (node instanceof FSBFileNode) { + ((FSBFileNode) node).updateFileAttributes(monitor); + } + } } @Override public String getName() { - return fsRef != null && !fsRef.isClosed() ? fsRef.getFilesystem().getName() : " Missing "; + return modelNode.fsRef != null && !modelNode.fsRef.isClosed() + ? modelNode.fsRef.getFilesystem().getName() + : " Missing "; } @Override @@ -83,37 +124,23 @@ public class FSBRootNode extends FSBNode { return false; } - @Override - public void dispose() { - releaseFSRefs(); - super.dispose(); - } - @Override public List generateChildren(TaskMonitor monitor) throws CancelledException { if (fsRef != null) { - try { - return FSBNode.getNodesFromFileList(fsRef.getFilesystem().getListing(null)); + return FSBNode.createNodesFromFileList(fsRef.getFilesystem().getListing(null), + monitor); } catch (IOException e) { FSUtilities.displayException(this, null, "Error Opening File System", "Problem generating children at root of file system", e); } } - return Collections.emptyList(); + return List.of(); } @Override public FSRL getFSRL() { - return fsRef.getFilesystem().getFSRL(); - } - - public FSBFileNode getPrevNode() { - return prevNode; - } - - public List getSubRootNodes() { - return subRootNodes; + return modelNode.fsRef.getFilesystem().getFSRL(); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java index 775a85d83d..7da421e420 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java @@ -18,10 +18,8 @@ package ghidra.plugins.fsbrowser; import java.util.ArrayList; import java.util.List; -import docking.ActionContext; -import docking.widgets.tree.GTreeNode; import ghidra.app.services.ProgramManager; -import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.SelectFromListDialog; import ghidra.framework.plugintool.PluginTool; import ghidra.util.Msg; @@ -30,41 +28,6 @@ import ghidra.util.Msg; */ public class FSBUtils { - public static FSRL getFileFSRLFromContext(ActionContext context) { - return getFSRLFromContext(context, false); - } - - public static FSRL getFSRLFromContext(ActionContext context, boolean dirsOk) { - if (context == null || !(context.getContextObject() instanceof FSBNode)) { - return null; - } - - FSBNode node = (FSBNode) context.getContextObject(); - FSRL fsrl = node.getFSRL(); - if (!dirsOk && node instanceof FSBRootNode && fsrlHasContainer(fsrl.getFS())) { - // 'convert' a file system root node back into its container file - return fsrl.getFS().getContainer(); - } - - boolean isDir = (node instanceof FSBDirNode) || (node instanceof FSBRootNode); - if (isDir && !dirsOk) { - return null; - } - - return fsrl; - } - - public static boolean fsrlHasContainer(FSRLRoot fsFSRL) { - return fsFSRL.hasContainer() && !fsFSRL.getProtocol().equals(LocalFileSystem.FSTYPE); - } - - public static FSBRootNode getNodesRoot(FSBNode node) { - GTreeNode tmp = node; - while (tmp != null && !(tmp instanceof FSBRootNode)) { - tmp = tmp.getParent(); - } - return (tmp instanceof FSBRootNode) ? (FSBRootNode) tmp : null; - } /** * Returns the {@link ProgramManager} associated with this fs browser plugin. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java index 22564eec1d..1e05252199 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java @@ -60,6 +60,7 @@ public class FileIconService { public static final String OVERLAY_IMPORTED = "imported"; public static final String OVERLAY_FILESYSTEM = "filesystem"; + public static final String OVERLAY_MISSING_PASSWORD = "password_missing"; private static final String FILEEXT_MAPPING_FILE = "file_extension_icons.xml"; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserComponentProvider.java index 6d5eeba21b..17c3a1599a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserComponentProvider.java @@ -17,7 +17,6 @@ package ghidra.plugins.fsbrowser; import java.awt.Color; import java.awt.Component; -import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; @@ -26,8 +25,8 @@ import javax.swing.*; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; -import docking.ActionContext; import docking.WindowPosition; +import docking.event.mouse.GMouseListenerAdapter; import docking.widgets.tree.GTree; import docking.widgets.tree.GTreeNode; import docking.widgets.tree.support.GTreeRenderer; @@ -37,7 +36,7 @@ import ghidra.formats.gfilesystem.*; import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.plugin.importer.ProgramMappingService; import ghidra.program.model.listing.Program; -import ghidra.util.HelpLocation; +import ghidra.util.*; /** * Plugin component provider for the {@link FileSystemBrowserPlugin}. @@ -46,13 +45,15 @@ import ghidra.util.HelpLocation; *

    * Visible to just this package. */ -class FileSystemBrowserComponentProvider extends ComponentProviderAdapter { +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 @@ -80,31 +81,17 @@ class FileSystemBrowserComponentProvider extends ComponentProviderAdapter { handleSingleClick(clickedNode); } }); - gTree.addMouseListener(new MouseAdapter() { - /** - * Keep track of the previous mouse button that was clicked so we - * can ensure that it was two left clicks that activated - * our dbl-click handler. - */ - int prevMouseButton = -1; - + gTree.addMouseListener(new GMouseListenerAdapter() { @Override - public void mouseReleased(MouseEvent e) { - // keep track of the mouse button so it can be checked next time - int localPrevMouseButton = prevMouseButton; - prevMouseButton = e.getButton(); - - if (e.isPopupTrigger()) { - return; - } - - GTreeNode clickedNode = gTree.getNodeForLocation(e.getX(), e.getY()); - if (e.getClickCount() == 1) { - handleSingleClick(clickedNode); - } - if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1 && - localPrevMouseButton == MouseEvent.BUTTON1) { - handleDoubleClick(clickedNode); + 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())); } } }); @@ -119,6 +106,9 @@ class FileSystemBrowserComponentProvider extends ComponentProviderAdapter { 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); } @@ -145,14 +135,22 @@ class FileSystemBrowserComponentProvider extends ComponentProviderAdapter { private void renderFile(FSBFileNode node, boolean selected) { FSRL fsrl = node.getFSRL(); String filename = fsrl.getName(); - Icon ico = FileIconService.getInstance().getImage(filename, - ProgramMappingService.isFileImportedIntoProject(fsrl) - ? FileIconService.OVERLAY_IMPORTED - : null, - FileSystemService.getInstance().isFilesystemMountedAt(fsrl) - ? FileIconService.OVERLAY_FILESYSTEM - : null); + + String importOverlay = ProgramMappingService.isFileImportedIntoProject(fsrl) + ? FileIconService.OVERLAY_IMPORTED + : null; + String mountedOverlay = fsService.isFilesystemMountedAt(fsrl) + ? FileIconService.OVERLAY_FILESYSTEM + : null; + + String missingPasswordOverlay = node.hasMissingPassword() + ? FileIconService.OVERLAY_MISSING_PASSWORD + : null; + + Icon ico = FileIconService.getInstance() + .getImage(filename, importOverlay, mountedOverlay, missingPasswordOverlay); setIcon(ico); + if (ProgramMappingService.isFileOpen(fsrl)) { // TODO: change this to a OVERLAY_OPEN option when fetching icon setForeground(selected ? Color.CYAN : Color.MAGENTA); @@ -171,6 +169,7 @@ class FileSystemBrowserComponentProvider extends ComponentProviderAdapter { setHelpLocation( new HelpLocation("FileSystemBrowserPlugin", "FileSystemBrowserIntroduction")); + fsRef.getFilesystem().getRefManager().addListener(this); } /** @@ -182,16 +181,37 @@ class FileSystemBrowserComponentProvider extends ComponentProviderAdapter { 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 via the plugin - if (plugin != null && rootNode.getFSRef() != null && - rootNode.getFSRef().getFilesystem() != null) { - plugin.removeFileSystemBrowser(rootNode.getFSRef().getFilesystem().getFSRL()); + // if the component is 'closed', nuke ourselves + if (plugin != null) { + plugin.removeFileSystemBrowserComponent(this); + dispose(); } } @@ -199,6 +219,17 @@ class FileSystemBrowserComponentProvider extends ComponentProviderAdapter { 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 + } + /*****************************************/ /** @@ -230,12 +261,26 @@ class FileSystemBrowserComponentProvider extends ComponentProviderAdapter { 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) { + if (clickedNode instanceof FSBFileNode && clickedNode.isLeaf()) { FSBFileNode node = (FSBFileNode) clickedNode; if (node.getFSRL() != null && !quickShowProgram(node.getFSRL())) { @@ -247,40 +292,34 @@ class FileSystemBrowserComponentProvider extends ComponentProviderAdapter { /*****************************************/ @Override - public ActionContext getActionContext(MouseEvent event) { + public FSBActionContext getActionContext(MouseEvent event) { + return new FSBActionContext(this, getSelectedNodes(event), event, gTree); + } + + private FSBNode[] getSelectedNodes(MouseEvent event) { TreePath[] selectionPaths = gTree.getSelectionPaths(); - if (selectionPaths != null && selectionPaths.length == 1) { - Object lastPathComponent = selectionPaths[0].getLastPathComponent(); - return new FSBActionContext(this, lastPathComponent, gTree); - } - if (selectionPaths != null && selectionPaths.length > 0) { - List list = new ArrayList<>(); - for (TreePath selectionPath : selectionPaths) { - Object lastPathComponent = selectionPath.getLastPathComponent(); - if (lastPathComponent instanceof FSBNode) { - FSBNode node = (FSBNode) lastPathComponent; - list.add(node); - } + List list = new ArrayList<>(selectionPaths.length); + for (TreePath selectionPath : selectionPaths) { + Object lastPathComponent = selectionPath.getLastPathComponent(); + if (lastPathComponent instanceof FSBNode) { + list.add((FSBNode) lastPathComponent); } - if (list.size() == 1) { - return new FSBActionContext(this, list.get(0), gTree); - } - FSBNode[] nodes = new FSBNode[list.size()]; - list.toArray(nodes); - return new FSBActionContext(this, nodes, gTree); } - if (event != null) { + 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)) { - return new FSBActionContext(this, gTree.getNodeForLocation(x, y), gTree); + GTreeNode nodeAtEventLocation = gTree.getNodeForLocation(x, y); + if (nodeAtEventLocation != null && nodeAtEventLocation instanceof FSBNode) { + list.add((FSBNode) nodeAtEventLocation); + } } } } - return null; + return list.toArray(FSBNode[]::new); } @Override @@ -298,15 +337,4 @@ class FileSystemBrowserComponentProvider extends ComponentProviderAdapter { return WindowPosition.WINDOW; } - void dispose() { - if (actionManager != null) { - actionManager.dispose(); - actionManager = null; - } - if (gTree != null) { - gTree.dispose(); - gTree = null; - } - plugin = null; - } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java index 7a2e9589a2..e15c088a7f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileSystemBrowserPlugin.java @@ -22,8 +22,10 @@ import java.io.File; import java.io.IOException; import java.util.*; -import docking.ActionContext; -import docking.action.*; +import javax.swing.KeyStroke; + +import docking.action.DockingAction; +import docking.action.builder.ActionBuilder; import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooserMode; import ghidra.app.CorePluginPackage; @@ -50,7 +52,7 @@ import utilities.util.FileUtilities; /** * A {@link Plugin} that supplies a {@link GFileSystem filesystem} browser component * that allows the user to view the contents of filesystems and perform actions on the - * files inside those filesystems.x + * files inside those filesystems. */ //@formatter:off @PluginInfo( @@ -65,14 +67,13 @@ import utilities.util.FileUtilities; ) //@formatter:on public class FileSystemBrowserPlugin extends Plugin implements FrontEndable, ProjectListener, - FileSystemEventListener, FileSystemBrowserService { - - private static final String MENU_GROUP = "Import"; + FileSystemBrowserService { /* package */ DockingAction openFilesystemAction; private GhidraFileChooser chooserOpen; private FrontEndService frontEndService; private Map currentBrowsers = new HashMap<>(); + private FileSystemService fsService; // don't use this directly, use fsService() instead public FileSystemBrowserPlugin(PluginTool tool) { super(tool); @@ -138,7 +139,6 @@ public class FileSystemBrowserPlugin extends Plugin implements FrontEndable, Pro fsRef.close(); } else { - fsRef.getFilesystem().getRefManager().addListener(this); provider = new FileSystemBrowserComponentProvider(this, fsRef); currentBrowsers.put(fsFSRL, provider); getTool().addComponentProvider(provider, false); @@ -148,25 +148,14 @@ public class FileSystemBrowserPlugin extends Plugin implements FrontEndable, Pro if (show) { getTool().showComponentProvider(provider, true); getTool().toFront(provider); + provider.contextChanged(); } } - /** - * Closes any FilesystemBrowser window component that is showing the specified filesystem. - * - * @param fsFSRL {@link FSRLRoot} of the filesystem to close. - */ - /* package */ void removeFileSystemBrowser(FSRLRoot fsFSRL) { - Swing.runIfSwingOrRunLater(() -> { - FileSystemBrowserComponentProvider fsbcp = currentBrowsers.get(fsFSRL); - if (fsbcp == null) { - return; - } - - currentBrowsers.remove(fsFSRL); - fsbcp.removeFromTool(); - fsbcp.dispose(); - }); + void removeFileSystemBrowserComponent(FileSystemBrowserComponentProvider componentProvider) { + if (componentProvider != null) { + Swing.runIfSwingOrRunLater(() -> currentBrowsers.remove(componentProvider.getFSRL())); + } } /** @@ -176,7 +165,6 @@ public class FileSystemBrowserPlugin extends Plugin implements FrontEndable, Pro Swing.runIfSwingOrRunLater(() -> { for (FileSystemBrowserComponentProvider fsbcp : new ArrayList<>( currentBrowsers.values())) { - fsbcp.removeFromTool(); fsbcp.dispose(); } currentBrowsers.clear(); @@ -197,7 +185,7 @@ public class FileSystemBrowserPlugin extends Plugin implements FrontEndable, Pro public void projectClosed(Project project) { removeAllFileSystemBrowsers(); if (FileSystemService.isInitialized()) { - FileSystemService.getInstance().closeUnusedFileSystems(); + fsService().closeUnusedFileSystems(); } } @@ -207,28 +195,14 @@ public class FileSystemBrowserPlugin extends Plugin implements FrontEndable, Pro } private void setupOpenFileSystemAction() { - String actionName = "Open File System"; - - openFilesystemAction = new DockingAction(actionName, this.getName()) { - @Override - public void actionPerformed(ActionContext context) { - doOpenFileSystem(); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return tool.getProject() != null; - } - }; - openFilesystemAction.setMenuBarData( - new MenuData(new String[] { "&File", actionName + "..." }, null, MENU_GROUP, - MenuData.NO_MNEMONIC, "z")); - openFilesystemAction.setKeyBindingData( - new KeyBindingData(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK)); - openFilesystemAction.setDescription(getPluginDescription().getDescription()); - openFilesystemAction.setEnabled(tool.getProject() != null); - - tool.addAction(openFilesystemAction); + openFilesystemAction = new ActionBuilder("Open File System", this.getName()) + .description(getPluginDescription().getDescription()) + .enabledWhen(ac -> tool.getProject() != null) + .menuPath("File", "Open File System...") + .menuGroup("Import", "z") + .keyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK)) + .onAction(ac -> doOpenFileSystem()) + .buildAndInstall(tool); } private void openChooser(String title, String buttonText, boolean multiSelect) { @@ -254,18 +228,15 @@ public class FileSystemBrowserPlugin extends Plugin implements FrontEndable, Pro private void doOpenFilesystem(FSRL containerFSRL, Component parent, TaskMonitor monitor) { try { monitor.setMessage("Probing " + containerFSRL.getName() + " for filesystems"); - FileSystemRef ref = FileSystemService.getInstance() - .probeFileForFilesystem( - containerFSRL, monitor, FileSystemProbeConflictResolver.GUI_PICKER); + FileSystemRef ref = fsService().probeFileForFilesystem(containerFSRL, monitor, + FileSystemProbeConflictResolver.GUI_PICKER); if (ref == null) { Msg.showWarn(this, parent, "Open Filesystem", "No filesystem provider for " + containerFSRL.getName()); return; } - Swing.runLater(() -> { - createNewFileSystemBrowser(ref, true); - }); + createNewFileSystemBrowser(ref, true); } catch (IOException | CancelledException e) { FSUtilities.displayException(this, parent, "Open Filesystem Error", @@ -301,22 +272,18 @@ public class FileSystemBrowserPlugin extends Plugin implements FrontEndable, Pro return; } - FSRL containerFSRL = FileSystemService.getInstance().getLocalFSRL(file); + FSRL containerFSRL = fsService().getLocalFSRL(file); TaskLauncher.launchModal("Open File System", (monitor) -> { doOpenFilesystem(containerFSRL, parent, monitor); }); } - @Override - public void onFilesystemClose(GFileSystem fs) { - Msg.info(this, "File system " + fs.getFSRL() + " was closed! Closing browser window"); - removeFileSystemBrowser(fs.getFSRL()); - } - - @Override - public void onFilesystemRefChange(GFileSystem fs, FileSystemRefManager refManager) { - //Msg.info(this, "File system " + fs.getFSRL() + " ref changed"); - // nada + private FileSystemService fsService() { + // use a delayed initialization so we don't force the FileSystemService to initialize + if (fsService == null) { + fsService = FileSystemService.getInstance(); + } + return fsService; } /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/tasks/GFileSystemExtractAllTask.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/tasks/GFileSystemExtractAllTask.java index 806061a00d..d9de3acdc2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/tasks/GFileSystemExtractAllTask.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/tasks/GFileSystemExtractAllTask.java @@ -17,22 +17,13 @@ package ghidra.plugins.fsbrowser.tasks; import java.awt.Component; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.util.LinkedHashMap; import java.util.Map; import docking.widgets.OptionDialog; -import ghidra.formats.gfilesystem.AbstractFileExtractorTask; -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.FileSystemService; -import ghidra.formats.gfilesystem.GFile; -import ghidra.formats.gfilesystem.GFileSystem; -import ghidra.formats.gfilesystem.RefdFile; -import ghidra.util.DateUtils; -import ghidra.util.HTMLUtilities; -import ghidra.util.Msg; +import ghidra.formats.gfilesystem.*; +import ghidra.util.*; import ghidra.util.exception.CancelledException; import ghidra.util.task.Task; import ghidra.util.task.TaskMonitor; @@ -154,12 +145,4 @@ public class GFileSystemExtractAllTask extends AbstractFileExtractorTask { } return true; } - - @Override - protected InputStream getSourceFileInputStream(GFile file, TaskMonitor monitor) - throws CancelledException, IOException { - File cacheFile = FileSystemService.getInstance().getFile(file.getFSRL(), monitor); - return new FileInputStream(cacheFile); - } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/batch/BatchImportDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/batch/BatchImportDialog.java index 09bc90d02a..4c8878cd3e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/batch/BatchImportDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/batch/BatchImportDialog.java @@ -39,7 +39,8 @@ import docking.widgets.filechooser.GhidraFileChooserMode; import docking.widgets.label.GDLabel; import docking.widgets.table.*; import ghidra.app.services.ProgramManager; -import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.formats.gfilesystem.FileSystemService; import ghidra.framework.model.DomainFolder; import ghidra.framework.plugintool.PluginTool; import ghidra.plugin.importer.ImporterUtilities; @@ -411,18 +412,9 @@ public class BatchImportDialog extends DialogComponentProvider { private boolean addSources(List filesToAdd) { - //@formatter:off - List updatedFiles = filesToAdd - .stream() - .map(fsrl -> { - if (fsrl instanceof FSRLRoot && fsrl.getFS().hasContainer()) { - fsrl = fsrl.getFS().getContainer(); - } - return fsrl; - }) - .collect(Collectors.toList()) - ; - //@formatter:on + List updatedFiles = filesToAdd.stream() + .map(FSRL::convertRootToContainer) + .collect(Collectors.toList()); List badFiles = batchInfo.addFiles(updatedFiles); if (!badFiles.isEmpty()) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/batch/BatchInfo.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/batch/BatchInfo.java index 7c752ee4bc..33eb8cfe0a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/batch/BatchInfo.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/batch/BatchInfo.java @@ -25,6 +25,7 @@ import javax.swing.SwingConstants; import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.opinion.*; import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.crypto.CryptoSession; import ghidra.plugins.importer.batch.BatchGroup.BatchLoadConfig; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; @@ -42,6 +43,8 @@ public class BatchInfo { public static final int MAXDEPTH_UNLIMITED = -1; public static final int MAXDEPTH_DEFAULT = 2; + private FileSystemService fsService = FileSystemService.getInstance(); + /* * These structures need to be synchronized to ensure thread visibility, since they are * written to by a background thread and read from the Swing thread. @@ -181,7 +184,7 @@ public class BatchInfo { public boolean addFile(FSRL fsrl, TaskMonitor taskMonitor) throws IOException, CancelledException { - fsrl = FileSystemService.getInstance().getFullyQualifiedFSRL(fsrl, taskMonitor); + fsrl = fsService.getFullyQualifiedFSRL(fsrl, taskMonitor); if (userAddedFSRLs.contains(fsrl)) { throw new IOException("Batch already contains file " + fsrl); } @@ -216,7 +219,7 @@ public class BatchInfo { // use the fsrl param instead of file.getFSRL() as param may have more info (ie. md5) - try (RefdFile refdFile = FileSystemService.getInstance().getRefdFile(fsrl, taskMonitor)) { + try (RefdFile refdFile = fsService.getRefdFile(fsrl, taskMonitor)) { GFile file = refdFile.file; if (file.isDirectory()) { processFS(file.getFilesystem(), file, taskMonitor); @@ -231,6 +234,9 @@ public class BatchInfo { return true; } + // the file was not of interest, let it be removed from the cache + fsService.releaseFileCache(fsrl); + return false; } } @@ -294,8 +300,8 @@ public class BatchInfo { private boolean processAsFS(FSRL fsrl, TaskMonitor taskMonitor) throws CancelledException { - try (FileSystemRef fsRef = FileSystemService.getInstance().probeFileForFilesystem(fsrl, - taskMonitor, FileSystemProbeConflictResolver.CHOOSEFIRST)) { + try (FileSystemRef fsRef = fsService.probeFileForFilesystem(fsrl, taskMonitor, + FileSystemProbeConflictResolver.CHOOSEFIRST)) { if (fsRef == null) { return false; } @@ -333,9 +339,15 @@ public class BatchInfo { // TODO: drop FSUtils.listFileSystem and do recursion here. for (GFile file : FSUtilities.listFileSystem(fs, startDir, null, taskMonitor)) { taskMonitor.checkCanceled(); - doAddFile( - FileSystemService.getInstance().getFullyQualifiedFSRL(file.getFSRL(), taskMonitor), - taskMonitor); + FSRL fqFSRL; + try { + fqFSRL = fsService.getFullyQualifiedFSRL(file.getFSRL(), taskMonitor); + } + catch (IOException e) { + Msg.warn(this, "Error getting info for " + file.getFSRL()); + continue; + } + doAddFile(fqFSRL, taskMonitor); currentUASI.incRawFileCount(); } } @@ -355,8 +367,7 @@ public class BatchInfo { private boolean processWithLoader(FSRL fsrl, TaskMonitor monitor) throws IOException, CancelledException { - try (ByteProvider provider = - FileSystemService.getInstance().getByteProvider(fsrl, monitor)) { + try (ByteProvider provider = fsService.getByteProvider(fsrl, false, monitor)) { LoaderMap loaderMap = pollLoadersForLoadSpecs(provider, fsrl, monitor); for (Loader loader : loaderMap.keySet()) { Collection loadSpecs = loaderMap.get(loader); @@ -478,36 +489,39 @@ public class BatchInfo { BatchTaskMonitor batchMonitor = new BatchTaskMonitor(monitor); - List badFiles = new ArrayList<>(); - int size = filesToAdd.size(); - for (int i = 0; i < size; i++) { + // start a new CryptoSession to group all password prompting by multiple container + // files into a single session, enabling "Cancel All" to really cancel all password + // prompts + try (CryptoSession cryptoSession = fsService.newCryptoSession()) { + List badFiles = new ArrayList<>(); + for (FSRL fsrl : filesToAdd) { + Msg.trace(this, "Adding " + fsrl); + batchMonitor.setPrefix("Processing " + fsrl.getName() + ": "); - FSRL fsrl = filesToAdd.get(i); - Msg.trace(this, "Adding " + fsrl); - batchMonitor.setPrefix("Processing " + fsrl.getName() + ": "); + try { + monitor.checkCanceled(); + addFile(fsrl, batchMonitor); + } + catch (CryptoException ce) { + FSUtilities.displayException(this, null, "Error Adding File To Batch Import", + "Error while adding " + fsrl.getName() + " to batch import", ce); + } + catch (IOException ioe) { + Msg.error(this, "Error while adding " + fsrl.getName() + " to batch import", + ioe); + badFiles.add(fsrl); + } + catch (CancelledException e) { + Msg.debug(this, "Cancelling Add File task while adding " + fsrl.getName()); + // Note: the workflow for this felt odd: press cancel; confirm cancel; press Ok + // on dialog showing files not processed. + // It seems like the user should not have to see the second dialog + // badFiles.addAll(filesToAdd.subList(i, filesToAdd.size())); + } + } - try { - monitor.checkCanceled(); - addFile(fsrl, batchMonitor); - } - catch (CryptoException ce) { - FSUtilities.displayException(this, null, "Error Adding File To Batch Import", - "Error while adding " + fsrl.getName() + " to batch import", ce); - } - catch (IOException ioe) { - Msg.error(this, "Error while adding " + fsrl.getName() + " to batch import", ioe); - badFiles.add(fsrl); - } - catch (CancelledException e) { - Msg.debug(this, "Cancelling Add File task while adding " + fsrl.getName()); - // Note: the workflow for this felt odd: press cancel; confirm cancel; press Ok - // on dialog showing files not processed. - // It seems like the user should not have to see the second dialog - // badFiles.addAll(filesToAdd.subList(i, filesToAdd.size())); - } + return badFiles; } - - return badFiles; } //================================================================================================== diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/tasks/ImportBatchTask.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/tasks/ImportBatchTask.java index 93cb17dc47..c10ed0a9be 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/tasks/ImportBatchTask.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/tasks/ImportBatchTask.java @@ -131,8 +131,9 @@ public class ImportBatchTask extends Task { private void doImportApp(BatchLoadConfig batchLoadConfig, BatchGroupLoadSpec selectedBatchGroupLoadSpec, TaskMonitor monitor) throws CancelledException, IOException { - try (ByteProvider byteProvider = - FileSystemService.getInstance().getByteProvider(batchLoadConfig.getFSRL(), monitor)) { + Msg.info(this, "Importing " + batchLoadConfig.getFSRL()); + try (ByteProvider byteProvider = FileSystemService.getInstance() + .getByteProvider(batchLoadConfig.getFSRL(), true, monitor)) { LoadSpec loadSpec = batchLoadConfig.getLoadSpec(selectedBatchGroupLoadSpec); if (loadSpec == null) { Msg.error(this, diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/formats/gfilesystem/FileSystemServiceTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/formats/gfilesystem/FileSystemServiceTest.java index b60d4b8d38..bb506fff70 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/formats/gfilesystem/FileSystemServiceTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/formats/gfilesystem/FileSystemServiceTest.java @@ -15,12 +15,16 @@ */ package ghidra.formats.gfilesystem; +import static org.junit.Assert.*; + import java.io.File; import java.io.IOException; -import org.junit.*; +import org.junit.Before; +import org.junit.Test; import generic.test.AbstractGenericTest; +import ghidra.app.util.bin.ByteProvider; import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -43,34 +47,27 @@ public class FileSystemServiceTest extends AbstractGhidraHeadedIntegrationTest { File localFile = new File(fssTestDir, "file.txt"); FileUtilities.writeStringToFile(localFile, "this is a test"); FSRL localFSRL = fsService.getLocalFSRL(localFile); - localFSRL = fsService.getFullyQualifiedFSRL(localFSRL, monitor); - File localResult = fsService.getFile(localFSRL, monitor); - - Assert.assertNotNull(localFSRL.getMD5()); - Assert.assertEquals(localFile, localResult); + try (ByteProvider byteProvider = fsService.getByteProvider(localFSRL, true, monitor)) { + assertEquals(localFile, byteProvider.getFile()); + } } - /** - * Verifies that a fully qualified FSRL with MD5 generates a IOException failure - * when the original file was changed. - * - * @throws IOException - * @throws CancelledException - */ @Test public void testChangedLocalFile() throws IOException, CancelledException { + // Verifies that a fully qualified FSRL with MD5 generates a IOException failure + // when the original file was changed. File localFile = new File(fssTestDir, "file.txt"); FileUtilities.writeStringToFile(localFile, "this is a test"); FSRL localFSRL = fsService.getLocalFSRL(localFile); localFSRL = fsService.getFullyQualifiedFSRL(localFSRL, monitor); FileUtilities.writeStringToFile(localFile, "this is a test with additional bytes"); - try { - File localResult2 = fsService.getFile(localFSRL, monitor); - Assert.fail("Should not get here, got: " + localResult2); + try (ByteProvider byteProvider = fsService.getByteProvider(localFSRL, false, monitor)) { + fail("Should not get here, got: " + byteProvider.getFSRL()); } - catch (IOException ioe) { - Assert.assertTrue(ioe.getMessage().contains("Exact file no longer exists")); + catch (IOException e) { + assertTrue(e.getMessage().contains("hash has changed")); + } } } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/FileByteProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/FileByteProviderTest.java new file mode 100644 index 0000000000..f3a7e5e1f5 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/FileByteProviderTest.java @@ -0,0 +1,110 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.AccessMode; +import java.util.Arrays; + +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.formats.gfilesystem.FSUtilities; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +public class FileByteProviderTest extends AbstractGenericTest { + + /* + * "NN 01 NN 03 NN 05 NN 07 NN 09"... (NN = blockNumber, 00-FF = offset in block) + */ + private ByteArrayProvider patternedBAP(int bs, int count) { + byte[] bytes = new byte[bs * count]; + for (int blockNum = 0; blockNum < count; blockNum++) { + int blockStart = blockNum * bs; + Arrays.fill(bytes, blockStart, blockStart + bs, (byte) blockNum); + for (int i = 1; i < bs; i += 2) { + bytes[i + blockStart] = (byte) (i % 256); + } + } + return new ByteArrayProvider(bytes); + } + + @Test + public void testSmallRead() throws IOException { + File file1 = createTempFileForTest("file1"); + FileUtilities.writeStringToFile(file1, "testing\nsecond line"); + try (FileByteProvider fbp = new FileByteProvider(file1, null, AccessMode.READ)) { + BinaryReader br = new BinaryReader(fbp, true); + assertEquals("testing", br.readAsciiString(0)); + assertEquals("second line", br.readAsciiString(8)); + } + } + + @Test + public void testReadAtBuffersBoundaries() throws IOException, CancelledException { + File file1 = createTempFileForTest("file1"); + int bs = FileByteProvider.BUFFER_SIZE; + FSUtilities.copyByteProviderToFile(patternedBAP(bs, 5), file1, TaskMonitor.DUMMY); + try (FileByteProvider fbp = new FileByteProvider(file1, null, AccessMode.READ)) { + BinaryReader br = new BinaryReader(fbp, false /*BE*/); + assertEquals(5 * bs, fbp.length()); + + assertEquals(0x0001, br.readUnsignedShort(0)); + assertEquals(0x00ff, br.readUnsignedShort(bs - 2)); + assertEquals(0x0101, br.readUnsignedShort(bs)); + assertEquals(0x01ff, br.readUnsignedShort(bs + bs - 2)); + assertEquals(0x0401, br.readUnsignedShort(bs * 4)); + } + + } + + @Test + public void testReadStraddleBuffersBoundaries() throws IOException, CancelledException { + File file1 = createTempFileForTest("file1"); + int bs = FileByteProvider.BUFFER_SIZE; + FSUtilities.copyByteProviderToFile(patternedBAP(bs, 5), file1, TaskMonitor.DUMMY); + try (FileByteProvider fbp = new FileByteProvider(file1, null, AccessMode.READ)) { + BinaryReader br = new BinaryReader(fbp, false /*BE*/); + assertEquals(5 * bs, fbp.length()); + + assertEquals(0x00ff0101, br.readUnsignedInt(bs - 2)); + assertEquals(0x01ff0201, br.readUnsignedInt(bs + bs - 2)); + } + + } + + @Test + public void testReadMultiStraddleBuffersBoundaries() throws IOException, CancelledException { + File file1 = createTempFileForTest("file1"); + int bs = FileByteProvider.BUFFER_SIZE; + FSUtilities.copyByteProviderToFile(patternedBAP(bs, 5), file1, TaskMonitor.DUMMY); + try (FileByteProvider fbp = new FileByteProvider(file1, null, AccessMode.READ)) { + assertEquals(5 * bs, fbp.length()); + + byte[] bytes = fbp.readBytes(bs - 2, bs + 4); // read from 3 adjacent blocks, 2+bs+2 + BinaryReader br = new BinaryReader(new ByteArrayProvider(bytes), false /*BE*/); + + assertEquals(0x00ff0101, br.readUnsignedInt(0)); + assertEquals(0x01ff0201, br.readUnsignedInt(bs)); + } + + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/FileCacheTest.java b/Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/FileCacheTest.java index 17a77a1ba2..74581b9dcf 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/FileCacheTest.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/FileCacheTest.java @@ -15,21 +15,24 @@ */ package ghidra.formats.gfilesystem; +import static org.junit.Assert.*; + import java.io.*; +import java.util.Arrays; import org.junit.*; import generic.test.AbstractGenericTest; +import ghidra.app.util.bin.ObfuscatedInputStream; +import ghidra.formats.gfilesystem.FileCache.FileCacheEntry; +import ghidra.formats.gfilesystem.FileCache.FileCacheEntryBuilder; import ghidra.util.DateUtils; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; import utilities.util.FileUtilities; public class FileCacheTest extends AbstractGenericTest { private FileCache cache; private File cacheDir; - private TaskMonitor monitor = TaskMonitor.DUMMY; @Before public void setup() throws IOException { @@ -46,12 +49,22 @@ public class FileCacheTest extends AbstractGenericTest { return new ByteArrayInputStream(s.getBytes()); } + FileCacheEntry createCacheFile(String payload) throws IOException { + // lie about the sizeHint to force the cache entry to be saved into a file instead of + // memory only + FileCacheEntryBuilder fceBuilder = cache.createCacheEntryBuilder(Integer.MAX_VALUE); + fceBuilder.write(payload.getBytes()); + FileCacheEntry fce = fceBuilder.finish(); + return fce; + } + @Test - public void testPurge() throws IOException, CancelledException { + public void testPurge() throws IOException { cache = new FileCache(cacheDir); - FileCacheEntry cfi = cache.addStream(toIS("This is a test1"), monitor); - Assert.assertTrue(cfi.file.exists()); + FileCacheEntry fce = createCacheFile("blah"); + + Assert.assertTrue(fce.file.exists()); File f1 = new File(cacheDir, "file1"); FileUtilities.writeStringToFile(f1, "test"); @@ -61,64 +74,148 @@ public class FileCacheTest extends AbstractGenericTest { FileUtilities.writeStringToFile(f2, "test2"); cache.purge(); - Assert.assertFalse(cfi.file.exists()); + Assert.assertFalse(fce.file.exists()); Assert.assertTrue(f1.exists()); Assert.assertTrue(f2.exists()); } @Test - public void testAgeOff() throws IOException, CancelledException { + public void testAgeOff() throws IOException { + // hack, delete lastmaint file to force a maint event during next cache startup + File lastMaintFile = new File(cacheDir, ".lastmaint"); + lastMaintFile.delete(); + cache = new FileCache(cacheDir); waitForCleanup(); // don't let the cache delete the file we are about to create - FileCacheEntry cfi = cache.addStream(toIS("This is a test1"), monitor); - Assert.assertTrue(cfi.file.exists()); + FileCacheEntry fce = createCacheFile("test"); + Assert.assertTrue(fce.file.exists()); - cfi.file.setLastModified(System.currentTimeMillis() - (DateUtils.MS_PER_DAY * 5)); + // backdate the file so it should appear to be old and age-off-able + fce.file.setLastModified(System.currentTimeMillis() - (DateUtils.MS_PER_DAY * 5)); + + // hack, delete lastmaint file to force a maint event during next cache startup + lastMaintFile.delete(); + + cache = new FileCache(cacheDir); + + // the file added before should have been deleted by the startup of cache2 + waitForCleanup(); + assertFalse(fce.file.exists()); + } + + @Test + public void testCacheFileBadFilename() throws IOException { + // test to ensure bad filenames in the cache dir don't cause problem // hack, delete lastmaint file to force a maint event during next cache startup File lastMaintFile = new File(cacheDir, ".lastmaint"); lastMaintFile.delete(); cache = new FileCache(cacheDir); + waitForCleanup(); // don't let the cache delete the file we are about to create - // the file added before should have been purged by the startup of cache2 - waitForCondition(() -> cfi.file.exists()); + FileCacheEntry fce = createCacheFile("test"); + Assert.assertTrue(fce.file.exists()); + + // backdate the file so it should appear to be old and age-off-able + fce.file.setLastModified(System.currentTimeMillis() - (DateUtils.MS_PER_DAY * 5)); + + // do same for file with bad filename + File badFile = new File(fce.file.getParentFile(), "bad_filename"); + FileUtilities.writeStringToFile(badFile, "bad file contents"); + badFile.setLastModified(System.currentTimeMillis() - (DateUtils.MS_PER_DAY * 5)); + + // hack, delete lastmaint file to force a maint event during next cache startup + lastMaintFile.delete(); + + cache = new FileCache(cacheDir); + + // the file added before should have been deleted by the startup of cache2 + waitForCleanup(); + assertFalse(fce.file.exists()); + assertTrue(badFile.exists()); } @Test - public void testAddFile() throws IOException, CancelledException { + public void testAddFile() throws IOException { cache = new FileCache(cacheDir); - File tmpFile = createTempFile("filecacheaddfile"); - FileUtilities.writeStringToFile(tmpFile, "This is a test1"); - - FileCacheEntry fce = cache.addFile(tmpFile, monitor); - Assert.assertTrue(fce.file.exists()); - Assert.assertEquals("10428da10f5aa2793cb73c0b680e1621", fce.md5); - Assert.assertEquals(1, cache.getFileAddCount()); + FileCacheEntry fce = createCacheFile("This is a test1"); + assertTrue(fce.file.exists()); + assertEquals("10428da10f5aa2793cb73c0b680e1621", fce.md5); } @Test - public void testAddStream() throws IOException, CancelledException { + public void testFileObfuscated() throws IOException { cache = new FileCache(cacheDir); - FileCacheEntry fce = cache.addStream(toIS("This is a test1"), monitor); - Assert.assertTrue(fce.file.exists()); - Assert.assertEquals("10428da10f5aa2793cb73c0b680e1621", fce.md5); - Assert.assertEquals(1, cache.getFileAddCount()); + FileCacheEntry fce = createCacheFile("This is a test1"); + assertTrue(fce.file.exists()); + assertEquals("10428da10f5aa2793cb73c0b680e1621", fce.md5); + + byte[] fileBytes = FileUtilities.getBytesFromFile(fce.file); + assertFalse(Arrays.equals("This is a test1".getBytes(), fileBytes)); + + try (ObfuscatedInputStream ois = new ObfuscatedInputStream(new FileInputStream(fce.file))) { + byte[] buffer = new byte[100]; + int bytesRead = ois.read(buffer); + assertEquals(15, bytesRead); + assertTrue(Arrays.equals("This is a test1".getBytes(), 0, 15, buffer, 0, 15)); + } } @Test - public void testAddStreamPush() throws IOException, CancelledException { + public void testAddSmallFile() throws IOException { cache = new FileCache(cacheDir); - FileCacheEntry fce = cache.pushStream((os) -> { - FileUtilities.copyStreamToStream(toIS("This is a test1"), os, monitor); - }, monitor); - Assert.assertTrue(fce.file.exists()); - Assert.assertEquals("10428da10f5aa2793cb73c0b680e1621", fce.md5); - Assert.assertEquals(1, cache.getFileAddCount()); + FileCacheEntryBuilder fceBuilder = cache.createCacheEntryBuilder(-1); + fceBuilder.write("This is a test1".getBytes()); + FileCacheEntry fce = fceBuilder.finish(); + assertNull(fce.file); + assertEquals("10428da10f5aa2793cb73c0b680e1621", fce.md5); + assertEquals(15, fce.bytes.length); + } + + @Test + public void testBoundaryCondition() throws IOException { + // test that writing 1 byte less than size cutoff doesn't trigger switch to disk file + cache = new FileCache(cacheDir); + + FileCacheEntryBuilder fceBuilder = cache.createCacheEntryBuilder(-1); + byte[] bytes = new byte[FileCache.MAX_INMEM_FILESIZE - 1]; + fceBuilder.write(bytes); + FileCacheEntry fce = fceBuilder.finish(); + assertNull(fce.file); + assertEquals(FileCache.MAX_INMEM_FILESIZE - 1, fce.bytes.length); + } + + @Test + public void testBoundaryCondition_Grow() throws IOException { + // test that writing more than size cutoff does trigger switch to disk file + cache = new FileCache(cacheDir); + + FileCacheEntryBuilder fceBuilder = cache.createCacheEntryBuilder(-1); + byte[] bytes = new byte[FileCache.MAX_INMEM_FILESIZE - 1]; + fceBuilder.write(bytes); + fceBuilder.write(0); + fceBuilder.write(0); + FileCacheEntry fce = fceBuilder.finish(); + assertNotNull(fce.file); + assertEquals("4eda5bcf5ef0cd4066425006dba9ffaa", fce.md5); + } + + @Test + public void testLargeFile() throws IOException { + cache = new FileCache(cacheDir); + + FileCacheEntryBuilder fceBuilder = + cache.createCacheEntryBuilder(FileCache.MAX_INMEM_FILESIZE + 1); + byte[] bytes = new byte[FileCache.MAX_INMEM_FILESIZE + 1]; + fceBuilder.write(bytes); + FileCacheEntry fce = fceBuilder.finish(); + assertNotNull(fce.file); + assertEquals("4eda5bcf5ef0cd4066425006dba9ffaa", fce.md5); } private void waitForCleanup() { diff --git a/Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/crypto/CachedPasswordProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/crypto/CachedPasswordProviderTest.java new file mode 100644 index 0000000000..dd4248f357 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/crypto/CachedPasswordProviderTest.java @@ -0,0 +1,71 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.formats.gfilesystem.crypto; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.List; + +import org.junit.*; + +import generic.test.AbstractGenericTest; +import ghidra.formats.gfilesystem.FSRL; +import util.CollectionUtils; + +public class CachedPasswordProviderTest extends AbstractGenericTest { + private CryptoProviders cryptoProviders = CryptoProviders.getInstance(); + + private List getPasswords(CryptoSession cryptoSession, String fsrlStr) + throws MalformedURLException { + return CollectionUtils + .asList(cryptoSession.getPasswordsFor(FSRL.fromString(fsrlStr), "badbeef")); + } + + private PopupGUIPasswordProvider popupGUIPasswordProvider; + + @Before + public void setUp() { + popupGUIPasswordProvider = + cryptoProviders.getCryptoProviderInstance(PopupGUIPasswordProvider.class); + cryptoProviders.unregisterCryptoProvider(popupGUIPasswordProvider); + cryptoProviders.getCachedCryptoProvider().clearCache(); + } + + @After + public void tearDown() { + if (popupGUIPasswordProvider != null) { + cryptoProviders.registerCryptoProvider(popupGUIPasswordProvider); + } + } + + @Test + public void testPassword() throws IOException { + try (CryptoSession cryptoSession = cryptoProviders.newSession()) { + assertEquals(0, getPasswords(cryptoSession, "file:///fake/path/file1.txt").size()); + + // shouldn't match passwords: file1.txt to file2.txt + cryptoSession.addSuccessfulPassword(FSRL.fromString("file:///fake/path/file1.txt"), + PasswordValue.wrap("password_for_file2.txt".toCharArray())); + assertEquals(1, getPasswords(cryptoSession, "file:///fake/path/file1.txt").size()); + assertEquals(0, getPasswords(cryptoSession, "file:///fake/path/file2.txt").size()); + + // should match file1.txt in 2 directories + assertEquals(1, getPasswords(cryptoSession, "file:///2nd/fake/path/file1.txt").size()); + } + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/crypto/CmdLinePasswordProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/crypto/CmdLinePasswordProviderTest.java new file mode 100644 index 0000000000..c32c01fc69 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/formats/gfilesystem/crypto/CmdLinePasswordProviderTest.java @@ -0,0 +1,105 @@ +/* ### + * 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.formats.gfilesystem.crypto; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.List; + +import org.junit.*; + +import generic.test.AbstractGenericTest; +import ghidra.formats.gfilesystem.FSRL; +import util.CollectionUtils; +import utilities.util.FileUtilities; + +public class CmdLinePasswordProviderTest extends AbstractGenericTest { + + private CryptoProviders cryptoProviders = CryptoProviders.getInstance(); + + private List getPasswords(CryptoSession cryptoSession, String fsrlStr) + throws MalformedURLException { + return CollectionUtils + .asList(cryptoSession.getPasswordsFor(FSRL.fromString(fsrlStr), "badbeef")); + } + + private String origCmdLinePasswordValue; + private PopupGUIPasswordProvider popupGUIPasswordProvider; + + @Before + + public void setUp() { + popupGUIPasswordProvider = + cryptoProviders.getCryptoProviderInstance(PopupGUIPasswordProvider.class); + cryptoProviders.unregisterCryptoProvider(popupGUIPasswordProvider); + cryptoProviders.getCachedCryptoProvider().clearCache(); + origCmdLinePasswordValue = System + .getProperty(CmdLinePasswordProvider.CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME, null); + } + + @After + public void tearDown() { + if (popupGUIPasswordProvider != null) { + cryptoProviders.registerCryptoProvider(popupGUIPasswordProvider); + } + if (origCmdLinePasswordValue == null) { + System.clearProperty(CmdLinePasswordProvider.CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME); + } + else { + System.setProperty(CmdLinePasswordProvider.CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME, + origCmdLinePasswordValue); + } + } + + @Test + public void testPassword() throws IOException { + File pwdFile = createTempFile("password_test"); + FileUtilities.writeStringToFile(pwdFile, + "password_for_file1.txt\tfile1.txt\n\npassword_for_file2.txt\t/path/to/file2.txt\ngeneral_password\n"); + + System.setProperty(CmdLinePasswordProvider.CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME, + pwdFile.getPath()); + try (CryptoSession cryptoSession = cryptoProviders.newSession()) { + List pwdList = + getPasswords(cryptoSession, "file:///fake/path/file1.txt"); + + assertEquals(2, pwdList.size()); + assertEquals("password_for_file1.txt", String.valueOf(pwdList.get(0).getPasswordChars())); + assertEquals("general_password", String.valueOf(pwdList.get(1).getPasswordChars())); + } + } + + @Test + public void testPassword2() throws IOException { + File pwdFile = createTempFile("password_test"); + FileUtilities.writeStringToFile(pwdFile, "password_for_file1.txt\t/path/to/a/file1.txt"); + + System.setProperty(CmdLinePasswordProvider.CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME, + pwdFile.getPath()); + try (CryptoSession cryptoSession = cryptoProviders.newSession()) { + List pwdList = + getPasswords(cryptoSession, "file:///not_matching/path/file1.txt"); + + assertEquals(0, pwdList.size()); + + List list2 = getPasswords(cryptoSession, "file:///path/to/a/file1.txt"); + assertEquals(1, list2.size()); + } + } +} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/eclipse/AndroidProjectCreator.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/eclipse/AndroidProjectCreator.java index 0deda2cec5..6469e1b0d5 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/eclipse/AndroidProjectCreator.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/eclipse/AndroidProjectCreator.java @@ -21,6 +21,7 @@ import java.util.List; import org.jdom.*; import generic.jar.ResourceFile; +import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.importer.MessageLog; import ghidra.file.formats.android.dex.DexToJarFileSystem; import ghidra.file.formats.android.xml.AndroidXmlFileSystem; @@ -53,7 +54,7 @@ public class AndroidProjectCreator { androidDirectory = directory; } - private GFile apkFile; + private FSRL apkFileFSRL; private File eclipseProjectDirectory; private File srcDirectory; private File genDirectory; @@ -65,20 +66,20 @@ public class AndroidProjectCreator { new ResourceFile(androidDirectory, "eclipse-classpath"); private MessageLog log = new MessageLog(); + private FileSystemService fsService = FileSystemService.getInstance(); - public AndroidProjectCreator(GFile apkFile, File eclipseProjectDirectory) { - this.apkFile = apkFile; + public AndroidProjectCreator(FSRL apkFileFSRL, File eclipseProjectDirectory) { + this.apkFileFSRL = apkFileFSRL; this.eclipseProjectDirectory = eclipseProjectDirectory; } public void create(TaskMonitor monitor) throws IOException, CancelledException { createEclipseProjectDirectories(); - try (ZipFileSystem fs = FileSystemService.getInstance() - .mountSpecificFileSystem( - apkFile.getFSRL(), ZipFileSystem.class, monitor)) { + try (ZipFileSystem fs = + fsService.mountSpecificFileSystem(apkFileFSRL, ZipFileSystem.class, monitor)) { List listing = fs.getListing(null); - processListing(eclipseProjectDirectory, listing, monitor); + processListing(eclipseProjectDirectory, fs, listing, monitor); } File destProjectFile = @@ -97,7 +98,7 @@ public class AndroidProjectCreator { Document projectDoc = XmlUtilities.readDocFromFile(projectFile); Element nameElement = projectDoc.getRootElement().getChild("name"); if (nameElement != null) { - nameElement.setText(apkFile.getName()); + nameElement.setText(apkFileFSRL.getName()); XmlUtilities.writeDocToFile(projectDoc, projectFile); } } @@ -113,7 +114,8 @@ public class AndroidProjectCreator { assetDirectory = FileUtilities.checkedMkdirs(new File(eclipseProjectDirectory, "asset")); } - private void processListing(File outputDirectory, List listing, TaskMonitor monitor) + private void processListing(File outputDirectory, GFileSystem fs, List listing, + TaskMonitor monitor) throws IOException, CancelledException { for (GFile child : listing) { @@ -130,14 +132,13 @@ public class AndroidProjectCreator { } File subDir = new File(outputDirectory, childName); FileUtilities.checkedMkdir(subDir); - processListing(subDir, child.getListing(), monitor); + processListing(subDir, fs, child.getListing(), monitor); continue; } - File cacheFile = FileSystemService.getInstance().getFile(child.getFSRL(), monitor); - try { + try (ByteProvider childBP = fs.getByteProvider(child, monitor)) { if (childName.endsWith(".xml") && - AndroidXmlFileSystem.isAndroidXmlFile(cacheFile, monitor)) { + AndroidXmlFileSystem.isAndroidXmlFile(childBP, monitor)) { processXML(outputDirectory, child, monitor); } else if (childName.endsWith("classes.dex")) { @@ -145,13 +146,13 @@ public class AndroidProjectCreator { } else if (childName.endsWith("resources.arsc")) { //TODO convert resources file back into actual resources - copyFile(cacheFile, outputDirectory, child.getName(), monitor); + copyStream(childBP, outputDirectory, child.getName(), monitor); } else if (childName.endsWith(".class")) { processClass(outputDirectory, child, monitor); } else { - copyFile(cacheFile, outputDirectory, childName, monitor); + copyStream(childBP, outputDirectory, childName, monitor); } } catch (Exception e) { @@ -164,9 +165,8 @@ public class AndroidProjectCreator { private void processDex(File outputDirectory, GFile dexFile, TaskMonitor monitor) throws IOException, CancelledException { - try (DexToJarFileSystem fs = FileSystemService.getInstance() - .mountSpecificFileSystem( - dexFile.getFSRL(), DexToJarFileSystem.class, monitor)) { + try (DexToJarFileSystem fs = fsService.mountSpecificFileSystem(dexFile.getFSRL(), + DexToJarFileSystem.class, monitor)) { GFile jarFile = fs.getJarFile(); processJar(srcDirectory, jarFile.getFSRL(), monitor); } @@ -190,8 +190,8 @@ public class AndroidProjectCreator { File destClassFile = new File(outputDirectory, classFileName); //File destJavaFile = new File(outputDirectory, PathUtils.stripExt(classFileName) + ".java"); - File classFile = FileSystemService.getInstance().getFile(classGFile.getFSRL(), monitor); - copyFile(classFile, outputDirectory, classFileName, monitor); + InputStream is = classGFile.getFilesystem().getInputStream(classGFile, monitor); + copyStream(is, outputDirectory, classFileName, monitor); JadProcessWrapper wrapper = new JadProcessWrapper(destClassFile); JadProcessController controller = new JadProcessController(wrapper, classGFile.getName()); @@ -201,9 +201,8 @@ public class AndroidProjectCreator { private void processXML(File outputDirectory, GFile containerFile, TaskMonitor monitor) throws CancelledException { - try (AndroidXmlFileSystem fs = FileSystemService.getInstance() - .mountSpecificFileSystem( - containerFile.getFSRL(), AndroidXmlFileSystem.class, monitor)) { + try (AndroidXmlFileSystem fs = fsService.mountSpecificFileSystem(containerFile.getFSRL(), + AndroidXmlFileSystem.class, monitor)) { GFile xmlFile = fs.getPayloadFile(); copyStream(fs.getInputStream(xmlFile, monitor), outputDirectory, containerFile.getName(), monitor); @@ -229,16 +228,9 @@ public class AndroidProjectCreator { } } - private static File copyFile(File inputFile, File outputDirectory, String outputName, - TaskMonitor monitor) throws IOException { - - FileUtilities.checkedMkdirs(outputDirectory); - File destFile = new File(outputDirectory, outputName); - - monitor.setMessage("Copying [" + inputFile.getName() + "] to Eclipse project..."); - FileUtilities.copyFile(inputFile, destFile, false, monitor); - - return destFile; + private static File copyStream(ByteProvider provider, File outputDirectory, + String outputName, TaskMonitor monitor) throws IOException { + return copyStream(provider.getInputStream(0), outputDirectory, outputName, monitor); } private static File copyStream(InputStream streamToCopy, File outputDirectory, diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/apk/ApkFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/apk/ApkFileSystem.java index cfc97ae8ff..066a93db5e 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/apk/ApkFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/apk/ApkFileSystem.java @@ -16,7 +16,6 @@ package ghidra.file.formats.android.apk; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -56,8 +55,7 @@ public class ApkFileSystem extends GFileSystemBase { } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) { return null; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageFileSystem.java index ede551438d..c57d46e461 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageFileSystem.java @@ -15,20 +15,15 @@ */ package ghidra.file.formats.android.bootimg; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; -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.formats.gfilesystem.GFile; -import ghidra.formats.gfilesystem.GFileImpl; -import ghidra.formats.gfilesystem.GFileSystemBase; +import ghidra.app.util.bin.ByteProviderWrapper; +import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; +import ghidra.formats.gfilesystem.fileinfo.*; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; @@ -95,38 +90,46 @@ public class BootImageFileSystem extends GFileSystemBase { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { if (file == kernelFile) { - return "This is the actual KERNEL for the android device. You can analyze this file."; + return FileAttributes.of( + FileAttribute.create(FileAttributeType.COMMENT_ATTR, + "This is the actual KERNEL for the android device. You can analyze this file.")); } - else if (file == ramdiskFile) { - return "This is a ramdisk, it is a GZIP file containing a CPIO archive."; + if (file == ramdiskFile) { + return FileAttributes.of( + FileAttribute.create(FileAttributeType.COMMENT_ATTR, + "This is a ramdisk, it is a GZIP file containing a CPIO archive.")); } else if (file == secondStageFile) { - return "This is a second stage loader file. It appears unused at this time."; + return FileAttributes.of( + FileAttribute.create(FileAttributeType.COMMENT_ATTR, + "This is a second stage loader file. It appears unused at this time.")); } return null; } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { + long offset; + long size; if (file == kernelFile) { - byte[] kernelBytes = - provider.readBytes(header.getKernelOffset(), header.getKernelSize()); - return new ByteArrayInputStream(kernelBytes); + offset = header.getKernelOffset(); + size = header.getKernelSize(); } else if (file == ramdiskFile) { - byte[] ramDiskBytes = - provider.readBytes(header.getRamdiskOffset(), header.getRamdiskSize()); - return new ByteArrayInputStream(ramDiskBytes); + offset = header.getRamdiskOffset(); + size = header.getRamdiskSize(); } else if (file == secondStageFile) { - byte[] secondStageBytes = - provider.readBytes(header.getSecondOffset(), header.getSecondSize()); - return new ByteArrayInputStream(secondStageBytes); + offset = header.getSecondOffset(); + size = header.getSecondSize(); } - return null; + else { + return null; + } + return new ByteProviderWrapper(provider, offset, size, file.getFSRL()); } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeader.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeader.java index 3920da986b..f11226ab28 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeader.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeader.java @@ -16,6 +16,7 @@ package ghidra.file.formats.android.bootimg; import ghidra.app.util.bin.StructConverter; +import ghidra.util.NumericUtilities; public abstract class BootImageHeader implements StructConverter { /** @@ -31,6 +32,16 @@ public abstract class BootImageHeader implements StructConverter { */ public abstract int getPageSize(); + /** + * Aligns a value upwards to nearest page boundary. + * + * @param value unsigned value to align + * @return value rounded up to next page size (if not already aligned) + */ + public long pageAlign(long value) { + return NumericUtilities.getUnsignedAlignedValue(value, getPageSize()); + } + /** * Returns the kernel size, as defined in the header. * @return the kernel size, as defined in the header diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV0.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV0.java index b93ace0256..1bdd5ec5a5 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV0.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV0.java @@ -19,7 +19,6 @@ import java.io.IOException; import ghidra.app.util.bin.BinaryReader; import ghidra.program.model.data.*; -import ghidra.util.NumericUtilities; import ghidra.util.exception.DuplicateNameException; /** @@ -83,9 +82,9 @@ public class BootImageHeaderV0 extends BootImageHeader { /** * n = (kernel_size + page_size - 1) / page_size */ + @Override public int getKernelPageCount() { - return (int) NumericUtilities.getUnsignedAlignedValue(Integer.toUnsignedLong(kernel_size), - Integer.toUnsignedLong(page_size)); + return (int) (pageAlign(kernel_size) / page_size); } @Override @@ -105,9 +104,9 @@ public class BootImageHeaderV0 extends BootImageHeader { /** * m = (ramdisk_size + page_size - 1) / page_size */ + @Override public int getRamdiskPageCount() { - return (int) NumericUtilities.getUnsignedAlignedValue(Integer.toUnsignedLong(ramdisk_size), - Integer.toUnsignedLong(page_size)); + return (int) (pageAlign(ramdisk_size) / page_size); } @Override @@ -127,9 +126,9 @@ public class BootImageHeaderV0 extends BootImageHeader { /** * o = (second_size + page_size - 1) / page_size */ + @Override public int getSecondPageCount() { - return (int) NumericUtilities.getUnsignedAlignedValue(Integer.toUnsignedLong(second_size), - Integer.toUnsignedLong(page_size)); + return (int) (pageAlign(second_size) / page_size); } /** diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV1.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV1.java index 2fa1d2cb4e..8388a61098 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV1.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV1.java @@ -9,7 +9,6 @@ import ghidra.app.util.bin.BinaryReader; import ghidra.program.model.data.DataType; import ghidra.program.model.data.Structure; import ghidra.util.InvalidNameException; -import ghidra.util.NumericUtilities; import ghidra.util.exception.DuplicateNameException; /** @@ -38,11 +37,10 @@ public class BootImageHeaderV1 extends BootImageHeaderV0 { /** * p = (recovery_dtbo_size + page_size - 1) / page_size - * @return the recovery DTBO adjusted size + * @return the recovery DTBO adjusted size, as page counts */ public int getRecoveryDtboSizeAdjusted() { - return (int) NumericUtilities.getUnsignedAlignedValue( - Integer.toUnsignedLong(recovery_dtbo_size), Integer.toUnsignedLong(getPageSize())); + return (int) (pageAlign(Integer.toUnsignedLong(recovery_dtbo_size)) / getPageSize()); } /** diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV2.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV2.java index 0f0075f238..bdf0bfaa8e 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV2.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV2.java @@ -21,7 +21,6 @@ import ghidra.app.util.bin.BinaryReader; import ghidra.program.model.data.DataType; import ghidra.program.model.data.Structure; import ghidra.util.InvalidNameException; -import ghidra.util.NumericUtilities; import ghidra.util.exception.DuplicateNameException; /** @@ -48,11 +47,10 @@ public class BootImageHeaderV2 extends BootImageHeaderV1 { /** * q = (dtb_size + page_size - 1) / page_size - * @return the DTB adjusted size + * @return the DTB adjusted size, as page counts */ public int getDtbSizeAdjusted() { - return (int) NumericUtilities.getUnsignedAlignedValue(Integer.toUnsignedLong(dtb_size), - Integer.toUnsignedLong(getPageSize())); + return (int) (pageAlign(Integer.toUnsignedLong(dtb_size)) / getPageSize()); } /** diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV3.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV3.java index 550d56dd5d..7ea072c480 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV3.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/BootImageHeaderV3.java @@ -18,11 +18,7 @@ package ghidra.file.formats.android.bootimg; import java.io.IOException; import ghidra.app.util.bin.BinaryReader; -import ghidra.program.model.data.ArrayDataType; -import ghidra.program.model.data.DataType; -import ghidra.program.model.data.Structure; -import ghidra.program.model.data.StructureDataType; -import ghidra.util.NumericUtilities; +import ghidra.program.model.data.*; import ghidra.util.exception.DuplicateNameException; /** @@ -71,8 +67,7 @@ public class BootImageHeaderV3 extends BootImageHeader { */ @Override public int getKernelPageCount() { - return (int) NumericUtilities.getUnsignedAlignedValue(Integer.toUnsignedLong(kernel_size), - Integer.toUnsignedLong(BootImageConstants.V3_PAGE_SIZE)); + return (int)(pageAlign(kernel_size) / BootImageConstants.V3_PAGE_SIZE); } @Override @@ -90,8 +85,8 @@ public class BootImageHeaderV3 extends BootImageHeader { */ @Override public int getRamdiskPageCount() { - return (int) NumericUtilities.getUnsignedAlignedValue(Integer.toUnsignedLong(ramdisk_size), - Integer.toUnsignedLong(BootImageConstants.V3_PAGE_SIZE)); + return (int) (pageAlign(Integer.toUnsignedLong(ramdisk_size)) / + BootImageConstants.V3_PAGE_SIZE); } @Override @@ -134,6 +129,7 @@ public class BootImageHeaderV3 extends BootImageHeader { return header_version; } + @Override public String getCommandLine() { return cmdline; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/VendorBootImageFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/VendorBootImageFileSystem.java index 33d2f0a840..a5a109f1ce 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/VendorBootImageFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootimg/VendorBootImageFileSystem.java @@ -15,20 +15,15 @@ */ package ghidra.file.formats.android.bootimg; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; -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.formats.gfilesystem.GFile; -import ghidra.formats.gfilesystem.GFileImpl; -import ghidra.formats.gfilesystem.GFileSystemBase; +import ghidra.app.util.bin.ByteProviderWrapper; +import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; +import ghidra.formats.gfilesystem.fileinfo.*; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; @@ -87,28 +82,31 @@ public class VendorBootImageFileSystem extends GFileSystemBase { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { if (file == ramdiskFile) { - return "This is a ramdisk, it is a GZIP file containing a CPIO archive."; + return FileAttributes.of( + FileAttribute.create(FileAttributeType.COMMENT_ATTR, + "This is a ramdisk, it is a GZIP file containing a CPIO archive.")); } else if (file == dtbFile) { - return "This is a DTB file. It appears unused at this time."; + return FileAttributes.of( + FileAttribute.create(FileAttributeType.COMMENT_ATTR, + "This is a DTB file. It appears unused at this time.")); } return null; } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { if (file == ramdiskFile) { - byte[] ramDiskBytes = - provider.readBytes(header.getVendorRamdiskOffset(), header.getVendorRamdiskSize()); - return new ByteArrayInputStream(ramDiskBytes); + return new ByteProviderWrapper(provider, header.getVendorRamdiskOffset(), + Integer.toUnsignedLong(header.getVendorRamdiskSize()), file.getFSRL()); } else if (file == dtbFile) { - byte[] dtbBytes = provider.readBytes(header.getDtbOffset(), header.getDtbSize()); - return new ByteArrayInputStream(dtbBytes); + return new ByteProviderWrapper(provider, header.getDtbOffset(), + Integer.toUnsignedLong(header.getDtbSize()), file.getFSRL()); } return null; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootldr/AndroidBootLoaderFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootldr/AndroidBootLoaderFileSystem.java index f3b5996569..4e2128fbdd 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootldr/AndroidBootLoaderFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/bootldr/AndroidBootLoaderFileSystem.java @@ -16,24 +16,18 @@ package ghidra.file.formats.android.bootldr; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; -import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.ByteProvider; -import ghidra.formats.gfilesystem.GFile; -import ghidra.formats.gfilesystem.GFileImpl; -import ghidra.formats.gfilesystem.GFileSystemBase; +import ghidra.app.util.bin.*; +import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; -@FileSystemInfo(type = "androidbootloader", // ([a-z0-9]+ only) - description = "Android Boot Loader Image", factory = GFileSystemBaseFactory.class) - +@FileSystemInfo(type = "androidbootloader", description = "Android Boot Loader Image", factory = GFileSystemBaseFactory.class) public class AndroidBootLoaderFileSystem extends GFileSystemBase { private List fileList = new ArrayList<>(); private List offsetList = new ArrayList<>(); @@ -71,18 +65,14 @@ public class AndroidBootLoaderFileSystem extends GFileSystemBase { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { - return null; - } - - @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { - + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { int index = fileList.indexOf(file); + if (index < 0) { + throw new IOException("Unknown file: " + file); + } int offset = offsetList.get(index); - - return provider.getInputStream(offset); + return new ByteProviderWrapper(provider, offset, file.getLength(), file.getFSRL()); } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/dex/DexToJarFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/dex/DexToJarFileSystem.java index 9e6f5b98b5..991afc5122 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/dex/DexToJarFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/dex/DexToJarFileSystem.java @@ -15,7 +15,7 @@ */ package ghidra.file.formats.android.dex; -import java.io.*; +import java.io.IOException; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -36,10 +36,8 @@ import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; -import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; import ghidra.util.task.UnknownProgressWrappingTaskMonitor; -import utilities.util.FileUtilities; /** * {@link GFileSystem} that converts a DEX file into a JAR file. @@ -58,14 +56,9 @@ public class DexToJarFileSystem extends GFileSystemBase { } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { - - if (file.equals(jarFile)) { - FileCacheEntry jarFileInfo = getJarFile(monitor); - return new FileInputStream(jarFileInfo.file); - } - return null; + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { + return file.equals(jarFile) ? getJarFile(jarFile.getFSRL(), monitor) : null; } @Override @@ -79,22 +72,22 @@ public class DexToJarFileSystem extends GFileSystemBase { return DexConstants.isDexFile(provider); } - private FileCacheEntry getJarFile(TaskMonitor monitor) throws CancelledException, IOException { + private ByteProvider getJarFile(FSRL jarFSRL, TaskMonitor monitor) + throws CancelledException, IOException { TaskMonitor upwtm = new UnknownProgressWrappingTaskMonitor(monitor, 1); upwtm.setMessage("Converting DEX to JAR..."); FSRLRoot targetFSRL = getFSRL(); FSRL containerFSRL = targetFSRL.getContainer(); - File containerFile = fsService.getFile(containerFSRL, monitor); - FileCacheEntry derivedFileInfo = - fsService.getDerivedFilePush(containerFSRL, "dex2jar", (os) -> { + ByteProvider jarBP = fsService.getDerivedByteProviderPush(containerFSRL, jarFSRL, + "dex2jar", -1, (os) -> { try (ZipOutputStream outputStream = new ZipOutputStream(os)) { DexToJarExceptionHandler exceptionHandler = new DexToJarExceptionHandler(); - DexFileReader reader = - new DexFileReader(FileUtilities.getBytesFromFile(containerFile)); + byte[] containerFileBytes = provider.readBytes(0, provider.length()); + DexFileReader reader = new DexFileReader(containerFileBytes); DexFileNode fileNode = new DexFileNode(); try { @@ -154,21 +147,21 @@ public class DexToJarFileSystem extends GFileSystemBase { }, monitor); - return derivedFileInfo; + return jarBP; } @Override public void open(TaskMonitor monitor) throws CancelledException, IOException { - FileCacheEntry jarFileInfo = getJarFile(monitor); + ByteProvider jarBP = getJarFile(null, monitor); FSRLRoot targetFSRL = getFSRL(); FSRL containerFSRL = targetFSRL.getContainer(); String baseName = FilenameUtils.removeExtension(containerFSRL.getName()); String jarName = baseName + ".jar"; - FSRL jarFSRL = targetFSRL.withPathMD5(jarName, jarFileInfo.md5); + FSRL jarFSRL = targetFSRL.withPathMD5(jarName, jarBP.getFSRL().getMD5()); this.jarFile = GFileImpl.fromFilename(this, root, baseName + ".jar", false, - jarFileInfo.file.length(), jarFSRL); + jarBP.length(), jarFSRL); } @Override diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/dex/DexToSmaliFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/dex/DexToSmaliFileSystem.java index 4d6a2dcd23..e3e167c04a 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/dex/DexToSmaliFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/dex/DexToSmaliFileSystem.java @@ -15,7 +15,9 @@ */ package ghidra.file.formats.android.dex; -import java.io.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.AccessMode; import java.util.*; import org.apache.commons.io.FileUtils; @@ -24,6 +26,7 @@ import org.jf.baksmali.baksmali; import org.jf.dexlib.DexFile; import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.FileByteProvider; import ghidra.file.formats.android.dex.format.DexConstants; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; @@ -42,10 +45,10 @@ public class DexToSmaliFileSystem extends GFileSystemBase { } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { File entry = map.get(file); - return new FileInputStream(entry); + return new FileByteProvider(entry, file.getFSRL(), AccessMode.READ); } @Override diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/fbpk/FBPK_FileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/fbpk/FBPK_FileSystem.java index 7b16662dab..2b0d45a667 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/fbpk/FBPK_FileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/fbpk/FBPK_FileSystem.java @@ -16,17 +16,10 @@ package ghidra.file.formats.android.fbpk; import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; -import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.ByteProvider; -import ghidra.formats.gfilesystem.GFile; -import ghidra.formats.gfilesystem.GFileImpl; -import ghidra.formats.gfilesystem.GFileSystemBase; +import ghidra.app.util.bin.*; +import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; import ghidra.util.exception.CancelledException; @@ -71,17 +64,15 @@ public class FBPK_FileSystem extends GFileSystemBase { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { + + FBPK_Partition partition = map.get(file); + if (partition != null) { + return new ByteProviderWrapper(provider, partition.getDataStartOffset(), + Integer.toUnsignedLong(partition.getDataSize()), file.getFSRL()); + } return null; } - @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { - - FBPK_Partition partition = map.get(file); - - return provider.getInputStream(partition.getDataStartOffset()); - } - } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/kernel/KernelFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/kernel/KernelFileSystem.java index 2d13f46efe..8897ddef29 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/kernel/KernelFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/kernel/KernelFileSystem.java @@ -16,10 +16,10 @@ package ghidra.file.formats.android.kernel; import java.io.IOException; -import java.io.InputStream; import java.util.*; import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.ByteProviderWrapper; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; @@ -96,10 +96,11 @@ public class KernelFileSystem extends GFileSystemBase { } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { - if (compressedKernelFile != null) { - return provider.getInputStream(compressedKernelIndex); + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { + if (compressedKernelFile != null && compressedKernelFile.equals(file)) { + return new ByteProviderWrapper(provider, compressedKernelIndex, + provider.length() - compressedKernelIndex, file.getFSRL()); } return null; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/lz4/LZ4ArchiveFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/lz4/LZ4ArchiveFileSystem.java index fa2be9e52a..a286f19b88 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/lz4/LZ4ArchiveFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/lz4/LZ4ArchiveFileSystem.java @@ -15,25 +15,19 @@ */ package ghidra.file.formats.android.lz4; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.io.*; +import java.util.*; import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorInputStream; import ghidra.app.util.bin.BinaryReader; import ghidra.app.util.bin.ByteProvider; -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.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; +import ghidra.util.task.UnknownProgressWrappingTaskMonitor; /** * @@ -74,59 +68,48 @@ public class LZ4ArchiveFileSystem extends GFileSystemBase { @Override public void open(TaskMonitor monitor) throws IOException, CancelledException { - byte[] decompressedBytes = decompress(monitor); - decompressedLZ4File = - GFileImpl.fromFilename(this, root, NAME, false, decompressedBytes.length, null); + try (ByteProvider payloadBP = getPayload(monitor, root.getFSRL().appendPath(NAME))) { + decompressedLZ4File = + GFileImpl.fromFSRL(this, root, payloadBP.getFSRL(), false, payloadBP.length()); + } } - private byte[] decompress(TaskMonitor monitor) throws IOException { - monitor.setShowProgressValue(true); - monitor.setMaximum(provider.length()); - monitor.setProgress(0); - monitor.setMessage("Decompressing LZ4 archive..."); + private ByteProvider getPayload(TaskMonitor monitor, FSRL payloadFSRL) + throws CancelledException, IOException { + return fsService.getDerivedByteProviderPush(provider.getFSRL(), payloadFSRL, NAME, -1, + os -> { + BinaryReader reader = new BinaryReader(provider, true);//always LE + int magic = reader.readNextInt(); + if (magic != ARCHIVE_MAGIC) { + throw new IOException("LZ4 archive: invalid magic"); + } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - BinaryReader reader = new BinaryReader(provider, true);//always LE - int magic = reader.readNextInt(); - if (magic != ARCHIVE_MAGIC) { - throw new IOException("invalid magic"); - } - try { - while (reader.getPointerIndex() < reader.length()) { - monitor.checkCanceled(); + UnknownProgressWrappingTaskMonitor upwtm = + new UnknownProgressWrappingTaskMonitor(monitor, provider.length()); + upwtm.setMessage("Decompressing LZ4 archive..."); + upwtm.setProgress(0); - int compressedChunkSize = reader.readNextInt(); - monitor.setMessage("Chunk size: 0x" + Integer.toHexString(compressedChunkSize)); - monitor.incrementProgress(compressedChunkSize); + while (reader.getPointerIndex() < reader.length()) { + monitor.checkCanceled(); - byte[] compressedChunk = reader.readNextByteArray(compressedChunkSize); - - try (InputStream compressedStream = - new BlockLZ4CompressorInputStream(new ByteArrayInputStream(compressedChunk))) { - while (true) { - monitor.checkCanceled(); - byte[] bytes = new byte[0x10000]; - int nRead = compressedStream.read(bytes); - if (nRead == -1) { - break; - } - baos.write(bytes, 0, nRead); + int compressedChunkSize = reader.readNextInt(); + byte[] compressedChunk = reader.readNextByteArray(compressedChunkSize); + try (InputStream compressedStream = + new BlockLZ4CompressorInputStream( + new ByteArrayInputStream(compressedChunk))) { + long bytesCopied = + FSUtilities.streamCopy(compressedStream, os, TaskMonitor.DUMMY); + upwtm.incrementProgress(bytesCopied); } } - } - } - catch (Exception e) { - throw new IOException(e); - } - return baos.toByteArray(); + }, monitor); } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { if (file == decompressedLZ4File || file.equals(decompressedLZ4File)) { - byte[] decompressedBytes = decompress(monitor); - return new ByteArrayInputStream(decompressedBytes); + return getPayload(monitor, file.getFSRL()); } return null; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/lz4/LZ4FrameFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/lz4/LZ4FrameFileSystem.java index e0cdfcebcd..1823254b70 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/lz4/LZ4FrameFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/lz4/LZ4FrameFileSystem.java @@ -15,24 +15,19 @@ */ package ghidra.file.formats.android.lz4; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream; import ghidra.app.util.bin.ByteProvider; -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.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; +import ghidra.util.task.UnknownProgressWrappingTaskMonitor; /** * @@ -62,42 +57,32 @@ public class LZ4FrameFileSystem extends GFileSystemBase { @Override public void open(TaskMonitor monitor) throws IOException, CancelledException { - byte[] decompressedBytes = decompress(monitor); - decompressedLZ4FFile = - GFileImpl.fromFilename(this, root, NAME, false, decompressedBytes.length, null); + try (ByteProvider payloadBP = getPayload(monitor, root.getFSRL().appendPath(NAME))) { + decompressedLZ4FFile = + GFileImpl.fromFSRL(this, root, payloadBP.getFSRL(), false, payloadBP.length()); + } } - private byte[] decompress(TaskMonitor monitor) throws IOException { - monitor.setShowProgressValue(true); - monitor.setMaximum(100000); - monitor.setProgress(0); - monitor.setMessage("Decompressing LZ4 Frame..."); - FramedLZ4CompressorInputStream lz4 = - new FramedLZ4CompressorInputStream(provider.getInputStream(0)); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - int chunk = 0; - while (!monitor.isCancelled()) { - byte[] bytes = new byte[0x1000]; - int nRead = lz4.read(bytes); - if (nRead <= 0) { - break; - } - ++chunk; - monitor.incrementProgress(1); - monitor.setMessage( - "Decompressing LZ4F ... " + chunk + " ... 0x" + Integer.toHexString(nRead)); - baos.write(bytes, 0, nRead); - } - lz4.close(); - return baos.toByteArray(); + private ByteProvider getPayload(TaskMonitor monitor, FSRL payloadFSRL) + throws CancelledException, IOException { + return fsService.getDerivedByteProviderPush(provider.getFSRL(), payloadFSRL, NAME, -1, + (os) -> { + UnknownProgressWrappingTaskMonitor upwtm = + new UnknownProgressWrappingTaskMonitor(monitor, provider.length()); + upwtm.setMessage("Decompressing LZ4 Frame..."); + upwtm.setProgress(0); + try (InputStream is = + new FramedLZ4CompressorInputStream(provider.getInputStream(0))) { + FSUtilities.streamCopy(is, os, upwtm); + } + }, monitor); } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { if (file == decompressedLZ4FFile || file.equals(decompressedLZ4FFile)) { - byte[] decompressedBytes = decompress(monitor); - return new ByteArrayInputStream(decompressedBytes); + return getPayload(monitor, file.getFSRL()); } return null; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/OatFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/OatFileSystem.java index c6c06d9858..eeeb0d7420 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/OatFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/oat/OatFileSystem.java @@ -16,27 +16,17 @@ package ghidra.file.formats.android.oat; import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.StringTokenizer; +import java.util.*; import generic.continues.RethrowContinuesFactory; -import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.ByteProvider; -import ghidra.app.util.bin.ByteProviderWrapper; -import ghidra.app.util.bin.format.elf.ElfConstants; -import ghidra.app.util.bin.format.elf.ElfHeader; -import ghidra.app.util.bin.format.elf.ElfSectionHeader; -import ghidra.app.util.bin.format.elf.ElfSectionHeaderConstants; -import ghidra.app.util.bin.format.elf.ElfSymbol; -import ghidra.app.util.bin.format.elf.ElfSymbolTable; +import ghidra.app.util.bin.*; +import ghidra.app.util.bin.format.elf.*; import ghidra.file.formats.android.dex.format.DexHeader; -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.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; @@ -45,8 +35,8 @@ import ghidra.util.task.TaskMonitor; public class OatFileSystem extends GFileSystemBase { private long baseOffset; - private List listing = new ArrayList(); - private List dexFileList = new ArrayList(); + private List listing = new ArrayList<>(); + private List dexFileList = new ArrayList<>(); public OatFileSystem(String fileSystemName, ByteProvider provider) { super(fileSystemName, provider); @@ -163,7 +153,7 @@ public class OatFileSystem extends GFileSystemBase { public void close() throws IOException { super.close(); listing.clear(); - dexFileList = new ArrayList();//prevent UnmodifiableException + dexFileList = new ArrayList<>();//prevent UnmodifiableException } @Override @@ -175,26 +165,27 @@ public class OatFileSystem extends GFileSystemBase { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { int index = listing.indexOf(file); + if (index < 0) { + return FileAttributes.EMPTY; + } + OatDexFile oatDexFileHeader = dexFileList.get(index); - return oatDexFileHeader.getDexFileLocation(); + return FileAttributes + .of(FileAttribute.create("Oat location", oatDexFileHeader.getDexFileLocation())); } @Override - public InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { int index = listing.indexOf(file); + if ( index < 0 ) { + throw new IOException("Invalid / unknown file: " + file); + } OatDexFile oatDexFileHeader = dexFileList.get(index); - return provider.getInputStream(baseOffset + oatDexFileHeader.getDexFileOffset()); - } - - @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) - throws CancelledException, IOException { - int index = listing.indexOf(file); - OatDexFile oatDexFileHeader = dexFileList.get(index); - return provider.getInputStream(baseOffset + oatDexFileHeader.getDexFileOffset()); + return new ByteProviderWrapper(provider, oatDexFileHeader.getDexFileOffset(), + oatDexFileHeader.getDexHeader().getFileSize(), file.getFSRL()); } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/odex/OdexFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/odex/OdexFileSystem.java index 541ca8b1c5..d74283936a 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/odex/OdexFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/odex/OdexFileSystem.java @@ -16,16 +16,13 @@ package ghidra.file.formats.android.odex; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; -import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.*; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; -import ghidra.util.BoundedInputStream; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; @@ -43,46 +40,26 @@ public class OdexFileSystem extends GFileSystemBase { } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException, CryptoException { if (file != null) { if (file.equals(dexFile)) { - return new BoundedInputStream(provider.getInputStream(odexHeader.getDexOffset()), - odexHeader.getDexLength()); + return new ByteProviderWrapper(provider, odexHeader.getDexOffset(), + odexHeader.getDexLength(), dexFile.getFSRL()); } if (file.equals(depsFile)) { - return new BoundedInputStream(provider.getInputStream(odexHeader.getDepsOffset()), - odexHeader.getDepsLength()); + return new ByteProviderWrapper(provider, odexHeader.getDepsOffset(), + odexHeader.getDepsLength(), depsFile.getFSRL()); } if (file.equals(auxFile)) { - return new BoundedInputStream(provider.getInputStream(odexHeader.getAuxOffset()), - odexHeader.getAuxLength()); + return new ByteProviderWrapper(provider, odexHeader.getAuxOffset(), + odexHeader.getAuxLength(), auxFile.getFSRL()); } } return null; } - @Override - public String getInfo(GFile file, TaskMonitor monitor) { - StringBuilder builder = new StringBuilder(); - builder.append("Magic: " + odexHeader.getMagic()).append("\n"); - builder.append("Dex Offset: " + Integer.toHexString(odexHeader.getDexOffset())) - .append("\n"); - builder.append("Dex Length: " + Integer.toHexString(odexHeader.getDexLength())) - .append("\n"); - builder.append("Deps Offset: " + Integer.toHexString(odexHeader.getDepsOffset())) - .append("\n"); - builder.append("Deps Length: " + Integer.toHexString(odexHeader.getDepsLength())) - .append("\n"); - builder.append("Aux Offset: " + Integer.toHexString(odexHeader.getAuxOffset())) - .append("\n"); - builder.append("Aux Length: " + Integer.toHexString(odexHeader.getAuxLength())) - .append("\n"); - builder.append("Flags: " + Integer.toHexString(odexHeader.getFlags())).append("\n"); - return builder.toString(); - } - @Override public List getListing(GFile directory) throws IOException { List list = new ArrayList<>(); diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/prof/ProfileFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/prof/ProfileFileSystem.java index 0656407131..5b9367affc 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/prof/ProfileFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/prof/ProfileFileSystem.java @@ -19,12 +19,13 @@ import java.io.*; import java.util.ArrayList; import java.util.List; -import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.*; import ghidra.file.formats.zlib.ZLIB; 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.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; @@ -39,44 +40,6 @@ public class ProfileFileSystem extends GFileSystemBase { super(fileSystemName, provider); } - @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { - - if (file != null) { - if (file.equals(dataFile)) { - InputStream compressedStream = - provider.getInputStream(header.getCompressedDataOffset()); - ZLIB zlib = new ZLIB(); - ByteArrayOutputStream decompressedBytes = - zlib.decompress(compressedStream, header.getUncompressedSizeOfZippedData()); - return new ByteArrayInputStream(decompressedBytes.toByteArray()); - } - } - return null; - } - - @Override - public String getInfo(GFile file, TaskMonitor monitor) { - StringBuilder builder = new StringBuilder(); - builder.append("Magic: " + header.getMagic()).append("\n"); - return builder.toString(); - } - - @Override - public List getListing(GFile directory) throws IOException { - List list = new ArrayList<>(); - if (directory == null || directory.equals(root)) { - list.add(dataFile); - } - return list; - } - - @Override - public boolean isValid(TaskMonitor monitor) throws IOException { - return ProfileConstants.isProfile(provider); - } - @Override public void open(TaskMonitor monitor) throws IOException, CryptoException, CancelledException { BinaryReader reader = new BinaryReader(provider, true); @@ -92,4 +55,40 @@ public class ProfileFileSystem extends GFileSystemBase { dataFile = null; } + @Override + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { + if (dataFile.equals(file)) { + InputStream compressedStream = + provider.getInputStream(header.getCompressedDataOffset()); + ZLIB zlib = new ZLIB(); + ByteArrayOutputStream decompressedBytes = + zlib.decompress(compressedStream, header.getUncompressedSizeOfZippedData()); + return new ByteArrayProvider(decompressedBytes.toByteArray(), file.getFSRL()); + } + return null; + } + + @Override + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + if (file == dataFile) { + return FileAttributes.of(FileAttribute.create("Magic", header.getMagic())); + } + return null; + } + + @Override + public List getListing(GFile directory) throws IOException { + List list = new ArrayList<>(); + if (directory == null || directory.equals(root)) { + list.add(dataFile); + } + return list; + } + + @Override + public boolean isValid(TaskMonitor monitor) throws IOException { + return ProfileConstants.isProfile(provider); + } + } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/vdex/VdexFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/vdex/VdexFileSystem.java index 6ebf03178a..1d731a869c 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/vdex/VdexFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/vdex/VdexFileSystem.java @@ -16,18 +16,12 @@ package ghidra.file.formats.android.vdex; import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; -import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.*; import ghidra.file.formats.android.cdex.CDexHeader; import ghidra.file.formats.android.dex.format.DexHeader; -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.util.exception.CancelledException; @@ -38,7 +32,7 @@ import ghidra.util.task.TaskMonitor; public class VdexFileSystem extends GFileSystemBase { private VdexHeader header; - private List listing = new ArrayList(); + private List listing = new ArrayList<>(); public VdexFileSystem(String fileSystemName, ByteProvider provider) { super(fileSystemName, provider); @@ -95,22 +89,16 @@ public class VdexFileSystem extends GFileSystemBase { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { int index = listing.indexOf(file); - - if (index >= 0 && index < header.getDexHeaderList().size() - 1) { - long length = Integer.toUnsignedLong(header.getDexHeaderList().get(index).getFileSize()); - return "DEX Header Size: 0x" + Long.toHexString(length); + if (index < 0) { + throw new IOException("Unknown file: " + file); } - return null; - } - - @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { - int index = listing.indexOf(file); + DexHeader dexHeader = header.getDexHeaderList().get(index); long startIndex = header.getDexStartOffset(index); - return provider.getInputStream(startIndex); + return new ByteProviderWrapper(provider, startIndex, + Integer.toUnsignedLong(dexHeader.getFileSize()), file.getFSRL()); } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/xml/AndroidXmlFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/xml/AndroidXmlFileSystem.java index 70b7cf3dbe..018c0c12b8 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/xml/AndroidXmlFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/android/xml/AndroidXmlFileSystem.java @@ -18,7 +18,8 @@ package ghidra.file.formats.android.xml; import java.io.*; import java.util.*; -import ghidra.app.util.bin.*; +import ghidra.app.util.bin.ByteArrayProvider; +import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; @@ -37,12 +38,6 @@ import ghidra.util.task.TaskMonitor; @FileSystemInfo(type = "androidxml", description = "Android XML", factory = GFileSystemBaseFactory.class) public class AndroidXmlFileSystem extends GFileSystemBase { - public static boolean isAndroidXmlFile(File f, TaskMonitor monitor) throws IOException { - try (RandomAccessByteProvider rabp = new RandomAccessByteProvider(f)) { - return isAndroidXmlFile(rabp, monitor); - } - } - public static boolean isAndroidXmlFile(ByteProvider provider, TaskMonitor monitor) throws IOException { byte[] actualBytes = @@ -51,7 +46,7 @@ public class AndroidXmlFileSystem extends GFileSystemBase { return false; } - try (InputStream is = new ByteProviderInputStream(provider, 0, provider.length())) { + try (InputStream is = provider.getInputStream(0)) { StringWriter sw = new StringWriter(); AndroidXmlConvertor.convert(is, new PrintWriter(sw), monitor); return true; @@ -79,7 +74,7 @@ public class AndroidXmlFileSystem extends GFileSystemBase { @Override public void open(TaskMonitor monitor) throws IOException, CryptoException, CancelledException { - try (InputStream is = new ByteProviderInputStream(provider, 0, provider.length())) { + try (InputStream is = provider.getInputStream(0)) { StringWriter sw = new StringWriter(); AndroidXmlConvertor.convert(is, new PrintWriter(sw), monitor); payloadBytes = sw.toString().getBytes(); @@ -91,9 +86,9 @@ public class AndroidXmlFileSystem extends GFileSystemBase { } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException, CryptoException { - return new ByteArrayInputStream(payloadBytes); + return new ByteArrayProvider(payloadBytes, file.getFSRL()); } @Override diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/coff/CoffArchiveFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/coff/CoffArchiveFileSystem.java index 023bf90269..45723d8a12 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/coff/CoffArchiveFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/coff/CoffArchiveFileSystem.java @@ -15,9 +15,11 @@ */ package ghidra.file.formats.coff; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + import java.io.IOException; -import java.io.InputStream; -import java.util.*; +import java.util.Date; +import java.util.List; import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.bin.ByteProviderWrapper; @@ -26,7 +28,7 @@ import ghidra.app.util.bin.format.coff.archive.CoffArchiveHeader; import ghidra.app.util.bin.format.coff.archive.CoffArchiveMemberHeader; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; -import ghidra.util.exception.CancelledException; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.task.TaskMonitor; @FileSystemInfo(type = "coff", description = "COFF Archive", factory = CoffArchiveFileSystemFactory.class) @@ -74,13 +76,6 @@ public class CoffArchiveFileSystem implements GFileSystem { } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) - throws IOException, CancelledException { - - ByteProvider bp = getByteProvider(file, monitor); - return bp != null ? bp.getInputStream(0) : null; - } - public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) { CoffArchiveMemberHeader entry = fsih.getMetadata(file); return (entry != null && entry.isCOFF()) @@ -110,21 +105,18 @@ public class CoffArchiveFileSystem implements GFileSystem { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { CoffArchiveMemberHeader entry = fsih.getMetadata(file); - return (entry == null) ? null : FSUtilities.infoMapToString(getInfoMap(entry)); - } - - public Map getInfoMap(CoffArchiveMemberHeader blob) { - Map info = new LinkedHashMap<>(); - info.put("Name", blob.getName()); - info.put("Size", - "" + Long.toString(blob.getSize()) + ", 0x" + Long.toHexString(blob.getSize())); - info.put("UserID", blob.getUserId()); - info.put("GroupID", blob.getGroupId()); - info.put("Mode", blob.getMode()); - info.put("Time", new Date(blob.getDate()).toString()); - return info; + FileAttributes result = new FileAttributes(); + if (entry != null) { + result.add(NAME_ATTR, entry.getName()); + result.add(SIZE_ATTR, entry.getSize()); + result.add(USER_ID_ATTR, (long) entry.getUserIdInt()); + result.add(GROUP_ID_ATTR, (long) entry.getGroupIdInt()); + result.add(MODIFIED_DATE_ATTR, new Date(entry.getDate())); + result.add("Mode", entry.getMode()); + } + return result; } @Override diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/coff/CoffArchiveFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/coff/CoffArchiveFileSystemFactory.java index d8a725f18d..8b501333db 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/coff/CoffArchiveFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/coff/CoffArchiveFileSystemFactory.java @@ -15,27 +15,26 @@ */ package ghidra.file.formats.coff; -import java.io.File; import java.io.IOException; import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.bin.format.coff.archive.CoffArchiveConstants; import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryFull; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; import utilities.util.ArrayUtilities; -public class CoffArchiveFileSystemFactory - implements GFileSystemFactoryFull, GFileSystemProbeBytesOnly { +public class CoffArchiveFileSystemFactory implements + GFileSystemFactoryByteProvider, GFileSystemProbeBytesOnly { public static final int PROBE_BYTES_REQUIRED = CoffArchiveConstants.MAGIC_LEN; @Override - public CoffArchiveFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, - ByteProvider byteProvider, File containerFile, FileSystemService fsService, - TaskMonitor monitor) throws IOException, CancelledException { + public CoffArchiveFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { CoffArchiveFileSystem fs = new CoffArchiveFileSystem(targetFSRL, byteProvider); fs.mount(monitor); diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/complzss/CompLzssFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/complzss/CompLzssFileSystem.java index 2f1837706b..3b9c20f406 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/complzss/CompLzssFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/complzss/CompLzssFileSystem.java @@ -15,16 +15,16 @@ */ package ghidra.file.formats.complzss; -import java.io.*; +import java.io.IOException; +import java.io.InputStream; import java.util.List; -import ghidra.app.util.bin.ByteArrayProvider; import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.ByteProviderWrapper; import ghidra.file.formats.lzss.LzssCodec; import ghidra.file.formats.lzss.LzssConstants; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; -import ghidra.util.HashUtilities; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -35,21 +35,19 @@ public class CompLzssFileSystem implements GFileSystem { private FileSystemRefManager fsRefManager = new FileSystemRefManager(this); private ByteProvider payloadProvider; - public CompLzssFileSystem(FSRLRoot fsrl, ByteProvider provider, TaskMonitor monitor) - throws IOException { + public CompLzssFileSystem(FSRLRoot fsrl, ByteProvider provider, FileSystemService fsService, + TaskMonitor monitor) throws IOException, CancelledException { monitor.setMessage("Decompressing LZSS..."); - byte[] compressedBytes = provider.readBytes(LzssConstants.HEADER_LENGTH, + try (ByteProvider tmpBP = new ByteProviderWrapper(provider, LzssConstants.HEADER_LENGTH, provider.length() - LzssConstants.HEADER_LENGTH); - ByteArrayOutputStream decompressedBOS = new ByteArrayOutputStream(); - LzssCodec.decompress(decompressedBOS, new ByteArrayInputStream(compressedBytes)); - byte[] decompressedBytes = decompressedBOS.toByteArray(); + InputStream tmpIS = tmpBP.getInputStream(0);) { - String md5 = HashUtilities.getHash(HashUtilities.MD5_ALGORITHM, decompressedBytes); - this.fsIndex = new SingleFileSystemIndexHelper(this, fsFSRL, "lzss_decompressed", - decompressedBytes.length, md5); - this.payloadProvider = - new ByteArrayProvider(decompressedBytes, fsIndex.getPayloadFile().getFSRL()); + this.payloadProvider = fsService.getDerivedByteProviderPush(provider.getFSRL(), null, + "decompressed lzss", -1, (os) -> LzssCodec.decompress(os, tmpIS), monitor); + this.fsIndex = new SingleFileSystemIndexHelper(this, fsFSRL, "lzss_decompressed", + payloadProvider.length(), payloadProvider.getFSRL().getMD5()); + } } @Override @@ -83,14 +81,10 @@ public class CompLzssFileSystem implements GFileSystem { } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) - throws IOException, CancelledException { - ByteProvider bp = getByteProvider(file, monitor); - return bp != null ? bp.getInputStream(0) : null; - } - - public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) { - return fsIndex.isPayloadFile(file) ? payloadProvider : null; + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException { + return fsIndex.isPayloadFile(file) + ? new ByteProviderWrapper(payloadProvider, file.getFSRL()) + : null; } @Override diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/complzss/CompLzssFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/complzss/CompLzssFileSystemFactory.java index 4c3237e87a..8e196f86dc 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/complzss/CompLzssFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/complzss/CompLzssFileSystemFactory.java @@ -15,26 +15,25 @@ */ package ghidra.file.formats.complzss; -import java.io.File; import java.io.IOException; import ghidra.app.util.bin.ByteProvider; import ghidra.file.formats.lzss.LzssCompressionHeader; import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryFull; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; public class CompLzssFileSystemFactory - implements GFileSystemFactoryFull, GFileSystemProbeBytesOnly { + implements GFileSystemFactoryByteProvider, GFileSystemProbeBytesOnly { @Override - public CompLzssFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, - ByteProvider byteProvider, File containerFile, FileSystemService fsService, - TaskMonitor monitor) throws IOException, CancelledException { - - CompLzssFileSystem fs = new CompLzssFileSystem(targetFSRL, byteProvider, monitor); + public CompLzssFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { + CompLzssFileSystem fs = + new CompLzssFileSystem(targetFSRL, byteProvider, fsService, monitor); return fs; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cpio/CpioFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cpio/CpioFileSystem.java index 165b15ff98..5d8a998110 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cpio/CpioFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cpio/CpioFileSystem.java @@ -15,34 +15,36 @@ */ package ghidra.file.formats.cpio; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + import java.io.*; -import java.util.Date; import java.util.List; import org.apache.commons.compress.archivers.cpio.CpioArchiveEntry; import org.apache.commons.compress.archivers.cpio.CpioArchiveInputStream; -import ghidra.app.util.bin.ByteArrayProvider; import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.FSUtilities.StreamCopyResult; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; -import ghidra.util.NumericUtilities; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; import utilities.util.FileUtilities; @FileSystemInfo(type = "cpio", description = "CPIO", factory = CpioFileSystemFactory.class) public class CpioFileSystem implements GFileSystem { + private FileSystemService fsService; private FSRLRoot fsFSRL; private FileSystemIndexHelper fsIndex; private FileSystemRefManager fsRefManager = new FileSystemRefManager(this); private ByteProvider provider; - public CpioFileSystem(FSRLRoot fsFSRL, ByteProvider provider, TaskMonitor monitor) + public CpioFileSystem(FSRLRoot fsFSRL, ByteProvider provider, FileSystemService fsService, + TaskMonitor monitor) throws IOException { monitor.setMessage("Opening CPIO..."); + this.fsService = fsService; this.fsFSRL = fsFSRL; this.provider = provider; this.fsIndex = new FileSystemIndexHelper<>(this, fsFSRL); @@ -112,50 +114,37 @@ public class CpioFileSystem implements GFileSystem { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + FileAttributes result = new FileAttributes(); CpioArchiveEntry entry = fsIndex.getMetadata(file); - if (entry == null) { - return null; + if (entry != null) { + result.add(NAME_ATTR, entry.getName()); + result.add(SIZE_ATTR, entry.getSize()); + result.add(MODIFIED_DATE_ATTR, entry.getLastModifiedDate()); + result.add(USER_ID_ATTR, entry.getUID()); + result.add(GROUP_ID_ATTR, entry.getGID()); + result.add("Mode", Long.toHexString(entry.getMode())); + result.add("Inode", Long.toHexString(entry.getInode())); + result.add("Format", Long.toHexString(entry.getFormat())); + try { + result.add("Device ID", Long.toHexString(entry.getDevice())); + result.add("Remote Device", Long.toHexString(entry.getRemoteDevice())); + } + catch (Exception e) { + // ignore old format missing exception + } + try { + result.add("Checksum", Long.toHexString(entry.getChksum())); + } + catch (Exception e) { + // ignore new format missing exception + } } - StringBuilder buffer = new StringBuilder(); - try { - buffer.append("Name: " + entry.getName() + "\n"); - buffer.append("Format: " + Long.toHexString(entry.getFormat()) + "\n"); - buffer.append("GID: " + Long.toHexString(entry.getGID()) + "\n"); - buffer.append("Inode: " + Long.toHexString(entry.getInode()) + "\n"); - buffer.append("Last Modified: " + entry.getLastModifiedDate() + "\n"); - buffer.append("Links: " + Long.toHexString(entry.getNumberOfLinks()) + "\n"); - buffer.append("Mode: " + Long.toHexString(entry.getMode()) + "\n"); - buffer.append("Size: " + Long.toHexString(entry.getSize()) + "\n"); - buffer.append("Time: " + new Date(entry.getTime()) + "\n"); - buffer.append("UID: " + Long.toHexString(entry.getUID()) + "\n"); - } - catch (Exception e) { - // ignore - } - try { - buffer.append("Device ID: " + Long.toHexString(entry.getDevice()) + "\n"); - buffer.append("Remote Device: " + Long.toHexString(entry.getRemoteDevice()) + "\n"); - } - catch (Exception e) { - // ignore old format missing exception - } - try { - buffer.append("Checksum: " + Long.toHexString(entry.getChksum()) + "\n"); - } - catch (Exception e) { - // ignore new format missing exception - } - return buffer.toString(); + + return result; } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) - throws IOException, CancelledException { - ByteProvider bp = getByteProvider(file, monitor); - return bp != null ? bp.getInputStream(0) : null; - } - public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { CpioArchiveEntry targetEntry = fsIndex.getMetadata(file); @@ -171,10 +160,12 @@ public class CpioFileSystem implements GFileSystem { CpioArchiveEntry currentEntry; while ((currentEntry = cpioInputStream.getNextCPIOEntry()) != null) { if (currentEntry.equals(targetEntry)) { - return getByteProviderForEntry(cpioInputStream, file.getFSRL(), monitor); + ByteProvider bp = + fsService.getDerivedByteProvider(provider.getFSRL(), file.getFSRL(), + file.getPath(), currentEntry.getSize(), () -> cpioInputStream, monitor); + return bp; } - FileUtilities.copyStreamToStream(cpioInputStream, OutputStream.nullOutputStream(), - monitor); + FSUtilities.streamCopy(cpioInputStream, OutputStream.nullOutputStream(), monitor); } } catch (IllegalArgumentException e) { @@ -182,14 +173,4 @@ public class CpioFileSystem implements GFileSystem { } throw new IOException("Unable to seek to entry: " + file.getName()); } - - private ByteProvider getByteProviderForEntry(CpioArchiveInputStream cpioInputStream, FSRL fsrl, - TaskMonitor monitor) throws CancelledException, IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - StreamCopyResult copyResult = FSUtilities.streamCopy(cpioInputStream, out, monitor); - if (fsrl.getMD5() == null) { - fsrl = fsrl.withMD5(NumericUtilities.convertBytesToString(copyResult.md5)); - } - return new ByteArrayProvider(out.toByteArray(), fsrl); - } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cpio/CpioFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cpio/CpioFileSystemFactory.java index ecccf07380..f575809197 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cpio/CpioFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cpio/CpioFileSystemFactory.java @@ -15,27 +15,25 @@ */ package ghidra.file.formats.cpio; -import java.io.File; import java.io.IOException; import org.apache.commons.compress.archivers.cpio.CpioArchiveInputStream; import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryFull; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; public class CpioFileSystemFactory - implements GFileSystemFactoryFull, GFileSystemProbeBytesOnly { + implements GFileSystemFactoryByteProvider, GFileSystemProbeBytesOnly { @Override - public CpioFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, - ByteProvider byteProvider, File containerFile, FileSystemService fsService, - TaskMonitor monitor) throws IOException, CancelledException { - - CpioFileSystem fs = new CpioFileSystem(targetFSRL, byteProvider, monitor); + public CpioFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { + CpioFileSystem fs = new CpioFileSystem(targetFSRL, byteProvider, fsService, monitor); return fs; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4FileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4FileSystem.java index 886f49ebad..ee581b160d 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4FileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4FileSystem.java @@ -15,16 +15,18 @@ */ package ghidra.file.formats.ext4; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + import java.io.IOException; -import java.io.InputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.BitSet; -import java.util.List; +import java.util.*; import ghidra.app.util.bin.*; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; +import ghidra.formats.gfilesystem.fileinfo.FileType; import ghidra.util.Msg; import ghidra.util.NumericUtilities; import ghidra.util.exception.CancelledException; @@ -183,31 +185,45 @@ public class Ext4FileSystem implements GFileSystem { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + FileAttributes result = new FileAttributes(); + Ext4File ext4File = fsih.getMetadata(file); - if (ext4File == null) { - return null; - } - Ext4Inode inode = ext4File.getInode(); - String info = ""; - if (inode.isSymLink()) { - try { - info += "Symlink to \"" + readLink(file, ext4File, monitor) + "\"\n"; - } - catch (IOException e) { - // ignore + if (ext4File != null) { + Ext4Inode inode = ext4File.getInode(); + result.add(NAME_ATTR, ext4File.getName()); + result.add(SIZE_ATTR, inode.getSize()); + result.add(FILE_TYPE_ATTR, inodeToFileType(inode)); + if (inode.isSymLink()) { + String symLinkDest = "unknown"; + try { + symLinkDest = readLink(file, ext4File, monitor); + } + catch (IOException e) { + // fall thru with default value + } + result.add(SYMLINK_DEST_ATTR, symLinkDest); } + result.add(MODIFIED_DATE_ATTR, new Date(inode.getI_mtime() * 1000)); + result.add(UNIX_ACL_ATTR, (long) (inode.getI_mode() & 0xFFF)); + result.add(USER_ID_ATTR, Short.toUnsignedLong(inode.getI_uid())); + result.add(GROUP_ID_ATTR, Short.toUnsignedLong(inode.getI_gid())); + result.add("Link Count", inode.getI_links_count()); } - long size = inode.getSize(); - info += "File size: " + FSUtilities.formatSize(size) + "\n"; - return info; + return result; } - @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) - throws IOException, CancelledException { - ByteProvider bp = getByteProvider(file, monitor); - return (bp != null) ? new ByteProviderInputStream(bp, 0, bp.length()) : null; + FileType inodeToFileType(Ext4Inode inode) { + if (inode.isDir()) { + return FileType.DIRECTORY; + } + if (inode.isSymLink()) { + return FileType.SYMBOLIC_LINK; + } + if (inode.isFile()) { + return FileType.FILE; + } + return FileType.UNKNOWN; } private static final int MAX_SYMLINK_LOOKUP_COUNT = 100; @@ -281,6 +297,7 @@ public class Ext4FileSystem implements GFileSystem { * responsible for closing the ByteProvider * @throws IOException if error */ + @Override public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException { Ext4Inode inode = getInodeFor(file, monitor); if (inode == null) { diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4FileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4FileSystemFactory.java index 617862f2e8..8f24d9c4c3 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4FileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4FileSystemFactory.java @@ -15,24 +15,24 @@ */ package ghidra.file.formats.ext4; +import java.io.IOException; + import ghidra.app.util.bin.BinaryReader; import ghidra.app.util.bin.ByteProvider; -import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryFull; -import ghidra.formats.gfilesystem.factory.GFileSystemProbeFull; +import ghidra.formats.gfilesystem.FSRLRoot; +import ghidra.formats.gfilesystem.FileSystemService; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; +import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -import java.io.File; -import java.io.IOException; - public class Ext4FileSystemFactory - implements GFileSystemProbeFull, GFileSystemFactoryFull { + implements GFileSystemProbeByteProvider, GFileSystemFactoryByteProvider { @Override - public Ext4FileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, ByteProvider byteProvider, - File containerFile, FileSystemService fsService, TaskMonitor monitor) - throws IOException, CancelledException { + public Ext4FileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { Ext4FileSystem fs = new Ext4FileSystem(targetFSRL, byteProvider); fs.mountFS(monitor); @@ -41,17 +41,14 @@ public class Ext4FileSystemFactory } @Override - public boolean probe(FSRL containerFSRL, ByteProvider byteProvider, File containerFile, - FileSystemService fsService, TaskMonitor taskMonitor) - throws IOException, CancelledException { + public boolean probe(ByteProvider byteProvider, FileSystemService fsService, + TaskMonitor taskMonitor) throws IOException, CancelledException { try { BinaryReader reader = new BinaryReader(byteProvider, true); //ext4 has a 1024 byte padding at the beginning - reader.setPointerIndex(0x400); + reader.setPointerIndex(Ext4Constants.SUPER_BLOCK_START); Ext4SuperBlock superBlock = new Ext4SuperBlock(reader); - if ((superBlock.getS_magic() & 0xffff) == Ext4Constants.SUPER_BLOCK_MAGIC) { - return true; - } + return superBlock.isValid(); } catch (IOException e) { // ignore diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4Inode.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4Inode.java index b02edb2ee3..93833e0f6b 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4Inode.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ext4/Ext4Inode.java @@ -66,7 +66,7 @@ public class Ext4Inode implements StructConverter { } public Ext4Inode(BinaryReader reader, int inodeSize) throws IOException { - if (inodeSize < MINIMAL_SIZEOF_INODE) { + if (inodeSize < INODE_BASE_SIZE) { throw new IOException("Bad inodeSize: " + inodeSize); } long inodeStart = reader.getPointerIndex(); @@ -88,21 +88,23 @@ public class Ext4Inode implements StructConverter { i_size_high = reader.readNextInt(); i_obso_faddr = reader.readNextInt(); i_osd2 = reader.readNextByteArray(12); //12 bytes long - i_extra_isize = reader.readNextShort(); - i_checksum_hi = reader.readNextShort(); - i_ctime_extra = reader.readNextInt(); - i_mtime_extra = reader.readNextInt(); - i_atime_extra = reader.readNextInt(); - i_crtime = reader.readNextInt(); - i_crtime_extra = reader.readNextInt(); - i_version_hi = reader.readNextInt(); - i_projid = reader.readNextInt(); + if (inodeSize > INODE_BASE_SIZE) { + i_extra_isize = reader.readNextShort(); + i_checksum_hi = reader.readNextShort(); + i_ctime_extra = reader.readNextInt(); + i_mtime_extra = reader.readNextInt(); + i_atime_extra = reader.readNextInt(); + i_crtime = reader.readNextInt(); + i_crtime_extra = reader.readNextInt(); + i_version_hi = reader.readNextInt(); + i_projid = reader.readNextInt(); - // skipping unknown fields here + // skipping unknown fields here - // read EAs if present - reader.setPointerIndex(inodeStart + INODE_BASE_SIZE + i_extra_isize); - xAttributes = Ext4Xattributes.readInodeXAttributes(reader, inodeStart + inodeSize); + // read EAs if present + reader.setPointerIndex(inodeStart + INODE_BASE_SIZE + i_extra_isize); + xAttributes = Ext4Xattributes.readInodeXAttributes(reader, inodeStart + inodeSize); + } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/gzip/GZipFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/gzip/GZipFileSystem.java index 78bccd2390..d707e49d08 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/gzip/GZipFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/gzip/GZipFileSystem.java @@ -15,14 +15,20 @@ */ package ghidra.file.formats.gzip; -import java.io.*; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + +import java.io.IOException; import java.util.*; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.apache.commons.compress.compressors.gzip.GzipParameters; +import org.apache.commons.io.FilenameUtils; +import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.ByteProviderWrapper; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; import ghidra.util.task.UnknownProgressWrappingTaskMonitor; @@ -41,61 +47,59 @@ public class GZipFileSystem implements GFileSystem { public static final String GZIP_PAYLOAD_FILENAME = "gzip_decompressed"; private final FSRLRoot fsFSRL; - private final FSRL containerFSRL; private final FileSystemRefManager refManager = new FileSystemRefManager(this); private final SingleFileSystemIndexHelper fsIndex; private final FileSystemService fsService; + private ByteProvider container; + private ByteProvider payloadProvider; - private String origFilename; + private String payloadFilename; private String payloadKey; private String origComment; private long origDate; - private long containerSize; - public GZipFileSystem(FSRL containerFSRL, FSRLRoot fsFSRL, File containerFile, - FileSystemService fsService, TaskMonitor monitor) - throws IOException, CancelledException { + public GZipFileSystem(ByteProvider container, FSRLRoot fsFSRL, FileSystemService fsService, + TaskMonitor monitor) throws IOException, CancelledException { this.fsFSRL = fsFSRL; - this.containerFSRL = containerFSRL; this.fsService = fsService; + this.container = container; - readGzipMetadata(containerFile, monitor); - FileCacheEntry fce = getPayloadFileCacheEntry(monitor); - this.fsIndex = - new SingleFileSystemIndexHelper(this, fsFSRL, origFilename, fce.file.length(), fce.md5); + readGzipMetadata(monitor); + payloadProvider = getPayloadByteProvider(monitor); + this.fsIndex = new SingleFileSystemIndexHelper(this, fsFSRL, payloadFilename, + payloadProvider.length(), payloadProvider.getFSRL().getMD5()); } - private void readGzipMetadata(File containerFile, TaskMonitor monitor) throws IOException { - this.containerSize = containerFile.length(); + private void readGzipMetadata(TaskMonitor monitor) throws IOException { try (GzipCompressorInputStream gzcis = - new GzipCompressorInputStream(new FileInputStream(containerFile))) { + new GzipCompressorInputStream(container.getInputStream(0))) { GzipParameters metaData = gzcis.getMetaData(); - origFilename = metaData.getFilename(); - if (origFilename == null) { - origFilename = GZIP_PAYLOAD_FILENAME; + payloadFilename = metaData.getFilename(); + if (payloadFilename == null) { + String containerName = fsFSRL.getContainer().getName(); + if (containerName.toLowerCase().endsWith(".gz")) { + payloadFilename = FilenameUtils.removeExtension(containerName); + } + else { + payloadFilename = GZIP_PAYLOAD_FILENAME; + } } else { - origFilename = FSUtilities.getSafeFilename(origFilename); + payloadFilename = FSUtilities.getSafeFilename(payloadFilename); } this.origComment = metaData.getComment(); - - // NOTE: the following line does not work in apache-commons-compress 1.8 - // Apache has a bug where the computed date value is truncated to 32 bytes before - // being saved to its 64 bit field. - // Bug not present in 1.13 (latest ver as of now) this.origDate = metaData.getModificationTime(); - this.payloadKey = "uncompressed " + origFilename; + this.payloadKey = "uncompressed " + payloadFilename; } } - private FileCacheEntry getPayloadFileCacheEntry(TaskMonitor monitor) + private ByteProvider getPayloadByteProvider(TaskMonitor monitor) throws CancelledException, IOException { UnknownProgressWrappingTaskMonitor upwtm = - new UnknownProgressWrappingTaskMonitor(monitor, containerSize); - FileCacheEntry derivedFile = fsService.getDerivedFile(containerFSRL, payloadKey, - (srcFile) -> new GzipCompressorInputStream(new FileInputStream(srcFile)), upwtm); - return derivedFile; + new UnknownProgressWrappingTaskMonitor(monitor, container.length()); + return fsService.getDerivedByteProvider(container.getFSRL(), null, payloadKey, -1, + () -> new GzipCompressorInputStream(container.getInputStream(0)), upwtm); } public GFile getPayloadFile() { @@ -116,6 +120,14 @@ public class GZipFileSystem implements GFileSystem { public void close() throws IOException { refManager.onClose(); fsIndex.clear(); + if (container != null) { + container.close(); + container = null; + } + if (payloadProvider != null) { + payloadProvider.close(); + payloadProvider = null; + } } @Override @@ -129,11 +141,10 @@ public class GZipFileSystem implements GFileSystem { } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { if (fsIndex.isPayloadFile(file)) { - FileCacheEntry fce = getPayloadFileCacheEntry(monitor); - return new FileInputStream(fce.file); + return new ByteProviderWrapper(payloadProvider, file.getFSRL()); } return null; } @@ -144,21 +155,45 @@ public class GZipFileSystem implements GFileSystem { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { - if (fsIndex.isPayloadFile(file)) { - return FSUtilities.infoMapToString(getInfoMap()); + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + long compSize = 0; + long payloadSize = 0; + try { + compSize = container.length(); + payloadSize = payloadProvider.length(); } - return null; + catch (IOException e) { + // ignore + } + GFile payload = fsIndex.getPayloadFile(); + + FileAttributes result = new FileAttributes(); + result.add(NAME_ATTR, payload.getName()); + result.add(SIZE_ATTR, payloadSize); + result.add(COMPRESSED_SIZE_ATTR, compSize); + result.add(MODIFIED_DATE_ATTR, origDate != 0 ? new Date(origDate) : null); + result.add(COMMENT_ATTR, origComment); + result.add("MD5", payload.getFSRL().getMD5()); + return result; } public Map getInfoMap() { + long compSize = 0; + long payloadSize = 0; + try { + compSize = container.length(); + payloadSize = payloadProvider.length(); + } + catch (IOException e) { + // ignore + } GFile payload = fsIndex.getPayloadFile(); Map info = new LinkedHashMap<>(); info.put("Name", payload.getName()); - info.put("Size", Long.toString(payload.getLength())); - info.put("Compressed Size", Long.toString(containerSize)); - info.put("Date", (origDate != 0) ? new Date(origDate).toString() : "unknown"); - info.put("Comment", (origComment != null) ? origComment : "unknown"); + info.put("Size", FSUtilities.formatSize(payloadSize)); + info.put("Compressed Size", FSUtilities.formatSize(compSize)); + info.put("Date", FSUtilities.formatFSTimestamp(origDate != 0 ? new Date(origDate) : null)); + info.put("Comment", Objects.requireNonNullElse(origComment, "unknown")); info.put("MD5", payload.getFSRL().getMD5()); return info; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/gzip/GZipFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/gzip/GZipFileSystemFactory.java index 090abe3e0c..072a276ce4 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/gzip/GZipFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/gzip/GZipFileSystemFactory.java @@ -15,27 +15,26 @@ */ package ghidra.file.formats.gzip; +import java.io.IOException; + +import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryWithFile; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -import java.io.File; -import java.io.IOException; - public class GZipFileSystemFactory - implements GFileSystemFactoryWithFile, GFileSystemProbeBytesOnly { + implements GFileSystemFactoryByteProvider, GFileSystemProbeBytesOnly { public static final int PROBE_BYTES_REQUIRED = GZipConstants.MAGIC_BYTES_COUNT; @Override - public GZipFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, File containerFile, + public GZipFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, FileSystemService fsService, TaskMonitor monitor) - throws IOException, CancelledException { + throws IOException, CancelledException { - GZipFileSystem fs = - new GZipFileSystem(containerFSRL, targetFSRL, containerFile, fsService, monitor); + GZipFileSystem fs = new GZipFileSystem(byteProvider, targetFSRL, fsService, monitor); return fs; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/apple8900/Apple8900FileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/apple8900/Apple8900FileSystem.java index bf220987a7..d05603395a 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/apple8900/Apple8900FileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/apple8900/Apple8900FileSystem.java @@ -16,7 +16,6 @@ package ghidra.file.formats.ios.apple8900; import java.io.IOException; -import java.io.InputStream; import java.util.*; import ghidra.app.util.bin.ByteProvider; @@ -43,13 +42,16 @@ public class Apple8900FileSystem extends GFileSystemBase { } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException, CryptoException { if (file != null && file.equals(dataFile)) { - Apple8900Decryptor decryptor = new Apple8900Decryptor(); - DecryptedPacket decrypt = decryptor.decrypt(null /* does not matter*/, - null /* does not matter */, provider, monitor); - return decrypt.decryptedStream; + return fsService.getDerivedByteProvider(provider.getFSRL(), file.getFSRL(), + file.getName(), -1, () -> { + Apple8900Decryptor decryptor = new Apple8900Decryptor(); + DecryptedPacket decrypt = decryptor.decrypt(null /* does not matter*/, + null /* does not matter */, provider, monitor); + return decrypt.decryptedStream; + }, monitor); } return null; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dmg/DmgClientFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dmg/DmgClientFileSystem.java index 8f37de003e..7e65360f73 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dmg/DmgClientFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dmg/DmgClientFileSystem.java @@ -15,12 +15,15 @@ */ package ghidra.file.formats.ios.dmg; -import java.io.*; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; +import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.task.*; @@ -47,7 +50,8 @@ public class DmgClientFileSystem implements GFileSystem { private final FSRLRoot fsrl; private FileSystemRefManager refManager = new FileSystemRefManager(this); private FileSystemIndexHelper fsih; - private File decrypted_dmg_file; + private File decryptedDmgFile; + private boolean deleteFileWhenDone; private DmgServerProcessManager processManager; private CancelledListener listener = () -> processManager.interruptCmd(); private FileSystemService fsService; @@ -56,21 +60,22 @@ public class DmgClientFileSystem implements GFileSystem { * Creates a {@link DmgClientFileSystem} instance, using a decrypted dmg file and * the filesystem's {@link FSRLRoot}. * - * @param decrypted_dmg_file path to a decrypted DMG file. The DmgClientFileSystemFactory + * @param decryptedDmgFile path to a decrypted DMG file. The DmgClientFileSystemFactory * takes care of decrypting for us. * @param fsrl {@link FSRLRoot} of this filesystem. */ - public DmgClientFileSystem(File decrypted_dmg_file, FSRLRoot fsrl, + public DmgClientFileSystem(File decryptedDmgFile, boolean deleteFileWhenDone, FSRLRoot fsrl, FileSystemService fsService) { this.fsrl = fsrl; this.fsih = new FileSystemIndexHelper<>(this, fsrl); - this.decrypted_dmg_file = decrypted_dmg_file; + this.decryptedDmgFile = decryptedDmgFile; + this.deleteFileWhenDone = deleteFileWhenDone; this.fsService = fsService; } public void mount(TaskMonitor monitor) throws CancelledException, IOException { processManager = - new DmgServerProcessManager(decrypted_dmg_file, fsrl.getContainer().getName()); + new DmgServerProcessManager(decryptedDmgFile, fsrl.getContainer().getName()); monitor.addCancelledListener(listener); try { @@ -95,6 +100,10 @@ public class DmgClientFileSystem implements GFileSystem { fsih.clear(); fsih = null; + if (deleteFileWhenDone) { + Msg.info(this, "Deleting DMG temp file:" + decryptedDmgFile); + decryptedDmgFile.delete(); + } } @Override @@ -118,7 +127,7 @@ public class DmgClientFileSystem implements GFileSystem { } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { monitor.addCancelledListener(listener); @@ -134,12 +143,9 @@ public class DmgClientFileSystem implements GFileSystem { if (!extractedFile.exists() || extractedFile.length() == 0) { return null; } - try (FileInputStream fis = new FileInputStream(extractedFile)) { - FileCacheEntry fce = fsService.addStreamToCache(fis, monitor); - fis.close(); - extractedFile.delete(); - return new FileInputStream(fce.file); - } + ByteProvider fileProvider = + fsService.pushFileToCache(extractedFile, file.getFSRL(), monitor); + return fileProvider; } finally { monitor.removeCancelledListener(listener); @@ -200,14 +206,21 @@ public class DmgClientFileSystem implements GFileSystem { } @Override - public String getInfo(GFile gFile, TaskMonitor monitor) { + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { monitor.addCancelledListener(listener); - StringBuffer buffer = new StringBuffer(); + FileAttributes fileAttributes = new FileAttributes(); try { - List infoResults = processManager.sendCmd("get_info " + gFile.getPath(), -1); + List infoResults = processManager.sendCmd("get_info " + file.getPath(), -1); + int count = 1; for (String s : infoResults) { - buffer.append(s).append("\n"); + String[] sParts = s.split(": *", 2); + if (sParts.length == 2) { + fileAttributes.add(sParts[0], sParts[1]); + } + else { + fileAttributes.add("Unknown Attribute " + (count++), s); + } } } catch (Exception e) { @@ -216,7 +229,7 @@ public class DmgClientFileSystem implements GFileSystem { finally { monitor.removeCancelledListener(listener); } - return buffer.toString(); + return fileAttributes; } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dmg/DmgClientFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dmg/DmgClientFileSystemFactory.java index f62199700c..6496650c61 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dmg/DmgClientFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dmg/DmgClientFileSystemFactory.java @@ -22,30 +22,28 @@ import generic.jar.ResourceFile; import ghidra.app.util.bin.ByteProvider; import ghidra.file.formats.xar.XARUtil; import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryWithFile; -import ghidra.formats.gfilesystem.factory.GFileSystemProbeFull; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; +import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider; import ghidra.framework.Application; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; import utilities.util.ArrayUtilities; -import utilities.util.FileUtilities; /** * Handles probing for and creating {@link DmgClientFileSystem} instances. *

    */ -public class DmgClientFileSystemFactory - implements GFileSystemFactoryWithFile, GFileSystemProbeFull { +public class DmgClientFileSystemFactory implements + GFileSystemFactoryByteProvider, GFileSystemProbeByteProvider { public DmgClientFileSystemFactory() { } @Override - public boolean probe(FSRL containerFSRL, ByteProvider byteProvider, File containerFile, - FileSystemService fsService, TaskMonitor taskMonitor) - throws IOException, CancelledException { + public boolean probe(ByteProvider byteProvider, FileSystemService fsService, + TaskMonitor taskMonitor) throws IOException, CancelledException { if (!isDmgPresent()) { return false; @@ -56,7 +54,7 @@ public class DmgClientFileSystemFactory return false; } - return hasUDIF(byteProvider) || isEncrypted(containerFile); + return hasUDIF(byteProvider) || isEncrypted(byteProvider); } private static boolean isEncrypted(byte[] startBytes) { @@ -66,9 +64,9 @@ public class DmgClientFileSystemFactory DmgConstants.DMG_MAGIC_BYTES_v2.length); } - private static boolean isEncrypted(File f) { + private static boolean isEncrypted(ByteProvider bp) { try { - byte[] startBytes = FileUtilities.getBytesFromFile(f, 0, DmgConstants.DMG_MAGIC_LENGTH); + byte[] startBytes = bp.readBytes(0, DmgConstants.DMG_MAGIC_LENGTH); return isEncrypted(startBytes); } catch (IOException ioe) { @@ -89,35 +87,41 @@ public class DmgClientFileSystemFactory } @Override - public DmgClientFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, File containerFile, + public DmgClientFileSystem create(FSRLRoot targetFSRL, ByteProvider provider, FileSystemService fsService, TaskMonitor monitor) throws IOException, CancelledException { + FSRL containerFSRL = provider.getFSRL(); String dmgName = containerFSRL.getName(); - File decrypted_dmg_file; - if (isEncrypted(containerFile)) { + ByteProvider decryptedProvider; + if (isEncrypted(provider)) { if (containerFSRL.getNestingDepth() < 2) { - throw new CryptoException( - "Unable to decrypt DMG data because DMG crypto keys are specific to the container it is embedded in and this DMG was not in a container"); + throw new CryptoException("Unable to decrypt DMG data because DMG crypto keys " + + "are specific to the container it is embedded in and this DMG was not " + + "in a container"); } - String containerName = containerFSRL.getName(1); // get the name of the iphone.ipsw container so we can lookup our crypto keys // based on that. + String containerName = containerFSRL.getName(1); - FileCacheEntry fce = - fsService.getDerivedFile(containerFSRL, "decrypted " + containerName, (srcFile) -> { - monitor.initialize(srcFile.length()); - return new DmgDecryptorStream(containerName, dmgName, srcFile); - }, monitor); - decrypted_dmg_file = fce.file; + decryptedProvider = fsService.getDerivedByteProvider(containerFSRL, null, + "decrypted " + containerName, provider.length(), + () -> new DmgDecryptorStream(containerName, dmgName, provider), monitor); } else { - decrypted_dmg_file = containerFile; + decryptedProvider = provider; } - DmgClientFileSystem fs = new DmgClientFileSystem(decrypted_dmg_file, targetFSRL, fsService); + File decryptedDmgFile = File.createTempFile("ghidra_decrypted_dmg_file", + Long.toString(System.currentTimeMillis())); + monitor.setMessage("Copying DMG container to temp file"); + monitor.initialize(decryptedProvider.length()); + FSUtilities.copyByteProviderToFile(decryptedProvider, decryptedDmgFile, monitor); + + DmgClientFileSystem fs = + new DmgClientFileSystem(decryptedDmgFile, true, targetFSRL, fsService); try { fs.mount(monitor); return fs; diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dyldcache/DyldCacheDylibExtractor.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dyldcache/DyldCacheDylibExtractor.java index d2c73caf71..474d2f8241 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dyldcache/DyldCacheDylibExtractor.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dyldcache/DyldCacheDylibExtractor.java @@ -15,15 +15,15 @@ */ package ghidra.file.formats.ios.dyldcache; -import java.io.*; +import java.io.IOException; import java.util.HashMap; import java.util.Map; import generic.continues.RethrowContinuesFactory; -import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.*; import ghidra.app.util.bin.format.macho.*; import ghidra.app.util.bin.format.macho.commands.*; +import ghidra.formats.gfilesystem.FSRL; import ghidra.util.*; import ghidra.util.exception.NotFoundException; import ghidra.util.task.TaskMonitor; @@ -34,18 +34,18 @@ import ghidra.util.task.TaskMonitor; public class DyldCacheDylibExtractor { /** - * Gets an {@link InputStream} that reads a DYLIB from a {@link DyldCacheFileSystem}. The + * Gets an {@link ByteProvider} that reads a DYLIB from a {@link DyldCacheFileSystem}. The * DYLIB's header will be altered to account for its segment bytes being packed down. * * @param dylibOffset The offset of the DYLIB in the given provider * @param provider The DYLD - * @param monitor A cancellable {@link TaskMonitor} - * @return An {@link InputStream} that reads the specified DYLIB from the given DYLD - * {@link ByteProvider} + * @param fsrl {@link FSRL} to assign to the resulting ByteProvider + * @param monitor {@link TaskMonitor} + * @return {@link ByteProvider} containing the bytes of the dylib * @throws IOException If there was an IO-related issue with extracting the DYLIB * @throws MachException If there was an error parsing the DYLIB headers */ - public static InputStream extractDylib(long dylibOffset, ByteProvider provider, + public static ByteProvider extractDylib(long dylibOffset, ByteProvider provider, FSRL fsrl, TaskMonitor monitor) throws IOException, MachException { // Make sure Mach-O header is valid @@ -81,7 +81,7 @@ public class DyldCacheDylibExtractor { } } - return packedDylib.getInputStream(); + return packedDylib.getByteProvider(fsrl); } /** @@ -236,13 +236,8 @@ public class DyldCacheDylibExtractor { } } - /** - * Gets an {@link InputStream} that reads the packed DYLIB - * - * @return An {@link InputStream} that reads the packed DYLIB - */ - public InputStream getInputStream() { - return new ByteArrayInputStream(packed); + ByteProvider getByteProvider(FSRL fsrl) { + return new ByteArrayProvider(packed, fsrl); } /** diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dyldcache/DyldCacheFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dyldcache/DyldCacheFileSystem.java index d67eb9ae02..e0c9d84409 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dyldcache/DyldCacheFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/dyldcache/DyldCacheFileSystem.java @@ -16,7 +16,6 @@ package ghidra.file.formats.ios.dyldcache; import java.io.IOException; -import java.io.InputStream; import java.util.*; import ghidra.app.util.bin.BinaryReader; @@ -50,7 +49,7 @@ public class DyldCacheFileSystem extends GFileSystemBase { } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) throws IOException { + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException { DyldCacheImageInfo data = map.get(file); if (data == null) { return null; @@ -58,7 +57,7 @@ public class DyldCacheFileSystem extends GFileSystemBase { long machHeaderStartIndexInProvider = data.getAddress() - header.getBaseAddress(); try { return DyldCacheDylibExtractor.extractDylib(machHeaderStartIndexInProvider, provider, - monitor); + file.getFSRL(), monitor); } catch (MachException e) { throw new IOException("Invalid Mach-O header detected at 0x" + diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/ibootim/iBootImFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/ibootim/iBootImFileSystem.java index 7a5580cb44..55931582f6 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/ibootim/iBootImFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/ibootim/iBootImFileSystem.java @@ -20,6 +20,7 @@ import java.util.*; import javax.swing.Icon; +import ghidra.app.util.bin.ByteArrayProvider; import ghidra.app.util.bin.ByteProvider; import ghidra.file.formats.lzss.LzssCodec; import ghidra.file.image.GImage; @@ -50,20 +51,18 @@ public class iBootImFileSystem extends GFileSystemBase implements GIconProvider } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { - return new ByteArrayInputStream(bytes); + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) { + return new ByteArrayProvider(bytes, file.getFSRL()); } @Override public Icon getIcon(GFile file, TaskMonitor monitor) throws IOException, CancelledException { - File cacheFile = fsService.getFile(file.getFSRL(), monitor); - try (InputStream cacheInputStream = new FileInputStream(cacheFile)) { - GImageFormat format = - (header.getFormat() == iBootImConstants.FORMAT_ARGB) ? GImageFormat.RGB_ALPHA_4BYTE - : GImageFormat.GRAY_ALPHA_2BYTE; + try (InputStream cacheInputStream = new ByteArrayInputStream(bytes)) { + GImageFormat format = (header.getFormat() == iBootImConstants.FORMAT_ARGB) + ? GImageFormat.RGB_ALPHA_4BYTE + : GImageFormat.GRAY_ALPHA_2BYTE; GImage image = new GImage(header.getWidth(), header.getHeight(), format, - cacheInputStream, cacheFile.length()); + cacheInputStream, bytes.length); return image.toPNG(); } } @@ -78,8 +77,8 @@ public class iBootImFileSystem extends GFileSystemBase implements GIconProvider @Override public boolean isValid(TaskMonitor monitor) throws IOException { - byte[] bytes = provider.readBytes(0, iBootImConstants.SIGNATURE_LENGTH); - return Arrays.equals(bytes, iBootImConstants.SIGNATURE_BYTES); + byte[] signatureBytes = provider.readBytes(0, iBootImConstants.SIGNATURE_LENGTH); + return Arrays.equals(signatureBytes, iBootImConstants.SIGNATURE_BYTES); } @Override diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2.java index 0bcbd729ac..0e72762268 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2.java @@ -15,13 +15,13 @@ */ package ghidra.file.formats.ios.img2; +import java.io.IOException; + import ghidra.app.util.bin.*; import ghidra.program.model.data.DataType; import ghidra.util.StringUtilities; import ghidra.util.exception.DuplicateNameException; -import java.io.IOException; - public class Img2 implements StructConverter { private int signature; @@ -60,6 +60,10 @@ public class Img2 implements StructConverter { unknown3 = reader.readNextByteArray( 0x394 ); } + public boolean isValid() { + return getSignature().equals(Img2Constants.IMG2_SIGNATURE); + } + public String getSignature() { return StringUtilities.toString(signature); } @@ -100,6 +104,7 @@ public class Img2 implements StructConverter { throw new RuntimeException("invalid unknown index"); } + @Override public DataType toDataType() throws DuplicateNameException, IOException { return StructConverterUtil.toDataType(this); } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2FileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2FileSystem.java index 7fd85ff48b..9c1a936675 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2FileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2FileSystem.java @@ -15,70 +15,95 @@ */ package ghidra.file.formats.ios.img2; -import java.io.*; -import java.util.*; +import java.io.IOException; +import java.util.List; import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.ByteProviderWrapper; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; -import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; import ghidra.util.exception.CancelledException; -import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; -@FileSystemInfo(type = "img2", description = "iOS " + - Img2Constants.IMG2_SIGNATURE, factory = GFileSystemBaseFactory.class) -public class Img2FileSystem extends GFileSystemBase { +//@formatter:off +@FileSystemInfo( + type = "img2", + description = "iOS " + Img2Constants.IMG2_SIGNATURE, + factory = Img2FileSystemFactory.class) +//@formatter:on +public class Img2FileSystem implements GFileSystem { + private FSRLRoot fsFSRL; + private SingleFileSystemIndexHelper fsIndexHelper; + private FileSystemRefManager refManager = new FileSystemRefManager(this); + private ByteProvider provider; private Img2 img2; - private GFileImpl imageTypeFile; - public Img2FileSystem(String fileSystemName, ByteProvider provider) { - super(fileSystemName, provider); + public Img2FileSystem(FSRLRoot fsFSRL, ByteProvider provider, TaskMonitor monitor) + throws IOException, CancelledException { + this.fsFSRL = fsFSRL; + this.provider = provider; + this.img2 = new Img2(provider); + if (!img2.isValid()) { + throw new IOException("Unable to decrypt file: invalid IMG2 file!"); + } + + try (ByteProvider tmpBP = + new ByteProviderWrapper(provider, Img2Constants.IMG2_LENGTH, img2.getDataLen(), null)) { + String payloadMD5 = FSUtilities.getMD5(tmpBP, monitor); + + this.fsIndexHelper = new SingleFileSystemIndexHelper(this, fsFSRL, img2.getImageType(), + img2.getDataLen(), payloadMD5); + } + } + + @Override + public FSRLRoot getFSRL() { + return fsFSRL; } @Override public void close() throws IOException { - super.close(); - imageTypeFile = null; + refManager.onClose(); + fsIndexHelper.clear(); + if (provider != null) { + provider.close(); + provider = null; + } } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { - if (file != null && file.equals(imageTypeFile)) { - - byte[] data = provider.readBytes(Img2Constants.IMG2_LENGTH, img2.getDataLen()); - - return new ByteArrayInputStream(data); + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) { + if (fsIndexHelper.isPayloadFile(file)) { + return new ByteProviderWrapper(provider, Img2Constants.IMG2_LENGTH, img2.getDataLen(), + fsIndexHelper.getPayloadFile().getFSRL()); } return null; } @Override public List getListing(GFile directory) throws IOException { - return (directory == null || directory.equals(root)) ? Arrays.asList(imageTypeFile) - : Collections.emptyList(); + return fsIndexHelper.getListing(directory); } @Override - public boolean isValid(TaskMonitor monitor) throws IOException { - byte[] bytes = provider.readBytes(0, Img2Constants.IMG2_SIGNATURE_BYTES.length); - return Arrays.equals(bytes, Img2Constants.IMG2_SIGNATURE_BYTES); + public String getName() { + return fsFSRL.getContainer().getName(); } @Override - public void open(TaskMonitor monitor) throws IOException, CryptoException, CancelledException { - monitor.setMessage("Opening IMG2..."); + public boolean isClosed() { + return fsIndexHelper.isClosed(); + } - this.img2 = new Img2(provider); + @Override + public FileSystemRefManager getRefManager() { + return refManager; + } - if (!img2.getSignature().equals(Img2Constants.IMG2_SIGNATURE)) { - throw new IOException("Unable to decrypt file: invalid IMG2 file!"); - } - - imageTypeFile = - GFileImpl.fromFilename(this, root, img2.getImageType(), false, img2.getDataLen(), null); + @Override + public GFile lookup(String path) throws IOException { + return fsIndexHelper.lookup(path); } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2FileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2FileSystemFactory.java new file mode 100644 index 0000000000..07379e32b1 --- /dev/null +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img2/Img2FileSystemFactory.java @@ -0,0 +1,48 @@ +/* ### + * 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.img2; + +import java.io.IOException; + +import ghidra.app.util.bin.ByteProvider; +import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; +import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.ArrayUtilities; + +public class Img2FileSystemFactory implements GFileSystemFactoryByteProvider, GFileSystemProbeBytesOnly { + + @Override + public int getBytesRequired() { + return Img2Constants.IMG2_SIGNATURE_BYTES.length; + } + + @Override + public boolean probeStartBytes(FSRL containerFSRL, byte[] startBytes) { + return ArrayUtilities.arrayRangesEquals(startBytes, 0, Img2Constants.IMG2_SIGNATURE_BYTES, + 0, Img2Constants.IMG2_SIGNATURE_BYTES.length); + } + + @Override + public Img2FileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { + return new Img2FileSystem(targetFSRL, byteProvider, monitor); + } + +} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img3/Img3FileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img3/Img3FileSystem.java index 39a467acea..a44b33a91a 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img3/Img3FileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img3/Img3FileSystem.java @@ -15,8 +15,8 @@ */ package ghidra.file.formats.ios.img3; -import java.io.*; -import java.util.*; +import java.io.IOException; +import java.util.List; import javax.swing.Icon; @@ -24,40 +24,33 @@ import ghidra.app.util.bin.ByteProvider; import ghidra.file.formats.ios.img3.tag.DataTag; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; -import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; @FileSystemInfo(type = "img3", description = "iOS " + - Img3Constants.IMG3_SIGNATURE, factory = GFileSystemBaseFactory.class) -public class Img3FileSystem extends GFileSystemBase { + Img3Constants.IMG3_SIGNATURE, factory = Img3FileSystemFactory.class) +public class Img3FileSystem implements GFileSystem { - private Img3 header; - private List dataFileList = new ArrayList<>(); + private FSRLRoot fsFSRL; + private FileSystemRefManager fsRefManager = new FileSystemRefManager(this); + private FileSystemIndexHelper fsIndexHelper; + private ByteProvider provider; + private FileSystemService fsService; - public Img3FileSystem(String fileSystemName, ByteProvider provider) { - super(fileSystemName, provider); - } + public Img3FileSystem(FSRLRoot fsFSRL, ByteProvider provider, FileSystemService fsService, + TaskMonitor monitor) throws IOException { + this.fsFSRL = fsFSRL; + this.fsIndexHelper = new FileSystemIndexHelper<>(this, fsFSRL); + this.provider = provider; + this.fsService = fsService; - @Override - public boolean isValid(TaskMonitor monitor) throws IOException { - byte[] bytes = provider.readBytes(0, Img3Constants.IMG3_SIGNATURE_LENGTH); - return Arrays.equals(bytes, Img3Constants.IMG3_SIGNATURE_BYTES); - } - - @Override - public void open(TaskMonitor monitor) throws IOException { monitor.setMessage("Opening IMG3..."); - - this.header = new Img3(provider); - + Img3 header = new Img3(provider); if (!header.getMagic().equals(Img3Constants.IMG3_SIGNATURE)) { throw new IOException("Unable to decrypt file: invalid IMG3 file!"); } - List tags = header.getTags(DataTag.class); - monitor.initialize(tags.size()); for (int i = 0; i < tags.size(); ++i) { @@ -68,9 +61,8 @@ public class Img3FileSystem extends GFileSystemBase { DataTag dataTag = tags.get(i); String filename = getDataTagFilename(dataTag, i, tags.size() > 1); - GFileImpl dataFile = GFileImpl.fromPathString(this, root, filename, null, false, - dataTag.getTotalLength()); - dataFileList.add(dataFile); + fsIndexHelper.storeFileWithParent(filename, fsIndexHelper.getRootDir(), i, false, + dataTag.getTotalLength(), dataTag); } } @@ -81,34 +73,33 @@ public class Img3FileSystem extends GFileSystemBase { @Override public void close() throws IOException { - super.close(); + fsRefManager.onClose(); + fsIndexHelper.clear(); + if (provider != null) { + provider.close(); + provider = null; + } } @Override - public InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CryptoException, CancelledException { - FSRLRoot fsFSRL = getFSRL(); + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { + if (fsFSRL.getNestingDepth() < 3) { throw new CryptoException( "Unable to decrypt IMG3 data because IMG3 crypto keys are specific to the container it is embedded in and this IMG3 was not in a container"); } - List tags = header.getTags(DataTag.class); - for (int i = 0; i < tags.size(); ++i) { - DataTag dataTag = tags.get(i); - String filename = getDataTagFilename(dataTag, i, tags.size() > 1); - if (file.getName().equals(filename)) { - FileCacheEntry derivedFile = - fsService.getDerivedFile(fsFSRL.getContainer(), "decrypted_img3_" + filename, - (srcFile) -> dataTag.getDecryptedInputStream(fsFSRL.getName(2), - fsFSRL.getName(1)), - monitor); - - return new FileInputStream(derivedFile.file); - } + DataTag dataTag = fsIndexHelper.getMetadata(file); + if (dataTag == null) { + throw new IOException("Unknown file: " + file); } - throw new IOException("Unable to get DATA for " + file.getPath()); + ByteProvider derivedBP = fsService.getDerivedByteProvider(fsFSRL.getContainer(), + file.getFSRL(), "decrypted_img3_" + file.getName(), dataTag.getTotalLength(), + () -> dataTag.getDecryptedInputStream(fsFSRL.getName(2), fsFSRL.getName(1)), monitor); + + return derivedBP; } public Icon getIcon() { @@ -116,39 +107,33 @@ public class Img3FileSystem extends GFileSystemBase { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { - return null; + public List getListing(GFile directory) { + return fsIndexHelper.getListing(directory); } @Override - public List getListing(GFile directory) { - if (directory == null || directory.equals(root)) { - if (dataFileList.isEmpty()) { - if (header != null) { - List tags = header.getTags(DataTag.class); - for (int i = 0; i < tags.size(); ++i) { - DataTag dataTag = tags.get(i); - String name = dataTag.getMagic(); - if (tags.size() > 1) { - name = name + i; - } - GFileImpl dataFile = GFileImpl.fromFilename(this, root, name, false, - dataTag.getTotalLength(), null); - dataFileList.add(dataFile); - } - } - } - return dataFileList; - } - return new ArrayList<>(); + public String getName() { + return fsFSRL.getContainer().getName(); } - public boolean isDirectory(GFileImpl directory) { - return directory.equals(root); + @Override + public FSRLRoot getFSRL() { + return fsFSRL; } - public boolean isFile(GFileImpl file) { - return !file.equals(root); + @Override + public boolean isClosed() { + return provider == null; + } + + @Override + public FileSystemRefManager getRefManager() { + return fsRefManager; + } + + @Override + public GFile lookup(String path) throws IOException { + return fsIndexHelper.lookup(path); } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img3/Img3FileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img3/Img3FileSystemFactory.java new file mode 100644 index 0000000000..33f6c4b4b0 --- /dev/null +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img3/Img3FileSystemFactory.java @@ -0,0 +1,48 @@ +/* ### + * 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.img3; + +import java.io.IOException; + +import ghidra.app.util.bin.ByteProvider; +import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; +import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.ArrayUtilities; + +public class Img3FileSystemFactory + implements GFileSystemFactoryByteProvider, GFileSystemProbeBytesOnly { + @Override + public int getBytesRequired() { + return Img3Constants.IMG3_SIGNATURE_LENGTH; + } + + @Override + public boolean probeStartBytes(FSRL containerFSRL, byte[] startBytes) { + return ArrayUtilities.arrayRangesEquals(startBytes, 0, Img3Constants.IMG3_SIGNATURE_BYTES, + 0, Img3Constants.IMG3_SIGNATURE_LENGTH); + } + + @Override + public Img3FileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { + return new Img3FileSystem(targetFSRL, byteProvider, fsService, monitor); + } + +} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img4/Img4FileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img4/Img4FileSystem.java index 7a0fdc5b76..8de3f48e72 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img4/Img4FileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/img4/Img4FileSystem.java @@ -15,13 +15,14 @@ */ package ghidra.file.formats.ios.img4; -import java.io.*; +import java.io.IOException; import java.util.*; import javax.swing.Icon; import org.bouncycastle.asn1.*; +import ghidra.app.util.bin.ByteArrayProvider; import ghidra.app.util.bin.ByteProvider; import ghidra.file.crypto.CryptoKey; import ghidra.file.crypto.CryptoKeyFactory; @@ -30,7 +31,6 @@ import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; import ghidra.util.exception.CancelledException; -import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; @FileSystemInfo(type = "img4", description = "iOS Img4", factory = GFileSystemBaseFactory.class) @@ -121,10 +121,10 @@ public class Img4FileSystem extends GFileSystemBase { } @Override - public InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CryptoException, CancelledException { + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { if (dataFileList.contains(file)) { - return new ByteArrayInputStream(decryptedBytes); + return new ByteArrayProvider(decryptedBytes, file.getFSRL()); } throw new IOException("Unable to get DATA for " + file.getPath()); } @@ -133,11 +133,6 @@ public class Img4FileSystem extends GFileSystemBase { return null; } - @Override - public String getInfo(GFile file, TaskMonitor monitor) { - return null; - } - @Override public List getListing(GFile directory) { if (directory == null || directory.equals(root)) { diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/ipsw/IpswFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/ipsw/IpswFileSystem.java index 861d99e3d4..f0c8aed2b1 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/ipsw/IpswFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/ipsw/IpswFileSystem.java @@ -16,7 +16,6 @@ package ghidra.file.formats.ios.ipsw; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -50,6 +49,7 @@ public class IpswFileSystem extends GFileSystemBase { @Override public void open(TaskMonitor monitor) throws IOException, CryptoException, CancelledException { + // does nothing yet } @Override @@ -58,8 +58,7 @@ public class IpswFileSystem extends GFileSystemBase { } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) { return null; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/png/CrushedPNGFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/png/CrushedPNGFileSystem.java index 33a58be34f..2f765d9790 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/png/CrushedPNGFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/png/CrushedPNGFileSystem.java @@ -15,17 +15,18 @@ */ package ghidra.file.formats.ios.png; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + import java.io.IOException; -import java.io.InputStream; import java.util.*; import org.apache.commons.io.FilenameUtils; -import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.*; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; @@ -92,24 +93,27 @@ public class CrushedPNGFileSystem extends GFileSystemBase { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { - return png.toString(); + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + FileAttributes result = new FileAttributes(); + + result.add(SIZE_ATTR, (long) png.getTotalLength()); + result.add("Type", "Crushed PNG Image"); + int chunkNum = 0; + for (PNGChunk chunk : png.getChunkArray()) { + result.add("PNG Chunk " + (chunkNum++), chunk.getIDString()); + } + return result; } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { - - CrushedPNGUtil util = new CrushedPNGUtil(); - InputStream is; + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) + throws IOException, CancelledException { try { - is = util.getUncrushedPNGBytes(png, monitor); + return new ByteArrayProvider(CrushedPNGUtil.getUncrushedPNGBytes(png), file.getFSRL()); } catch (Exception e) { - - return null; + throw new IOException("Error converting crushed PNG bitmap", e); } - return is; } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/png/CrushedPNGUtil.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/png/CrushedPNGUtil.java index a14e5ac296..9ccad6b99c 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/png/CrushedPNGUtil.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/png/CrushedPNGUtil.java @@ -4,13 +4,12 @@ */ package ghidra.file.formats.ios.png; -import java.io.*; +import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.util.*; import java.util.zip.*; import ghidra.file.formats.zlib.ZLIB; -import ghidra.util.task.TaskMonitor; public class CrushedPNGUtil { @@ -21,7 +20,7 @@ public class CrushedPNGUtil { * @return An InputStream of the correctly formated bytes of a png * @throws Exception */ - public InputStream getUncrushedPNGBytes(ProcessedPNG png, TaskMonitor monitor) throws Exception { + public static byte[] getUncrushedPNGBytes(ProcessedPNG png) throws Exception { boolean foundIHDR = false; boolean foundIDAT = false; boolean foundCgBI = false; @@ -184,9 +183,7 @@ public class CrushedPNGUtil { } - InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); - return inputStream; - + return outputStream.toByteArray(); } /** @@ -195,7 +192,7 @@ public class CrushedPNGUtil { * @param decompressedResult result of the zlib decompression * @throws PNGFormatException */ - private void processIDATChunks(IHDRChunk ihdrChunk, byte[] decompressedResult) + private static void processIDATChunks(IHDRChunk ihdrChunk, byte[] decompressedResult) throws PNGFormatException { int width; int height; @@ -318,7 +315,7 @@ public class CrushedPNGUtil { * @param data the image data * @param offset the offset into data */ - private void removeRowFilters(int width, int height, byte[] data, int offset) { + private static void removeRowFilters(int width, int height, byte[] data, int offset) { /* * Yes, it is generally bad convention to have x in function scope in this way. However the @@ -426,7 +423,7 @@ public class CrushedPNGUtil { * @param data the image data * @param offset the offset into the data */ - private void applyRowFilters(int width, int height, byte[] data, int offset) { + private static void applyRowFilters(int width, int height, byte[] data, int offset) { /* * Yes, it is generally bad convention to have x in function scope in this way. However the @@ -525,7 +522,7 @@ public class CrushedPNGUtil { * @param data image data * @param offset offset into data */ - private void demultiplyAlpha(int width, int height, byte[] data, int offset) { + private static void demultiplyAlpha(int width, int height, byte[] data, int offset) { int srcPtr = offset; for (int i = 0; i < height; i++) { @@ -558,7 +555,7 @@ public class CrushedPNGUtil { * @param idatChunks the set of idat chunks * @return idat chunks with the new header */ - private byte[] getFixedIdatDataBytes(ByteArrayOutputStream idatChunks) { + private static byte[] getFixedIdatDataBytes(ByteArrayOutputStream idatChunks) { //Prepend the needed Zlib header info to the IDAT chunk data byte[] idatData = idatChunks.toByteArray(); @@ -576,7 +573,7 @@ public class CrushedPNGUtil { * @param data the byte array to calculate crc32 from * @return The crc32 result */ - private byte[] calculateCRC32(byte[] data) { + private static byte[] calculateCRC32(byte[] data) { CRC32 checksum = new CRC32(); checksum.update(data); long result = checksum.getValue(); @@ -588,7 +585,7 @@ public class CrushedPNGUtil { * @param chunk the chunk to calculate crc32 from * @return The crc32 result */ - private byte[] calculateCRC32(PNGChunk chunk) { + private static byte[] calculateCRC32(PNGChunk chunk) { CRC32 checksum = new CRC32(); checksum.update(ByteBuffer.allocate(4 + chunk.getLength()).putInt(chunk.getChunkID()).put( chunk.getData()).array()); diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/PrelinkFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/PrelinkFileSystem.java index 2ec66e6462..3e384c1c6f 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/PrelinkFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/ios/prelink/PrelinkFileSystem.java @@ -34,6 +34,7 @@ import ghidra.app.util.opinion.*; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; +import ghidra.formats.gfilesystem.fileinfo.*; import ghidra.framework.store.local.LocalFileSystem; import ghidra.macosx.MacosxLanguageHelper; import ghidra.program.database.ProgramDB; @@ -120,9 +121,11 @@ public class PrelinkFileSystem extends GFileSystemBase implements GFileSystemPro } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { PrelinkMap info = fileToPrelinkInfoMap.get(file); - return (info != null) ? info.toString() : null; + return FileAttributes.of(info != null + ? FileAttribute.create(FileAttributeType.COMMENT_ATTR, info.toString()) + : null); } @Override @@ -215,7 +218,7 @@ public class PrelinkFileSystem extends GFileSystemBase implements GFileSystemPro } @Override - protected InputStream getData(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException, CryptoException { if (isChildOf(systemKextFile, file)) { @@ -227,7 +230,8 @@ public class PrelinkFileSystem extends GFileSystemBase implements GFileSystemPro if (offset == null) { return null; } - return new ByteProviderInputStream(provider, offset, provider.length() - offset); + return new ByteProviderWrapper(provider, offset, provider.length() - offset, + file.getFSRL()); } /** diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/iso9660/ISO9660FileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/iso9660/ISO9660FileSystem.java index 312dac9f50..bd8992b369 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/iso9660/ISO9660FileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/iso9660/ISO9660FileSystem.java @@ -16,7 +16,6 @@ package ghidra.file.formats.iso9660; import java.io.IOException; -import java.io.InputStream; import java.util.*; import ghidra.app.util.bin.BinaryReader; @@ -132,25 +131,6 @@ public class ISO9660FileSystem extends GFileSystemBase { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { - ISO9660Directory dir = fileToDirectoryMap.get(file); - if (dir != null) { - return dir.toString(); - } - return null; - } - - /* - * Returns the actual file data from a given Gfile(linked to directory) - */ - @Override - protected InputStream getData(GFile file, TaskMonitor monitor) - throws IOException, CancelledException, CryptoException { - - ByteProvider bp = getByteProvider(file, monitor); - return bp != null ? bp.getInputStream(0) : null; - } - public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { ISO9660Directory dir = fileToDirectoryMap.get(file); @@ -161,10 +141,10 @@ public class ISO9660FileSystem extends GFileSystemBase { * From a given parent directory create each child directory * under that parent directory and add them to a list */ - private ArrayList createDirectoryList(BinaryReader reader, + private List createDirectoryList(BinaryReader reader, ISO9660Directory parentDir, long blockSize, TaskMonitor monitor) throws IOException { - ArrayList directoryList = new ArrayList<>(); + List directoryList = new ArrayList<>(); ISO9660Directory childDir = null; long dirIndex; long endIndex; @@ -239,8 +219,8 @@ public class ISO9660FileSystem extends GFileSystemBase { } } - private void addAndStoreDirectory(TaskMonitor monitor, - ArrayList directoryList, ISO9660Directory childDir) { + private void addAndStoreDirectory(TaskMonitor monitor, List directoryList, + ISO9660Directory childDir) { directoryList.add(childDir); diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/java/JavaClassDecompilerFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/java/JavaClassDecompilerFileSystem.java index fedbe1c25f..f86de1d061 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/java/JavaClassDecompilerFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/java/JavaClassDecompilerFileSystem.java @@ -16,14 +16,17 @@ package ghidra.file.formats.java; import java.io.*; -import java.util.*; +import java.util.List; import org.apache.commons.io.FilenameUtils; +import ghidra.app.util.bin.ByteProvider; import ghidra.file.jad.JadProcessController; import ghidra.file.jad.JadProcessWrapper; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; +import ghidra.formats.gfilesystem.fileinfo.FileAttribute; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; import utilities.util.FileUtilities; @@ -39,53 +42,58 @@ public class JavaClassDecompilerFileSystem implements GFileSystem { private FSRLRoot fsFSRL; private FileSystemRefManager refManager = new FileSystemRefManager(this); private SingleFileSystemIndexHelper fsIndexHelper; + private ByteProvider provider; private FSRL containerFSRL; private String className; private String javaSrcFilename; private FileSystemService fsService; - public JavaClassDecompilerFileSystem(FSRLRoot fsFSRL, FileSystemService fsService, - TaskMonitor monitor) throws CancelledException, IOException { + public JavaClassDecompilerFileSystem(FSRLRoot fsFSRL, ByteProvider provider, + FileSystemService fsService, TaskMonitor monitor) + throws CancelledException, IOException { this.fsService = fsService; this.fsFSRL = fsFSRL; + this.provider = provider; - this.containerFSRL = fsFSRL.getContainer(); + this.containerFSRL = provider.getFSRL(); this.className = FilenameUtils.removeExtension(containerFSRL.getName()); this.javaSrcFilename = className + ".java"; - FileCacheEntry fce = getDecompiledJavaSrcFileEntry(monitor); - this.fsIndexHelper = new SingleFileSystemIndexHelper(this, fsFSRL, javaSrcFilename, - fce.file.length(), fce.md5); + try (ByteProvider decompiledBP = getDecompiledJavaSrcFileEntry(null, monitor)) { + this.fsIndexHelper = new SingleFileSystemIndexHelper(this, fsFSRL, javaSrcFilename, + decompiledBP.length(), decompiledBP.getFSRL().getMD5()); + } } - private FileCacheEntry getDecompiledJavaSrcFileEntry(TaskMonitor monitor) + private ByteProvider getDecompiledJavaSrcFileEntry(FSRL targetFSRL, TaskMonitor monitor) throws CancelledException, IOException { - FileCacheEntry derivedFileInfo = - fsService.getDerivedFilePush(containerFSRL, javaSrcFilename, (os) -> { - File tempDir = null; - try { - tempDir = FileUtilities.createTempDirectory("JavaClassDecompilerFileSystem"); + return fsService.getDerivedByteProviderPush(containerFSRL, targetFSRL, javaSrcFilename, -1, + (os) -> decompileClassFileToStream(os, monitor), monitor); + } - File srcClassFile = fsService.getFile(containerFSRL, monitor); - File tempClassFile = new File(tempDir, containerFSRL.getName()); - FileUtilities.copyFile(srcClassFile, tempClassFile, false, monitor); + private void decompileClassFileToStream(OutputStream os, TaskMonitor monitor) + throws CancelledException, IOException { + File tempDir = null; + try { + tempDir = FileUtilities.createTempDirectory("JavaClassDecompilerFileSystem"); - // tempDestJavaSrcFile (ie. "javaclass.java") contents are automagically - // created by the Jad process based on the class name it finds inside - // the binary "javaclass.class" file. Class, class, class. - File tempDestJavaSrcFile = new File(tempDir, javaSrcFilename); + File tempClassFile = new File(tempDir, containerFSRL.getName()); + FSUtilities.copyByteProviderToFile(provider, tempClassFile, monitor); - JadProcessWrapper wrapper = new JadProcessWrapper(tempClassFile); - JadProcessController controller = new JadProcessController(wrapper, className); - controller.decompile(5, monitor); + // tempDestJavaSrcFile (ie. "javaclass.java") contents are automagically + // created by the Jad process based on the class name it finds inside + // the binary "javaclass.class" file. Class, class, class. + File tempDestJavaSrcFile = new File(tempDir, javaSrcFilename); - FileUtilities.copyFileToStream(tempDestJavaSrcFile, os, monitor); - } - finally { - FileUtilities.deleteDir(tempDir, monitor); - } - }, monitor); - return derivedFileInfo; + JadProcessWrapper wrapper = new JadProcessWrapper(tempClassFile); + JadProcessController controller = new JadProcessController(wrapper, className); + controller.decompile(5, monitor); + + FileUtilities.copyFileToStream(tempDestJavaSrcFile, os, monitor); + } + finally { + FileUtilities.deleteDir(tempDir, monitor); + } } public GFile getPayloadFile() { @@ -101,6 +109,7 @@ public class JavaClassDecompilerFileSystem implements GFileSystem { public void close() throws IOException { refManager.onClose(); fsIndexHelper.clear(); + provider.close(); } @Override @@ -124,11 +133,10 @@ public class JavaClassDecompilerFileSystem implements GFileSystem { } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { if (fsIndexHelper.isPayloadFile(file)) { - FileCacheEntry fce = getDecompiledJavaSrcFileEntry(monitor); - return new FileInputStream(fce.file); + return getDecompiledJavaSrcFileEntry(file.getFSRL(), monitor); } return null; } @@ -139,13 +147,8 @@ public class JavaClassDecompilerFileSystem implements GFileSystem { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { - if (fsIndexHelper.isPayloadFile(file)) { - Map info = new HashMap<>(); - info.put("Class name", className); - return FSUtilities.infoMapToString(info); - } - return null; + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + return FileAttributes.of(FileAttribute.create("Class name", className)); } @Override diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/java/JavaClassDecompilerFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/java/JavaClassDecompilerFileSystemFactory.java index 9be8da4d98..81c0f955ca 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/java/JavaClassDecompilerFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/java/JavaClassDecompilerFileSystemFactory.java @@ -15,14 +15,14 @@ */ package ghidra.file.formats.java; -import java.io.File; import java.io.IOException; import org.apache.commons.io.FilenameUtils; +import ghidra.app.util.bin.ByteProvider; import ghidra.file.jad.JadProcessWrapper; import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryWithFile; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -32,7 +32,7 @@ import utilities.util.ArrayUtilities; * Creates instances of {@link JavaClassDecompilerFileSystem}. */ public class JavaClassDecompilerFileSystemFactory implements - GFileSystemFactoryWithFile, GFileSystemProbeBytesOnly { + GFileSystemFactoryByteProvider, GFileSystemProbeBytesOnly { @Override public int getBytesRequired() { @@ -48,12 +48,12 @@ public class JavaClassDecompilerFileSystemFactory implements } @Override - public JavaClassDecompilerFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, - File containerFile, FileSystemService fsService, TaskMonitor monitor) + public JavaClassDecompilerFileSystem create(FSRLRoot targetFSRL, ByteProvider provider, + FileSystemService fsService, TaskMonitor monitor) throws IOException, CancelledException { JavaClassDecompilerFileSystem fs = - new JavaClassDecompilerFileSystem(targetFSRL, fsService, monitor); + new JavaClassDecompilerFileSystem(targetFSRL, provider, fsService, monitor); return fs; } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/omf/OmfArchiveFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/omf/OmfArchiveFileSystem.java index 11173f1928..7cf6e2bc9b 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/omf/OmfArchiveFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/omf/OmfArchiveFileSystem.java @@ -15,16 +15,18 @@ */ package ghidra.file.formats.omf; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + import java.io.IOException; -import java.io.InputStream; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import ghidra.app.util.bin.*; import ghidra.app.util.bin.format.omf.OmfFileHeader; import ghidra.app.util.bin.format.omf.OmfLibraryRecord; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; -import ghidra.util.exception.CancelledException; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.task.TaskMonitor; @FileSystemInfo(type = "omf", description = "OMF Archive", factory = OmfArchiveFileSystemFactory.class) @@ -95,13 +97,6 @@ public class OmfArchiveFileSystem implements GFileSystem { } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) - throws IOException, CancelledException { - - ByteProvider bp = getByteProvider(file, monitor); - return bp != null ? bp.getInputStream(0) : null; - } - public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) { OmfLibraryRecord.MemberHeader member = fsih.getMetadata(file); return (member != null) @@ -116,16 +111,15 @@ public class OmfArchiveFileSystem implements GFileSystem { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { - OmfLibraryRecord.MemberHeader entry = fsih.getMetadata(file); - return (entry == null) ? null : FSUtilities.infoMapToString(getInfoMap(entry)); - } + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + FileAttributes result = new FileAttributes(); - public Map getInfoMap(OmfLibraryRecord.MemberHeader member) { - Map info = new LinkedHashMap<>(); - info.put("Name", member.name); - info.put("Size", "" + Long.toString(member.size) + ", 0x" + Long.toHexString(member.size)); - return info; + OmfLibraryRecord.MemberHeader entry = fsih.getMetadata(file); + if (entry != null) { + result.add(NAME_ATTR, entry.name); + result.add(SIZE_ATTR, entry.size); + } + return result; } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/omf/OmfArchiveFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/omf/OmfArchiveFileSystemFactory.java index 807956ca7c..4e2e6d4d05 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/omf/OmfArchiveFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/omf/OmfArchiveFileSystemFactory.java @@ -15,7 +15,6 @@ */ package ghidra.file.formats.omf; -import java.io.File; import java.io.IOException; import ghidra.app.util.bin.BinaryReader; @@ -23,19 +22,20 @@ import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.bin.format.omf.OmfFileHeader; import ghidra.app.util.bin.format.omf.OmfLibraryRecord; import ghidra.app.util.opinion.OmfLoader; -import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryFull; -import ghidra.formats.gfilesystem.factory.GFileSystemProbeFull; +import ghidra.formats.gfilesystem.FSRLRoot; +import ghidra.formats.gfilesystem.FileSystemService; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; +import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -public class OmfArchiveFileSystemFactory - implements GFileSystemFactoryFull, GFileSystemProbeFull { +public class OmfArchiveFileSystemFactory implements + GFileSystemFactoryByteProvider, GFileSystemProbeByteProvider { @Override - public OmfArchiveFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, - ByteProvider byteProvider, File containerFile, FileSystemService fsService, - TaskMonitor monitor) throws IOException, CancelledException { + public OmfArchiveFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { OmfArchiveFileSystem fs = new OmfArchiveFileSystem(targetFSRL, byteProvider); fs.mount(monitor); @@ -43,9 +43,8 @@ public class OmfArchiveFileSystemFactory } @Override - public boolean probe(FSRL containerFSRL, ByteProvider byteProvider, File containerFile, - FileSystemService fsService, TaskMonitor monitor) - throws IOException, CancelledException { + public boolean probe(ByteProvider byteProvider, FileSystemService fsService, + TaskMonitor monitor) throws IOException, CancelledException { if (byteProvider.length() < OmfLoader.MIN_BYTE_LENGTH) { return false; diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SZByteProviderStream.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SZByteProviderStream.java new file mode 100644 index 0000000000..ef7efea9a3 --- /dev/null +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SZByteProviderStream.java @@ -0,0 +1,93 @@ +/* ### + * 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.sevenzip; + +import java.io.IOException; + +import ghidra.app.util.bin.ByteProvider; +import net.sf.sevenzipjbinding.IInStream; +import net.sf.sevenzipjbinding.SevenZipException; + +/** + * Adapter from Ghidra's {@link ByteProvider} to SZ's {@link IInStream} + */ +class SZByteProviderStream implements IInStream { + + private ByteProvider bp; + private long position; + + SZByteProviderStream(ByteProvider bp) { + this.bp = bp; + } + + @Override + public synchronized long seek(long offset, int seekOrigin) throws SevenZipException { + try { + switch (seekOrigin) { + case SEEK_SET: + setPos(offset); + break; + + case SEEK_CUR: + setPos(position + offset); + break; + + case SEEK_END: + setPos(bp.length() + offset); + break; + + default: + throw new RuntimeException("Seek: unknown origin: " + seekOrigin); + } + } + catch (IOException e) { + throw new SevenZipException(e); + } + + return position; + } + + private void setPos(long newPos) throws SevenZipException { + if (newPos < 0) { + throw new SevenZipException("Invalid offset: " + newPos); + } + position = newPos; + } + + @Override + public synchronized int read(byte[] data) throws SevenZipException { + try { + int bytesToRead = (int)Math.min(data.length, bp.length() - position); + if (bytesToRead <= 0) { + return 0; + } + byte[] bytes = bp.readBytes(position, bytesToRead); + System.arraycopy(bytes, 0, data, 0, bytes.length); + + position += bytes.length; + return bytes.length; + } + catch (IOException e) { + throw new SevenZipException("Error reading random access file", e); + } + } + + @Override + public void close() throws IOException { + bp.close(); + } + +} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystem.java index ae4d47fdc9..b42e9d9cd6 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystem.java @@ -15,111 +15,236 @@ */ package ghidra.file.formats.sevenzip; -import java.util.*; -import java.io.*; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + +import java.io.Closeable; +import java.io.IOException; +import java.util.*; import org.apache.commons.io.FilenameUtils; +import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.FileCache.FileCacheEntry; +import ghidra.formats.gfilesystem.FileCache.FileCacheEntryBuilder; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; +import ghidra.formats.gfilesystem.crypto.CryptoSession; +import ghidra.formats.gfilesystem.crypto.PasswordValue; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; +import ghidra.formats.gfilesystem.fileinfo.FileType; import ghidra.util.Msg; -import ghidra.util.NumericUtilities; import ghidra.util.exception.CancelledException; +import ghidra.util.exception.CryptoException; import ghidra.util.task.TaskMonitor; import net.sf.sevenzipjbinding.*; -import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; import net.sf.sevenzipjbinding.simple.ISimpleInArchive; import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem; -import utilities.util.FileUtilities; @FileSystemInfo(type = "7zip", description = "7Zip", factory = SevenZipFileSystemFactory.class) public class SevenZipFileSystem implements GFileSystem { - - private FileSystemService fileSystemService; + private FileSystemService fsService; private FileSystemIndexHelper fsIndexHelper; private FSRLRoot fsrl; private FileSystemRefManager refManager = new FileSystemRefManager(this); + private Map passwords = new HashMap<>(); private IInArchive archive; private ISimpleInArchive archiveInterface; - private RandomAccessFile randomAccessFile; + private SZByteProviderStream szBPStream; + private ISimpleInArchiveItem[] items; + private ArchiveFormat archiveFormat; - public SevenZipFileSystem(FSRLRoot fsrl) { + public SevenZipFileSystem(FSRLRoot fsrl, FileSystemService fsService) { + this.fsService = fsService; this.fsrl = fsrl; this.fsIndexHelper = new FileSystemIndexHelper<>(this, fsrl); - this.fileSystemService = FileSystemService.getInstance(); } /** * Opens the specified sevenzip container file and initializes this file system with the * contents. - * - * @param containerFile file to open + * + * @param byteProvider container file * @param monitor {@link TaskMonitor} to allow the user to monitor and cancel * @throws CancelledException if user cancels * @throws IOException if error when reading data */ - public void mount(File containerFile, TaskMonitor monitor) + public void mount(ByteProvider byteProvider, TaskMonitor monitor) throws CancelledException, IOException { - randomAccessFile = new RandomAccessFile(containerFile, "r"); try { - archive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile)); + szBPStream = new SZByteProviderStream(byteProvider); + SevenZip.initSevenZipFromPlatformJAR(); // calling this multiple times is ok + archive = SevenZip.openInArchive(null, szBPStream); + archiveFormat = archive.getArchiveFormat(); archiveInterface = archive.getSimpleInterface(); + items = archiveInterface.getArchiveItems(); - ISimpleInArchiveItem[] items = archiveInterface.getArchiveItems(); - for (ISimpleInArchiveItem item : items) { - if (monitor.isCancelled()) { - throw new CancelledException(); - } - - String itemPath = item.getPath(); - if (items.length == 1 && itemPath.isBlank()) { - // special case when there is a single unnamed file. - // use the name of the 7zip file itself, minus the extension - itemPath = FilenameUtils.getBaseName(fsrl.getContainer().getName()); - } - fsIndexHelper.storeFile(itemPath, item.getItemIndex(), item.isFolder(), - getSize(item), item); - } - preCacheAll(monitor); + indexFiles(monitor); + ensurePasswords(monitor); } - catch (SevenZipException e) { + catch (SevenZipException | SevenZipNativeInitializationException e) { throw new IOException("Failed to open archive: " + fsrl, e); } } - private void preCacheAll(TaskMonitor monitor) throws SevenZipException { - // Because the performance of single file extract is SOOOOOO SLOOOOOOOW, we pre-load - // all files in the sevenzip archive into the file cache using the faster sevenzip - // bulk extract method. - // Single file extract is still possible if file cache info is evicted from memory due - // to pressure. - SZExtractCallback szCallback = new SZExtractCallback(monitor); - archive.extract(null, false, szCallback); - } - @Override public void close() throws IOException { refManager.onClose(); - if (randomAccessFile != null) { - // FYI: no need to close the iface, because the archive will - // shut it down on close anyways - try { - archive.close(); - } - catch (SevenZipException e) { - Msg.warn(this, "Problem closing 7-Zip archive", e); - } - archive = null; - archiveInterface = null; + FSUtilities.uncheckedClose(archive, "Problem closing 7-Zip archive"); + archive = null; + archiveInterface = null; - randomAccessFile.close(); - randomAccessFile = null; - } + FSUtilities.uncheckedClose(szBPStream, null); + szBPStream = null; fsIndexHelper.clear(); + items = null; + } + + private void indexFiles(TaskMonitor monitor) throws CancelledException, SevenZipException { + monitor.initialize(items.length); + monitor.setMessage("Indexing files"); + for (ISimpleInArchiveItem item : items) { + if (monitor.isCancelled()) { + throw new CancelledException(); + } + + long itemSize = Objects.requireNonNullElse(item.getSize(), -1L); + fsIndexHelper.storeFile(fixupItemPath(item), item.getItemIndex(), item.isFolder(), + itemSize, item); + } + } + + private String fixupItemPath(ISimpleInArchiveItem item) throws SevenZipException { + String itemPath = item.getPath(); + if (items.length == 1 && itemPath.isBlank()) { + // special case when there is a single unnamed file. + // use the name of the 7zip file itself, minus the extension + itemPath = FilenameUtils.getBaseName(fsrl.getContainer().getName()); + } + return itemPath; + } + + private String getPasswordForFile(GFile file, ISimpleInArchiveItem encryptedItem, + TaskMonitor monitor) { + int itemIndex = encryptedItem.getItemIndex(); + if (!passwords.containsKey(itemIndex)) { + try (CryptoSession cryptoSession = fsService.newCryptoSession()) { + String prompt = passwords.isEmpty() + ? fsrl.getContainer().getName() + : String.format("%s in %s", file.getName(), fsrl.getContainer().getName()); + for (Iterator pwIt = + cryptoSession.getPasswordsFor(fsrl.getContainer(), prompt); pwIt.hasNext();) { + try (PasswordValue passwordValue = pwIt.next()) { + monitor.setMessage("Testing password for " + file.getName()); + + String password = String.valueOf(passwordValue.getPasswordChars()); // we are forced to use strings by 7zip's api + int[] encryptedItemIndexes = getEncryptedItemIndexes(); + TestPasswordsCallback testCB = + new TestPasswordsCallback(password, encryptedItemIndexes[0], monitor); + + // call the SZ extract method using "TEST" mode (ie. no bytes are extracted) + // on any files that don't have a password yet + archive.extract(encryptedItemIndexes, true /* test mode */, testCB); + List successFileIndexes = testCB.getSuccessFileIndexes(); + for (Integer unlockedFileIndex : successFileIndexes) { + passwords.put(unlockedFileIndex, password); + } + if (!successFileIndexes.isEmpty()) { + cryptoSession.addSuccessfulPassword(fsrl.getContainer(), passwordValue); + } + if (passwords.containsKey(itemIndex)) { + break; + } + } + catch (SevenZipException e) { + Msg.error(this, "Error when testing password for " + file.getFSRL(), e); + return null; + } + } + } + } + return passwords.get(itemIndex); + + } + + private int[] getEncryptedItemIndexes() throws SevenZipException { + List result = new ArrayList<>(); + for (ISimpleInArchiveItem item : items) { + if (item.isEncrypted() && !passwords.containsKey(item.getItemIndex())) { + result.add(item.getItemIndex()); + } + } + int[] arrayResult = new int[result.size()]; + int arrayResultIndex = 0; + for (Integer i : result) { + arrayResult[arrayResultIndex++] = i; + } + return arrayResult; + } + + private void ensurePasswords(TaskMonitor monitor) throws CancelledException, IOException { + // Alert! Unusual code! + // Background: contrary to normal expectations, zip container files can have a + // unique password per-embedded-file. + // Other archive formats may not have that feature, but the SevenZip jbinding + // API is designed to allow a per-embedded-file password. + // The following loop tests passwords against the file, first trying a + // common password against all the embedded files (this is the most likely + // scenario), and then when a password has been found that successfully unlocks + // the first subset of files, each remaining subsequent encrypted file's name is used to + // prompt for the next password. + // If the loop ends without finding a password for an encrypted file, + // that file will not be readable unless a password is found for it (see + // getPasswordForFile()). + + try (CryptoSession cryptoSession = fsService.newCryptoSession()) { + List encryptedItems = getEncryptedItemsWithoutPasswords(); + ISimpleInArchiveItem encryptedItem = null; + while ((encryptedItem = getFirstItemWithoutPassword(encryptedItems)) != null && + !monitor.isCancelled()) { + GFile gFile = fsIndexHelper.getFileByIndex(encryptedItem.getItemIndex()); + if (gFile == null) { + throw new IOException("Unable to retrieve file " + encryptedItem.getPath()); + } + getPasswordForFile(gFile, encryptedItem, monitor); + if (passwords.isEmpty()) { + // we didn't find any password for any file in the archive. Abort the loop + // instead of badgering the user by using other files as prompts + break; + } + encryptedItems.remove(encryptedItem); + } + List noPasswordFoundList = getEncryptedItemsWithoutPasswords(); + if (!noPasswordFoundList.isEmpty()) { + Msg.warn(this, + "Unable to find password for " + noPasswordFoundList.size() + " file(s) in " + + fsrl.getContainer().getName()); + } + } + } + + private ISimpleInArchiveItem getFirstItemWithoutPassword( + List encryptedItems) { + for (ISimpleInArchiveItem item : encryptedItems) { + if (!passwords.containsKey(item.getItemIndex())) { + return item; + } + } + return null; + } + + private List getEncryptedItemsWithoutPasswords() + throws SevenZipException { + List result = new LinkedList<>(); + for (ISimpleInArchiveItem item : items) { + if (item.isEncrypted() && !passwords.containsKey(item.getItemIndex())) { + result.add(item); + } + } + return result; } @Override @@ -134,7 +259,7 @@ public class SevenZipFileSystem implements GFileSystem { @Override public boolean isClosed() { - return randomAccessFile == null; + return szBPStream == null; } @Override @@ -153,221 +278,150 @@ public class SevenZipFileSystem implements GFileSystem { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { - ISimpleInArchiveItem entry = fsIndexHelper.getMetadata(file); - return (entry != null) ? FSUtilities.infoMapToString(getInfoMap(entry)) : null; - } + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + FileAttributes result = new FileAttributes(); + if (fsIndexHelper.getRootDir().equals(file)) { + result.add(NAME_ATTR, "/"); + result.add("Archive Format", archiveFormat.toString()); + } + else { + ISimpleInArchiveItem item = fsIndexHelper.getMetadata(file); + if (item == null) { + return result; + } - private Map getInfoMap(ISimpleInArchiveItem entry) { - Map info = new LinkedHashMap<>(); - try { - info.put("Name", entry.getPath()); - info.put("Folder?", Boolean.toString(isFolder(entry))); - info.put("Encrypted?", Boolean.toString(entry.isEncrypted())); - info.put("Comment", entry.getComment()); - Long compressedSize = getCompressedSize(entry); - info.put("Compressed Size", - compressedSize != null ? NumericUtilities.toHexString(compressedSize) : "NA"); - info.put("Uncompressed Size", NumericUtilities.toHexString(getSize(entry))); - Integer crc = getCRC(entry); - info.put("CRC", - crc != null ? NumericUtilities.toHexString(crc.intValue() & 0xffffffffL) : "NA"); - info.put("Compression Method", entry.getMethod()); - Date creationTime = getCreateDate(entry); - info.put("Time", creationTime != null ? creationTime.toGMTString() : "NA"); + result.add(NAME_ATTR, FilenameUtils.getName(uncheckedGet(item::getPath, "unknown"))); + result.add(FILE_TYPE_ATTR, + uncheckedGet(item::isFolder, false) ? FileType.DIRECTORY : FileType.FILE); + boolean encrypted = uncheckedGet(item::isEncrypted, false); + result.add(IS_ENCRYPTED_ATTR, encrypted); + if (encrypted) { + result.add(HAS_GOOD_PASSWORD_ATTR, passwords.get(item.getItemIndex()) != null); + } + String comment = uncheckedGet(item::getComment, null); + result.add(COMMENT_ATTR, !comment.isBlank() ? comment : null); + result.add(COMPRESSED_SIZE_ATTR, uncheckedGet(item::getPackedSize, null)); + result.add(SIZE_ATTR, uncheckedGet(item::getSize, null)); + + Integer crc = uncheckedGet(item::getCRC, null); + result.add("CRC", crc != null ? String.format("%08X", crc) : null); + result.add("Compression Method", uncheckedGet(item::getMethod, null)); + result.add(CREATE_DATE_ATTR, uncheckedGet(item::getCreationTime, null)); + result.add(MODIFIED_DATE_ATTR, uncheckedGet(item::getLastWriteTime, null)); } - catch (SevenZipException e) { - Msg.warn(this, "7-Zip exception trying to get info on item", e); - } - return info; + return result; } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { - ISimpleInArchiveItem entry = fsIndexHelper.getMetadata(file); - - if (entry == null) { - return null; - } - try { - if (entry.isFolder()) { + ISimpleInArchiveItem item = fsIndexHelper.getMetadata(file); + + if (item == null) { + return null; + } + + int itemIndex = item.getItemIndex(); + + if (item.isFolder()) { throw new IOException("Not a file: " + file.getName()); } + if (item.isEncrypted()) { + String password = getPasswordForFile(file, item, monitor); + if (password == null) { + throw new CryptoException( + "Unable to extract encrypted file, missing password: " + item.getPath()); + } + } + try (SZExtractCallback szCallback = new SZExtractCallback(monitor, itemIndex, true)) { + archive.extract(new int[] { itemIndex }, false /* extract mode */, szCallback); + FileCacheEntry result = szCallback.getExtractResult(itemIndex); + if (result == null) { + throw new IOException("Unable to extract " + file.getFSRL()); + } + return result.asByteProvider(file.getFSRL()); + } } catch (SevenZipException e) { - throw new IOException("Error getting status of file: " + file.getName(), e); + throw unwrapSZException(e); } - File cachedFile = extractSZFile(file, entry, monitor); - return new FileInputStream(cachedFile); - } - - private File extractSZFile(GFile file, ISimpleInArchiveItem entry, TaskMonitor monitor) - throws CancelledException, IOException { - // push the sevenzip compressed file into the file cache (if not already there) - FileCacheEntry fce = FileSystemService.getInstance().getDerivedFilePush(fsrl.getContainer(), - Integer.toString(entry.getItemIndex()), (os) -> { - Msg.info(this, "Extracting singleton file from sevenzip (slow): " + file.getFSRL()); - try { - ExtractOperationResult operationResult = - entry.extractSlow(new ISequentialOutStream() { - - @Override - public int write(byte[] data) throws SevenZipException { - try { - os.write(data); - return data.length; - } - catch (IOException ioe) { - throw new SevenZipException(ioe); - } - } - }); - extractOperationResultToException(operationResult); - } - catch (SevenZipException e) { - Throwable cause = e.getCause(); - if (cause != null && cause instanceof IOException) { - throw (IOException) cause; - } - if (cause != null && cause instanceof CancelledException) { - throw (CancelledException) cause; - } - throw new IOException("7-Zip exception", e); - } - - }, monitor); - return fce.file; } //---------------------------------------------------------------------------------------------- - - private static void extractOperationResultToException(ExtractOperationResult operationResult) - throws IOException { - if (operationResult == null) { - throw new IOException("7-Zip returned null operation result"); - } - switch (operationResult) { - case CRCERROR: { - throw new IOException("7-Zip returned CRC error"); - } - case DATAERROR: { - throw new IOException("7-Zip returned data error"); - } - case UNSUPPORTEDMETHOD: { - throw new IOException("Unexpected: 7-Zip returned unsupported method"); - } - case UNKNOWN_OPERATION_RESULT: { - throw new IOException("Unexpected: 7-Zip returned unknown operation result"); - } - case WRONG_PASSWORD: { - throw new IOException("7-Zip wrong password"); - } - case OK: - default: { - // it's all ok! - } - } - } - - private static long getSize(ISimpleInArchiveItem entry) throws SevenZipException { - Long tempSize = entry.getSize(); - return tempSize == null ? -1 : tempSize.intValue(); - } - - private static Long getCompressedSize(ISimpleInArchiveItem entry) { - try { - return entry.getPackedSize(); - } - catch (SevenZipException e) { - //don't care - } - return null; - } - - private static Integer getCRC(ISimpleInArchiveItem entry) { - try { - return entry.getCRC(); - } - catch (SevenZipException e) { - //don't care - } - return null; - } - - private static Date getCreateDate(ISimpleInArchiveItem entry) { - try { - return entry.getCreationTime(); - } - catch (SevenZipException e) { - //don't care - } - return null; - } - - private static boolean isFolder(ISimpleInArchiveItem entry) { - try { - return entry.isFolder(); - } - catch (SevenZipException e) { - //don't care - } - return false; - } - - //---------------------------------------------------------------------------------------------- - /** * Implements SevenZip bulk extract callback. *

    * For each file in the archive, SZ will call this class's 1) getStream(), 2) prepare(), * 3) lots of write()s, and then 4) setOperationResult(). *

    - * This class writes the extracted bytes to a temp file, and then pushes that temp file - * into the FileSystem cache, and then deletes that temp file. + * This class writes the extracted bytes to the FileCache. *

    - * Without this bulk extract method, SevenZip takes ~500ms per file when used via the singleton - * extract method. */ - private class SZExtractCallback implements IArchiveExtractCallback, ISequentialOutStream { + private class SZExtractCallback + implements IArchiveExtractCallback, ISequentialOutStream, ICryptoGetTextPassword, + Closeable { private TaskMonitor monitor; private int currentIndex; - private File currentTempFile; - private OutputStream currentTempFileOutputStream; + private ISimpleInArchiveItem currentItem; + private String currentName = "unknown"; + private FileCacheEntryBuilder currentCacheEntryBuilder; + private boolean saveResults; + private Map extractResults = new HashMap<>(); - public SZExtractCallback(TaskMonitor monitor) { + public SZExtractCallback(TaskMonitor monitor, int initalIndex, boolean saveResults) { this.monitor = monitor; + this.currentIndex = initalIndex; + this.saveResults = saveResults; + } + + FileCacheEntry getExtractResult(int itemIndex) { + return extractResults.get(itemIndex); + } + + @Override + public void close() throws IOException { + if (currentCacheEntryBuilder != null) { + currentCacheEntryBuilder.close(); + } } @Override public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException { + currentIndex = index; + // STEP 1: SevenZip calls this method to get a object it can use to write the bytes to. - // If we return null, SZ treats it as a skip. - try { - if (!fileSystemService.hasDerivedFile(fsrl.getContainer(), Integer.toString(index), - monitor)) { - this.currentIndex = index; - return this; - } + // If we return null, SZ treats it as a skip. (except for folders) + currentItem = items[currentIndex]; + currentName = currentItem.getPath(); + + if (currentItem.isFolder() || extractAskMode != ExtractAskMode.EXTRACT) { + return null; } - catch (CancelledException | IOException e) { - // ignore + + if (currentItem.isEncrypted() && !passwords.containsKey(currentIndex)) { + // if we lack a password for this item, don't try to extract it + Msg.debug(SevenZipFileSystem.this, + "No password for file[" + currentIndex + "] " + currentName + " of " + + fsrl.getContainer().getName() + ", unable to extract."); + return null; } - return null; + + return this; } @Override public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException { // STEP 2: SevenZip calls this method to further prepare to operate on the file. // In our case, we only handle extract operations. - if (extractAskMode == ExtractAskMode.EXTRACT) { + if (!currentItem.isFolder() && extractAskMode == ExtractAskMode.EXTRACT) { try { - currentTempFile = File.createTempFile("ghidra_sevenzip_", ".tmp"); - currentTempFileOutputStream = new FileOutputStream(currentTempFile); + currentCacheEntryBuilder = fsService.createTempFile(currentItem.getSize()); + monitor.initialize(currentItem.getSize()); + monitor.setMessage("Extracting " + currentName); } catch (IOException e) { throw new SevenZipException(e); @@ -375,12 +429,34 @@ public class SevenZipFileSystem implements GFileSystem { } } + @Override + public String cryptoGetTextPassword() throws SevenZipException { + // STEP 2.5 or 0: SevenZip calls this method to get the password of the file (if encrypted). + // Sometimes after prepareOperation(), sometimes before getStream(). + String password = passwords.get(currentIndex); + if (password == null) { + + Msg.debug(SevenZipFileSystem.this, + "No password for file[" + currentIndex + "] " + currentName + " of " + + fsrl.getContainer().getName()); + // hack, return a non-null bad password. normally shouldn't get here as + // encrypted files w/missing password are skipped by getStream() + password = ""; + } + return password; + } + @Override public int write(byte[] data) throws SevenZipException { // STEP 3: SevenZip calls this multiple times for all the bytes in the file. // We write them to our temp file. + if (currentCacheEntryBuilder == null) { + throw new SevenZipException( + "Bad Sevenzip Extract Callback state, " + currentIndex + ", " + currentName); + } try { - currentTempFileOutputStream.write(data); + currentCacheEntryBuilder.write(data); + monitor.incrementProgress(data.length); return data.length; } catch (IOException e) { @@ -392,26 +468,41 @@ public class SevenZipFileSystem implements GFileSystem { public void setOperationResult(ExtractOperationResult extractOperationResult) throws SevenZipException { // STEP 4: SevenZip calls this to signal that the extract is done for this file. - if (currentTempFileOutputStream != null) { - try { - currentTempFileOutputStream.close(); + if (currentCacheEntryBuilder == null) { + return; + } + try { + FileCacheEntry fce = currentCacheEntryBuilder.finish(); + if (extractOperationResult == ExtractOperationResult.OK) { + GFile gFile = fsIndexHelper.getFileByIndex(currentIndex); + if (gFile != null && gFile.getFSRL().getMD5() == null) { + fsIndexHelper.updateFSRL(gFile, gFile.getFSRL().withMD5(fce.getMD5())); + } + if (saveResults) { + extractResults.put(currentIndex, fce); + } + Msg.debug(SevenZipFileSystem.this, "Wrote file to cache: " + gFile + ", " + + FSUtilities.formatSize(fce.length())); + } + else { + Msg.warn(SevenZipFileSystem.this, "Failed to push file[" + currentIndex + + "] " + currentName + " to cache: " + extractOperationResult); extractOperationResultToException(extractOperationResult); - fileSystemService.getDerivedFilePush(fsrl.getContainer(), - Integer.toString(currentIndex), (os) -> { - try (InputStream is = new FileInputStream(currentTempFile)) { - FileUtilities.copyStreamToStream(is, os, monitor); - } - }, monitor); - currentTempFile.delete(); - } - catch (IOException | CancelledException e) { - throw new SevenZipException(e); - } - finally { - currentTempFile = null; - currentTempFileOutputStream = null; } } + catch (IOException e) { + throw new SevenZipException(e); + } + finally { + FSUtilities.uncheckedClose(currentCacheEntryBuilder, null); + currentCacheEntryBuilder = null; + + // hack to advance the currentIndex for the next file so cryptoGetTextPassword + // will have a correct currentIndex value if it is called before getStream(), + // which does happen depending on the phase of the moon or the 7zip + // library's mood. + currentIndex++; + } } //@formatter:off @@ -419,6 +510,116 @@ public class SevenZipFileSystem implements GFileSystem { @Override public void setCompleted(long complete) throws SevenZipException {/* nada */ } //@formatter:on + private void extractOperationResultToException(ExtractOperationResult operationResult) + throws IOException { + if (operationResult == null) { + throw new IOException("7-Zip returned null operation result"); + } + switch (operationResult) { + case CRCERROR: + throw new IOException("7-Zip returned CRC error"); + case DATAERROR: + throw new IOException("7-Zip returned data error"); + case UNSUPPORTEDMETHOD: + throw new IOException("Unexpected: 7-Zip returned unsupported method"); + case UNKNOWN_OPERATION_RESULT: + throw new IOException("Unexpected: 7-Zip returned unknown operation result"); + case WRONG_PASSWORD: + throw new IOException("7-Zip wrong password"); + default: + throw new IOException("7-Zip unknown error " + operationResult); + case OK: + // it's all ok! + } + } + + } + + /** + * This class is has the same layout and hacks re: setting currentIndex as {@link SZExtractCallback}, + * but is specialized to test passwords against the encrypted entries in the file. + */ + private class TestPasswordsCallback implements IArchiveExtractCallback, ICryptoGetTextPassword { + + private int currentIndex; + private String currentPassword; + private List successFileIndexes = new ArrayList<>(); + private TaskMonitor monitor; + + TestPasswordsCallback(String currentPassword, int initialIndex, TaskMonitor monitor) { + this.currentPassword = currentPassword; + this.currentIndex = initialIndex; + this.monitor = monitor; + } + + List getSuccessFileIndexes() { + return successFileIndexes; + } + + @Override + public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) + throws SevenZipException { + currentIndex = index; + ISimpleInArchiveItem item = items[currentIndex]; + monitor.setMessage("Testing password for " + item.getPath()); + return null; + } + + @Override + public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException { + // nothing + } + + @Override + public String cryptoGetTextPassword() throws SevenZipException { + return currentPassword; + } + + @Override + public void setOperationResult(ExtractOperationResult extractOperationResult) + throws SevenZipException { + ISimpleInArchiveItem item = items[currentIndex]; + if (item.isEncrypted() && extractOperationResult == ExtractOperationResult.OK && + !passwords.containsKey(currentIndex)) { + successFileIndexes.add(currentIndex); + } + currentIndex++; + } + + @Override + public void setTotal(long total) throws SevenZipException { + monitor.initialize(total); + } + + @Override + public void setCompleted(long complete) throws SevenZipException { + monitor.setProgress(complete); + } + + } + + interface SZGetter { + T get() throws SevenZipException; + } + + private static T uncheckedGet(SZGetter getter, T defaultValue) { + try { + return getter.get(); + } + catch (SevenZipException e) { + // don't care + return defaultValue; + } + } + + private IOException unwrapSZException(SevenZipException e) { + SevenZipException tmp = e; + while (tmp != null && tmp.getCause() instanceof SevenZipException) { + tmp = (SevenZipException) tmp.getCause(); + } + return (tmp != null && tmp.getCause() instanceof IOException) + ? (IOException) tmp.getCause() + : new IOException(e); } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystemFactory.java index 1daab2d33e..6b7a241816 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sevenzip/SevenZipFileSystemFactory.java @@ -15,19 +15,19 @@ */ package ghidra.file.formats.sevenzip; -import java.io.File; import java.io.IOException; import java.util.List; +import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.recognizer.*; import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryWithFile; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; public class SevenZipFileSystemFactory - implements GFileSystemFactoryWithFile, GFileSystemProbeBytesOnly { + implements GFileSystemFactoryByteProvider, GFileSystemProbeBytesOnly { private List recognizers = List.of(new SevenZipRecognizer(), new XZRecognizer(), new Bzip2Recognizer(), new MSWIMRecognizer(), new ArjRecognizer(), new CabarcRecognizer(), @@ -62,13 +62,13 @@ public class SevenZipFileSystemFactory } @Override - public SevenZipFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, File containerFile, + public SevenZipFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, FileSystemService fsService, TaskMonitor monitor) throws IOException, CancelledException { - SevenZipFileSystem fs = new SevenZipFileSystem(targetFSRL); + SevenZipFileSystem fs = new SevenZipFileSystem(targetFSRL, fsService); try { - fs.mount(containerFile, monitor); + fs.mount(byteProvider, monitor); return fs; } catch (IOException ioe) { diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sparseimage/SparseImageFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sparseimage/SparseImageFileSystem.java index 4ed6ca4fb1..0a3a77fd56 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sparseimage/SparseImageFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sparseimage/SparseImageFileSystem.java @@ -15,12 +15,16 @@ */ package ghidra.file.formats.sparseimage; -import java.io.*; -import java.util.*; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + +import java.io.IOException; +import java.util.List; import ghidra.app.util.bin.ByteProvider; +import ghidra.app.util.bin.ByteProviderWrapper; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -32,40 +36,44 @@ import ghidra.util.task.TaskMonitor; public class SparseImageFileSystem implements GFileSystem { private final FSRLRoot fsFSRL; - private final FSRL containerFSRL; private final FileSystemRefManager refManager = new FileSystemRefManager(this); private final FileSystemService fsService; - private GFileImpl root; - private GFileImpl payload; - private long containerSize; + private ByteProvider byteProvider; + private ByteProvider payloadProvider; + private SingleFileSystemIndexHelper fsIndexHelper; - public SparseImageFileSystem(FSRLRoot fsFSRL, FSRL containerFSRL, FileSystemService fsService, + public SparseImageFileSystem(FSRLRoot fsFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) throws CancelledException, IOException { this.fsFSRL = fsFSRL; this.fsService = fsService; - this.root = GFileImpl.fromFilename(this, null, null, true, -1, fsFSRL.withPath("/")); - this.containerFSRL = containerFSRL; + this.byteProvider = byteProvider; - File containerFile = fsService.getFile(containerFSRL, monitor); - containerSize = containerFile.length(); - - FileCacheEntry pli = getPayloadInfo(monitor); + this.payloadProvider = getPayload(null, monitor); + FSRL containerFSRL = byteProvider.getFSRL(); String payloadName = containerFSRL.getName() + ".raw"; - FSRL payloadFSRL = root.getFSRL().appendPath(payloadName).withMD5(pli.md5); - this.payload = - GFileImpl.fromFilename(this, root, payloadName, false, pli.file.length(), payloadFSRL); + this.fsIndexHelper = new SingleFileSystemIndexHelper(this, fsFSRL, payloadName, + payloadProvider.length(), payloadProvider.getFSRL().getMD5()); } @Override public void close() throws IOException { refManager.onClose(); - payload = null; + fsIndexHelper.clear(); + if (byteProvider != null) { + byteProvider.close(); + byteProvider = null; + } + if (payloadProvider != null) { + payloadProvider.close(); + payloadProvider = null; + } } @Override public boolean isClosed() { - return payload == null; + return fsIndexHelper.isClosed(); } @Override @@ -84,58 +92,46 @@ public class SparseImageFileSystem implements GFileSystem { } @Override - public GFileImpl lookup(String path) throws IOException { - if (path == null || path.equals("/")) { - return root; - } - else if (path.equals(payload.getFSRL().getPath())) { - return payload; - } - return null; + public GFile lookup(String path) throws IOException { + return fsIndexHelper.lookup(path); } - private FileCacheEntry getPayloadInfo(TaskMonitor monitor) + private ByteProvider getPayload(FSRL payloadFSRL, TaskMonitor monitor) throws CancelledException, IOException { - return fsService.getDerivedFilePush(containerFSRL, "sparse", os -> { - try (ByteProvider provider = fsService.getByteProvider(containerFSRL, monitor)) { - SparseImageDecompressor sid = new SparseImageDecompressor(provider, os); + return fsService.getDerivedByteProviderPush(byteProvider.getFSRL(), payloadFSRL, "sparse", + -1, os -> { + SparseImageDecompressor sid = new SparseImageDecompressor(byteProvider, os); sid.decompress(monitor); - } - }, monitor); + }, monitor); } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { - if (payload.equals(file)) { - FileCacheEntry pli = getPayloadInfo(monitor); - return new FileInputStream(pli.file); + if (fsIndexHelper.isPayloadFile(file)) { + return new ByteProviderWrapper(payloadProvider, file.getFSRL()); } return null; } @Override public List getListing(GFile directory) throws IOException { - if (directory == null || root.equals(directory)) { - return Arrays.asList(payload); - } - return Collections.emptyList(); + return fsIndexHelper.getListing(directory); } @Override - public String getInfo(GFile file, TaskMonitor monitor) { - if (payload.equals(file)) { - return FSUtilities.infoMapToString(getInfoMap()); + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + FileAttributes result = new FileAttributes(); + if (fsIndexHelper.isPayloadFile(file)) { + try { + result.add(SIZE_ATTR, payloadProvider.length()); + result.add(COMPRESSED_SIZE_ATTR, byteProvider.length()); + } + catch (IOException e) { + // ignore and continue + } + result.add("MD5", fsIndexHelper.getPayloadFile().getFSRL().getMD5()); } - return null; - } - - public Map getInfoMap() { - Map info = new LinkedHashMap<>(); - info.put("Name", payload.getName()); - info.put("Size", Long.toString(payload.getLength())); - info.put("Compressed Size", Long.toString(containerSize)); - info.put("MD5", payload.getFSRL().getMD5()); - return info; + return result; } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sparseimage/SparseImageFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sparseimage/SparseImageFileSystemFactory.java index 5eecdb217f..e825726882 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sparseimage/SparseImageFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/sparseimage/SparseImageFileSystemFactory.java @@ -15,32 +15,30 @@ */ package ghidra.file.formats.sparseimage; -import java.io.File; import java.io.IOException; import ghidra.app.util.bin.BinaryReader; import ghidra.app.util.bin.ByteProvider; -import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryWithFile; -import ghidra.formats.gfilesystem.factory.GFileSystemProbeFull; +import ghidra.formats.gfilesystem.FSRLRoot; +import ghidra.formats.gfilesystem.FileSystemService; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; +import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -public class SparseImageFileSystemFactory - implements GFileSystemFactoryWithFile, GFileSystemProbeFull { +public class SparseImageFileSystemFactory implements + GFileSystemFactoryByteProvider, GFileSystemProbeByteProvider { @Override - public SparseImageFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, File containerFile, + public SparseImageFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, FileSystemService fsService, TaskMonitor monitor) throws IOException, CancelledException { - - return new SparseImageFileSystem(targetFSRL, containerFSRL, fsService, monitor); + return new SparseImageFileSystem(targetFSRL, byteProvider, fsService, monitor); } @Override - public boolean probe(FSRL containerFSRL, ByteProvider byteProvider, File containerFile, - FileSystemService fsService, TaskMonitor taskMonitor) - throws IOException, CancelledException { + public boolean probe(ByteProvider byteProvider, FileSystemService fsService, + TaskMonitor taskMonitor) throws IOException, CancelledException { BinaryReader reader = new BinaryReader(byteProvider, true); SparseHeader header = new SparseHeader(reader); diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystem.java index bb5d3e7826..0f60c5d6b4 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystem.java @@ -15,14 +15,18 @@ */ package ghidra.file.formats.tar; -import java.io.*; -import java.util.*; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + +import java.io.IOException; +import java.util.List; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; +import ghidra.formats.gfilesystem.fileinfo.*; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -48,7 +52,7 @@ public class TarFileSystem implements GFileSystem { private FSRLRoot fsrl; private FileSystemService fsService; - private File containerFile; + private ByteProvider provider; private FileSystemIndexHelper fsih; private FileSystemRefManager refManager = new FileSystemRefManager(this); private int fileCount; @@ -56,42 +60,46 @@ public class TarFileSystem implements GFileSystem { /** * Creates a new TarFileSystem instance. * - * @param file uncompressed tar file to open. * @param fsrl {@link FSRLRoot} of the new filesystem. + * @param provider {@link ByteProvider} container file * @param fsService reference to the {@link FileSystemService}. */ - public TarFileSystem(File file, FSRLRoot fsrl, FileSystemService fsService) { + public TarFileSystem(FSRLRoot fsrl, ByteProvider provider, FileSystemService fsService) { this.fsrl = fsrl; this.fsih = new FileSystemIndexHelper<>(this, fsrl); - this.containerFile = file; + this.provider = provider; this.fsService = fsService; } - void mount(boolean precache, TaskMonitor monitor) throws IOException, CancelledException { + ByteProvider getProvider() { + return provider; + } + + void mount(TaskMonitor monitor) throws IOException, CancelledException { try (TarArchiveInputStream tarInput = - new TarArchiveInputStream(new FileInputStream(containerFile))) { + new TarArchiveInputStream(provider.getInputStream(0))) { TarArchiveEntry tarEntry; while ((tarEntry = tarInput.getNextTarEntry()) != null) { monitor.setMessage(tarEntry.getName()); monitor.checkCanceled(); - GFileImpl newFile = - fsih.storeFile(tarEntry.getName(), fileCount, tarEntry.isDirectory(), - tarEntry.getSize(), new TarMetadata(tarEntry, fileCount)); - fileCount++; + int fileNum = fileCount++; + GFile newFile = fsih.storeFile(tarEntry.getName(), fileCount, + tarEntry.isDirectory(), tarEntry.getSize(), new TarMetadata(tarEntry, fileNum)); - if (precache) { - FileCacheEntry fce = fsService.addFileToCache(newFile, tarInput, monitor); - newFile.setFSRL(newFile.getFSRL().withMD5(fce.md5)); + if (tarEntry.getSize() < FileCache.MAX_INMEM_FILESIZE) { + // because tar files are sequential access, we cache smaller files if they + // will fit in a in-memory ByteProvider + try (ByteProvider bp = + fsService.getDerivedByteProvider(fsrl.getContainer(), newFile.getFSRL(), + newFile.getPath(), tarEntry.getSize(), () -> tarInput, monitor)) { + fsih.updateFSRL(newFile, newFile.getFSRL().withMD5(bp.getFSRL().getMD5())); + } } } } } - public File getRawContainerFile() { - return containerFile; - } - @Override public String getName() { return fsrl.getContainer().getName(); @@ -101,12 +109,15 @@ public class TarFileSystem implements GFileSystem { public void close() throws IOException { refManager.onClose(); fsih.clear(); - containerFile = null; + if (provider != null) { + provider.close(); + provider = null; + } } @Override public boolean isClosed() { - return containerFile == null; + return provider == null; } @Override @@ -119,15 +130,36 @@ public class TarFileSystem implements GFileSystem { return fileCount; } - public Map getInfoMap(TarArchiveEntry blob) { - Map info = new LinkedHashMap<>(); - info.put("Name", blob.getName()); - info.put("Mode", Integer.toUnsignedString(blob.getMode(), 8)); - info.put("Size", Long.toString(blob.getSize())); - info.put("Date", blob.getLastModifiedDate().toString()); - info.put("User/Group", blob.getUserName() + " / " + blob.getGroupName()); - info.put("UserId/GroupId", blob.getUserId() + " / " + blob.getGroupId()); - return info; + @Override + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + TarMetadata tmd = fsih.getMetadata(file); + if (tmd == null) { + return null; + } + TarArchiveEntry blob = tmd.tarArchiveEntry; + return FileAttributes.of( + FileAttribute.create(NAME_ATTR, blob.getName()), + FileAttribute.create(SIZE_ATTR, blob.getSize()), + FileAttribute.create(MODIFIED_DATE_ATTR, blob.getLastModifiedDate()), + FileAttribute.create(FILE_TYPE_ATTR, tarToFileType(blob)), + FileAttribute.create(USER_NAME_ATTR, blob.getUserName()), + FileAttribute.create(GROUP_NAME_ATTR, blob.getGroupName()), + FileAttribute.create(USER_ID_ATTR, blob.getLongUserId()), + FileAttribute.create(GROUP_ID_ATTR, blob.getLongGroupId()), + FileAttribute.create(UNIX_ACL_ATTR, (long) blob.getMode())); + } + + private FileType tarToFileType(TarArchiveEntry tae) { + if (tae.isDirectory()) { + return FileType.DIRECTORY; + } + if (tae.isSymbolicLink()) { + return FileType.SYMBOLIC_LINK; + } + if (tae.isFile()) { + return FileType.FILE; + } + return FileType.UNKNOWN; } @Override @@ -136,7 +168,7 @@ public class TarFileSystem implements GFileSystem { } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { TarMetadata tmd = fsih.getMetadata(file); @@ -144,23 +176,26 @@ public class TarFileSystem implements GFileSystem { throw new IOException("Unknown file " + file); } - // Open a new instance of the tar file, seek to the requested embedded file, - // and return the inputstream to the caller, who will close it when done. - TarArchiveInputStream tarInput = - new TarArchiveInputStream(new FileInputStream(containerFile)); + ByteProvider fileBP = fsService.getDerivedByteProvider(provider.getFSRL(), file.getFSRL(), + file.getPath(), tmd.tarArchiveEntry.getSize(), () -> { + TarArchiveInputStream tarInput = new TarArchiveInputStream(provider.getInputStream(0)); - int fileNum = 0; - TarArchiveEntry tarEntry; - while ((tarEntry = tarInput.getNextTarEntry()) != null) { - if (fileNum == tmd.fileNum) { - if (!tmd.tarArchiveEntry.getName().equals(tarEntry.getName())) { - throw new IOException("Mismatch between filenum and tarEntry for " + file); + int fileNum = 0; + TarArchiveEntry tarEntry; + while ((tarEntry = tarInput.getNextTarEntry()) != null) { + if (fileNum == tmd.fileNum) { + if (!tmd.tarArchiveEntry.getName().equals(tarEntry.getName())) { + throw new IOException( + "Mismatch between filenum and tarEntry for " + file); + } + return tarInput; + } + fileNum++; } - return tarInput; - } - fileNum++; - } - throw new IOException("Could not find requested file " + file); + throw new IOException("Could not find requested file " + file); + }, monitor); + + return fileBP; } @Override @@ -168,12 +203,6 @@ public class TarFileSystem implements GFileSystem { return fsih.getListing(directory); } - @Override - public String getInfo(GFile file, TaskMonitor monitor) { - TarMetadata tmd = fsih.getMetadata(file); - return (tmd != null) ? FSUtilities.infoMapToString(getInfoMap(tmd.tarArchiveEntry)) : null; - } - @Override public FileSystemRefManager getRefManager() { return refManager; diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystemFactory.java index 0fba0845f7..cefee0cbf2 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/tar/TarFileSystemFactory.java @@ -15,7 +15,8 @@ */ package ghidra.file.formats.tar; -import java.io.*; +import java.io.IOException; +import java.io.InputStream; import java.util.zip.GZIPInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; @@ -23,6 +24,8 @@ import org.apache.commons.compress.archivers.tar.TarConstants; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; import org.apache.commons.lang3.ArrayUtils; +import ghidra.app.util.bin.BinaryReader; +import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.recognizer.Bzip2Recognizer; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.factory.*; @@ -31,8 +34,8 @@ import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; import ghidra.util.task.UnknownProgressWrappingTaskMonitor; -public class TarFileSystemFactory implements GFileSystemFactoryWithFile, - GFileSystemProbeBytesOnly, GFileSystemProbeWithFile { +public class TarFileSystemFactory implements GFileSystemFactoryByteProvider, + GFileSystemProbeBytesOnly, GFileSystemProbeByteProvider { public static final int TAR_MAGIC_BYTES_REQUIRED = TarConstants.VERSION_OFFSET + TarConstants.VERSIONLEN; @@ -40,22 +43,24 @@ public class TarFileSystemFactory implements GFileSystemFactoryWithFile { + new UnknownProgressWrappingTaskMonitor(monitor, provider.length()); + uncompressedBP = fsService.getDerivedByteProvider(containerFSRL, null, + "uncompressed tar", -1, () -> { Msg.info(TarFileSystem.class, "Uncompressing tar file " + containerFSRL); - return newFileInputStreamAutoDetectCompressed(srcFile); + return newFileInputStreamAutoDetectCompressed(provider); }, upwtm); - containerFile = fce.file; + provider.close(); } - TarFileSystem fs = new TarFileSystem(containerFile, targetFSRL, fsService); - fs.mount(false, monitor); + TarFileSystem fs = new TarFileSystem(targetFSRL, uncompressedBP, fsService); + fs.mount(monitor); return fs; } @@ -78,9 +83,9 @@ public class TarFileSystemFactory implements GFileSystemFactoryWithFile + * 7Zip's features are superior to the native java zip handling (ie. passwords) + */ @FileSystemInfo(type = "zip", description = "ZIP", factory = ZipFileSystemFactory.class, priority = FileSystemInfo.PRIORITY_HIGH) -public class ZipFileSystem implements GFileSystem { +public class ZipFileSystem extends SevenZipFileSystem { - private FileSystemIndexHelper fsIndexHelper; - private FSRLRoot fsrl; - private ZipFile zipFile; - private FileSystemRefManager refManager = new FileSystemRefManager(this); - - public ZipFileSystem(FSRLRoot fsrl) { - this.fsrl = fsrl; - this.fsIndexHelper = new FileSystemIndexHelper<>(this, fsrl); + public ZipFileSystem(FSRLRoot fsrl, FileSystemService fsService) { + super(fsrl, fsService); } - @Override - public String getName() { - return fsrl.getContainer().getName(); - } - - @Override - public void close() throws IOException { - refManager.onClose(); - if (zipFile != null) { - zipFile.close(); - zipFile = null; - } - fsIndexHelper.clear(); - } - - @Override - public boolean isClosed() { - return zipFile == null; - } - - @Override - public FSRLRoot getFSRL() { - return fsrl; - } - - @Override - public int getFileCount() { - return fsIndexHelper.getFileCount(); - } - - public void mount(File f, TaskMonitor monitor) throws CancelledException, IOException { - this.zipFile = new ZipFile(f); - - Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { - monitor.checkCanceled(); - ZipEntry currentEntry = entries.nextElement(); - fsIndexHelper.storeFile(currentEntry.getName(), -1, currentEntry.isDirectory(), - currentEntry.getSize(), currentEntry); - } - } - - public Map getInfoMap(ZipEntry blob) { - Map info = new HashMap<>(); - info.put("Name", blob.getName()); - info.put("Comment", blob.getComment()); - info.put("Compressed Size", "0x" + Long.toHexString(blob.getCompressedSize())); - info.put("Uncompressed Size", "0x" + Long.toHexString(blob.getSize())); - info.put("CRC", "0x" + Long.toHexString(blob.getCrc())); - info.put("Compression Method", "0x" + Integer.toHexString(blob.getMethod())); - info.put("Time", new Date(blob.getTime()).toString()); - info.put("Extra Bytes", - (blob.getExtra() == null ? "null" : Arrays.toString(blob.getExtra()))); - return info; - } - - @Override - public String toString() { - return "ZipFilesystem [ fsrl=" + fsrl + ", filename=" + zipFile.getName() + " ]"; - } - - @Override - public GFile lookup(String path) throws IOException { - return fsIndexHelper.lookup(path); - } - - @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) - throws IOException, CancelledException { - ZipEntry zipEntry = fsIndexHelper.getMetadata(file); - return (zipEntry != null) ? zipFile.getInputStream(zipEntry) : null; - } - - @Override - public List getListing(GFile directory) throws IOException { - return fsIndexHelper.getListing(directory); - } - - @Override - public String getInfo(GFile file, TaskMonitor monitor) { - ZipEntry zipEntry = fsIndexHelper.getMetadata(file); - return (zipEntry != null) ? FSUtilities.infoMapToString(getInfoMap(zipEntry)) : null; - } - - @Override - public FileSystemRefManager getRefManager() { - return refManager; - } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/zip/ZipFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/zip/ZipFileSystemFactory.java index 950e82386f..6ba38cce49 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/zip/ZipFileSystemFactory.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/zip/ZipFileSystemFactory.java @@ -15,35 +15,18 @@ */ package ghidra.file.formats.zip; -import java.io.File; import java.io.IOException; +import ghidra.app.util.bin.ByteProvider; import ghidra.formats.gfilesystem.*; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryWithFile; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; public class ZipFileSystemFactory - implements GFileSystemFactoryWithFile, GFileSystemProbeBytesOnly { - - public static final int START_BYTES_REQUIRED = 2; - - @Override - public ZipFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, File containerFile, - FileSystemService fsService, TaskMonitor monitor) - throws IOException, CancelledException { - - ZipFileSystem fs = new ZipFileSystem(targetFSRL); - try { - fs.mount(containerFile, monitor); - return fs; - } - catch (IOException ioe) { - fs.close(); - throw ioe; - } - } + implements GFileSystemFactoryByteProvider, GFileSystemProbeBytesOnly { + private static final int START_BYTES_REQUIRED = 2; @Override public int getBytesRequired() { @@ -55,4 +38,19 @@ public class ZipFileSystemFactory return startBytes[0] == 'P' && startBytes[1] == 'K'; } + @Override + public ZipFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { + ZipFileSystem fs = new ZipFileSystem(targetFSRL, fsService); + try { + fs.mount(byteProvider, monitor); + return fs; + } + catch (IOException ioe) { + fs.close(); + throw ioe; + } + } + } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/jad/JarDecompiler.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/jad/JarDecompiler.java index c6ee7063cd..6269819f2b 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/jad/JarDecompiler.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/jad/JarDecompiler.java @@ -16,18 +16,18 @@ package ghidra.file.jad; import java.io.*; -import java.util.Enumeration; import java.util.Iterator; -import java.util.zip.*; +import java.util.List; +import java.util.zip.ZipException; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.filefilter.FalseFileFilter; import org.apache.commons.io.filefilter.TrueFileFilter; +import ghidra.app.util.bin.ByteProvider; import ghidra.app.util.importer.MessageLog; -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.FileSystemService; +import ghidra.formats.gfilesystem.*; import ghidra.util.exception.CancelledException; import ghidra.util.task.*; import utilities.util.FileUtilities; @@ -92,73 +92,28 @@ public class JarDecompiler { private void unzip(TaskMonitor monitor) throws ZipException, IOException, FileNotFoundException, CancelledException { - File file = FileSystemService.getInstance().getFile(jarFile, monitor); - ZipFile zipFile = new ZipFile(file); - - monitor.initialize(countZipEntries(zipFile, monitor)); - - try { - Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { + FileSystemService fsService = FileSystemService.getInstance(); + try (GFileSystem fs = fsService.openFileSystemContainer(jarFile, monitor)) { + List files = FSUtilities.listFileSystem(fs, null, null, monitor); + monitor.initialize(files.size()); + for (GFile file : files) { if (monitor.isCancelled()) { break; } - ZipEntry zipEntry = entries.nextElement(); - File absoluteFile = new File(outputDirectory.getAbsolutePath(), zipEntry.getName()); - if (!FileUtilities.isPathContainedWithin(outputDirectory, absoluteFile)) { - throw new IOException("Extracted file " + absoluteFile.getPath() + + File outputFile = new File(outputDirectory.getAbsolutePath(), file.getPath()); + if (!FileUtilities.isPathContainedWithin(outputDirectory, outputFile)) { + throw new IOException("Extracted file " + outputFile.getPath() + " would be outside of root destination directory: " + outputDirectory); } - FileUtilities.checkedMkdirs(absoluteFile.getParentFile()); - if (!zipEntry.isDirectory()) { + FileUtilities.checkedMkdirs(outputFile.getParentFile()); + if (!file.isDirectory()) { monitor.setMessage("Unzipping jar file... "); monitor.incrementProgress(1); - writeFile(zipFile, zipEntry, absoluteFile, monitor); + try (ByteProvider fileBP = fs.getByteProvider(file, monitor)) { + FSUtilities.copyByteProviderToFile(fileBP, outputFile, monitor); + } } } } - finally { - zipFile.close(); - } - } - - private void writeFile(ZipFile zipFile, ZipEntry zipEntry, File absoluteFile, - TaskMonitor monitor) throws IOException { - byte[] bytes = new byte[0x100000]; - InputStream inputStream = zipFile.getInputStream(zipEntry); - try { - OutputStream outputStream = new FileOutputStream(absoluteFile); - try { - while (true) { - if (monitor.isCancelled()) { - break; - } - int nRead = inputStream.read(bytes); - if (nRead == -1) { - break; - } - outputStream.write(bytes, 0, nRead); - } - } - finally { - outputStream.close(); - } - } - finally { - inputStream.close(); - } - } - - private int countZipEntries(ZipFile zipFile, TaskMonitor monitor) { - int count = 0; - Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { - if (monitor.isCancelled()) { - break; - } - entries.nextElement(); - ++count; - } - return count; } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/plugins/fileformats/FileFormatsPlugin.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/plugins/fileformats/FileFormatsPlugin.java index 0297ce60f8..1960457bc6 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/plugins/fileformats/FileFormatsPlugin.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/plugins/fileformats/FileFormatsPlugin.java @@ -22,9 +22,8 @@ import java.util.List; import org.apache.commons.io.FilenameUtils; -import docking.ActionContext; import docking.action.DockingAction; -import docking.action.MenuData; +import docking.action.builder.ActionBuilder; import docking.widgets.OptionDialog; import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooserMode; @@ -91,176 +90,126 @@ public class FileFormatsPlugin extends Plugin implements FrontEndable { 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; + } - FSBAction action = new FSBAction("Export Eclipse Project", this) { - @Override - public void actionPerformed(ActionContext context) { - if (context instanceof FSBActionContext) { - FSBActionContext fsbContext = (FSBActionContext) context; - FSRL fsrl = FSBUtils.getFileFSRLFromContext(context); - 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; - } - fsbContext.getTree() - .runTask( - monitor -> doExportToEclipse(fsrl, outputDirectory, monitor)); - } - } - - private void doExportToEclipse(FSRL fsrl, File outputDirectory, TaskMonitor monitor) { - try (RefdFile refdFile = - FileSystemService.getInstance().getRefdFile(fsrl, monitor)) { - AndroidProjectCreator creator = - new AndroidProjectCreator(refdFile.file, 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); - } - - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - if (JadProcessWrapper.isJadPresent() && (context instanceof FSBActionContext)) { - FSBActionContext fsbContext = (FSBActionContext) context; - FSRL fsrl = FSBUtils.getFileFSRLFromContext(context); - return !fsbContext.getTree().isBusy() && (fsrl != null) && - (fsrl.getName() != null) && - ("apk".equalsIgnoreCase(FilenameUtils.getExtension(fsrl.getName()))); - } - return false; - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return context instanceof FSBActionContext; - } - }; - action.setPopupMenuData( - new MenuData(new String[] { action.getMenuText() }, ImageManager.ECLIPSE, "H")); - action.setEnabled(true); - return action; + 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; + } - FSBAction action = new FSBAction("Decompile JAR", this) { - @Override - public void actionPerformed(ActionContext context) { - if (context instanceof FSBActionContext) { - FSBActionContext fsbContext = (FSBActionContext) context; - FSRL jarFSRL = FSBUtils.getFileFSRLFromContext(context); - 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 (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 = fsbContext.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()); + 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); - } - }); - } - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - if (JadProcessWrapper.isJadPresent() && (context instanceof FSBActionContext)) { - FSBActionContext fsbContext = (FSBActionContext) context; - FSRL fsrl = FSBUtils.getFileFSRLFromContext(context); - return !fsbContext.getTree().isBusy() && (fsrl != null) && - JarDecompiler.isJarFilename(fsrl.getName()); - } - return false; - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return context instanceof FSBActionContext; - } - - }; - action.setPopupMenuData( - new MenuData(new String[] { action.getMenuText() }, ImageManager.JAR, "J")); - action.setEnabled(true); - return action; + catch (Exception e) { + FSUtilities.displayException(this, gTree, "Error Decompiling Jar", + e.getMessage(), e); + } + }); + }) + .build(); } private DockingAction createCryptoTemplateAction() { - FSBAction action = new FSBAction("Create Crypto Key Template", this) { - @Override - public void actionPerformed(ActionContext context) { - FSRL fsrl = FSBUtils.getFSRLFromContext(context, true); - if (context.getContextObject() instanceof FSBRootNode && fsrl != null) { - createCryptoTemplate(fsrl, (FSBRootNode) context.getContextObject()); - } - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - if (context instanceof FSBActionContext) { - FSBActionContext fsbContext = (FSBActionContext) context; - FSRL fsrl = FSBUtils.getFSRLFromContext(context, true); - return !fsbContext.getTree().isBusy() && (fsrl != null) && - (context.getContextObject() instanceof FSBRootNode); - } - return false; - } - - @Override - public boolean isAddToPopup(ActionContext context) { - return context instanceof FSBActionContext; - } - }; - action.setPopupMenuData(new MenuData(new String[] { action.getMenuText() + "..." }, - ImageManager.KEY, "Z", MenuData.NO_MNEMONIC, "B")); - action.setEnabled(true); - return action; + 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(); } /** @@ -316,76 +265,52 @@ public class FileFormatsPlugin extends Plugin implements FrontEndable { } private DockingAction createLoadKernelAction() { - FSBAction action = new FSBAction("Load iOS Kernel", this) { - @Override - public void actionPerformed(ActionContext context) { - if (context instanceof FSBActionContext) { - FSBActionContext fsbContext = (FSBActionContext) context; - Object contextObject = fsbContext.getContextObject(); - - FSRL fsrl = FSBUtils.getFSRLFromContext(context, true); - List fileList = new ArrayList<>(); - - if (fsrl != null) { - if (contextObject instanceof FSBRootNode) { - List children = ((FSBRootNode) contextObject).getChildren(); - for (GTreeNode childNode : children) { - if (childNode instanceof FSBNode) { - FSBNode baseNode = (FSBNode) childNode; - fileList.add(baseNode.getFSRL()); - } - } - } - else if (contextObject instanceof FSBFileNode || - contextObject 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."); - } - } - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - if (context instanceof FSBActionContext) { - FSBActionContext fsbContext = (FSBActionContext) context; - if (fsbContext.getTree().isBusy()) { + return new ActionBuilder("FSB Load iOS Kernel", this.getName()) + .withContext(FSBActionContext.class) + .enabledWhen(ac -> { + if (ac.isBusy()) { return false; } - Object contextObject = context.getContextObject(); - if (contextObject instanceof FSBFileNode || - contextObject instanceof FSBDirNode) { - contextObject = FSBUtils.getNodesRoot((FSBNode) contextObject); - } - if (contextObject instanceof FSBRootNode) { - FSBRootNode node = (FSBRootNode) contextObject; - return node.getFSRef() != null && - node.getFSRef().getFilesystem() instanceof PrelinkFileSystem; - } - } - return false; - } + FSBRootNode rootNode = ac.getRootOfSelectedNode(); + return rootNode != null && rootNode.getFSRef() != null && + rootNode.getFSRef().getFilesystem() instanceof PrelinkFileSystem; + }) + .popupMenuPath("Load iOS Kernel") + .popupMenuIcon(ImageManager.iOS) + .popupMenuGroup("I") + .onAction( + ac -> { + FSRL fsrl = ac.getFSRL(true); + List fileList = new ArrayList<>(); - @Override - public boolean isAddToPopup(ActionContext context) { - return context instanceof FSBActionContext; - } - }; + 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); + } + } - action.setPopupMenuData( - new MenuData(new String[] { action.getMenuText() }, ImageManager.iOS, "I")); - action.setEnabled(true); - return action; + 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(); } /** diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/util/MonitoredOutputStream.java b/Ghidra/Framework/Utility/src/main/java/ghidra/util/MonitoredOutputStream.java index 39b06ad558..430735620f 100644 --- a/Ghidra/Framework/Utility/src/main/java/ghidra/util/MonitoredOutputStream.java +++ b/Ghidra/Framework/Utility/src/main/java/ghidra/util/MonitoredOutputStream.java @@ -15,12 +15,12 @@ */ package ghidra.util; -import ghidra.util.exception.IOCancelledException; -import ghidra.util.task.TaskMonitor; - import java.io.IOException; import java.io.OutputStream; +import ghidra.util.exception.IOCancelledException; +import ghidra.util.task.TaskMonitor; + /** * An OutputStream which utilizes a TaskMonitor to indicate output progress and * allows the operation to be cancelled via the TaskMonitor. @@ -32,20 +32,13 @@ public class MonitoredOutputStream extends OutputStream { protected OutputStream out; private TaskMonitor monitor; private int smallCount = 0; - private int count = 0; + private long count = 0; public MonitoredOutputStream(OutputStream out, TaskMonitor monitor) { this.out = out; this.monitor = monitor; } - /** - * Reset the current progress count to the specified value. - */ - public void setProgress(int count) { - this.count = count; - } - /** * Writes the specified byte to this output stream. *

    @@ -63,8 +56,9 @@ public class MonitoredOutputStream extends OutputStream { out.write(b); ++smallCount; if (smallCount >= PROGRESS_INCREMENT) { - if (monitor.isCancelled()) + if (monitor.isCancelled()) { throw new IOCancelledException(); + } count += smallCount; smallCount = 0; monitor.setProgress(count); @@ -118,8 +112,9 @@ public class MonitoredOutputStream extends OutputStream { smallCount += len; if (smallCount >= PROGRESS_INCREMENT) { - if (monitor.isCancelled()) + if (monitor.isCancelled()) { throw new IOCancelledException(); + } count += smallCount; smallCount = 0; monitor.setProgress(count); diff --git a/GhidraBuild/Skeleton/src/main/java/skeleton/SkeletonFileSystem.java b/GhidraBuild/Skeleton/src/main/java/skeleton/SkeletonFileSystem.java index d6119ea9b8..ed31d90772 100644 --- a/GhidraBuild/Skeleton/src/main/java/skeleton/SkeletonFileSystem.java +++ b/GhidraBuild/Skeleton/src/main/java/skeleton/SkeletonFileSystem.java @@ -15,15 +15,17 @@ */ package skeleton; -import java.io.*; -import java.util.*; +import java.io.IOException; +import java.util.List; import ghidra.app.util.bin.ByteProvider; -import ghidra.app.util.bin.ByteProviderInputStream; +import ghidra.app.util.bin.ByteProviderWrapper; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; -import ghidra.formats.gfilesystem.factory.GFileSystemFactoryFull; -import ghidra.formats.gfilesystem.factory.GFileSystemProbeFull; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; +import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider; +import ghidra.formats.gfilesystem.fileinfo.FileAttributeType; +import ghidra.formats.gfilesystem.fileinfo.FileAttributes; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -111,14 +113,14 @@ public class SkeletonFileSystem implements GFileSystem { } @Override - public InputStream getInputStream(GFile file, TaskMonitor monitor) + public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { - // TODO: Get an input stream for a file. The following is an example of how the metadata - // might be used to get an input stream from a stored provider offset. + // TODO: Get an ByteProvider for a file. The following is an example of how the metadata + // might be used to get an sub-ByteProvider from a stored provider offset. MyMetadata metadata = fsih.getMetadata(file); return (metadata != null) - ? new ByteProviderInputStream(provider, metadata.offset, metadata.size) + ? new ByteProviderWrapper(provider, metadata.offset, metadata.size, file.getFSRL()) : null; } @@ -128,30 +130,25 @@ public class SkeletonFileSystem implements GFileSystem { } @Override - public String getInfo(GFile file, TaskMonitor monitor) { + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { MyMetadata metadata = fsih.getMetadata(file); - return (metadata == null) ? null : FSUtilities.infoMapToString(getInfoMap(metadata)); - } - - public Map getInfoMap(MyMetadata metadata) { - Map info = new LinkedHashMap<>(); - - // TODO: Customize information about a file system entry. The following is sample - // information that might be useful. - info.put("Name", metadata.name); - info.put("Size", - "" + Long.toString(metadata.size) + ", 0x" + Long.toHexString(metadata.size)); - return info; + FileAttributes result = new FileAttributes(); + if (metadata != null) { + result.add(FileAttributeType.NAME_ATTR, metadata.name); + result.add(FileAttributeType.SIZE_ATTR, metadata.size); + } + return result; } // TODO: Customize for the real file system. public static class MyFileSystemFactory - implements GFileSystemFactoryFull, GFileSystemProbeFull { + implements GFileSystemFactoryByteProvider, + GFileSystemProbeByteProvider { @Override - public SkeletonFileSystem create(FSRL containerFSRL, FSRLRoot targetFSRL, - ByteProvider byteProvider, File containerFile, FileSystemService fsService, - TaskMonitor monitor) throws IOException, CancelledException { + public SkeletonFileSystem create(FSRLRoot targetFSRL, + ByteProvider byteProvider, FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { SkeletonFileSystem fs = new SkeletonFileSystem(targetFSRL, byteProvider); fs.mount(monitor); @@ -159,9 +156,8 @@ public class SkeletonFileSystem implements GFileSystem { } @Override - public boolean probe(FSRL containerFSRL, ByteProvider byteProvider, File containerFile, - FileSystemService fsService, TaskMonitor monitor) - throws IOException, CancelledException { + public boolean probe(ByteProvider byteProvider, FileSystemService fsService, + TaskMonitor monitor) throws IOException, CancelledException { // TODO: Quickly and efficiently examine the bytes in 'byteProvider' to determine if // it's a valid file system. If it is, return true.