diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 4d5b050cc..0defb16d8 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -334,6 +334,7 @@ + diff --git a/ICSharpCode.Decompiler/Pdb/PortablePdbWriter.cs b/ICSharpCode.Decompiler/Pdb/PortablePdbWriter.cs new file mode 100644 index 000000000..79b1cbdb9 --- /dev/null +++ b/ICSharpCode.Decompiler/Pdb/PortablePdbWriter.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Security.Cryptography; +using System.Text; +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.OutputVisitor; +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.TypeSystem; +using ICSharpCode.Decompiler.IL; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Util; + +namespace ICSharpCode.Decompiler.Pdb +{ + public class PortablePdbWriter + { + public static readonly Guid CSharpLanguageGuid = new Guid("3f5162f8-07c6-11d3-9053-00c04fa302a1"); + + public static readonly Guid DebugInfoEmbeddedSource = new Guid("0e8a571b-6926-466e-b4ad-8ab04611f5fe"); + + public static readonly Guid HashAlgorithmSHA1 = new Guid("ff1816ec-aa5e-4d10-87f7-6f4963833460"); + public static readonly Guid HashAlgorithmSHA256 = new Guid("8829d00f-11b8-4213-878b-770e8597ac16"); + static readonly FileVersionInfo decompilerVersion = FileVersionInfo.GetVersionInfo(typeof(CSharpDecompiler).Assembly.Location); + + + public static void WritePdb(PEFile file, CSharpDecompiler decompiler, DecompilerSettings settings, Stream targetStream) + { + MetadataBuilder metadata = new MetadataBuilder(); + MetadataReader reader = file.GetMetadataReader(); + var entrypointHandle = MetadataTokens.MethodDefinitionHandle(file.Reader.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress); + + var hasher = SHA256.Create(); + var sequencePointBlobs = new Dictionary(); + var importScopeBlobs = new Dictionary(); + var emptyList = new List(); + + foreach (var handle in reader.GetTopLevelTypeDefinitions()) { + var type = reader.GetTypeDefinition(handle); + var name = metadata.GetOrAddDocumentName("ILSpy_Generated_" + type.GetFullTypeName(reader) + "_" + Guid.NewGuid() + ".cs"); + var ast = decompiler.DecompileTypes(new[] { handle }); + ast.InsertChildAfter(null, new Comment(" PDB and source generated by ICSharpCode.Decompiler " + decompilerVersion.FileVersion), Roles.Comment); + var sourceText = SyntaxTreeToString(ast, settings); + var sequencePoints = decompiler.CreateSequencePoints(ast).ToDictionary(sp => (MethodDefinitionHandle)sp.Key.Method.MetadataToken, sp => sp.Value); + var sourceCheckSum = hasher.ComputeHash(Encoding.UTF8.GetBytes(sourceText)); + var sourceBlob = WriteSourceToBlob(metadata, sourceText); + + var document = metadata.AddDocument(name, + hashAlgorithm: metadata.GetOrAddGuid(HashAlgorithmSHA256), + hash: metadata.GetOrAddBlob(sourceCheckSum), + language: metadata.GetOrAddGuid(CSharpLanguageGuid)); + + metadata.AddCustomDebugInformation(document, metadata.GetOrAddGuid(DebugInfoEmbeddedSource), sourceBlob); + + foreach (var method in type.GetMethods()) { + var methodDef = reader.GetMethodDefinition(method); + if (!sequencePoints.TryGetValue(method, out var points)) + points = emptyList; + int localSignatureRowId; + MethodBodyBlock methodBody; + if (methodDef.RelativeVirtualAddress != 0) { + methodBody = file.Reader.GetMethodBody(methodDef.RelativeVirtualAddress); + localSignatureRowId = methodBody.LocalSignature.IsNil ? 0 : MetadataTokens.GetRowNumber(methodBody.LocalSignature); + } else { + methodBody = null; + localSignatureRowId = 0; + } + if (points.Count == 0) + sequencePointBlobs.Add(method, (default, default)); + else + sequencePointBlobs.Add(method, (document, EncodeSequencePoints(metadata, localSignatureRowId, points))); + importScopeBlobs.Add(method, (document, EncodeImportScope(metadata, reader, ast, decompiler.TypeSystem.Compilation))); + } + + foreach (var nestedTypeHandle in type.GetNestedTypes()) { + var nestedType = reader.GetTypeDefinition(nestedTypeHandle); + + foreach (var method in nestedType.GetMethods()) { + + } + } + } + + foreach (var method in reader.MethodDefinitions) { + if (sequencePointBlobs.TryGetValue(method, out var info)) { + metadata.AddMethodDebugInformation(info.Document, info.SequencePoints); + //metadata.AddMethodDebugInformation(default, default); + } else { + metadata.AddMethodDebugInformation(default, default); + } + if (importScopeBlobs.TryGetValue(method, out var scopeInfo)) { + //metadata.AddImportScope(default, scopeInfo.ImportScope); + metadata.AddMethodDebugInformation(default, default); + } else { + metadata.AddImportScope(default, default); + } + } + var debugDir = file.Reader.ReadDebugDirectory().FirstOrDefault(dir => dir.IsPortableCodeView); + var portable = file.Reader.ReadCodeViewDebugDirectoryData(debugDir); + var contentId = new BlobContentId(portable.Guid, debugDir.Stamp); + PortablePdbBuilder serializer = new PortablePdbBuilder(metadata, GetRowCounts(reader), entrypointHandle, blobs => contentId); + BlobBuilder blobBuilder = new BlobBuilder(); + serializer.Serialize(blobBuilder); + blobBuilder.WriteContentTo(targetStream); + } + + static BlobHandle WriteSourceToBlob(MetadataBuilder metadata, string sourceText) + { + var builder = new BlobBuilder(); + builder.WriteInt32(0); // uncompressed + builder.WriteUTF8(sourceText); + + return metadata.GetOrAddBlob(builder); + } + + static BlobHandle EncodeImportScope(MetadataBuilder metadata, MetadataReader reader, SyntaxTree ast, ICompilation compilation) + { + var scope = ast.Annotation()?.Resolve(compilation); + + if (scope == null) + return default; + + Dictionary assemblyReferences = new Dictionary(); + + var writer = new BlobBuilder(); + + foreach (var import in scope.Usings) { + foreach (var asm in import.ContributingAssemblies) { + if (asm == compilation.MainAssembly) { + writer.WriteByte(1); + writer.WriteCompressedInteger(MetadataTokens.GetHeapOffset(metadata.GetOrAddBlobUTF8(import.FullName))); + } else { + writer.WriteByte(2); + if (!assemblyReferences.TryGetValue(asm, out var referenceHandle)) { + foreach (var h in reader.AssemblyReferences) { + var reference = reader.GetAssemblyReference(h); + string asmName = reader.GetString(reference.Name); + if (asmName == asm.AssemblyName) { + assemblyReferences.Add(asm, referenceHandle = h); + } + } + } + Debug.Assert(!referenceHandle.IsNil); + writer.WriteCompressedInteger(MetadataTokens.GetRowNumber(referenceHandle)); + writer.WriteCompressedInteger(MetadataTokens.GetHeapOffset(metadata.GetOrAddBlobUTF8(import.FullName))); + } + } + } + + return metadata.GetOrAddBlob(writer); + } + + static BlobHandle EncodeSequencePoints(MetadataBuilder metadata, int localSignatureRowId, List sequencePoints) + { + if (sequencePoints.Count == 0) + return default; + var writer = new BlobBuilder(); + // header: + writer.WriteCompressedInteger(localSignatureRowId); + + int previousOffset = -1; + int previousStartLine = -1; + int previousStartColumn = -1; + + for (int i = 0; i < sequencePoints.Count; i++) { + var sequencePoint = sequencePoints[i]; + // delta IL offset: + if (i > 0) + writer.WriteCompressedInteger(sequencePoint.Offset - previousOffset); + else + writer.WriteCompressedInteger(sequencePoint.Offset); + previousOffset = sequencePoint.Offset; + + if (sequencePoint.IsHidden) { + writer.WriteInt16(0); + continue; + } + + int lineDelta = sequencePoint.EndLine - sequencePoint.StartLine; + int columnDelta = sequencePoint.EndColumn - sequencePoint.StartColumn; + + writer.WriteCompressedInteger(lineDelta); + + if (lineDelta == 0) { + writer.WriteCompressedInteger(columnDelta); + } else { + writer.WriteCompressedSignedInteger(columnDelta); + } + + if (previousStartLine < 0) { + writer.WriteCompressedInteger(sequencePoint.StartLine); + writer.WriteCompressedInteger(sequencePoint.StartColumn); + } else { + writer.WriteCompressedSignedInteger(sequencePoint.StartLine - previousStartLine); + writer.WriteCompressedSignedInteger(sequencePoint.StartColumn - previousStartColumn); + } + + previousStartLine = sequencePoint.StartLine; + previousStartColumn = sequencePoint.StartColumn; + } + + return metadata.GetOrAddBlob(writer); + } + + static ImmutableArray GetRowCounts(MetadataReader reader) + { + var builder = ImmutableArray.CreateBuilder(MetadataTokens.TableCount); + for (int i = 0; i < MetadataTokens.TableCount; i++) { + builder.Add(reader.GetTableRowCount((TableIndex)i)); + } + + return builder.MoveToImmutable(); + } + + static string SyntaxTreeToString(SyntaxTree syntaxTree, DecompilerSettings settings) + { + StringWriter w = new StringWriter(); + TokenWriter tokenWriter = new TextWriterTokenWriter(w); + syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true }); + tokenWriter = TokenWriter.WrapInWriterThatSetsLocationsInAST(tokenWriter); + syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); + return w.ToString(); + } + } +} diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 65ff6cae4..20bc2b973 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -192,6 +192,7 @@ + diff --git a/ILSpy/TreeNodes/GeneratePdbContextMenuEntry.cs b/ILSpy/TreeNodes/GeneratePdbContextMenuEntry.cs new file mode 100644 index 000000000..e8374e0ba --- /dev/null +++ b/ILSpy/TreeNodes/GeneratePdbContextMenuEntry.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.Pdb; +using ICSharpCode.ILSpy.TextView; +using Microsoft.Win32; + +namespace ICSharpCode.ILSpy.TreeNodes +{ + [ExportContextMenuEntry(Header = "Generate portable PDB")] + class GeneratePdbContextMenuEntry : IContextMenuEntry + { + public void Execute(TextViewContext context) + { + var language = MainWindow.Instance.CurrentLanguage; + var assembly = (context.SelectedTreeNodes?.FirstOrDefault() as AssemblyTreeNode)?.LoadedAssembly; + if (assembly == null) return; + SaveFileDialog dlg = new SaveFileDialog(); + dlg.FileName = DecompilerTextView.CleanUpName(assembly.ShortName) + ".pdb"; + dlg.Filter = "Portable PDB|*.pdb|All files|*.*"; + if (dlg.ShowDialog() != true) return; + DecompilationOptions options = new DecompilationOptions(); + string fileName = dlg.FileName; + MainWindow.Instance.TextView.RunWithCancellation(ct => Task.Factory.StartNew(() => { + AvalonEditTextOutput output = new AvalonEditTextOutput(); + Stopwatch stopwatch = Stopwatch.StartNew(); + using (FileStream stream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write)) { + try { + var file = assembly.GetPEFileOrNull(); + var decompiler = new CSharpDecompiler(file, options.DecompilerSettings); + PortablePdbWriter.WritePdb(file, decompiler, options.DecompilerSettings, stream); + } catch (OperationCanceledException) { + output.WriteLine(); + output.WriteLine("Generation was cancelled."); + throw; + } + } + stopwatch.Stop(); + output.WriteLine("Generation complete in " + stopwatch.Elapsed.TotalSeconds.ToString("F1") + " seconds."); + output.WriteLine(); + output.AddButton(null, "Open Explorer", delegate { Process.Start("explorer", "/select,\"" + fileName + "\""); }); + output.WriteLine(); + return output; + }, ct)).Then(output => MainWindow.Instance.TextView.ShowText(output)).HandleExceptions(); + } + + public bool IsEnabled(TextViewContext context) => true; + + public bool IsVisible(TextViewContext context) + { + return context.SelectedTreeNodes?.Length == 1 + && context.SelectedTreeNodes?.FirstOrDefault() is AssemblyTreeNode tn + && !tn.LoadedAssembly.HasLoadError; + } + } +}