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.