GP-253, GP-725 Gfilesystem ByteProviders, obfuscated temp files, passwords

Migrate GFilesystem methods to use ByteProviders instead of java Files for their source, and to produce ByteProviders instead of InputStreams.

Refactor file info query method to return a structured collection of enum specified meta data instead of a free-form multiline string.

Add locked icon badge to files in the file system browser that are password protected.

Reduces the number of temp files created on disk, and obfuscates files that are created to avoid the wrath of virus scanners (in the same manner that ghidra db files are obfuscated).

Add support for filesystems to query for passwords to decrypt files.

Refactor the SevenZipFileSystem implementation to decrypt files embedded inside zips that were created with passwords.

Fix Ext4 to support 128 byte inodes.
This commit is contained in:
dev747368
2021-10-01 10:09:38 -04:00
parent a0afc0b8c2
commit 72fe7b89d2
156 changed files with 7948 additions and 4714 deletions
@@ -38,6 +38,7 @@
<file_overlay name="imported" icon="images/checkmark_green.gif" />
<file_overlay name="filesystem" icon="images/nuvola/16x16/ledgreen.png" quadrant="LL" />
<file_overlay name="password_missing" icon="images/lock.png" quadrant="UR" />
</file_extensions>
@@ -123,6 +123,19 @@
of the file systems will display that file system's browser tree.</P>
</BLOCKQUOTE>
<H3><A name="FSB_Clear_Cached_Passwords"></A>Clear Cached Passwords</H3>
<BLOCKQUOTE>
<P>Clears any cached passwords that previously entered when accessing a password
protected container file.</P>
</BLOCKQUOTE>
<H3><A name="FSB_Refresh"></A>Refresh</H3>
<BLOCKQUOTE>
<P>Refreshes the status of the selected items.</P>
</BLOCKQUOTE>
<H3><A name="FSB_Close"></A>Close</H3>
<BLOCKQUOTE>
@@ -150,6 +163,20 @@
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="PasswordDialog"></A>Password Dialog</H2>
<BLOCKQUOTE>
<P>This dialog is used to prompt for a password when opening a password protected container.</P>
<P>If the password isn't known, the <b>Cancel</b> 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 <b>Cancel All</b>
will stop Ghidra from prompting for passwords for any file during the current operation.</P>
<P>Passwords can also be provided via a text file specified on the java command line, useful
for headless operations. See <b>ghidra.formats.gfilesystem.crypto.CmdLinePasswordProvider</b></P>
</BLOCKQUOTE>
<H2>How To Handle Unsupported File Systems</H2>
<BLOCKQUOTE>
@@ -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.
* <p>
* 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.
* <p>
* 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
@@ -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 {
* <p>
* The caller is responsible for closing the returned {@link InputStream} instance.
* <p>
* 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);
}
}
@@ -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}.
* <p>
* 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 <b>DOES</b>
* {@link ByteProvider#close() close()} the underlying ByteProvider when
* closed itself.
* <p>
*/
public static class ClosingInputStream extends ByteProviderInputStream {
/**
* Creates an {@link InputStream} that reads from a {@link ByteProvider}, that
* <b>DOES</b> {@link ByteProvider#close() close()} the underlying ByteProvider when
* closed itself.
* <p>
* @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;
}
}
@@ -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.
* <p>
*
* @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
@@ -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();
}
}
@@ -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<Long, Buffer> buffers = new ReferenceMap<>();
private LRUMap<Long, Buffer> 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.
* <p>
* See {@link InputStream#read(byte[], int, int)}.
* <p>
*
* @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.
* <p>
* 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.
* <p>
* 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";
}
}
}
@@ -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
}
}
}
@@ -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);
}
}
@@ -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);
}
}
}
}
@@ -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);
}
}
@@ -29,7 +29,10 @@ import ghidra.util.Msg;
* {@link ArrayIndexOutOfBoundsException}s.
* <p>
* See {@link SynchronizedByteProvider} as a solution.
* <p>
* @deprecated See {@link FileByteProvider} as replacement ByteProvider.
*/
@Deprecated(since = "10.1", forRemoval = true)
public class RandomAccessByteProvider implements ByteProvider {
protected File file;
protected GhidraRandomAccessFile randomAccessFile;
@@ -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);
@@ -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);
}
}
@@ -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);
@@ -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<String> stringTable = new ArrayList<String>();
private List<String> stringTable = new ArrayList<>();
private long _fileOffset;
private List<Integer> stringLengths = new ArrayList<Integer>();
private List<Integer> 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<String>(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<String>(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);
@@ -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
@@ -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<Loader> {
* @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("[\\\\:|]+", "/");
}
/**
@@ -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)) {
@@ -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.
* <p>
* 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)}.
* <p>
* The implementation needs to return an {@link InputStream} that contains the bytes
* of the derived file.
* <p>
* @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;
}
@@ -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)}.
* <p>
* The implementation needs to write bytes to the supplied {@link OutputStream}.
* <p>
* @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;
}
@@ -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.
* <p>
@@ -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 {
* <p>
* 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.
* <p>
* @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) {
@@ -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> 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 -&gt; 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.
* <p>
* 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.
* <p>
* 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<String> 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);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -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.
* <p>
* Used to quickly verify that a local file hasn't changed.
*
*/
public class FileFingerprintCache {
private ReferenceMap<FileFingerprintRec, String> 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;
}
}
}
@@ -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.
* <p>
* Threadsafe after initial use of {@link #storeFile(String, int, boolean, long, Object) storeFile()}
* by the owning filesystem.
* Threadsafe (methods are synchronized).
* <p>
* 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<METADATATYPE> {
private GFile rootDir;
static class FileData<METADATATYPE> {
GFile file;
METADATATYPE metaData;
long fileIndex;
}
protected Map<GFile, METADATATYPE> fileToEntryMap = new HashMap<>();
protected Map<GFile, Map<String, GFile>> directoryToListing = new HashMap<>();
protected Map<GFile, FileData<METADATATYPE>> fileToEntryMap = new HashMap<>();
protected Map<Long, FileData<METADATATYPE>> fileIndexToEntryMap = new HashMap<>();
protected Map<GFile, Map<String, FileData<METADATATYPE>>> directoryToListing = new HashMap<>();
/**
* Creates a new {@link FileSystemIndexHelper} for the specified {@link GFileSystem}.
@@ -49,6 +59,17 @@ public class FileSystemIndexHelper<METADATATYPE> {
*/
public FileSystemIndexHelper(GFileSystem fs, FSRLRoot fsFSRL) {
this.rootDir = GFileImpl.fromFSRL(fs, null, fsFSRL.withPath("/"), true, -1);
initRootDir(null);
}
private void initRootDir(METADATATYPE metadata) {
FileData<METADATATYPE> 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<METADATATYPE> {
/**
* 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<METADATATYPE> {
/**
* 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<METADATATYPE> 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<METADATATYPE> 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<METADATATYPE> 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<GFile> getListing(GFile directory) {
Map<String, GFile> dirListing = getDirectoryContents(directory, false);
List<GFile> results =
(dirListing != null) ? new ArrayList<>(dirListing.values()) : Collections.emptyList();
return results;
public synchronized List<GFile> getListing(GFile directory) {
Map<String, FileData<METADATATYPE>> 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<String, GFile> dirListing = getDirectoryContents(parent, false);
return (dirListing != null) ? dirListing.get(name) : null;
Map<String, FileData<METADATATYPE>> dirListing = getDirectoryContents(parent, false);
FileData<METADATATYPE> fileData = (dirListing != null) ? dirListing.get(name) : null;
return (fileData != null) ? fileData.file : null;
}
/**
@@ -133,38 +182,27 @@ public class FileSystemIndexHelper<METADATATYPE> {
* suffix added to the resultant GFile name, where nnn is the file's
* order of occurrence in the container file.
* <p>
*
* @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<String, GFile> 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<METADATATYPE> fileData =
doStoreFile(lastpart, parent, fileIndex, isDirectory, length, metadata);
return fileData.file;
}
/**
@@ -176,47 +214,81 @@ public class FileSystemIndexHelper<METADATATYPE> {
* suffix added to the resultant GFile name, where nnn is the file's
* order of occurrence in the container file.
* <p>
*
* @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<METADATATYPE> fileData =
doStoreFile(filename, parent, fileIndex, isDirectory, length, metadata);
return fileData.file;
}
private FileData<METADATATYPE> doStoreMissingDir(String filename, GFile parent) {
parent = (parent == null) ? rootDir : parent;
int fileNum = (fileIndex != -1) ? fileIndex : fileToEntryMap.size();
Map<String, GFile> dirContents = getDirectoryContents(parent, true);
String uniqueName = dirContents.containsKey(filename) && !isDirectory
? filename + "[" + Integer.toString(fileNum) + "]"
: filename;
Map<String, FileData<METADATATYPE>> dirContents = getDirectoryContents(parent, true);
GFile file = createNewFile(parent, filename, true, -1, null);
GFile file = createNewFile(parent, uniqueName, isDirectory, length, fileInfo);
FileData<METADATATYPE> 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<METADATATYPE> 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<String, FileData<METADATATYPE>> dirContents = getDirectoryContents(parent, true);
String uniqueName = makeUniqueFilename(dirContents.containsKey(filename) && !isDirectory,
filename, fileNum);
GFile file = createNewFile(parent, uniqueName, isDirectory, length, metadata);
FileData<METADATATYPE> 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-&gt;GFile map that holds the contents of a single directory.
* @param directoryFile
* @return
*/
protected Map<String, GFile> getDirectoryContents(GFile directoryFile,
private String makeUniqueFilename(boolean wasNameCollision, String filename, long fileIndex) {
return wasNameCollision
? filename + "[" + Long.toString(fileIndex) + "]"
: filename;
}
private Map<String, FileData<METADATATYPE>> getDirectoryContents(GFile directoryFile,
boolean createIfMissing) {
directoryFile = (directoryFile != null) ? directoryFile : rootDir;
Map<String, GFile> dirContents = directoryToListing.get(directoryFile);
Map<String, FileData<METADATATYPE>> dirContents = directoryToListing.get(directoryFile);
if (dirContents == null && createIfMissing) {
dirContents = new HashMap<>();
directoryToListing.put(directoryFile, dirContents);
@@ -242,23 +314,21 @@ public class FileSystemIndexHelper<METADATATYPE> {
protected GFile lookupParent(String[] nameparts) {
GFile currentDir = rootDir;
GFile currentFile = rootDir;
for (int i = 0; i < nameparts.length - 1; i++) {
Map<String, GFile> currentDirContents = getDirectoryContents(currentDir, true);
Map<String, FileData<METADATATYPE>> 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<METADATATYPE> 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<METADATATYPE> {
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<METADATATYPE> 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<String, FileData<METADATATYPE>> 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();
@@ -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);
}
}
}
@@ -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());
}
}
File diff suppressed because it is too large Load Diff
@@ -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;
}
@@ -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();
}
}
@@ -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;
* <p>
* Operations take a {@link TaskMonitor} if they need to be cancel-able.
* <p>
* 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}.
* <p>
* NOTE:<p>
* 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}.
* <p>
* 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<GFile> 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.
* <p>
* TODO:{@literal this method needs to be refactored to return a Map<String, String>; instead of}
* a pre-formatted multi-line string.
* <p>
* @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}.
* <p>
*
* @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;
}
}
@@ -129,26 +129,6 @@ public abstract class GFileSystemBase implements GFileSystem {
@Override
abstract public List<GFile> 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
@@ -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;
}
@@ -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<GFile> emptyDir = Collections.emptyList();
private final List<GFile> emptyDir = List.of();
private final FSRLRoot fsFSRL;
private final FileSystemRefManager refManager = new FileSystemRefManager(this);
private final ReferenceMap<FileFingerprintRec, String> 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;
}
}
}
@@ -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<GFile> 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<GFile> 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);
}
}
@@ -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.
* <p>
* 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();
}
}
@@ -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;
@@ -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<T> 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<Object> listModel = new DefaultListModel<Object>() {
DefaultListModel<Object> listModel = new DefaultListModel<>() {
@Override
public String getElementAt(int index) {
@SuppressWarnings("unchecked")
T t = (T) super.getElementAt(index);
return toStringFunc.apply(t);
}
@@ -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())) {
@@ -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.
* <p>
* Threadsafe.
*/
public class CachedPasswordProvider implements PasswordProvider {
private Map<String, List<CryptoRec>> 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<CryptoRec> 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<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt,
Session session) {
Set<CryptoRec> 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<PasswordValue> 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<PasswordValue> {
Iterator<PasswordValue> delegate;
CloningPasswordIterator(Iterator<PasswordValue> it) {
this.delegate = it;
}
@Override
public boolean hasNext() {
return delegate.hasNext();
}
@Override
public PasswordValue next() {
PasswordValue result = delegate.next();
return result.clone();
}
}
}
@@ -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.
* <p>
* Example: <pre>java -Dfilesystem.passwords=/fullpath/to/textfile</pre>
* <p>
* The password file is a plain text tabbed-csv file, where each line
* specifies a password and an optional file identifier.
* <p>
* 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):
* <p>
* <pre>
* <code>password1 [tab] myfirstzipfile.zip</code> <b>&larr; supplies a password for the named file located in any directory</b>
* <code>someOtherPassword [tab] /full/path/tozipfile.zip</code> <b>&larr; supplies password for file at specified location</b>
* <code>anotherPassword [tab] file:///full/path/tozipfile.zip|zip:///subdir/in/zip/somefile.txt</code> <b>&larr; supplies password for file embedded inside a zip</b>
* <code>yetAnotherPassword</code> <b>&larr; a password to try for any file that needs a password</b>
* </pre>
*
*
*/
public class CmdLinePasswordProvider implements PasswordProvider {
public static final String CMDLINE_PASSWORD_PROVIDER_PROPERTY_NAME = "filesystem.passwords";
@Override
public Iterator<PasswordValue> 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<PasswordValue> load(File f, FSRL fsrl) {
List<PasswordValue> 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;
}
}
@@ -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.
* <p>
* 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 <T> 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> T getStateValue(CryptoProvider cryptoProvider, Supplier<T> stateFactory);
/**
* Returns the {@link CryptoProviders} instance that created this session.
*
* @return the {@link CryptoProviders} instance that created this session
*/
CryptoProviders getCryptoProviders();
}
}
@@ -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<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt) {
return parentSession.getPasswordsFor(fsrl, prompt);
}
@Override
public void addSuccessfulPassword(FSRL fsrl, PasswordValue password) {
parentSession.addSuccessfulPassword(fsrl, password);
}
}
@@ -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<CryptoProvider> 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.
* <p>
* 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}.
* <p>
* (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 <T> CryptoProvider type
* @param providerClass {@link CryptoProvider} class
* @return previously registered CryptoProvider instance, or null if not found
*/
public <T extends CryptoProvider> T getCryptoProviderInstance(Class<T> providerClass) {
return cryptoProviders.stream()
.filter(providerClass::isInstance)
.map(providerClass::cast)
.findFirst()
.orElse(null);
}
/**
* Creates a new {@link CryptoSession}.
* <p>
* 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<CryptoProvider> providers;
private Map<CryptoProvider, Object> sessionStateValues = new IdentityHashMap<>();
private boolean closed;
public CryptoProviderSessionImpl(List<CryptoProvider> 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> T getStateValue(CryptoProvider cryptoProvider,
Supplier<T> 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<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt) {
return new PasswordIterator(providers, fsrl, prompt);
}
/**
* Union iterator of all password providers
*/
class PasswordIterator implements Iterator<PasswordValue> {
private List<PasswordProvider> providers;
private Iterator<PasswordValue> currentIt;
private String prompt;
private FSRL fsrl;
PasswordIterator(List<CryptoProvider> 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();
}
}
}
}
@@ -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.
* <p>
* Typically used to query passwords and to add known good passwords
* to caches for later re-retrieval.
* <p>
* 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.
* <p>
* 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<PasswordValue> 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();
}
@@ -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.
* <p>
* User can cancel, or cancel-all, which can be determined by inspecting
* the value of the semi-visible member variables.
* <p>
* 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;
}
}
}
@@ -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.
* <p>
* Instances are typically not called directly, instead are used
* by a {@link CryptoSession} along with other provider instances to provide
* a balanced breakfast.
* <p>
* 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.
* <p>
* 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<PasswordValue> getPasswordsFor(FSRL fsrl, String prompt, Session session);
}
@@ -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.
* <p>
* {@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.
* <p>
* 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);
}
}

Some files were not shown because too many files have changed in this diff Show More