From a0a8e5bbf308d8d99b743bcb5eaca8aae56cb8ee Mon Sep 17 00:00:00 2001 From: dev747368 <48332326+dev747368@users.noreply.github.com> Date: Wed, 8 Nov 2023 00:41:12 +0000 Subject: [PATCH] GP-4017 allow pdbs to be found in container files Add feature to allow pdbs co-located with their .exe in a zip or other container file to be found by the "Program's Import Location" search location. --- .../java/ghidra/formats/gfilesystem/FSRL.java | 23 +++ .../formats/gfilesystem/RefdInputStream.java | 113 ++++++++++++ .../ContainerFileSymbolServer.java | 161 ++++++++++++++++++ .../pdb/symbolserver/SameDirSymbolStore.java | 18 ++ .../SymbolServerInstanceCreatorContext.java | 26 ++- .../SymbolServerInstanceCreatorRegistry.java | 12 +- .../pdb/symbolserver/ui/LoadPdbDialog.java | 3 +- .../symbolserver/ui/SymbolServerPanel.java | 13 +- 8 files changed, 350 insertions(+), 19 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdInputStream.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ContainerFileSymbolServer.java diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java index 74dc1726a1..daf75db464 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FSRL.java @@ -19,6 +19,8 @@ import java.io.IOException; import java.net.MalformedURLException; import java.util.*; +import ghidra.plugin.importer.ProgramMappingService; +import ghidra.program.model.listing.Program; import ghidra.util.SystemUtilities; /** @@ -63,6 +65,27 @@ import ghidra.util.SystemUtilities; public class FSRL { public static final String PARAM_MD5 = "MD5"; + /** + * Returns the {@link FSRL} stored in a {@link Program}'s properties, or null if not present + * or malformed. + * + * @param program {@link Program} + * @return {@link FSRL} from program's properties, or null if not present or invalid + */ + public static FSRL fromProgram(Program program) { + String fsrlStr = program.getOptions(Program.PROGRAM_INFO) + .getString(ProgramMappingService.PROGRAM_SOURCE_FSRL, null); + if (fsrlStr != null) { + try { + return FSRL.fromString(fsrlStr); + } + catch (MalformedURLException e) { + // fall thru, return null + } + } + return null; + } + /** * Creates a {@link FSRL} from a raw string. The parent portions of the FSRL * are not intern()'d so will not be shared with other FSRL instances. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdInputStream.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdInputStream.java new file mode 100644 index 0000000000..a30401d854 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/RefdInputStream.java @@ -0,0 +1,113 @@ +/* ### + * 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.*; + +/** + * An {@link InputStream} wrapper that keeps a {@link FileSystemRef} pinned. + */ +public class RefdInputStream extends InputStream { + + private FileSystemRef fsRef; + private InputStream delegate; + + /** + * Creates a new {@link RefdInputStream}. + * + * @param fsRef {@link FileSystemRef} + * @param delegate the wrapped {@link InputStream} + */ + public RefdInputStream(FileSystemRef fsRef, InputStream delegate) { + this.fsRef = fsRef; + this.delegate = delegate; + } + + @Override + public void close() throws IOException { + if (fsRef != null) { + fsRef.close(); + fsRef = null; + } + delegate.close(); + } + + @Override + public int available() throws IOException { + return delegate.available(); + } + + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return delegate.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return delegate.read(b, off, len); + } + + @Override + public byte[] readAllBytes() throws IOException { + return delegate.readAllBytes(); + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + return delegate.readNBytes(b, off, len); + } + + @Override + public byte[] readNBytes(int len) throws IOException { + return delegate.readNBytes(len); + } + + @Override + public synchronized void mark(int readlimit) { + delegate.mark(readlimit); + } + + @Override + public boolean markSupported() { + return delegate.markSupported(); + } + + @Override + public long skip(long n) throws IOException { + return delegate.skip(n); + } + + @Override + public synchronized void reset() throws IOException { + delegate.reset(); + } + + @Override + public long transferTo(OutputStream out) throws IOException { + return delegate.transferTo(out); + } + + @Override + public void skipNBytes(long n) throws IOException { + delegate.skipNBytes(n); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ContainerFileSymbolServer.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ContainerFileSymbolServer.java new file mode 100644 index 0000000000..3a42b4a10b --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ContainerFileSymbolServer.java @@ -0,0 +1,161 @@ +/* ### + * 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 pdb.symbolserver; + +import java.io.*; +import java.nio.file.Files; +import java.util.List; +import java.util.Set; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.app.util.bin.ByteProvider; +import ghidra.formats.gfilesystem.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * Companion to the {@link SameDirSymbolStore}, handles the case where the imported binary + * was located in a container file (eg. zip file). + *

+ * Instances of this class are conditionally created by the + * {@link SameDirSymbolStore#createInstance(String, SymbolServerInstanceCreatorContext) registry factory method} + * when it detects that the imported binary's {@link FSRL} isn't a simple local file. + */ +public class ContainerFileSymbolServer implements SymbolServer { + + private final FileSystemService fsService; + private final FSRLRoot fsFSRL; + private final String subdir; + + public ContainerFileSymbolServer(FSRL programFSRL) { + this.fsFSRL = programFSRL.getFS(); + this.subdir = FilenameUtils.getFullPath(programFSRL.getPath()); + this.fsService = FileSystemService.getInstance(); + } + + @Override + public String getName() { + return "."; + } + + @Override + public String getDescriptiveName() { + return SameDirSymbolStore.PROGRAMS_IMPORT_LOCATION_DESCRIPTION_STR + " - " + + fsFSRL.toPrettyFullpathString(); + } + + @Override + public boolean isValid(TaskMonitor monitor) { + return true; + } + + @Override + public boolean exists(String filename, TaskMonitor monitor) { + try (RefdFile file = getFile(filename, monitor)) { + return file != null; + } + catch (IOException e1) { + // fall thru + } + return false; + } + + private RefdFile getFile(String filename, TaskMonitor monitor) { + try (FileSystemRef fsRef = fsService.getFilesystem(fsFSRL, monitor)) { + if (fsRef != null) { + GFileSystem fs = fsRef.getFilesystem(); + String path = FSUtilities.appendPath(subdir, filename); + GFile file = fs.lookup(path); + if (file != null && !file.isDirectory()) { + return new RefdFile(fsRef.dup(), file); + } + } + } + catch (IOException | CancelledException e) { + // fall thru + } + return null; + } + + @Override + public List find(SymbolFileInfo fileInfo, Set findOptions, + TaskMonitor monitor) { + + try (RefdFile fref = getFile(fileInfo.getName(), monitor)) { + // TODO: need to be able to read info from candidate pdb file using some type of stream + if (fref != null) { + GFile file = fref.file; + GFileSystem fs = file.getFilesystem(); + try (ByteProvider bp = fs.getByteProvider(file, monitor)) { + File tmpPdbFile = fsService.createPlaintextTempFile(bp, "temp_pdb", monitor); + File tmpPdbDir = + Files.createTempDirectory(tmpPdbFile.getParentFile().toPath(), "temp_pdb") + .toFile(); + File destPdbFile = new File(tmpPdbDir, file.getName()).getCanonicalFile(); + if (!destPdbFile.getParentFile().equals(tmpPdbDir)) { + throw new IOException("Bad filename: " + destPdbFile); + } + if (!tmpPdbFile.renameTo(destPdbFile)) { + throw new IOException( + "Unable to move file: " + tmpPdbFile + " to " + destPdbFile); + } + SymbolFileInfo foundInfo = SymbolFileInfo.fromFile(destPdbFile, monitor); + destPdbFile.delete(); + tmpPdbDir.delete(); + return List.of(new SymbolFileLocation(file.getName(), this, foundInfo)); + } + } + } + catch (IOException | CancelledException e) { + // fall thru + } + return List.of(); + } + + @Override + public SymbolServerInputStream getFileStream(String filename, TaskMonitor monitor) + throws IOException { + try (RefdFile fref = getFile(filename, monitor)) { + if (fref != null) { + GFile file = fref.file; + InputStream is = file.getFilesystem().getInputStream(file, monitor); + is = new RefdInputStream(fref.fsRef.dup(), is); + return new SymbolServerInputStream(is, file.getLength()); + } + } + catch (CancelledException e) { + // fall thru + } + throw new IOException(); + } + + @Override + public String getFileLocation(String filename) { + return fsFSRL.withPath(filename).toPrettyFullpathString(); + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public String toString() { + return "ContainerFileSymbolServer: [ fsrl: %s ]".formatted(fsFSRL); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SameDirSymbolStore.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SameDirSymbolStore.java index 0c1655d752..8567c87920 100644 --- a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SameDirSymbolStore.java +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SameDirSymbolStore.java @@ -18,6 +18,7 @@ package pdb.symbolserver; import java.io.*; import java.util.*; +import ghidra.formats.gfilesystem.FSRL; import ghidra.util.task.TaskMonitor; /** @@ -61,6 +62,23 @@ public class SameDirSymbolStore implements SymbolStore { return symbolFileLocation; } + /** + * Creates a {@link SymbolServer} for the "Program's Import Location" item. May return either + * a {@link SameDirSymbolStore} instance, or a {@link ContainerFileSymbolServer}. + * + * @param locationString will be "." + * @param context {@link SymbolServerInstanceCreatorContext} + * @return new {@link SymbolServer} + */ + public static SymbolServer createInstance(String locationString, + SymbolServerInstanceCreatorContext context) { + FSRL programFSRL = context.getProgramFSRL(); + if (programFSRL != null && programFSRL.getNestingDepth() != 1) { + return new ContainerFileSymbolServer(programFSRL); + } + return new SameDirSymbolStore(context.getRootDir()); + } + private final File rootDir; /** diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorContext.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorContext.java index 4e880ec480..363d9af1f1 100644 --- a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorContext.java +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorContext.java @@ -17,6 +17,11 @@ package pdb.symbolserver; import java.io.File; +import org.apache.commons.io.FilenameUtils; + +import ghidra.formats.gfilesystem.FSRL; +import ghidra.program.model.listing.Program; + /** * Context for the {@link SymbolServerInstanceCreatorRegistry} when creating new * {@link SymbolServer} instances. @@ -30,6 +35,7 @@ import java.io.File; */ public class SymbolServerInstanceCreatorContext { private final File rootDir; + private final FSRL programFSRL; private final SymbolServerInstanceCreatorRegistry symbolServerInstanceCreatorRegistry; SymbolServerInstanceCreatorContext( @@ -37,9 +43,16 @@ public class SymbolServerInstanceCreatorContext { this(null, symbolServerInstanceCreatorRegistry); } - SymbolServerInstanceCreatorContext(File rootDir, + SymbolServerInstanceCreatorContext(Program program, SymbolServerInstanceCreatorRegistry symbolServerInstanceCreatorRegistry) { - this.rootDir = rootDir; + if (program != null) { + this.programFSRL = FSRL.fromProgram(program); + this.rootDir = new File(FilenameUtils.getFullPath(program.getExecutablePath())); + } + else { + this.programFSRL = null; + this.rootDir = null; + } this.symbolServerInstanceCreatorRegistry = symbolServerInstanceCreatorRegistry; } @@ -61,4 +74,13 @@ public class SymbolServerInstanceCreatorContext { return rootDir; } + /** + * Returns the FSRL of imported binary. + * + * @return {@link FSRL} of the imported binary, or null if not present + */ + public FSRL getProgramFSRL() { + return programFSRL; + } + } diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistry.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistry.java index 9905b4477a..b3b4417833 100644 --- a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistry.java +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistry.java @@ -15,13 +15,10 @@ */ package pdb.symbolserver; -import java.util.*; -import java.util.function.Predicate; - import java.io.File; import java.net.URI; - -import org.apache.commons.io.FilenameUtils; +import java.util.*; +import java.util.function.Predicate; import ghidra.program.model.listing.Program; import ghidra.util.Msg; @@ -163,8 +160,7 @@ public class SymbolServerInstanceCreatorRegistry { * @return new {@link SymbolServerInstanceCreatorContext} */ public SymbolServerInstanceCreatorContext getContext(Program program) { - File exeLocation = new File(FilenameUtils.getFullPath(program.getExecutablePath())); - return new SymbolServerInstanceCreatorContext(exeLocation, this); + return new SymbolServerInstanceCreatorContext(program, this); } private void registerDefaultSymbolServerInstanceCreators() { @@ -173,7 +169,7 @@ public class SymbolServerInstanceCreatorRegistry { registerSymbolServerInstanceCreator(100, HttpSymbolServer::isHttpSymbolServerLocation, (loc, context) -> new HttpSymbolServer(URI.create(loc))); registerSymbolServerInstanceCreator(200, SameDirSymbolStore::isSameDirLocation, - (loc, context) -> new SameDirSymbolStore(context.getRootDir())); + SameDirSymbolStore::createInstance); registerSymbolServerInstanceCreator(300, LocalSymbolStore::isLocalSymbolStoreLocation, (loc, context) -> new LocalSymbolStore(new File(loc))); } diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/LoadPdbDialog.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/LoadPdbDialog.java index 751c14a24b..a4b7c0040d 100644 --- a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/LoadPdbDialog.java +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/LoadPdbDialog.java @@ -787,10 +787,9 @@ public class LoadPdbDialog extends DialogComponentProvider { return null; } SymbolServer symbolServer = symbolFileLocation.getSymbolServer(); - if (!(symbolServer instanceof SymbolStore)) { + if (!(symbolServer instanceof SymbolStore symbolStore)) { return null; } - SymbolStore symbolStore = (SymbolStore) symbolServer; File file = symbolStore.getFile(symbolFileLocation.getPath()); return SymbolStore.isCompressedFilename(file.getName()) ? null : file; } diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerPanel.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerPanel.java index 724bffd0a1..a0e4de296d 100644 --- a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerPanel.java +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerPanel.java @@ -91,10 +91,9 @@ class SymbolServerPanel extends JPanel { SymbolServerService temporarySymbolServerService = PdbPlugin.getSymbolServerService(symbolServerInstanceCreatorContext); - if (temporarySymbolServerService.getSymbolStore() instanceof LocalSymbolStore) { - setSymbolStorageLocation( - ((LocalSymbolStore) temporarySymbolServerService.getSymbolStore()).getRootDir(), - false); + if (temporarySymbolServerService + .getSymbolStore() instanceof LocalSymbolStore tempLocalSymbolStore) { + setSymbolStorageLocation(tempLocalSymbolStore.getRootDir(), false); } tableModel.addSymbolServers(temporarySymbolServerService.getSymbolServers()); setConfigChanged(false); @@ -368,8 +367,8 @@ class SymbolServerPanel extends JPanel { SymbolServer symbolServer = symbolServerInstanceCreatorContext.getSymbolServerInstanceCreatorRegistry() .newSymbolServer(firstSearchPath, symbolServerInstanceCreatorContext); - if (symbolServer instanceof LocalSymbolStore && - ((LocalSymbolStore) symbolServer).isValid()) { + if (symbolServer instanceof LocalSymbolStore localSymbolStore && + localSymbolStore.isValid()) { int choice = OptionDialog.showYesNoCancelDialog(this, "Set Symbol Storage Location", "Set symbol storage location to " + firstSearchPath + "?"); if (choice == OptionDialog.CANCEL_OPTION) { @@ -378,7 +377,7 @@ class SymbolServerPanel extends JPanel { if (choice == OptionDialog.YES_OPTION) { symbolServerPaths.remove(0); configChanged = true; - setSymbolStorageLocation(((LocalSymbolStore) symbolServer).getRootDir(), true); + setSymbolStorageLocation(localSymbolStore.getRootDir(), true); symbolStorageLocationTextField.setText(symbolServer.getName()); } }