diff --git a/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs b/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs index 7047f9abc..b22dcad56 100644 --- a/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PdbGenerationTestRunner.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection.Metadata; @@ -64,7 +65,8 @@ namespace ICSharpCode.Decompiler.Tests using (FileStream pdbStream = File.Open(Path.Combine(TestCasePath, nameof(CustomPdbId) + ".pdb"), FileMode.OpenOrCreate, FileAccess.ReadWrite)) { pdbStream.SetLength(0); - PortablePdbWriter.WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream, noLogo: true, pdbId: expectedPdbId); + new PortablePdbWriter { NoLogo = true, PdbId = expectedPdbId } + .WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream); pdbStream.Position = 0; var metadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader(); @@ -75,6 +77,68 @@ namespace ICSharpCode.Decompiler.Tests } } + [Test] + public void EmbedSourceFiles_False_Omits_Embedded_Source() + { + // Generating a PDB alongside a project export (where the .cs are already on disk) should be + // able to skip embedding the source again. EmbedSourceFiles = false must drop the + // EmbeddedSource custom debug info (smaller PDB, no embeddedSourceLength) while keeping the + // document checksum/hash intact. + + // Compile HelloWorld's source into a uniquely-named assembly so this test never collides + // with the HelloWorld fixture's .expected files under the fixture's parallel scope. + string sourceXml = Path.Combine(TestCasePath, nameof(HelloWorld) + ".xml"); + var files = XDocument.Parse(File.ReadAllText(sourceXml)) + .Descendants("file").ToDictionary(f => f.Attribute("name").Value, f => f.Value); + string outputBase = Path.Combine(TestCasePath, nameof(EmbedSourceFiles_False_Omits_Embedded_Source) + ".expected"); + Tester.CompileCSharpWithPdb(outputBase, files); + string peFileName = outputBase + ".dll"; + + var module = new PEFile(peFileName); + var resolver = new UniversalAssemblyResolver(peFileName, false, + module.Metadata.DetectTargetFrameworkId(), null, PEStreamOptions.PrefetchEntireImage); + + byte[] WritePdbBytes(bool embedSources) + { + var decompiler = new CSharpDecompiler(module, resolver, new DecompilerSettings()); + using var ms = new MemoryStream(); + new PortablePdbWriter { NoLogo = true, EmbedSourceFiles = embedSources } + .WritePdb(module, decompiler, new DecompilerSettings(), ms); + return ms.ToArray(); + } + + // Inspect the portable PDB metadata directly (environment-independent): count the + // EmbeddedSource custom-debug-information rows and the source documents. + (int EmbeddedSources, int Documents) Inspect(byte[] pdb) + { + using var ms = new MemoryStream(pdb); + var reader = MetadataReaderProvider.FromPortablePdbStream(ms).GetMetadataReader(); + int embedded = 0; + foreach (var handle in reader.CustomDebugInformation) + { + var cdi = reader.GetCustomDebugInformation(handle); + if (reader.GetGuid(cdi.Kind) == KnownGuids.EmbeddedSource) + embedded++; + } + return (embedded, reader.Documents.Count); + } + + var withEmbed = WritePdbBytes(embedSources: true); + var withoutEmbed = WritePdbBytes(embedSources: false); + + var embedInfo = Inspect(withEmbed); + var noEmbedInfo = Inspect(withoutEmbed); + + Assert.That(embedInfo.EmbeddedSources, Is.GreaterThan(0), + "the default (embed = true) keeps the embedded-source blobs"); + Assert.That(noEmbedInfo.EmbeddedSources, Is.EqualTo(0), + "embed = false must omit every embedded-source blob"); + Assert.That(noEmbedInfo.Documents, Is.EqualTo(embedInfo.Documents).And.GreaterThan(0), + "the source documents (with their checksum/hash) must remain even without embedded source"); + Assert.That(withoutEmbed.Length, Is.LessThan(withEmbed.Length), + "omitting embedded source must produce a strictly smaller PDB"); + } + [Test] public void ProgressReporting() { @@ -104,7 +168,8 @@ namespace ICSharpCode.Decompiler.Tests using (FileStream pdbStream = File.Open(Path.Combine(TestCasePath, nameof(ProgressReporting) + ".pdb"), FileMode.OpenOrCreate, FileAccess.ReadWrite)) { pdbStream.SetLength(0); - PortablePdbWriter.WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream, noLogo: true, progress: new TestProgressReporter(reportFunc)); + new PortablePdbWriter { NoLogo = true, Progress = new TestProgressReporter(reportFunc) } + .WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream); pdbStream.Position = 0; var metadataReader = MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader(); @@ -142,7 +207,8 @@ namespace ICSharpCode.Decompiler.Tests using (FileStream pdbStream = File.Open(Path.Combine(TestCasePath, testName + ".pdb"), FileMode.OpenOrCreate, FileAccess.ReadWrite)) { pdbStream.SetLength(0); - PortablePdbWriter.WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream, noLogo: true); + new PortablePdbWriter { NoLogo = true } + .WritePdb(moduleDefinition, decompiler, new DecompilerSettings(), pdbStream); pdbStream.Position = 0; using (Stream peStream = File.OpenRead(peFileName)) using (Stream expectedPdbStream = File.OpenRead(pdbFileName)) diff --git a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs index e43abdcfe..9ece8f447 100644 --- a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs +++ b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs @@ -45,6 +45,29 @@ namespace ICSharpCode.Decompiler.DebugInfo { const string decompilerVersion = DecompilerVersionInfo.Version; + /// Suppresses the "generated by ICSharpCode.Decompiler" header comment in each source file. + public bool NoLogo { get; set; } + + /// + /// The PDB id (GUID + stamp) to stamp into the generated PDB. When null (the default) the + /// id is taken from the assembly's CodeView debug-directory entry so the PDB matches the PE. + /// + public BlobContentId? PdbId { get; set; } + + /// Optional per-file progress reporter. + public IProgress Progress { get; set; } + + /// The title surfaced through . + public string CurrentProgressTitle { get; set; } = "Generating portable PDB..."; + + /// + /// When true (the default) each decompiled source file is embedded into the PDB. Set to + /// false when the source is already written to disk (e.g. alongside a project export) so + /// the PDB stays small and references the on-disk files instead. The per-document checksum/hash + /// is written either way. + /// + public bool EmbedSourceFiles { get; set; } = true; + public static bool HasCodeViewDebugDirectoryEntry(PEFile file) { return file != null && file.Reader.ReadDebugDirectory().Any(entry => entry.Type == DebugDirectoryEntryType.CodeView); @@ -65,16 +88,13 @@ namespace ICSharpCode.Decompiler.DebugInfo return true; } - public static void WritePdb( + public void WritePdb( PEFile file, CSharpDecompiler decompiler, DecompilerSettings settings, - Stream targetStream, - bool noLogo = false, - BlobContentId? pdbId = null, - IProgress progress = null, - string currentProgressTitle = "Generating portable PDB...") + Stream targetStream) { + BlobContentId? pdbId = PdbId; MetadataBuilder metadata = new MetadataBuilder(); MetadataReader reader = file.Metadata; var entrypointHandle = MetadataTokens.MethodDefinitionHandle(file.Reader.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress); @@ -100,7 +120,7 @@ namespace ICSharpCode.Decompiler.DebugInfo DecompilationProgress currentProgress = new() { TotalUnits = sourceFiles.Count, UnitsCompleted = 0, - Title = currentProgressTitle + Title = CurrentProgressTitle }; foreach (var sourceFile in sourceFiles) @@ -108,17 +128,17 @@ namespace ICSharpCode.Decompiler.DebugInfo // Generate syntax tree var syntaxTree = decompiler.DecompileTypes(sourceFile); - if (progress != null) + if (Progress != null) { currentProgress.UnitsCompleted++; - progress.Report(currentProgress); + Progress.Report(currentProgress); } if (!syntaxTree.HasChildren) continue; // Generate source and checksum - if (!noLogo) + if (!NoLogo) syntaxTree.InsertChildAfter(null, new Comment(" PDB and source generated by ICSharpCode.Decompiler " + decompilerVersion), Roles.Comment); var sourceText = SyntaxTreeToString(syntaxTree, settings); @@ -131,7 +151,8 @@ namespace ICSharpCode.Decompiler.DebugInfo lock (metadata) { - var sourceBlob = WriteSourceToBlob(metadata, sourceText, out var sourceCheckSum); + // The document hash is always required; the source blob is only embedded when asked. + var sourceCheckSum = ComputeSourceChecksum(sourceText); var name = metadata.GetOrAddDocumentName(sourceFile.Key); // Create Document(Handle) @@ -141,9 +162,12 @@ namespace ICSharpCode.Decompiler.DebugInfo language: metadata.GetOrAddGuid(KnownGuids.CSharpLanguageGuid)); // Add embedded source to the PDB - customDebugInfo.Add((document, - metadata.GetOrAddGuid(KnownGuids.EmbeddedSource), - sourceBlob)); + if (EmbedSourceFiles) + { + customDebugInfo.Add((document, + metadata.GetOrAddGuid(KnownGuids.EmbeddedSource), + WriteSourceToBlob(metadata, sourceText))); + } debugInfoGen.GenerateImportScopes(metadata, globalImportScope); @@ -291,7 +315,14 @@ namespace ICSharpCode.Decompiler.DebugInfo return builder; } - static BlobHandle WriteSourceToBlob(MetadataBuilder metadata, string sourceText, out byte[] sourceCheckSum) + static byte[] ComputeSourceChecksum(string sourceText) + { + byte[] bytes = Encoding.UTF8.GetBytes(sourceText); + using var hasher = SHA256.Create(); + return hasher.ComputeHash(bytes); + } + + static BlobHandle WriteSourceToBlob(MetadataBuilder metadata, string sourceText) { var builder = new BlobBuilder(); using (var memory = new MemoryStream()) @@ -303,10 +334,6 @@ namespace ICSharpCode.Decompiler.DebugInfo byte[] buffer = memory.ToArray(); builder.WriteInt32(bytes.Length); // compressed builder.WriteBytes(buffer); - using (var hasher = SHA256.Create()) - { - sourceCheckSum = hasher.ComputeHash(bytes); - } } return metadata.GetOrAddBlob(builder); diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs index 221b74e09..fe770ef8d 100644 --- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs +++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs @@ -613,7 +613,7 @@ Examples: using (FileStream stream = new FileStream(pdbFileName, FileMode.Create, FileAccess.Write)) { var decompiler = GetDecompiler(assemblyFileName); - PortablePdbWriter.WritePdb(module, decompiler, GetSettings(module), stream); + new PortablePdbWriter().WritePdb(module, decompiler, GetSettings(module), stream); } return 0; diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index 213cb38b9..6e3994fbf 100644 --- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs +++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs @@ -125,7 +125,7 @@ namespace ILSpy.Commands var decompiler = new CSharpDecompiler(file, resolver, settings) { CancellationToken = token, }; - PortablePdbWriter.WritePdb(file, decompiler, settings, stream); + new PortablePdbWriter().WritePdb(file, decompiler, settings, stream); output.Write(string.Format(Resources.GeneratedPDBFile, pdbFileName)); output.WriteLine(); }