Merge remote-tracking branch 'origin/GP-5842_dev747368_zstd_cli--SQUASHED'

This commit is contained in:
Ryan Kurtz
2025-11-21 09:39:39 -05:00
50 changed files with 1525 additions and 762 deletions
@@ -4,9 +4,9 @@
* 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.
@@ -28,7 +28,7 @@ public interface ByteProvider extends Closeable {
/**
* A static re-usable empty {@link ByteProvider} instance.
*/
public static final ByteProvider EMPTY_BYTEPROVIDER = new EmptyByteProvider();
ByteProvider EMPTY_BYTEPROVIDER = new EmptyByteProvider();
/**
* Returns the {@link FSRL} of the underlying file for this byte provider,
@@ -37,7 +37,7 @@ public interface ByteProvider extends Closeable {
* @return The {@link FSRL} of the underlying {@link File}, or null if no associated
* {@link File}.
*/
default public FSRL getFSRL() {
default FSRL getFSRL() {
File f = getFile();
return (f != null) ? FileSystemService.getInstance().getLocalFSRL(f) : null;
}
@@ -48,14 +48,14 @@ public interface ByteProvider extends Closeable {
*
* @return the underlying file for this byte provider
*/
public File getFile();
File getFile();
/**
* Returns the name of the {@link ByteProvider}. For example, the underlying file name.
*
* @return the name of the {@link ByteProvider} or null if there is no name
*/
public String getName();
String getName();
/**
* Returns the absolute path (similar to, but not a, URI) to the {@link ByteProvider}.
@@ -64,28 +64,22 @@ public interface ByteProvider extends Closeable {
* @return the absolute path to the {@link ByteProvider} or null if not associated with a
* {@link File}.
*/
public String getAbsolutePath();
String getAbsolutePath();
/**
* Returns the length of the {@link ByteProvider}
*
* @return the length of the {@link ByteProvider}
* @throws IOException if an I/O error occurs
*/
public long length() throws IOException;
long length();
/**
* Returns true if this ByteProvider does not contain any bytes.
*
* @return boolean true if this provider is empty, false if contains bytes
*/
default public boolean isEmpty() {
try {
return length() == 0;
}
catch (IOException e) {
return true;
}
default boolean isEmpty() {
return length() == 0;
}
/**
@@ -94,7 +88,7 @@ public interface ByteProvider extends Closeable {
* @param index the index in the byte provider to check
* @return true if the specified index is valid
*/
public boolean isValidIndex(long index);
boolean isValidIndex(long index);
/**
* Releases any resources the {@link ByteProvider} may have occupied
@@ -102,7 +96,7 @@ public interface ByteProvider extends Closeable {
* @throws IOException if an I/O error occurs
*/
@Override
public void close() throws IOException;
void close() throws IOException;
/**
* Reads a byte at the specified index
@@ -111,7 +105,7 @@ public interface ByteProvider extends Closeable {
* @return the byte read from the specified index
* @throws IOException if an I/O error occurs
*/
public byte readByte(long index) throws IOException;
byte readByte(long index) throws IOException;
/**
* Reads a byte array at the specified index
@@ -121,7 +115,7 @@ public interface ByteProvider extends Closeable {
* @return the byte array read from the specified index
* @throws IOException if an I/O error occurs
*/
public byte[] readBytes(long index, long length) throws IOException;
byte[] readBytes(long index, long length) throws IOException;
/**
* Returns an input stream to the underlying byte provider starting at the specified index.
@@ -135,7 +129,7 @@ public interface ByteProvider extends Closeable {
* @return the {@link InputStream}
* @throws IOException if an I/O error occurs
*/
default public InputStream getInputStream(long index) throws IOException {
default InputStream getInputStream(long index) throws IOException {
if (index < 0 || index > length()) {
throw new IOException("Invalid start position: " + index);
}
@@ -103,7 +103,7 @@ public class ByteProviderWrapper implements ByteProvider {
}
@Override
public long length() throws IOException {
public long length() {
return subLength;
}
@@ -106,7 +106,7 @@ public class FileByteProvider implements MutableByteProvider {
}
@Override
public long length() throws IOException {
public long length() {
return currentLength;
}
@@ -72,7 +72,7 @@ public class InputStreamByteProvider implements ByteProvider {
}
@Override
public long length() throws IOException {
public long length() {
return length;
}
@@ -219,7 +219,7 @@ public class MemoryByteProvider implements ByteProvider {
}
@Override
public long length() throws IOException {
public long length() {
if (isEmtpy) {
return 0;
}
@@ -129,7 +129,7 @@ public class RangeMappedByteProvider implements ByteProvider {
}
@Override
public long length() throws IOException {
public long length() {
return length;
}
@@ -4,9 +4,9 @@
* 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.
@@ -55,7 +55,7 @@ public class SynchronizedByteProvider implements ByteProvider {
}
@Override
public synchronized long length() throws IOException {
public synchronized long length() {
return provider.length();
}
@@ -1067,12 +1067,7 @@ public class ElfHeader implements StructConverter {
* @return true if provider contains specified byte offset range
*/
private boolean providerContainsRegion(long offset, int length) {
try {
return offset >= 0 && (offset + length) <= provider.length();
}
catch (IOException e) {
return false;
}
return offset >= 0 && (offset + length) <= provider.length();
}
protected void parseSectionHeaders() throws IOException {
@@ -27,7 +27,6 @@ import ghidra.program.model.address.*;
import ghidra.program.model.lang.*;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.Memory;
import ghidra.util.Msg;
import ghidra.util.NumericUtilities;
import ghidra.util.exception.CancelledException;
@@ -94,12 +93,7 @@ public class BinaryLoader extends AbstractProgramLoader {
long fileOffset = 0;
long origFileLength;
boolean isOverlay = false;
try {
origFileLength = provider.length();
}
catch (IOException e) {
return "Error determining length: " + e.getMessage();
}
origFileLength = provider.length();
for (Option option : options) {
String optName = option.getName();
@@ -352,13 +346,7 @@ public class BinaryLoader extends AbstractProgramLoader {
public List<Option> getDefaultOptions(ByteProvider provider, LoadSpec loadSpec,
DomainObject domainObject, boolean loadIntoProgram, boolean mirrorFsLayout) {
long fileOffset = 0;
long origFileLength = -1;
try {
origFileLength = provider.length();
}
catch (IOException e) {
Msg.warn(this, "Error determining length", e);
}
long origFileLength = provider.length();
long length = origFileLength;
boolean isOverlay = false;
String blockName = "";
@@ -0,0 +1,118 @@
/* ###
* 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 java.util.Comparator;
import java.util.List;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.bin.ByteProviderWrapper;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Base class with common functionality for file systems that only contain a single file
*/
abstract public class AbstractSinglePayloadFileSystem implements GFileSystem {
protected final FSRLRoot fsFSRL;
protected final FileSystemRefManager refManager = new FileSystemRefManager(this);
protected final SingleFileSystemIndexHelper fsIndex;
protected ByteProvider payloadProvider;
public AbstractSinglePayloadFileSystem(FSRLRoot fsFSRL, ByteProvider payloadProvider,
String payloadFilename) {
this(fsFSRL, payloadProvider, payloadFilename, FileAttributes.EMPTY);
}
public AbstractSinglePayloadFileSystem(FSRLRoot fsFSRL, ByteProvider payloadProvider,
String payloadFilename, FileAttributes payloadAttrs) {
this.fsFSRL = fsFSRL;
this.payloadProvider = payloadProvider;
String md5 = payloadProvider.getFSRL() != null ? payloadProvider.getFSRL().getMD5() : null;
this.fsIndex = new SingleFileSystemIndexHelper(this, fsFSRL, payloadFilename,
payloadProvider.length(), md5);
this.fsIndex.setPayloadFileAttributes(payloadAttrs);
}
public GFile getPayloadFile() {
return fsIndex.getPayloadFile();
}
@Override
public void close() throws IOException {
refManager.onClose();
fsIndex.clear();
FSUtilities.uncheckedClose(payloadProvider, null);
payloadProvider = null;
}
@Override
public String getName() {
return fsFSRL.getContainer().getName();
}
@Override
public FSRLRoot getFSRL() {
return fsFSRL;
}
@Override
public boolean isClosed() {
return fsIndex.isClosed();
}
@Override
public FileSystemRefManager getRefManager() {
return refManager;
}
@Override
public int getFileCount() {
return 1;
}
@Override
public GFile lookup(String path) throws IOException {
return fsIndex.lookup(path);
}
@Override
public GFile lookup(String path, Comparator<String> nameComp) throws IOException {
return fsIndex.lookup(null, path, nameComp);
}
@Override
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor)
throws IOException, CancelledException {
if (fsIndex.isPayloadFile(file)) {
return new ByteProviderWrapper(payloadProvider, file.getFSRL());
}
return null;
}
@Override
public List<GFile> getListing(GFile directory) throws IOException {
return fsIndex.getListing(directory);
}
@Override
public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) {
return fsIndex.getFileAttributes(file);
}
}
@@ -33,6 +33,7 @@ import org.apache.commons.io.FilenameUtils;
import docking.widgets.OptionDialog;
import ghidra.app.util.bin.ByteProvider;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
import ghidra.formats.gfilesystem.factory.FileSystemFactoryDependencyException;
import ghidra.formats.gfilesystem.fileinfo.FileType;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
@@ -301,7 +302,12 @@ public class FSUtilities {
displayCryptoException(originator, parent, title, message, (CryptoException) throwable);
}
else {
Msg.showError(originator, parent, title, message, throwable);
if (throwable instanceof FileSystemFactoryDependencyException) {
Msg.showError(originator, parent, title, message + "\n\n" + throwable.getMessage());
}
else {
Msg.showError(originator, parent, title, message, throwable);
}
}
}
@@ -270,6 +270,11 @@ public class FileSystemIndexHelper<METADATATYPE> {
return fd != null ? fd.file : null;
}
public synchronized String getSymlinkPath(GFile file) {
FileData<METADATATYPE> fd = file == null ? rootDir : fileToEntryMap.get(file);
return fd != null ? fd.symlinkPath : null;
}
private FileData<METADATATYPE> getFileData(GFile f) throws IOException {
if (f == null) {
return rootDir;
@@ -22,8 +22,7 @@ import java.util.function.Predicate;
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.formats.gfilesystem.fileinfo.*;
import ghidra.util.Msg;
import ghidra.util.classfinder.ExtensionPoint;
import ghidra.util.exception.CancelledException;
@@ -225,17 +224,31 @@ public interface GFileSystem extends Closeable, Iterable<GFile>, ExtensionPoint
/**
* Converts the specified (symlink) file into it's destination, or if not a symlink,
* returns the original file unchanged.
* returns the original file unchanged, or null if invalid symlink.
*
* @param file symlink file to follow
* @return destination of symlink, or original file if not a symlink
* @throws IOException if error following symlink path, typically outside of the hosting
* file system
* @return destination of symlink, or original file if not a symlink, or {@code null} if symlink
* destination was invalid
* @throws IOException if error following symlink path, typically because of recursive paths
*/
default GFile resolveSymlinks(GFile file) throws IOException {
return null;
}
/**
* Returns the {@link FileType} of the specified file.
*
* @param f {@link GFile} to query
* @param monitor {@link TaskMonitor}
* @return {@link FileType} of the specified file
*/
default FileType getFileType(GFile f, TaskMonitor monitor) {
FileAttributes attrs = getFileAttributes(f, monitor);
FileType fileType = attrs.get(FileAttributeType.FILE_TYPE_ATTR, FileType.class,
f.isDirectory() ? FileType.DIRECTORY : FileType.FILE);
return fileType;
}
/**
* Default implementation of getting an {@link InputStream} from a {@link GFile}'s
* {@link ByteProvider}.
@@ -187,6 +187,11 @@ public class LocalFileSystem implements GFileSystem, GFileHashProvider {
return getFileAttributes(f);
}
@Override
public FileType getFileType(GFile f, TaskMonitor monitor) {
return FSUtilities.getFileType(new File(f.getPath()));
}
/**
* Create a {@link FileAttributes} container with info about the specified local file.
*
@@ -4,9 +4,9 @@
* 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.
@@ -76,7 +76,7 @@ public class RefdByteProvider implements ByteProvider {
}
@Override
public long length() throws IOException {
public long length() {
return provider.length();
}
@@ -18,6 +18,8 @@ package ghidra.formats.gfilesystem;
import java.io.IOException;
import java.util.*;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
/**
* A helper class used by GFilesystem implementors that have a single file to handle lookups
* and requests for that file.
@@ -27,6 +29,7 @@ import java.util.*;
public class SingleFileSystemIndexHelper {
private GFile rootDir;
private GFileImpl payloadFile;
private FileAttributes payloadAttrs;
/**
* Creates a new instance.
@@ -165,6 +168,14 @@ public class SingleFileSystemIndexHelper {
nameComp.compare(path, payloadFile.getFSRL().getName()) == 0 ? payloadFile : null;
}
public void setPayloadFileAttributes(FileAttributes attrs) {
this.payloadAttrs = attrs;
}
public FileAttributes getFileAttributes(GFile file) {
return payloadAttrs != null && isPayloadFile(file) ? payloadAttrs : FileAttributes.EMPTY;
}
@Override
public String toString() {
return "SingleFileSystemIndexHelper for " + rootDir.getFilesystem();
@@ -0,0 +1,34 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.formats.gfilesystem.factory;
import java.io.IOException;
/**
* An {@link IOException} that signals that there was a problem opening a file system because
* the user's environment is missing a required element.
*/
public class FileSystemFactoryDependencyException extends IOException {
public FileSystemFactoryDependencyException(String message) {
super(message);
}
public FileSystemFactoryDependencyException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -17,7 +17,6 @@ package ghidra.formats.gfilesystem.factory;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import ghidra.app.util.bin.ByteProvider;
import ghidra.formats.gfilesystem.*;
@@ -64,7 +63,7 @@ public class FileSystemFactoryMgr {
for (Class<? extends GFileSystem> fsClass : ClassSearcher.getClasses(GFileSystem.class)) {
addFactory(fsClass);
}
Collections.sort(sortedFactories, FileSystemInfoRec.BY_PRIORITY);
sortedFactories.sort(FileSystemInfoRec.BY_PRIORITY);
}
private void addFactory(Class<? extends GFileSystem> fsClass) {
@@ -76,9 +75,9 @@ public class FileSystemFactoryMgr {
if (fsByType.containsKey(fsir.getType())) {
FileSystemInfoRec prevFSI = fsByType.get(fsir.getType());
Msg.error(this,
"GFileSystem type '" + fsir.getType() + "' registered more than one time: " +
fsClass.getName() + ", " + prevFSI.getFSClass().getName() +
", ommitting second instance.");
"GFileSystem type '%s' registered more than one time: %s, %s, ommitting second instance."
.formatted(fsir.getType(), fsClass.getName(),
prevFSI.getFSClass().getName()));
return;
}
@@ -86,13 +85,11 @@ public class FileSystemFactoryMgr {
// don't register any filesystem that uses this factory
return;
}
if (fsir.getFactory() instanceof GFileSystemProbeBytesOnly) {
GFileSystemProbeBytesOnly pbo = (GFileSystemProbeBytesOnly) fsir.getFactory();
if (fsir.getFactory() instanceof GFileSystemProbeBytesOnly pbo) {
if (pbo.getBytesRequired() > GFileSystemProbeBytesOnly.MAX_BYTESREQUIRED) {
Msg.error(this,
"GFileSystemProbeBytesOnly for " + fsClass.getName() +
" specifies too large value for bytes_required: " + pbo.getBytesRequired() +
", skipping this probe.");
"GFileSystemProbeBytesOnly for %s specifies too large value for bytes_required: %s, skipping this probe."
.formatted(fsClass.getName(), pbo.getBytesRequired()));
}
else {
largestBytesRequired = Math.max(largestBytesRequired, pbo.getBytesRequired());
@@ -115,7 +112,7 @@ public class FileSystemFactoryMgr {
.stream()
.map(fsType -> fsByType.get(fsType).getDescription())
.sorted(String::compareToIgnoreCase)
.collect(Collectors.toList());
.toList();
//@formatter:on
}
@@ -147,7 +144,7 @@ public class FileSystemFactoryMgr {
throws IOException, CancelledException {
FileSystemInfoRec fsir = fsByType.get(fsType);
if (fsir == null) {
byteProvider.close();
FSUtilities.uncheckedClose(byteProvider, null);
throw new IOException("Unknown file system type " + fsType);
}
@@ -164,23 +161,27 @@ public class FileSystemFactoryMgr {
GFileSystem result = null;
boolean bpTaken = false;
try {
GFileSystemFactory<?> factory = fsir.getFactory();
if (factory instanceof GFileSystemFactoryByteProvider) {
GFileSystemFactoryByteProvider<?> bpFactory =
(GFileSystemFactoryByteProvider<?>) factory;
if (fsir.getFactory() instanceof GFileSystemFactoryByteProvider bpFactory) {
bpTaken = true;
result = bpFactory.create(targetFSRL, byteProvider, fsService, monitor);
}
// add additional GFileSystemFactoryXYZ support blocks here
}
catch (IOException | CancelledException e) {
Msg.warn(this,
"Error during fs factory create: " + fsir.getType() + ", " + fsir.getFSClass(), e);
if (e instanceof FileSystemFactoryDependencyException) {
// don't need stacktrace
Msg.warn(this, "File system dependency error: %s (%s)".formatted(e.getMessage(),
fsir.getType()));
}
else {
Msg.warn(this, "Error during fs factory create: %s, %s".formatted(fsir.getType(),
fsir.getFSClass()), e);
}
throw e;
}
finally {
if (byteProvider != null && !bpTaken) {
byteProvider.close();
FSUtilities.uncheckedClose(byteProvider, null);
}
}
@@ -190,15 +191,15 @@ public class FileSystemFactoryMgr {
/**
* Returns true if the specified file contains a supported {@link GFileSystem}.
*
* @param byteProvider
* @param byteProvider container {@link ByteProvider}
* @param fsService reference to the {@link FileSystemService} instance.
* @param monitor {@link TaskMonitor} to use for canceling and updating progress.
* @return {@code true} if the file seems to contain a filesystem, {@code false} if it does not.
* @throws IOException if error when accessing the containing file
* @throws CancelledException if the user canceled the operation
*/
public boolean test(ByteProvider byteProvider, FileSystemService fsService,
TaskMonitor monitor) throws IOException, CancelledException {
public boolean test(ByteProvider byteProvider, FileSystemService fsService, TaskMonitor monitor)
throws IOException, CancelledException {
int pboByteCount = Math.min(
(int) Math.min(byteProvider.length(), GFileSystemProbeBytesOnly.MAX_BYTESREQUIRED),
@@ -221,8 +222,8 @@ public class FileSystemFactoryMgr {
}
}
catch (IOException e) {
Msg.trace(this, "File system probe error for " + fsir.getDescription() +
" with " + containerFSRL, e);
Msg.trace(this, "File system probe error for " + fsir.getDescription() + " with " +
containerFSRL, e);
}
}
return false;
@@ -272,7 +273,8 @@ public class FileSystemFactoryMgr {
FileSystemProbeConflictResolver conflictResolver, int priorityFilter,
TaskMonitor monitor) throws IOException, CancelledException {
conflictResolver = (conflictResolver == null) ? FileSystemProbeConflictResolver.CHOOSEFIRST
conflictResolver = (conflictResolver == null)
? FileSystemProbeConflictResolver.CHOOSEFIRST
: conflictResolver;
FSRL containerFSRL = byteProvider.getFSRL();
@@ -288,9 +290,7 @@ public class FileSystemFactoryMgr {
if (fsir.getPriority() < priorityFilter) {
break;
}
if (fsir.getFactory() instanceof GFileSystemProbeBytesOnly) {
GFileSystemProbeBytesOnly factoryProbe =
(GFileSystemProbeBytesOnly) fsir.getFactory();
if (fsir.getFactory() instanceof GFileSystemProbeBytesOnly factoryProbe) {
if (factoryProbe.getBytesRequired() <= startBytes.length) {
if (factoryProbe.probeStartBytes(containerFSRL, startBytes)) {
probeMatches.add(fsir);
@@ -298,9 +298,7 @@ public class FileSystemFactoryMgr {
}
}
}
if (fsir.getFactory() instanceof GFileSystemProbeByteProvider) {
GFileSystemProbeByteProvider factoryProbe =
(GFileSystemProbeByteProvider) fsir.getFactory();
if (fsir.getFactory() instanceof GFileSystemProbeByteProvider factoryProbe) {
if (factoryProbe.probe(byteProvider, fsService, monitor)) {
probeMatches.add(fsir);
continue;
@@ -308,8 +306,9 @@ public class FileSystemFactoryMgr {
}
}
catch (IOException e) {
Msg.trace(this, "File system probe error for " + fsir.getDescription() +
" with " + containerFSRL, e);
Msg.trace(this, "File system probe error for %s with %s"
.formatted(fsir.getDescription(), containerFSRL),
e);
}
}
@@ -330,9 +329,7 @@ public class FileSystemFactoryMgr {
return fs;
}
finally {
if (byteProvider != null) {
byteProvider.close();
}
FSUtilities.uncheckedClose(byteProvider, null);
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -49,8 +49,7 @@ public class FileAttribute<T> {
* type specified in {@link FileAttributeType#getValueType()})
* @return new FileAttribute instance
*/
public static <T> FileAttribute<T> create(FileAttributeType attributeType,
T attributeValue) {
public static <T> FileAttribute<T> create(FileAttributeType attributeType, T attributeValue) {
return create(attributeType, attributeType.getDisplayName(), attributeValue);
}
@@ -67,9 +66,12 @@ public class FileAttribute<T> {
*/
public static <T> FileAttribute<T> create(FileAttributeType attributeType,
String attributeDisplayName, T attributeValue) {
if (attributeValue == null) {
return null;
}
if (!attributeType.getValueType().isInstance(attributeValue)) {
throw new IllegalArgumentException("FileAttribute type " + attributeType +
" does not match value: " + attributeValue.getClass());
throw new IllegalArgumentException("FileAttribute type %s does not match value: %s"
.formatted(attributeType, attributeValue.getClass()));
}
return new FileAttribute<>(attributeType, attributeDisplayName, attributeValue);
}
@@ -26,6 +26,7 @@ import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.opinion.*;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.crypto.CryptoSession;
import ghidra.formats.gfilesystem.fileinfo.FileType;
import ghidra.plugins.importer.batch.BatchGroup.BatchLoadConfig;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
@@ -337,6 +338,9 @@ public class BatchInfo {
for (GFile file : fs.files(startDir)) {
taskMonitor.checkCancelled();
if (fs.getFileType(file, taskMonitor) != FileType.FILE) {
continue;
}
FSRL fqFSRL;
try {
fqFSRL = fsService.getFullyQualifiedFSRL(file.getFSRL(), taskMonitor);
@@ -4,9 +4,9 @@
* 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.
@@ -208,7 +208,7 @@ public class XmlImportOpinionsTest extends AbstractGhidraHeadlessIntegrationTest
}
@Override
public long length() throws IOException {
public long length() {
throw new UnsupportedOperationException();
}
@@ -0,0 +1,208 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.cliwrapper;
import java.io.*;
import java.lang.ProcessBuilder.Redirect;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import org.apache.commons.io.FilenameUtils;
import ghidra.formats.gfilesystem.FSUtilities;
import ghidra.framework.OperatingSystem;
import ghidra.util.Msg;
import ghidra.util.task.*;
/**
* Base class for common cli tool handling logic
*/
public abstract class AbstractCliToolWrapper implements CliToolWrapper {
/**
* Searches directories specified in the operating system's PATH env var for the specified
* executable name.
*
* @param name executable file name to find
* @return file path of found executable, or {@code null} if not found
*/
public static File findInOSPathEnv(String name) {
for (String pathEntry : System.getenv("PATH").split(File.pathSeparator)) {
try {
File pathDir = new File(pathEntry);
File testFile = normalizeExecutablePath(new File(pathDir, name));
if (testFile != null) {
return testFile;
}
}
catch (IOException e) {
// ignore, try next
}
}
return null;
}
/**
* Fixes an executable name to conform to the current operating system's naming rules. (add
* ".exe" to windows exe's)
*
* @param f executable filename
* @return updated executable filename
* @throws IOException if error resolving filename
*/
public static File normalizeExecutablePath(File f) throws IOException {
if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS &&
!FilenameUtils.getExtension(f.getName()).equals("exe")) {
f = new File(f.getParentFile(), f.getName() + ".exe");
}
return f.isFile() ? f.getCanonicalFile() : null;
}
/**
* Searches the directories specified in the operating system's PATH env var for an executable
* that matches one of the specified names, and also passes the
* {@link CliToolWrapper#isValid(TaskMonitor)} test.
*
* @param <T> specific CliToolWrapper type
* @param exeNames list of executable names
* @param monitor {@link TaskMonitor}
* @param toolCreator creates instance of the cli tool wrapper (eg. MyCliToolWrapper::new)
* @return new cli tool wrapper instance, or {@code null} if none are found or none pass the
* isValid() check
*/
public static <T extends AbstractCliToolWrapper> T findToolWrapper(List<String> exeNames,
TaskMonitor monitor, Function<File, T> toolCreator) {
for (String exeName : exeNames) {
File exeFile = findInOSPathEnv(exeName);
if (exeFile != null) {
T tmp = toolCreator.apply(exeFile);
if (tmp.isValid(monitor)) {
return tmp;
}
}
}
return null;
}
private final static long DEFAULT_TIMEOUT_MS = 5000;
protected long timeoutMS = DEFAULT_TIMEOUT_MS;
protected File nativeExecutable;
protected AbstractCliToolWrapper(File nativeExecutable) {
this.nativeExecutable = nativeExecutable;
}
protected List<String> getCmdLine(List<String> args) {
List<String> cmdLine = new ArrayList<>(args.size() + 1);
cmdLine.add(nativeExecutable.getPath());
cmdLine.addAll(args);
return cmdLine;
}
protected int execAndReadStdOut(List<String> args, TaskMonitor monitor,
Consumer<String> stdoutConsumer) throws IOException {
List<String> cmd = getCmdLine(args);
Process process = new ProcessBuilder(cmd).redirectError(Redirect.DISCARD).start();
process.getOutputStream().close();
CancelledListener l = () -> process.destroyForcibly();
try {
monitor.addCancelledListener(l);
BufferedReader inputReader = process.inputReader();
String line;
while (!monitor.isCancelled() && (line = inputReader.readLine()) != null) {
stdoutConsumer.accept(line);
}
if (!monitor.isCancelled() && process.waitFor(timeoutMS, TimeUnit.MILLISECONDS)) {
return process.exitValue();
}
process.destroyForcibly();
throw new IOException("Process %s timeout".formatted(cmd));
}
catch (InterruptedException e) {
throw new IOException(e);
}
finally {
monitor.removeCancelledListener(l);
}
}
protected int execAndRedirectStdOut(List<String> args, InputStream is, OutputStream os,
TaskMonitor monitor) throws IOException {
TaskMonitor upwtm = new UnknownProgressWrappingTaskMonitor(monitor);
List<String> cmd = getCmdLine(args);
Process process = new ProcessBuilder(cmd).redirectError(Redirect.DISCARD).start();
CancelledListener l = () -> process.destroyForcibly();
try {
monitor.addCancelledListener(l);
if (is != null) {
OutputStream processStdin = process.getOutputStream();
Thread stdinThread = new Thread(() -> {
byte[] buffer = new byte[1024 * 64];
int bytesRead;
try {
while (!upwtm.isCancelled() && (bytesRead = is.read(buffer)) > 0) {
processStdin.write(buffer, 0, bytesRead);
}
processStdin.flush();
}
catch (IOException e) {
Msg.error(AbstractCliToolWrapper.this, "Error streaming to tool stdin", e);
}
finally {
FSUtilities.uncheckedClose(processStdin, null);
}
}, nativeExecutable.getName() + " stdin stream");
stdinThread.setDaemon(true);
stdinThread.start();
}
InputStream processStdout = process.getInputStream();
byte[] buffer = new byte[1024 * 64];
int bytesRead;
long totalBytesRead = 0;
while (!monitor.isCancelled() && (bytesRead = processStdout.read(buffer)) > 0) {
os.write(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
upwtm.setProgress(totalBytesRead);
}
}
finally {
monitor.removeCancelledListener(l);
}
try {
if (!monitor.isCancelled() && process.waitFor(timeoutMS, TimeUnit.MILLISECONDS)) {
return process.exitValue();
}
process.destroyForcibly();
throw new IOException("Process %s timeout".formatted(cmd));
}
catch (InterruptedException e) {
throw new IOException(e);
}
}
}
@@ -0,0 +1,36 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.cliwrapper;
import java.io.*;
import java.util.List;
import ghidra.formats.gfilesystem.fileinfo.FileType;
import ghidra.util.task.TaskMonitor;
/**
* Functionality that an archiver cli tool can expose
*/
public interface ArchiverCliToolWrapper extends CliToolWrapper {
record Entry(String name, long size, FileType fileType) {}
List<Entry> getListing(File archiveFile, TaskMonitor monitor);
void extract(File archiveFile, Entry entry, OutputStream os, TaskMonitor monitor)
throws IOException;
}
@@ -0,0 +1,25 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.cliwrapper;
import ghidra.util.task.TaskMonitor;
/**
* Common interface for cli tools
*/
public interface CliToolWrapper {
boolean isValid(TaskMonitor monitor);
}
@@ -0,0 +1,130 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.cliwrapper;
import java.util.Objects;
/**
* Record that represents a semantic version number. (eg. X.Y.Z)
*
* @param major major version number
* @param minor minor version number
* @param patch patch version number
*/
public record SemVer(int major, int minor, int patch) implements Comparable<SemVer> {
public static final SemVer INVALID = new SemVer(0, 0, 0);
public static final SemVer ANY = new SemVer(-1, -1, -1);
/**
* Parses a version string ("1.2.0") and returns a SemVer instance, or INVALID if bad data.
* <p>
* Missing patch numbers will be defaulted to 0.
*
* @param s string to parse, "1.2.3", or "1.2"
* @return SemVer instance, or INVALID
*/
public static SemVer parse(String s) {
return parse(s, 0);
}
/**
* Parses a version string ("1.2.0") and returns a SemVer instance, or INVALID if bad data.
* <p>
* Missing patch numbers will be replaced with the wildcard value.
*
* @param s string to parse, "1.2.3", or "1.2"
* @return SemVer instance, or INVALID
*/
public static SemVer parseWildcardPatch(String s) {
return parse(s, -1);
}
private static SemVer parse(String s, int missingPatchValue) {
// handle extra info at end of ver string: "1.22.8 blah"
String[] parts =
Objects.requireNonNullElse(s, "").replaceAll("[^.0-9].*$", "").split("\\.");
if (parts.length < 2) {
return INVALID;
}
try {
int major = Integer.parseInt(parts[0]);
int minor = Integer.parseInt(parts[1]);
int patch = parts.length > 2 ? Integer.parseInt(parts[2]) : missingPatchValue;
return new SemVer(major, minor, patch);
}
catch (NumberFormatException e) {
// fall thru, return invalid
}
return INVALID;
}
public boolean isInvalid() {
return major == 0 && minor == 0;
}
public boolean isWildcard() {
return major == -1 && minor == -1;
}
/**
* {@return major value}
*/
public int getMajor() {
return major;
}
/**
* {@return minor value}
*/
public int getMinor() {
return minor;
}
/**
* {@return patch value}
*/
public int getPatch() {
return patch;
}
public SemVer prevPatch() {
return new SemVer(major, minor, patch > 0 ? patch - 1 : 0);
}
public SemVer withPatch(int newPatchNum) {
return new SemVer(major, minor, newPatchNum);
}
@Override
public int compareTo(SemVer o) {
int result = Integer.compare(major, o.major);
if (result == 0) {
result = Integer.compare(minor, o.minor);
}
if (result == 0) {
result = patch == -1 || o.patch == -1 ? 0 : Integer.compare(patch, o.patch);
}
return result;
}
@Override
public String toString() {
return patch != -1
? "%d.%d.%d".formatted(major, minor, patch)
: "%d.%d".formatted(major, minor);
}
}
@@ -0,0 +1,31 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.cliwrapper;
import java.io.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Functionality common to stream decompressor cli tools
*/
public interface StreamDecompressorCliToolWrapper extends CliToolWrapper {
void decompressStream(InputStream is, OutputStream os, TaskMonitor monitor)
throws IOException, CancelledException;
}
@@ -4,9 +4,9 @@
* 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.
@@ -52,7 +52,7 @@ public class OverlayByteProvider implements ByteProvider {
}
@Override
public long length() throws IOException {
public long length() {
long currentMax = 0;
for (OverlayRange range : overlayList) {
currentMax = Math.max(currentMax, range.getEndIndex());
@@ -4,9 +4,9 @@
* 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.
@@ -54,7 +54,7 @@ class ImmutableMemoryRangeByteProvider implements ByteProvider {
}
@Override
public long length() throws IOException {
public long length() {
return end.subtract(start) + 1;
}
@@ -15,94 +15,18 @@
*/
package ghidra.file.formats.complzss;
import java.io.IOException;
import java.io.InputStream;
import java.util.Comparator;
import java.util.List;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.bin.ByteProviderWrapper;
import ghidra.file.formats.lzss.LzssCodec;
import ghidra.file.formats.lzss.LzssConstants;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.AbstractSinglePayloadFileSystem;
import ghidra.formats.gfilesystem.FSRLRoot;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
@FileSystemInfo(type = "lzss", description = "LZSS Compression", factory = CompLzssFileSystemFactory.class)
public class CompLzssFileSystem implements GFileSystem {
private FSRLRoot fsFSRL;
private SingleFileSystemIndexHelper fsIndex;
private FileSystemRefManager fsRefManager = new FileSystemRefManager(this);
private ByteProvider payloadProvider;
public class CompLzssFileSystem extends AbstractSinglePayloadFileSystem {
public CompLzssFileSystem(FSRLRoot fsrl, ByteProvider provider, FileSystemService fsService,
TaskMonitor monitor) throws IOException, CancelledException {
this.fsFSRL = fsrl;
monitor.setMessage("Decompressing LZSS...");
try (ByteProvider tmpBP = new ByteProviderWrapper(provider, LzssConstants.HEADER_LENGTH,
provider.length() - LzssConstants.HEADER_LENGTH);
InputStream tmpIS = tmpBP.getInputStream(0);) {
this.payloadProvider = fsService.getDerivedByteProviderPush(provider.getFSRL(), null,
"decompressed lzss", -1, (os) -> LzssCodec.decompress(os, tmpIS), monitor);
this.fsIndex = new SingleFileSystemIndexHelper(this, fsFSRL, "lzss_decompressed",
payloadProvider.length(), payloadProvider.getFSRL().getMD5());
}
}
@Override
public FSRLRoot getFSRL() {
return fsFSRL;
}
@Override
public String getName() {
return fsFSRL.getContainer().getName();
}
@Override
public FileSystemRefManager getRefManager() {
return fsRefManager;
}
@Override
public boolean isClosed() {
return payloadProvider == null;
}
@Override
public void close() throws IOException {
fsRefManager.onClose();
if (payloadProvider != null) {
payloadProvider.close();
payloadProvider = null;
}
fsIndex.clear();
}
@Override
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException {
return fsIndex.isPayloadFile(file)
? new ByteProviderWrapper(payloadProvider, file.getFSRL())
: null;
}
@Override
public List<GFile> getListing(GFile directory) throws IOException {
return fsIndex.getListing(directory);
}
@Override
public GFile lookup(String path) throws IOException {
return fsIndex.lookup(path);
}
@Override
public GFile lookup(String path, Comparator<String> nameComp) throws IOException {
return fsIndex.lookup(null, path, nameComp);
public CompLzssFileSystem(FSRLRoot fsFSRL, ByteProvider payloadProvider, String payloadFilename,
FileAttributes attrs) {
super(fsFSRL, payloadProvider, payloadFilename, attrs);
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -16,12 +16,15 @@
package ghidra.file.formats.complzss;
import java.io.IOException;
import java.io.InputStream;
import ghidra.app.util.bin.ByteProvider;
import ghidra.file.formats.lzss.LzssCompressionHeader;
import ghidra.app.util.bin.ByteProviderWrapper;
import ghidra.file.formats.lzss.*;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider;
import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@@ -29,16 +32,23 @@ public class CompLzssFileSystemFactory
implements GFileSystemFactoryByteProvider<CompLzssFileSystem>, GFileSystemProbeBytesOnly {
@Override
public CompLzssFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider,
public CompLzssFileSystem create(FSRLRoot targetFSRL, ByteProvider provider,
FileSystemService fsService, TaskMonitor monitor)
throws IOException, CancelledException {
try {
CompLzssFileSystem fs =
new CompLzssFileSystem(targetFSRL, byteProvider, fsService, monitor);
try (ByteProvider tmpBP = new ByteProviderWrapper(provider, LzssConstants.HEADER_LENGTH,
provider.length() - LzssConstants.HEADER_LENGTH);
InputStream tmpIS = tmpBP.getInputStream(0)) {
ByteProvider payloadProvider = fsService.getDerivedByteProviderPush(provider.getFSRL(),
null, "decompressed lzss", -1, (os) -> LzssCodec.decompress(os, tmpIS), monitor);
CompLzssFileSystem fs = new CompLzssFileSystem(targetFSRL, payloadProvider,
"lzss_decompressed", FileAttributes.EMPTY);
return fs;
}
finally {
byteProvider.close();
FSUtilities.uncheckedClose(provider, null);
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -17,7 +17,9 @@ package ghidra.file.formats.cpio;
import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*;
import java.io.*;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.compress.archivers.cpio.CpioArchiveEntry;
import org.apache.commons.compress.archivers.cpio.CpioArchiveInputStream;
@@ -26,11 +28,14 @@ import ghidra.app.util.bin.ByteProvider;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
import ghidra.formats.gfilesystem.fileinfo.FileType;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@FileSystemInfo(type = "cpio", description = "CPIO", factory = CpioFileSystemFactory.class)
public class CpioFileSystem extends AbstractFileSystem<CpioArchiveEntry> {
private static final int MAX_SANE_SYMLINK = 64 * 1024;
private ByteProvider provider;
public CpioFileSystem(FSRLRoot fsFSRL, ByteProvider provider, FileSystemService fsService,
@@ -44,12 +49,19 @@ public class CpioFileSystem extends AbstractFileSystem<CpioArchiveEntry> {
new CpioArchiveInputStream(provider.getInputStream(0))) {
CpioArchiveEntry entry;
int fileNum = 0;
while ((entry = cpioInputStream.getNextCPIOEntry()) != null) {
FSUtilities.streamCopy(cpioInputStream, OutputStream.nullOutputStream(), monitor);
while ((entry = cpioInputStream.getNextEntry()) != null) {
monitor.setMessage(entry.getName());
fsIndex.storeFile(entry.getName(), fileNum++, entry.isDirectory(), entry.getSize(),
entry);
if (entry.isSymbolicLink()) {
String linkDest = entry.getSize() < MAX_SANE_SYMLINK
? new String(cpioInputStream.readAllBytes(), StandardCharsets.UTF_8)
: "???badsymlink???";
fsIndex.storeSymlink(entry.getName(), fileNum++, linkDest, entry.getSize(),
entry);
}
else {
fsIndex.storeFile(entry.getName(), fileNum++, entry.isDirectory(),
entry.getSize(), entry);
}
}
}
catch (EOFException e) {
@@ -88,6 +100,8 @@ public class CpioFileSystem extends AbstractFileSystem<CpioArchiveEntry> {
result.add(MODIFIED_DATE_ATTR, entry.getLastModifiedDate());
result.add(USER_ID_ATTR, entry.getUID());
result.add(GROUP_ID_ATTR, entry.getGID());
result.add(FILE_TYPE_ATTR, getFileType(entry));
result.add(SYMLINK_DEST_ATTR, fsIndex.getSymlinkPath(file));
result.add("Mode", Long.toHexString(entry.getMode()));
result.add("Inode", Long.toHexString(entry.getInode()));
result.add("Format", Long.toHexString(entry.getFormat()));
@@ -109,9 +123,16 @@ public class CpioFileSystem extends AbstractFileSystem<CpioArchiveEntry> {
return result;
}
@Override
public FileType getFileType(GFile f, TaskMonitor monitor) {
CpioArchiveEntry entry = fsIndex.getMetadata(f);
return entry != null ? getFileType(entry) : FileType.UNKNOWN;
}
@Override
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor)
throws IOException, CancelledException {
file = fsIndex.resolveSymlinks(file);
CpioArchiveEntry targetEntry = fsIndex.getMetadata(file);
if (targetEntry == null) {
return null;
@@ -123,14 +144,13 @@ public class CpioFileSystem extends AbstractFileSystem<CpioArchiveEntry> {
new CpioArchiveInputStream(provider.getInputStream(0))) {
CpioArchiveEntry currentEntry;
while ((currentEntry = cpioInputStream.getNextCPIOEntry()) != null) {
while ((currentEntry = cpioInputStream.getNextEntry()) != null) {
if (currentEntry.equals(targetEntry)) {
ByteProvider bp =
fsService.getDerivedByteProvider(provider.getFSRL(), file.getFSRL(),
file.getPath(), currentEntry.getSize(), () -> cpioInputStream, monitor);
return bp;
}
FSUtilities.streamCopy(cpioInputStream, OutputStream.nullOutputStream(), monitor);
}
}
catch (IllegalArgumentException e) {
@@ -138,4 +158,19 @@ public class CpioFileSystem extends AbstractFileSystem<CpioArchiveEntry> {
}
throw new IOException("Unable to seek to entry: " + file.getName());
}
private FileType getFileType(CpioArchiveEntry entry) {
if (entry.isSymbolicLink()) {
return FileType.SYMBOLIC_LINK;
}
else if (entry.isDirectory()) {
return FileType.DIRECTORY;
}
else if (entry.isRegularFile()) {
return FileType.FILE;
}
else {
return FileType.OTHER;
}
}
}
@@ -224,6 +224,12 @@ public class Ext4FileSystem extends AbstractFileSystem<Ext4File> {
return result;
}
@Override
public FileType getFileType(GFile f, TaskMonitor monitor) {
Ext4File ext4File = fsIndex.getMetadata(f);
return ext4File != null ? inodeToFileType(ext4File.getInode()) : FileType.UNKNOWN;
}
FileType inodeToFileType(Ext4Inode inode) {
if (inode.isDir()) {
return FileType.DIRECTORY;
@@ -4,9 +4,9 @@
* 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.
@@ -15,9 +15,7 @@
*/
package ghidra.file.formats.ext4;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import ghidra.app.util.bin.ByteProvider;
import ghidra.program.model.address.Address;
@@ -96,7 +94,7 @@ class MultiProgramMemoryByteProvider implements ByteProvider {
}
@Override
public long length( ) throws IOException {
public long length() {
int length = 0;
for ( Program program : programs ) {
length += program.getMemory().getSize( );
@@ -15,23 +15,11 @@
*/
package ghidra.file.formats.gzip;
import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*;
import java.io.IOException;
import java.util.*;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.compressors.gzip.GzipParameters;
import org.apache.commons.io.FilenameUtils;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.bin.ByteProviderWrapper;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.AbstractSinglePayloadFileSystem;
import ghidra.formats.gfilesystem.FSRLRoot;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import ghidra.util.task.UnknownProgressWrappingTaskMonitor;
/**
* A pseudo-filesystem that contains a single file that represents the decompressed
@@ -41,168 +29,10 @@ import ghidra.util.task.UnknownProgressWrappingTaskMonitor;
* name of the singleton file, otherwise the name "gzip_decompressed" will be used.
*/
@FileSystemInfo(type = "gzip", description = "GZIP", priority = FileSystemInfo.PRIORITY_LOW, factory = GZipFileSystemFactory.class)
public class GZipFileSystem implements GFileSystem {
public static final String GZIP_PAYLOAD_FILENAME = "gzip_decompressed";
public class GZipFileSystem extends AbstractSinglePayloadFileSystem {
private final FSRLRoot fsFSRL;
private final FileSystemRefManager refManager = new FileSystemRefManager(this);
private final SingleFileSystemIndexHelper fsIndex;
private final FileSystemService fsService;
private ByteProvider container;
private ByteProvider payloadProvider;
private String payloadFilename;
private String payloadKey;
private String origComment;
private long origDate;
public GZipFileSystem(ByteProvider container, FSRLRoot fsFSRL, FileSystemService fsService,
TaskMonitor monitor) throws IOException, CancelledException {
this.fsFSRL = fsFSRL;
this.fsService = fsService;
this.container = container;
readGzipMetadata(monitor);
payloadProvider = getPayloadByteProvider(monitor);
this.fsIndex = new SingleFileSystemIndexHelper(this, fsFSRL, payloadFilename,
payloadProvider.length(), payloadProvider.getFSRL().getMD5());
}
private void readGzipMetadata(TaskMonitor monitor) throws IOException {
try (GzipCompressorInputStream gzcis =
new GzipCompressorInputStream(container.getInputStream(0))) {
GzipParameters metaData = gzcis.getMetaData();
payloadFilename = metaData.getFilename();
if (payloadFilename == null) {
String containerName = fsFSRL.getContainer().getName();
if (containerName.toLowerCase().endsWith(".gz")) {
payloadFilename = FilenameUtils.removeExtension(containerName);
}
else {
payloadFilename = GZIP_PAYLOAD_FILENAME;
}
}
else {
payloadFilename = FSUtilities.getSafeFilename(payloadFilename);
}
this.origComment = metaData.getComment();
this.origDate = metaData.getModificationTime();
this.payloadKey = "uncompressed " + payloadFilename;
}
}
private ByteProvider getPayloadByteProvider(TaskMonitor monitor)
throws CancelledException, IOException {
UnknownProgressWrappingTaskMonitor upwtm =
new UnknownProgressWrappingTaskMonitor(monitor, container.length());
return fsService.getDerivedByteProvider(container.getFSRL(), null, payloadKey, -1,
() -> new GzipCompressorInputStream(container.getInputStream(0)), upwtm);
}
public GFile getPayloadFile() {
return fsIndex.getPayloadFile();
}
@Override
public String getName() {
return fsFSRL.getContainer().getName();
}
@Override
public FSRLRoot getFSRL() {
return fsFSRL;
}
@Override
public void close() throws IOException {
refManager.onClose();
fsIndex.clear();
if (container != null) {
container.close();
container = null;
}
if (payloadProvider != null) {
payloadProvider.close();
payloadProvider = null;
}
}
@Override
public boolean isClosed() {
return fsIndex.isClosed();
}
@Override
public GFile lookup(String path) {
return fsIndex.lookup(path);
}
@Override
public GFile lookup(String path, Comparator<String> nameComp) throws IOException {
return fsIndex.lookup(null, path, nameComp);
}
@Override
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor)
throws IOException, CancelledException {
if (fsIndex.isPayloadFile(file)) {
return new ByteProviderWrapper(payloadProvider, file.getFSRL());
}
return null;
}
@Override
public List<GFile> getListing(GFile directory) throws IOException {
return fsIndex.getListing(directory);
}
@Override
public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) {
long compSize = 0;
long payloadSize = 0;
try {
compSize = container.length();
payloadSize = payloadProvider.length();
}
catch (IOException e) {
// ignore
}
GFile payload = fsIndex.getPayloadFile();
FileAttributes result = new FileAttributes();
result.add(NAME_ATTR, payload.getName());
result.add(SIZE_ATTR, payloadSize);
result.add(COMPRESSED_SIZE_ATTR, compSize);
result.add(MODIFIED_DATE_ATTR, origDate != 0 ? new Date(origDate) : null);
result.add(COMMENT_ATTR, origComment);
result.add("MD5", payload.getFSRL().getMD5());
return result;
}
public Map<String, String> getInfoMap() {
long compSize = 0;
long payloadSize = 0;
try {
compSize = container.length();
payloadSize = payloadProvider.length();
}
catch (IOException e) {
// ignore
}
GFile payload = fsIndex.getPayloadFile();
Map<String, String> info = new LinkedHashMap<>();
info.put("Name", payload.getName());
info.put("Size", FSUtilities.formatSize(payloadSize));
info.put("Compressed Size", FSUtilities.formatSize(compSize));
info.put("Date", FSUtilities.formatFSTimestamp(origDate != 0 ? new Date(origDate) : null));
info.put("Comment", Objects.requireNonNullElse(origComment, "unknown"));
info.put("MD5", payload.getFSRL().getMD5());
return info;
}
@Override
public FileSystemRefManager getRefManager() {
return refManager;
public GZipFileSystem(FSRLRoot fsFSRL, ByteProvider payloadProvider, String payloadFilename,
FileAttributes payloadAttrs) {
super(fsFSRL, payloadProvider, payloadFilename, payloadAttrs);
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -15,27 +15,87 @@
*/
package ghidra.file.formats.gzip;
import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*;
import java.io.IOException;
import java.util.Date;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.compressors.gzip.GzipParameters;
import org.apache.commons.io.FilenameUtils;
import ghidra.app.util.bin.ByteProvider;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider;
import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly;
import ghidra.formats.gfilesystem.fileinfo.FileAttribute;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import ghidra.util.task.UnknownProgressWrappingTaskMonitor;
public class GZipFileSystemFactory
implements GFileSystemFactoryByteProvider<GZipFileSystem>, GFileSystemProbeBytesOnly {
public static final int PROBE_BYTES_REQUIRED = GZipConstants.MAGIC_BYTES_COUNT;
public static final String GZIP_PAYLOAD_FILENAME = "gzip_decompressed";
@Override
public GZipFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider,
public GZipFileSystem create(FSRLRoot targetFSRL, ByteProvider provider,
FileSystemService fsService, TaskMonitor monitor)
throws IOException, CancelledException {
GZipFileSystem fs = new GZipFileSystem(byteProvider, targetFSRL, fsService, monitor);
return fs;
try {
String containerName = targetFSRL.getContainer().getName();
FileAttributes payloadAttrs = getGZFileAttributes(provider, containerName);
String payloadName = payloadAttrs.get(NAME_ATTR, String.class, GZIP_PAYLOAD_FILENAME);
UnknownProgressWrappingTaskMonitor upwtm =
new UnknownProgressWrappingTaskMonitor(monitor, provider.length());
ByteProvider payloadProvider = fsService.getDerivedByteProvider(provider.getFSRL(),
null, "uncompressed " + payloadName, -1,
() -> new GzipCompressorInputStream(provider.getInputStream(0)), upwtm);
payloadAttrs.add(SIZE_ATTR, payloadProvider.length());
GZipFileSystem fs =
new GZipFileSystem(targetFSRL, payloadProvider, payloadName, payloadAttrs);
return fs;
}
finally {
FSUtilities.uncheckedClose(provider, null);
}
}
private FileAttributes getGZFileAttributes(ByteProvider provider, String containerName)
throws IOException {
String payloadFilename = GZIP_PAYLOAD_FILENAME;
String origComment = null;
long origDate = 0;
try (GzipCompressorInputStream gzcis =
new GzipCompressorInputStream(provider.getInputStream(0))) {
GzipParameters metaData = gzcis.getMetaData();
payloadFilename = metaData.getFileName();
if (payloadFilename == null) {
if (containerName.toLowerCase().endsWith(".gz")) {
payloadFilename = FilenameUtils.removeExtension(containerName);
}
else {
payloadFilename = GZIP_PAYLOAD_FILENAME;
}
}
else {
payloadFilename = FSUtilities.getSafeFilename(payloadFilename);
}
origComment = metaData.getComment();
origDate = metaData.getModificationTime();
}
return FileAttributes.of( // attrs
FileAttribute.create(NAME_ATTR, payloadFilename),
FileAttribute.create(COMPRESSED_SIZE_ATTR, provider.length()),
FileAttribute.create(MODIFIED_DATE_ATTR, origDate != 0 ? new Date(origDate) : null),
FileAttribute.create(COMMENT_ATTR, origComment));
}
@Override
@@ -114,7 +114,7 @@ public class DyldCacheSlidProvider implements ByteProvider {
}
@Override
public long length() throws IOException {
public long length() {
return origProvider.length();
}
@@ -16,15 +16,11 @@
package ghidra.file.formats.ios.img2;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.bin.ByteProviderWrapper;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
//@formatter:off
@FileSystemInfo(
@@ -32,84 +28,18 @@ import ghidra.util.task.TaskMonitor;
description = "iOS " + Img2Constants.IMG2_SIGNATURE,
factory = Img2FileSystemFactory.class)
//@formatter:on
public class Img2FileSystem implements GFileSystem {
public class Img2FileSystem extends AbstractSinglePayloadFileSystem {
private final FSRLRoot fsFSRL;
private SingleFileSystemIndexHelper fsIndexHelper;
private FileSystemRefManager refManager = new FileSystemRefManager(this);
private ByteProvider provider;
private Img2 img2;
private ByteProvider containerProvider;
public Img2FileSystem(FSRLRoot fsFSRL, ByteProvider provider, TaskMonitor monitor)
throws IOException, CancelledException {
this.fsFSRL = fsFSRL;
this.provider = provider;
this.img2 = new Img2(provider);
if (!img2.isValid()) {
throw new IOException("Unable to decrypt file: invalid IMG2 file!");
}
try (ByteProvider tmpBP =
new ByteProviderWrapper(provider, Img2Constants.IMG2_LENGTH, img2.getDataLen(), null)) {
String payloadMD5 = FSUtilities.getMD5(tmpBP, monitor);
this.fsIndexHelper = new SingleFileSystemIndexHelper(this, fsFSRL, img2.getImageType(),
img2.getDataLen(), payloadMD5);
}
}
@Override
public FSRLRoot getFSRL() {
return fsFSRL;
public Img2FileSystem(FSRLRoot fsFSRL, ByteProvider payloadProvider, String payloadFilename,
FileAttributes attrs, ByteProvider containerProvider) {
super(fsFSRL, payloadProvider, payloadFilename, attrs);
}
@Override
public void close() throws IOException {
refManager.onClose();
fsIndexHelper.clear();
if (provider != null) {
provider.close();
provider = null;
}
super.close();
FSUtilities.uncheckedClose(containerProvider, null);
}
@Override
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) {
if (fsIndexHelper.isPayloadFile(file)) {
return new ByteProviderWrapper(provider, Img2Constants.IMG2_LENGTH, img2.getDataLen(),
fsIndexHelper.getPayloadFile().getFSRL());
}
return null;
}
@Override
public List<GFile> getListing(GFile directory) throws IOException {
return fsIndexHelper.getListing(directory);
}
@Override
public String getName() {
return fsFSRL.getContainer().getName();
}
@Override
public boolean isClosed() {
return fsIndexHelper.isClosed();
}
@Override
public FileSystemRefManager getRefManager() {
return refManager;
}
@Override
public GFile lookup(String path) {
return fsIndexHelper.lookup(path);
}
@Override
public GFile lookup(String path, Comparator<String> nameComp) throws IOException {
return fsIndexHelper.lookup(null, path, nameComp);
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -15,18 +15,19 @@
*/
package ghidra.file.formats.ios.img2;
import java.io.IOException;
import java.util.Arrays;
import java.io.IOException;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.bin.ByteProviderWrapper;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider;
import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class Img2FileSystemFactory implements GFileSystemFactoryByteProvider<Img2FileSystem>, GFileSystemProbeBytesOnly {
public class Img2FileSystemFactory
implements GFileSystemFactoryByteProvider<Img2FileSystem>, GFileSystemProbeBytesOnly {
@Override
public int getBytesRequired() {
@@ -40,10 +41,19 @@ public class Img2FileSystemFactory implements GFileSystemFactoryByteProvider<Img
}
@Override
public Img2FileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider,
public Img2FileSystem create(FSRLRoot targetFSRL, ByteProvider provider,
FileSystemService fsService, TaskMonitor monitor)
throws IOException, CancelledException {
return new Img2FileSystem(targetFSRL, byteProvider, monitor);
Img2 img2 = new Img2(provider);
if (!img2.isValid()) {
FSUtilities.uncheckedClose(provider, null);
throw new IOException("Invalid IMG2 file!");
}
ByteProviderWrapper payloadProvider =
new ByteProviderWrapper(provider, Img2Constants.IMG2_LENGTH, img2.getDataLen(), null);
return new Img2FileSystem(targetFSRL, payloadProvider, img2.getImageType(), null, provider);
}
}
@@ -0,0 +1,92 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.formats.lzfse;
import java.io.*;
import java.util.List;
import ghidra.file.cliwrapper.AbstractCliToolWrapper;
import ghidra.file.cliwrapper.StreamDecompressorCliToolWrapper;
import ghidra.framework.Application;
import ghidra.framework.OperatingSystem;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Wrapper around the lzfse cmd line tool. This tool is typically provided in the Ghidra distro
* (see {@link Application#getOSFile(String)}.
*/
public class LzfseCliToolWrapper extends AbstractCliToolWrapper
implements StreamDecompressorCliToolWrapper {
private static final String LZFSE_NATIVE_BINARY_NAME = "lzfse";
/**
* Creates a new tool wrapper around the OS specific lzfse cmd line tool found in Ghidra's
* distro.
*
* @param monitor {@link TaskMonitor}
* @return new {@link LzfseCliToolWrapper} instance, or {@code null} if not found
*/
public static LzfseCliToolWrapper findTool(TaskMonitor monitor) {
try {
String lzfseName = LZFSE_NATIVE_BINARY_NAME;
if (OperatingSystem.CURRENT_OPERATING_SYSTEM.equals(OperatingSystem.WINDOWS)) {
lzfseName += ".exe";
}
File lzfseNativeBinary = Application.getOSFile(lzfseName);
if (lzfseNativeBinary != null) {
LzfseCliToolWrapper tmpTool = new LzfseCliToolWrapper(lzfseNativeBinary);
if (tmpTool.isValid(monitor)) {
return tmpTool;
}
}
}
catch (IOException e) {
// fall thru, return null
}
return null;
}
public LzfseCliToolWrapper(File nativeExecutable) {
super(nativeExecutable);
}
@Override
public boolean isValid(TaskMonitor monitor) {
try {
// if it executes with good exit value, consider it valid
StringBuilder sb = new StringBuilder();
if (execAndReadStdOut(List.of("-h"), monitor, sb::append) == 0) {
return true;
}
}
catch (IOException e) {
// fall thru
}
return false;
}
@Override
public void decompressStream(InputStream is, OutputStream os, TaskMonitor monitor)
throws IOException, CancelledException {
monitor.initialize(0, "Extracting");
int exitVal = execAndRedirectStdOut(List.of("-decode"), is, os, monitor);
if (exitVal != 0) {
throw new IOException("lzfse tool error " + exitVal);
}
}
}
@@ -15,106 +15,33 @@
*/
package ghidra.file.formats.lzfse;
import java.io.File;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.bin.ByteProviderWrapper;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
/**
* A {@link GFileSystem} implementation LZFSE compressed files
* <p>
* This implementation depends on a cmd line native binary lzfse (see
* {@link LzfseFileSystemFactory#ensureTool})
*
* @see <a href="https://github.com/lzfse/lzfse">lzfse reference implementation</a>
*/
@FileSystemInfo(type = "lzfse", description = "LZFSE", factory = LzfseFileSystemFactory.class, priority = FileSystemInfo.PRIORITY_HIGH)
public class LzfseFileSystem implements GFileSystem {
private FSRLRoot fsFSRL;
private SingleFileSystemIndexHelper fsIndex;
private FileSystemRefManager fsRefManager = new FileSystemRefManager(this);
private ByteProvider decompressedProvider;
public class LzfseFileSystem extends AbstractSinglePayloadFileSystem {
/**
* Creates a new {@link LzfseFileSystem}.
* <p>
* NOTE: Successful completion of this constructor will result in {@code decompressedFile}
* being deleted.
*
* @param fsrlRoot This filesystem's {@link FSRLRoot}
* @param decompressedFile The decompressed lzfse {@link File file} (will be deleted after use)
* @param fsService The {@link FileSystemService}
* @param monitor {@link TaskMonitor}
* @throws IOException If there was an IO-related error
* @throws CancelledException If the user cancelled the operation
* @param fsFSRL This filesystem's {@link FSRLRoot}
* @param payloadProvider {@link ByteProvider}
* @param payloadFilename name of the single payload file
* @param payloadAttrs attributes of the payload file
*/
public LzfseFileSystem(FSRLRoot fsrlRoot, File decompressedFile, FileSystemService fsService,
TaskMonitor monitor) throws IOException, CancelledException {
monitor.setMessage("Decompressing LZFSE...");
this.fsFSRL = fsrlRoot;
String name = "lzfse_decompressed";
decompressedProvider =
fsService.pushFileToCache(decompressedFile, fsFSRL.appendPath(name), monitor);
fsIndex = new SingleFileSystemIndexHelper(this, fsFSRL, name,
decompressedProvider.length(), decompressedProvider.getFSRL().getMD5());
public LzfseFileSystem(FSRLRoot fsFSRL, ByteProvider payloadProvider, String payloadFilename,
FileAttributes payloadAttrs) {
super(fsFSRL, payloadProvider, payloadFilename, payloadAttrs);
}
@Override
public FSRLRoot getFSRL() {
return fsFSRL;
}
@Override
public String getName() {
return fsFSRL.getContainer().getName();
}
@Override
public FileSystemRefManager getRefManager() {
return fsRefManager;
}
@Override
public boolean isClosed() {
return decompressedProvider == null;
}
@Override
public void close() throws IOException {
fsRefManager.onClose();
if (decompressedProvider != null) {
decompressedProvider.close();
decompressedProvider = null;
}
fsIndex.clear();
}
@Override
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException {
return fsIndex.isPayloadFile(file)
? new ByteProviderWrapper(decompressedProvider, file.getFSRL())
: null;
}
@Override
public List<GFile> getListing(GFile directory) throws IOException {
return fsIndex.getListing(directory);
}
@Override
public GFile lookup(String path) throws IOException {
return fsIndex.lookup(path);
}
@Override
public GFile lookup(String path, Comparator<String> nameComp) throws IOException {
return fsIndex.lookup(null, path, nameComp);
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -15,20 +15,18 @@
*/
package ghidra.file.formats.lzfse;
import java.io.File;
import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import ghidra.app.util.bin.ByteProvider;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider;
import ghidra.formats.gfilesystem.factory.GFileSystemProbeBytesOnly;
import ghidra.framework.Application;
import ghidra.framework.OperatingSystem;
import ghidra.formats.gfilesystem.factory.*;
import ghidra.formats.gfilesystem.fileinfo.FileAttribute;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@@ -41,9 +39,6 @@ public class LzfseFileSystemFactory
implements GFileSystemFactoryByteProvider<LzfseFileSystem>, GFileSystemProbeBytesOnly {
private static final int START_BYTES_REQUIRED = 4;
private static final String LZFSE_NATIVE_BINARY_NAME = "lzfse";
private static final String LZFSE_TEMP_PREFIX = "lzfse";
private static final int LZFSE_NATIVE_TIMEOUT_SECONDS = 10;
private static final int LZFSE_ENDOFSTREAM_BLOCK_MAGIC = 0x24787662; // bvx$ (end of stream)
private static final int LZFSE_UNCOMPRESSED_BLOCK_MAGIC = 0x2d787662; // bvx- (raw data)
@@ -51,6 +46,8 @@ public class LzfseFileSystemFactory
private static final int LZFSE_COMPRESSEDV2_BLOCK_MAGIC = 0x32787662; // bvx2 (lzfse compressed, compressed tables)
private static final int LZFSE_COMPRESSEDLZVN_BLOCK_MAGIC = 0x6e787662; // bvxn (lzvn compressed)
private LzfseCliToolWrapper cliTool;
@Override
public int getBytesRequired() {
return START_BYTES_REQUIRED;
@@ -75,71 +72,34 @@ public class LzfseFileSystemFactory
public GFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider,
FileSystemService fsService, TaskMonitor monitor)
throws IOException, CancelledException {
File compressedFile = null;
File decompressedFile = null;
try {
compressedFile =
fsService.createPlaintextTempFile(byteProvider, LZFSE_TEMP_PREFIX, monitor);
decompressedFile = lzfseDecompress(compressedFile);
return new LzfseFileSystem(targetFSRL, decompressedFile, fsService, monitor);
ensureTool(monitor);
ByteProvider payloadProvider = fsService.getDerivedByteProviderPush(
byteProvider.getFSRL(), null, "lzfse_decompressed", -1, os -> {
try (InputStream is = byteProvider.getInputStream(0)) {
cliTool.decompressStream(is, os, monitor);
}
}, monitor);
FileAttributes fileAttrs = FileAttributes.of( // attrs
FileAttribute.create(COMPRESSED_SIZE_ATTR, byteProvider.length()),
FileAttribute.create(SIZE_ATTR, payloadProvider.length()));
LzfseFileSystem fs =
new LzfseFileSystem(targetFSRL, payloadProvider, "lzfse_decompressed", fileAttrs);
return fs;
}
finally {
byteProvider.close();
if (compressedFile != null && compressedFile.exists()) {
compressedFile.delete();
}
if (decompressedFile != null && decompressedFile.exists()) {
decompressedFile.delete();
}
FSUtilities.uncheckedClose(byteProvider, null);
}
}
/**
* Uses the native lzfse decompressor to decompress the given compressed file
*
* @param compressedFile The lzfse-compressed {@link File file} to decompress
* @return The lzfse-decompressed {@link File}
* @throws IOException If there was an IO-related error
*/
private File lzfseDecompress(File compressedFile) throws IOException {
String lzfseName = LZFSE_NATIVE_BINARY_NAME;
if (OperatingSystem.CURRENT_OPERATING_SYSTEM.equals(OperatingSystem.WINDOWS)) {
lzfseName += ".exe";
private void ensureTool(TaskMonitor monitor) throws IOException {
if (cliTool == null) {
cliTool = LzfseCliToolWrapper.findTool(monitor);
}
File lzfseNativeBinary = Application.getOSFile(lzfseName);
File decompressedFile = Application.createTempFile(LZFSE_TEMP_PREFIX,
Long.toString(System.currentTimeMillis()));
List<String> command = new ArrayList<>();
command.add(lzfseNativeBinary.getPath());
command.add("-decode");
command.add("-i");
command.add(compressedFile.getPath());
command.add("-o");
command.add(decompressedFile.getPath());
Process p = new ProcessBuilder(command).start();
boolean success = false;
try {
if (!p.waitFor(LZFSE_NATIVE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
p.destroyForcibly();
throw new IOException("lzfse native decompressor timed out");
}
if (p.exitValue() != 0) {
throw new IOException(
"lzfse native decompressor failed with exit code: " + p.exitValue());
}
success = true;
return decompressedFile;
}
catch (InterruptedException e) {
throw new IOException(e);
}
finally {
if (!success) {
decompressedFile.delete();
}
if (cliTool == null) {
throw new FileSystemFactoryDependencyException("lzfse native decompressor not present");
}
}
}
@@ -0,0 +1,183 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.formats.sevenzip;
import java.io.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ghidra.file.cliwrapper.*;
import ghidra.formats.gfilesystem.fileinfo.FileType;
import ghidra.framework.OperatingSystem;
import ghidra.util.task.TaskMonitor;
/**
* Wrapper around 7z cli tool
*/
public class SevenZipCliToolWrapper extends AbstractCliToolWrapper
implements ArchiverCliToolWrapper {
private static final SemVer MIN_SUPPORTED_VER = SemVer.parse("23.0");
private static final List<String> NATIVE_UNIX_EXE_NAMES = List.of("7zz", "7zzs", "7z");
private static final List<String> NATIVE_WIN_EXE_NAMES = List.of("7z.exe");
private static final Pattern SZ_VER_PATTERN =
Pattern.compile(".*7-Zip \\(z\\) ([0-9.]+) .*Copyright \\(c\\).*");
private static final List<String> getCurrentOSNativeExeNames() {
return switch (OperatingSystem.CURRENT_OPERATING_SYSTEM) {
case OperatingSystem.WINDOWS -> NATIVE_WIN_EXE_NAMES;
default -> NATIVE_UNIX_EXE_NAMES;
};
}
public static SevenZipCliToolWrapper findTool(TaskMonitor monitor) {
return findToolWrapper(getCurrentOSNativeExeNames(), monitor, SevenZipCliToolWrapper::new);
}
public SevenZipCliToolWrapper(File nativeExecutable) {
super(nativeExecutable);
}
@Override
public boolean isValid(TaskMonitor monitor) {
try {
StringBuilder sb = new StringBuilder();
if (execAndReadStdOut(List.of("--help"), monitor, sb::append) == 0) {
Matcher m = SZ_VER_PATTERN.matcher(sb.toString());
if (m.matches()) {
SemVer ver = SemVer.parse(m.group(1));
return ver != SemVer.INVALID && ver.compareTo(MIN_SUPPORTED_VER) >= 0;
}
}
}
catch (IOException e) {
// fall thru
}
return false;
}
@Override
public void extract(File archiveFile, Entry entry, OutputStream os, TaskMonitor monitor)
throws IOException {
// -so = pipe extract output to stdout
// e = extract
int exitVal = execAndRedirectStdOut(
List.of("-so", "e", archiveFile.getPath(), entry.name()), null, os, monitor);
if (exitVal != 0) {
throw new IOException(
"Error during extraction: %s, retVal: %d".formatted(nativeExecutable, exitVal));
}
}
@Override
public List<Entry> getListing(File archiveFile, TaskMonitor monitor) {
try {
List<String> lines = new ArrayList<>();
if (execAndReadStdOut(List.of("l", archiveFile.getPath()), monitor, lines::add) != 0) {
return List.of();
}
boolean inListingSection = false;
List<Entry> results = new ArrayList<>();
for (int lineNum = 0; lineNum < lines.size(); lineNum++) {
String line = lines.get(lineNum);
if (isListingStartEndLine(line)) {
if (inListingSection) {
break;
}
inListingSection = true;
}
else if (inListingSection && line.length() >= NAME_START) {
SevenZipListingLine ll = parseListingLine(line);
results.add(new Entry(ll.name, ll.size, ll.fileType));
}
}
return results;
}
catch (IOException e) {
// fall thru
}
return List.of();
}
private static final int DATETIME_START = 0;
private static final int DATETIME_LEN = 10 + 1 + 8;
private static final int ATTR_START = DATETIME_START + DATETIME_LEN + 1;
private static final int ATTR_LEN = 5;
private static final int SIZE_START = ATTR_START + ATTR_LEN + 1;
private static final int SIZE_LEN = 12;
private static final int COMPRESSED_START = SIZE_START + SIZE_LEN + 1;
private static final int COMPRESSED_LEN = 12;
private static final int NAME_START = COMPRESSED_START + COMPRESSED_LEN + 1 + 1 /* 2 spaces */;
private SimpleDateFormat listingDateTimeFormat = new SimpleDateFormat("yyy-MM-dd HH:mm:ss");
private SevenZipListingLine parseListingLine(String s) {
String datetimeStr = s.substring(DATETIME_START, DATETIME_START + DATETIME_LEN);
String attrStr = s.substring(ATTR_START, ATTR_START + ATTR_LEN);
String sizeStr = s.substring(SIZE_START, SIZE_START + SIZE_LEN);
String compressedStr = s.substring(COMPRESSED_START, COMPRESSED_START + COMPRESSED_LEN);
String name = s.substring(NAME_START);
long dateMS = parseDateElse(listingDateTimeFormat, datetimeStr, 0);
FileType fileType = parseAttrs(attrStr);
long size = parseSize(sizeStr);
long compressedSize = parseSize(compressedStr);
return new SevenZipListingLine(dateMS, fileType, size, compressedSize, name);
}
private FileType parseAttrs(String s) {
return s.length() == 5 && s.charAt(0) == 'D' ? FileType.DIRECTORY : FileType.FILE;
}
private static long parseSize(String s) {
try {
return Long.parseLong(s.trim());
}
catch (NumberFormatException e) {
return 0;
}
}
private static long parseDateElse(SimpleDateFormat sdf, String s, long defaultValue) {
try {
return sdf.parse(s).getTime();
}
catch (ParseException e) {
return defaultValue;
}
}
record SevenZipListingLine(long date, FileType fileType, long size, long compressedSize,
String name) {
}
private boolean isListingStartEndLine(String s) {
String[] parts = s.split(" +");
return parts.length == 5 && isAll(parts[0], "-") && isAll(parts[1], "-") &&
isAll(parts[2], "-") && isAll(parts[3], "-") && isAll(parts[4], "-");
}
private boolean isAll(String s, String ch) {
return !s.isEmpty() && ch.repeat(s.length()).equals(s);
}
}
@@ -293,6 +293,19 @@ public class SevenZipFileSystem extends AbstractFileSystem<ISimpleInArchiveItem>
}
}
@Override
public FileType getFileType(GFile f, TaskMonitor monitor) {
synchronized (fsIndex) {
ISimpleInArchiveItem item = fsIndex.getMetadata(f);
try {
return item.isFolder() ? FileType.DIRECTORY : FileType.FILE;
}
catch (SevenZipException e) {
return FileType.UNKNOWN;
}
}
}
@Override
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor)
throws IOException, CancelledException {
@@ -15,129 +15,22 @@
*/
package ghidra.file.formats.sparseimage;
import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import ghidra.app.util.bin.ByteProvider;
import ghidra.app.util.bin.ByteProviderWrapper;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.AbstractSinglePayloadFileSystem;
import ghidra.formats.gfilesystem.FSRLRoot;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* A pseudo filesystem that contains a single file that is the decompressed contents
* of the sparse container file.
*/
@FileSystemInfo(type = "simg", description = "Android Sparse Image (simg)", factory = SparseImageFileSystemFactory.class)
public class SparseImageFileSystem implements GFileSystem {
public class SparseImageFileSystem extends AbstractSinglePayloadFileSystem {
private final FSRLRoot fsFSRL;
private final FileSystemRefManager refManager = new FileSystemRefManager(this);
private final FileSystemService fsService;
private ByteProvider byteProvider;
private ByteProvider payloadProvider;
private SingleFileSystemIndexHelper fsIndexHelper;
public SparseImageFileSystem(FSRLRoot fsFSRL, ByteProvider byteProvider,
FileSystemService fsService,
TaskMonitor monitor) throws CancelledException, IOException {
this.fsFSRL = fsFSRL;
this.fsService = fsService;
this.byteProvider = byteProvider;
this.payloadProvider = getPayload(null, monitor);
FSRL containerFSRL = byteProvider.getFSRL();
String payloadName = containerFSRL.getName() + ".raw";
this.fsIndexHelper = new SingleFileSystemIndexHelper(this, fsFSRL, payloadName,
payloadProvider.length(), payloadProvider.getFSRL().getMD5());
}
@Override
public void close() throws IOException {
refManager.onClose();
fsIndexHelper.clear();
if (byteProvider != null) {
byteProvider.close();
byteProvider = null;
}
if (payloadProvider != null) {
payloadProvider.close();
payloadProvider = null;
}
}
@Override
public boolean isClosed() {
return fsIndexHelper.isClosed();
}
@Override
public String getName() {
return fsFSRL.getContainer().getName();
}
@Override
public FSRLRoot getFSRL() {
return fsFSRL;
}
@Override
public FileSystemRefManager getRefManager() {
return refManager;
}
@Override
public GFile lookup(String path) {
return fsIndexHelper.lookup(path);
}
@Override
public GFile lookup(String path, Comparator<String> nameComp) throws IOException {
return fsIndexHelper.lookup(null, path, nameComp);
}
private ByteProvider getPayload(FSRL payloadFSRL, TaskMonitor monitor)
throws CancelledException, IOException {
return fsService.getDerivedByteProviderPush(byteProvider.getFSRL(), payloadFSRL, "sparse",
-1, os -> {
SparseImageDecompressor sid = new SparseImageDecompressor(byteProvider, os);
sid.decompress(monitor);
}, monitor);
}
@Override
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor)
throws IOException, CancelledException {
if (fsIndexHelper.isPayloadFile(file)) {
return new ByteProviderWrapper(payloadProvider, file.getFSRL());
}
return null;
}
@Override
public List<GFile> getListing(GFile directory) throws IOException {
return fsIndexHelper.getListing(directory);
}
@Override
public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) {
FileAttributes result = new FileAttributes();
if (fsIndexHelper.isPayloadFile(file)) {
try {
result.add(SIZE_ATTR, payloadProvider.length());
result.add(COMPRESSED_SIZE_ATTR, byteProvider.length());
}
catch (IOException e) {
// ignore and continue
}
result.add("MD5", fsIndexHelper.getPayloadFile().getFSRL().getMD5());
}
return result;
public SparseImageFileSystem(FSRLRoot fsFSRL, ByteProvider payloadProvider,
String payloadFilename, FileAttributes attrs) {
super(fsFSRL, payloadProvider, payloadFilename, attrs);
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -15,14 +15,17 @@
*/
package ghidra.file.formats.sparseimage;
import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*;
import java.io.IOException;
import ghidra.app.util.bin.BinaryReader;
import ghidra.app.util.bin.ByteProvider;
import ghidra.formats.gfilesystem.FSRLRoot;
import ghidra.formats.gfilesystem.FileSystemService;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider;
import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider;
import ghidra.formats.gfilesystem.fileinfo.FileAttribute;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@@ -33,7 +36,25 @@ public class SparseImageFileSystemFactory implements
public SparseImageFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider,
FileSystemService fsService, TaskMonitor monitor)
throws IOException, CancelledException {
return new SparseImageFileSystem(targetFSRL, byteProvider, fsService, monitor);
try {
ByteProvider payloadProvider = fsService
.getDerivedByteProviderPush(byteProvider.getFSRL(), null, "sparse", -1, os -> {
SparseImageDecompressor sid = new SparseImageDecompressor(byteProvider, os);
sid.decompress(monitor);
}, monitor);
FileAttributes payloadAttrs = FileAttributes.of( // attrs
FileAttribute.create(SIZE_ATTR, payloadProvider.length()),
FileAttribute.create(COMPRESSED_SIZE_ATTR, byteProvider.length()));
String payloadName = targetFSRL.getContainer().getName() + ".raw";
return new SparseImageFileSystem(targetFSRL, payloadProvider, payloadName,
payloadAttrs);
}
finally {
FSUtilities.uncheckedClose(byteProvider, null);
}
}
@Override
@@ -310,6 +310,22 @@ public class SquashFileSystem extends AbstractFileSystem<SquashedFile> {
return result;
}
@Override
public FileType getFileType(GFile f, TaskMonitor monitor) {
SquashedFile squashedFile = fsIndex.getMetadata(f);
Object squashInfo = fsIndex.getRootDir().equals(f) ? superBlock
: squashedFile != null ? squashedFile.getInode() : null;
return switch (squashInfo) {
case SquashSuperBlock sb -> FileType.DIRECTORY;
case SquashBasicDirectoryInode dir -> FileType.DIRECTORY;
case SquashBasicFileInode fileInode -> fileInode.isDir()
? FileType.DIRECTORY
: FileType.FILE;
case SquashSymlinkInode symlinkInode -> FileType.SYMBOLIC_LINK;
default -> FileType.UNKNOWN;
};
}
@Override
public void close() throws IOException {
refManager.onClose();
@@ -63,17 +63,18 @@ public class TarFileSystem extends AbstractFileSystem<TarMetadata> {
try (TarArchiveInputStream tarInput =
new TarArchiveInputStream(provider.getInputStream(0))) {
TarArchiveEntry tarEntry;
while ((tarEntry = tarInput.getNextTarEntry()) != null) {
while ((tarEntry = tarInput.getNextEntry()) != null) {
monitor.setMessage(tarEntry.getName());
monitor.checkCancelled();
int fileNum = fileCount++;
String linkName = tarEntry.getLinkName();
TarMetadata tmd = new TarMetadata(tarEntry, fileNum);
GFile newFile = !tarEntry.isSymbolicLink()
? fsIndex.storeFile(tarEntry.getName(), fileCount, tarEntry.isDirectory(),
tarEntry.getSize(), new TarMetadata(tarEntry, fileNum))
: fsIndex.storeSymlink(tarEntry.getName(), fileCount,
linkName, linkName.length(), new TarMetadata(tarEntry, fileNum));
tarEntry.getSize(), tmd)
: fsIndex.storeSymlink(tarEntry.getName(), fileCount, linkName,
linkName.length(), tmd);
if (!tarEntry.isSymbolicLink() &&
tarEntry.getSize() < FileCache.MAX_INMEM_FILESIZE) {
@@ -142,6 +143,12 @@ public class TarFileSystem extends AbstractFileSystem<TarMetadata> {
return FileType.UNKNOWN;
}
@Override
public FileType getFileType(GFile f, TaskMonitor monitor) {
TarMetadata tmd = fsIndex.getMetadata(f);
return tmd != null ? tarToFileType(tmd.tarArchiveEntry) : FileType.UNKNOWN;
}
@Override
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor)
throws IOException, CancelledException {
@@ -0,0 +1,76 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.formats.zstd;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ghidra.file.cliwrapper.*;
import ghidra.util.task.TaskMonitor;
/**
* Wrapper around the zstd cmd line tool
*/
public class ZstdCliToolWrapper extends AbstractCliToolWrapper
implements StreamDecompressorCliToolWrapper {
private static final SemVer MIN_VER = SemVer.parse("1.1.0");
private static final SemVer MAX_VER_EX = SemVer.parse("2.0");
private static final String NATIVE_EXE_NAME = "zstd";
private static final Pattern VERSION_STR_PATTERN = Pattern.compile(".*Zstandard.*v([^,]+),.*");
public static ZstdCliToolWrapper findTool(TaskMonitor monitor) {
return findToolWrapper(List.of(NATIVE_EXE_NAME), monitor, ZstdCliToolWrapper::new);
}
public ZstdCliToolWrapper(File nativeExecutable) {
super(nativeExecutable);
}
@Override
public boolean isValid(TaskMonitor monitor) {
try {
List<String> stdoutLines = new ArrayList<>();
if (execAndReadStdOut(List.of("--version"), monitor, stdoutLines::add) == 0 &&
stdoutLines.size() == 1) {
Matcher m = VERSION_STR_PATTERN.matcher(stdoutLines.get(0));
if (m.matches()) {
SemVer ver = SemVer.parse(m.group(1));
return ver != SemVer.INVALID && MIN_VER.compareTo(ver) <= 0 &&
MAX_VER_EX.compareTo(ver) > 0;
}
}
}
catch (IOException e) {
// fall thru
}
return false;
}
@Override
public void decompressStream(InputStream is, OutputStream os, TaskMonitor monitor)
throws IOException {
monitor.initialize(0, "Extracting");
int exitVal;
// -dcf == decompress, stdout, force
if ((exitVal = execAndRedirectStdOut(List.of("-dcf"), is, os, monitor)) != 0) {
throw new IOException("zstd tool error " + exitVal);
}
}
}
@@ -0,0 +1,41 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.formats.zstd;
import ghidra.app.util.bin.ByteProvider;
import ghidra.file.formats.sevenzip.SevenZipCliToolWrapper;
import ghidra.formats.gfilesystem.AbstractSinglePayloadFileSystem;
import ghidra.formats.gfilesystem.FSRLRoot;
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
/**
* GFileSystem that decompresses a zstd file and presents the decompressed payload as the file
* system's single file.
* <p>
* Depends on the user already having a zstd or 7zip cmd line tool installed somewhere within their
* operating system's PATH.
* <p>
* See {@link ZstdCliToolWrapper} and {@link SevenZipCliToolWrapper}.
*/
@FileSystemInfo(type = "zstd", description = "zStandard", factory = ZstdFileSystemFactory.class)
public class ZstdFileSystem extends AbstractSinglePayloadFileSystem {
public ZstdFileSystem(FSRLRoot fsFSRL, ByteProvider payloadProvider, String payloadFilename,
FileAttributes attrs) {
super(fsFSRL, payloadProvider, payloadFilename, attrs);
}
}
@@ -0,0 +1,129 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.file.formats.zstd;
import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*;
import java.io.*;
import java.util.Arrays;
import java.util.List;
import ghidra.app.util.bin.ByteProvider;
import ghidra.file.cliwrapper.*;
import ghidra.file.cliwrapper.ArchiverCliToolWrapper.Entry;
import ghidra.file.formats.sevenzip.SevenZipCliToolWrapper;
import ghidra.formats.gfilesystem.*;
import ghidra.formats.gfilesystem.factory.*;
import ghidra.formats.gfilesystem.fileinfo.FileAttribute;
import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
import ghidra.util.NumericUtilities;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* File system factory for zstd compressed files.
*/
public class ZstdFileSystemFactory
implements GFileSystemFactoryByteProvider<ZstdFileSystem>, GFileSystemProbeBytesOnly {
private static final byte[] MAGIC = NumericUtilities.convertStringToBytes("28b52ffd");
private CliToolWrapper cliTool;
public ZstdFileSystemFactory() {
}
@Override
public int getBytesRequired() {
return MAGIC.length;
}
@Override
public boolean probeStartBytes(FSRL containerFSRL, byte[] startBytes) {
return Arrays.equals(startBytes, 0, MAGIC.length, MAGIC, 0, MAGIC.length);
}
@Override
public GFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider,
FileSystemService fsService, TaskMonitor monitor)
throws IOException, CancelledException {
try {
ensureTool(monitor);
String containerName = targetFSRL.getContainer().getName();
String payloadFilename = containerName.endsWith(".zstd")
? containerName.substring(0, containerName.length() - ".zstd".length())
: containerName + ".uncompressed";
ByteProvider payloadProvider = extract(byteProvider, fsService, monitor);
FileAttributes fileAttrs = FileAttributes.of( // attrs
FileAttribute.create(COMPRESSED_SIZE_ATTR, byteProvider.length()),
FileAttribute.create(SIZE_ATTR, payloadProvider.length()));
ZstdFileSystem fs =
new ZstdFileSystem(targetFSRL, payloadProvider, payloadFilename, fileAttrs);
return fs;
}
finally {
byteProvider.close();
}
}
ByteProvider extract(ByteProvider byteProvider, FileSystemService fsService,
TaskMonitor monitor) throws CancelledException, IOException {
return fsService.getDerivedByteProviderPush(byteProvider.getFSRL(), null,
"zstd decompressed", -1, os -> {
if (cliTool instanceof StreamDecompressorCliToolWrapper decompTool) {
try (InputStream is = byteProvider.getInputStream(0)) {
decompTool.decompressStream(is, os, monitor);
return; // success
}
}
else if (cliTool instanceof ArchiverCliToolWrapper archiverTool) {
File f = fsService.getFileIfAvailable(byteProvider);
File tmpFile = f == null
? fsService.createPlaintextTempFile(byteProvider, "zstd_tmp_", monitor)
: null;
File archiveFile = f != null ? f : tmpFile;
try {
List<Entry> listing = archiverTool.getListing(archiveFile, monitor);
if (listing.size() == 1) {
archiverTool.extract(archiveFile, listing.get(0), os, monitor);
return; // success
}
}
finally {
if (tmpFile != null) {
tmpFile.delete();
}
}
}
throw new IOException("Failed to extract " + byteProvider.getFSRL());
}, monitor);
}
private void ensureTool(TaskMonitor monitor) throws IOException {
if (cliTool == null) {
cliTool = ZstdCliToolWrapper.findTool(monitor);
}
if (cliTool == null) {
cliTool = SevenZipCliToolWrapper.findTool(monitor);
}
if (cliTool == null) {
throw new FileSystemFactoryDependencyException("No zstd or 7z cli tool found in PATH");
}
}
}