mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1094 lines
37 KiB
1094 lines
37 KiB
// Copyright (c) 2016 Daniel Grunwald |
|
// |
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this |
|
// software and associated documentation files (the "Software"), to deal in the Software |
|
// without restriction, including without limitation the rights to use, copy, modify, merge, |
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons |
|
// to whom the Software is furnished to do so, subject to the following conditions: |
|
// |
|
// The above copyright notice and this permission notice shall be included in all copies or |
|
// substantial portions of the Software. |
|
// |
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR |
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE |
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
|
// DEALINGS IN THE SOFTWARE. |
|
|
|
using System; |
|
using System.Collections.Concurrent; |
|
using System.Collections.Generic; |
|
using System.Diagnostics; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Reflection.Metadata; |
|
using System.Runtime.InteropServices; |
|
using System.Text; |
|
using System.Threading; |
|
using System.Threading.Tasks; |
|
|
|
using ICSharpCode.Decompiler.CSharp.OutputVisitor; |
|
using ICSharpCode.Decompiler.CSharp.Syntax; |
|
using ICSharpCode.Decompiler.CSharp.Transforms; |
|
using ICSharpCode.Decompiler.DebugInfo; |
|
using ICSharpCode.Decompiler.Instrumentation; |
|
using ICSharpCode.Decompiler.Metadata; |
|
using ICSharpCode.Decompiler.Semantics; |
|
using ICSharpCode.Decompiler.Solution; |
|
using ICSharpCode.Decompiler.TypeSystem; |
|
using ICSharpCode.Decompiler.Util; |
|
|
|
using static ICSharpCode.Decompiler.Metadata.MetadataExtensions; |
|
|
|
#nullable enable |
|
|
|
namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler |
|
{ |
|
/// <summary> |
|
/// Decompiles an assembly into a visual studio project file. |
|
/// </summary> |
|
public class WholeProjectDecompiler : IProjectInfoProvider |
|
{ |
|
const int maxSegmentLength = 255; |
|
|
|
#region Settings |
|
/// <summary> |
|
/// Gets the setting this instance uses for decompiling. |
|
/// </summary> |
|
public DecompilerSettings Settings { get; } |
|
|
|
LanguageVersion? languageVersion; |
|
|
|
/// <summary> |
|
/// The C# language version written into the exported project file as LangVersion. |
|
/// This is an export parameter, not decompiler state: when not set explicitly, it defaults |
|
/// to <see cref="DecompilerSettings.GetMinimumRequiredVersion"/> of the current settings, |
|
/// and an explicit value below that minimum is rejected (here and again when the export |
|
/// starts) because the emitted code could not compile under it. |
|
/// </summary> |
|
public LanguageVersion LanguageVersion { |
|
get { return languageVersion ?? Settings.GetMinimumRequiredVersion(); } |
|
set { |
|
ValidateLanguageVersion(value); |
|
languageVersion = value; |
|
} |
|
} |
|
|
|
void ValidateLanguageVersion(LanguageVersion version) |
|
{ |
|
var minVersion = Settings.GetMinimumRequiredVersion(); |
|
if (version < minVersion) |
|
{ |
|
throw new InvalidOperationException($"The chosen settings require at least {minVersion}." + |
|
" Please change the DecompilerSettings accordingly."); |
|
} |
|
} |
|
|
|
bool IProjectInfoProvider.CheckForOverflowUnderflow => Settings.CheckForOverflowUnderflow; |
|
|
|
public IAssemblyResolver AssemblyResolver { get; } |
|
|
|
public IAssemblyReferenceClassifier AssemblyReferenceClassifier { get; } |
|
|
|
public IDebugInfoProvider? DebugInfoProvider { get; } |
|
|
|
/// <summary> |
|
/// The MSBuild ProjectGuid to use for the new project. |
|
/// </summary> |
|
public Guid ProjectGuid { get; } |
|
|
|
/// <summary> |
|
/// The target directory that the decompiled files are written to. |
|
/// </summary> |
|
/// <remarks> |
|
/// This property is set by DecompileProject() and protected so that overridden protected members |
|
/// can access it. |
|
/// </remarks> |
|
public string TargetDirectory { get; protected set; } = string.Empty; |
|
|
|
/// <summary> |
|
/// Path to the snk file to use for signing. |
|
/// <c>null</c> to not sign. |
|
/// </summary> |
|
public string? StrongNameKeyFile { get; set; } |
|
|
|
public int MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount; |
|
|
|
public IProgress<DecompilationProgress>? ProgressIndicator { get; set; } |
|
#endregion |
|
|
|
public WholeProjectDecompiler(IAssemblyResolver assemblyResolver) |
|
: this(new DecompilerSettings(), assemblyResolver, projectWriter: null, assemblyReferenceClassifier: null, debugInfoProvider: null) |
|
{ |
|
} |
|
|
|
public WholeProjectDecompiler( |
|
DecompilerSettings settings, |
|
IAssemblyResolver assemblyResolver, |
|
IProjectFileWriter? projectWriter, |
|
IAssemblyReferenceClassifier? assemblyReferenceClassifier, |
|
IDebugInfoProvider? debugInfoProvider) |
|
: this(settings, Guid.NewGuid(), assemblyResolver, projectWriter ?? IProjectFileWriter.FromSettings(settings), assemblyReferenceClassifier ?? new AssemblyReferenceClassifier(), debugInfoProvider) |
|
{ |
|
} |
|
|
|
protected WholeProjectDecompiler( |
|
DecompilerSettings settings, |
|
Guid projectGuid, |
|
IAssemblyResolver assemblyResolver, |
|
IProjectFileWriter projectWriter, |
|
IAssemblyReferenceClassifier assemblyReferenceClassifier, |
|
IDebugInfoProvider? debugInfoProvider) |
|
{ |
|
Settings = settings ?? throw new ArgumentNullException(nameof(settings)); |
|
ProjectGuid = projectGuid; |
|
AssemblyResolver = assemblyResolver ?? throw new ArgumentNullException(nameof(assemblyResolver)); |
|
AssemblyReferenceClassifier = assemblyReferenceClassifier ?? throw new ArgumentNullException(nameof(assemblyReferenceClassifier)); |
|
DebugInfoProvider = debugInfoProvider; |
|
this.projectWriter = projectWriter ?? throw new ArgumentNullException(nameof(projectWriter)); |
|
} |
|
|
|
// per-run members |
|
HashSet<string> directories = new HashSet<string>(Platform.FileNameComparer); |
|
readonly List<DecompilerException> errors = new List<DecompilerException>(); |
|
readonly IProjectFileWriter projectWriter; |
|
|
|
/// <summary> |
|
/// Everything that went wrong during the last <see cref="DecompileProject(MetadataFile, string, CancellationToken)"/>. |
|
/// An export never aborts on a member, file or resource it cannot handle; it writes the |
|
/// error text where the content would have gone and continues, so a single unsupported |
|
/// method still yields a complete project. Callers should show this list to the user - |
|
/// otherwise the failures ship silently and never get reported. |
|
/// </summary> |
|
public IReadOnlyList<DecompilerException> Errors => errors; |
|
|
|
void RecordError(DecompilerException error) |
|
{ |
|
lock (errors) |
|
{ |
|
errors.Add(error); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Yields the items of <paramref name="items"/> until one of them throws; the failure is |
|
/// recorded instead of aborting the export. |
|
/// </summary> |
|
IEnumerable<T> RecordingErrors<T>(IEnumerable<T> items, MetadataFile file, string what) |
|
{ |
|
using var enumerator = items.GetEnumerator(); |
|
bool lastMoveFailed = false; |
|
while (true) |
|
{ |
|
T item; |
|
try |
|
{ |
|
if (!enumerator.MoveNext()) |
|
yield break; |
|
item = enumerator.Current; |
|
} |
|
catch (Exception ex) when (!(ex is OperationCanceledException)) |
|
{ |
|
RecordError(ex as DecompilerException ?? new DecompilerException(file, $"Error writing {what}", ex)); |
|
// Skip the item that failed and try the next one, but give up once two attempts |
|
// in a row fail: an enumerator that throws without advancing - which nothing |
|
// stops an override from being - would otherwise loop forever. |
|
if (lastMoveFailed) |
|
yield break; |
|
lastMoveFailed = true; |
|
continue; |
|
} |
|
lastMoveFailed = false; |
|
yield return item; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Puts the error text where the file's contents would have gone. The writer itself may be |
|
/// what failed - a full disk, a stream already closed - so a second failure while reporting |
|
/// the first is dropped rather than allowed to take the export down. |
|
/// </summary> |
|
static void WriteErrorComment(TextWriter? writer, Exception error) |
|
{ |
|
if (writer == null) |
|
return; |
|
try |
|
{ |
|
// The failure may have interrupted the output visitor mid-line. |
|
writer.WriteLine(); |
|
foreach (string line in CSharpDecompiler.GetErrorCommentLines(error)) |
|
{ |
|
writer.WriteLine("// " + line); |
|
} |
|
} |
|
catch (Exception ex) when (!(ex is OperationCanceledException)) |
|
{ |
|
} |
|
} |
|
|
|
public void DecompileProject(MetadataFile file, string targetDirectory, CancellationToken cancellationToken = default(CancellationToken)) |
|
{ |
|
string projectFileName = Path.Combine(targetDirectory, CleanUpFileName(file.Name, ".csproj")); |
|
using (var writer = CreateFile(projectFileName)) |
|
{ |
|
DecompileProject(file, targetDirectory, writer, cancellationToken); |
|
} |
|
} |
|
|
|
public ProjectId DecompileProject(MetadataFile file, string targetDirectory, TextWriter projectFileWriter, CancellationToken cancellationToken = default(CancellationToken)) |
|
{ |
|
if (string.IsNullOrEmpty(targetDirectory)) |
|
{ |
|
throw new InvalidOperationException("Must set TargetDirectory"); |
|
} |
|
// The LanguageVersion setter already rejects a version below what the settings require, |
|
// but Settings is mutable and shared, so re-validate against the settings actually in |
|
// effect now - otherwise the exported project would carry a LangVersion under which the |
|
// emitted code cannot compile. |
|
if (languageVersion is { } explicitVersion) |
|
{ |
|
ValidateLanguageVersion(explicitVersion); |
|
} |
|
DecompilerEventSource.Log.ProjectDecompilationStart(file.Name); |
|
int codeFileCount = 0, resourceFileCount = 0; |
|
try |
|
{ |
|
TargetDirectory = targetDirectory; |
|
directories.Clear(); |
|
errors.Clear(); |
|
var resources = RecordingErrors(WriteResourceFilesInProject(file), file, "resource files").ToList(); |
|
resourceFileCount = resources.Count; |
|
var files = WriteCodeFilesInProject(file, resources.SelectMany(r => r.PartialTypes ?? Enumerable.Empty<PartialTypeInfo>()).ToList(), cancellationToken).ToList(); |
|
codeFileCount = files.Count; |
|
files.AddRange(resources); |
|
var module = file as PEFile; |
|
if (module != null) |
|
{ |
|
files.AddRange(RecordingErrors(WriteMiscellaneousFilesInProject(module), file, "miscellaneous files")); |
|
} |
|
if (StrongNameKeyFile != null) |
|
{ |
|
File.Copy(StrongNameKeyFile, Path.Combine(targetDirectory, Path.GetFileName(StrongNameKeyFile)), overwrite: true); |
|
} |
|
|
|
projectWriter.Write(projectFileWriter, this, files, file); |
|
|
|
string platformName = module != null ? TargetServices.GetPlatformName(module) : "AnyCPU"; |
|
return new ProjectId(platformName, ProjectGuid, ProjectTypeGuids.CSharpWindows); |
|
} |
|
finally |
|
{ |
|
DecompilerEventSource.Log.ProjectDecompilationStop(file.Name, codeFileCount, resourceFileCount); |
|
} |
|
} |
|
|
|
#region WriteCodeFilesInProject |
|
protected virtual bool IncludeTypeWhenDecompilingProject(MetadataFile module, TypeDefinitionHandle type) |
|
{ |
|
var metadata = module.Metadata; |
|
var typeDef = metadata.GetTypeDefinition(type); |
|
string name = metadata.GetString(typeDef.Name); |
|
string ns = metadata.GetString(typeDef.Namespace); |
|
if (name == "<Module>" || CSharpDecompiler.MemberIsHidden(module, type, Settings)) |
|
return false; |
|
if (ns == "XamlGeneratedNamespace" && name == "GeneratedInternalTypeHelper") |
|
return false; |
|
if (!typeDef.IsNested && RemoveEmbeddedAttributes.attributeNames.Contains(ns + "." + name)) |
|
return false; |
|
return true; |
|
} |
|
|
|
protected virtual TextWriter CreateFile(string path) |
|
{ |
|
return new StreamWriter(path); |
|
} |
|
|
|
protected virtual void CreateDirectory(string path) |
|
{ |
|
try |
|
{ |
|
Directory.CreateDirectory(path); |
|
} |
|
catch (IOException) |
|
{ |
|
File.Delete(path); |
|
Directory.CreateDirectory(path); |
|
} |
|
} |
|
|
|
protected virtual CSharpDecompiler CreateDecompiler(DecompilerTypeSystem ts) |
|
{ |
|
var decompiler = new CSharpDecompiler(ts, Settings); |
|
decompiler.DebugInfoProvider = DebugInfoProvider; |
|
decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers()); |
|
decompiler.AstTransforms.Add(new RemoveCLSCompliantAttribute()); |
|
return decompiler; |
|
} |
|
|
|
IEnumerable<ProjectItemInfo> WriteAssemblyInfo(DecompilerTypeSystem ts, CancellationToken cancellationToken) |
|
{ |
|
var decompiler = CreateDecompiler(ts); |
|
decompiler.CancellationToken = cancellationToken; |
|
decompiler.AstTransforms.Add(new RemoveCompilerGeneratedAssemblyAttributes()); |
|
SyntaxTree syntaxTree = decompiler.DecompileModuleAndAssemblyAttributes(); |
|
|
|
const string prop = "Properties"; |
|
if (directories.Add(prop)) |
|
CreateDirectory(Path.Combine(TargetDirectory, prop)); |
|
string assemblyInfo = Path.Combine(prop, "AssemblyInfo.cs"); |
|
using (var w = CreateFile(Path.Combine(TargetDirectory, assemblyInfo))) |
|
{ |
|
syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, Settings.CSharpFormattingOptions)); |
|
} |
|
return new[] { new ProjectItemInfo("Compile", assemblyInfo) }; |
|
} |
|
|
|
IEnumerable<ProjectItemInfo> WriteCodeFilesInProject(MetadataFile module, IList<PartialTypeInfo> partialTypes, CancellationToken cancellationToken) |
|
{ |
|
var metadata = module.Metadata; |
|
var files = module.Metadata.GetTopLevelTypeDefinitions().Where(td => IncludeTypeWhenDecompilingProject(module, td)) |
|
.GroupBy(GetFileFileNameForHandle, StringComparer.OrdinalIgnoreCase).ToList(); |
|
var progressReporter = ProgressIndicator; |
|
var progress = new DecompilationProgress { TotalUnits = files.Count, Title = "Exporting project..." }; |
|
DecompilerTypeSystem ts = new DecompilerTypeSystem(module, AssemblyResolver, Settings); |
|
var missingFiles = new ConcurrentBag<string>(); |
|
var workList = new HashSet<TypeDefinitionHandle>(); |
|
var processedTypes = new HashSet<TypeDefinitionHandle>(); |
|
ProcessFiles(files); |
|
while (workList.Count > 0) |
|
{ |
|
var additionalFiles = workList |
|
.GroupBy(GetFileFileNameForHandle, StringComparer.OrdinalIgnoreCase).ToList(); |
|
workList.Clear(); |
|
ProcessFiles(additionalFiles); |
|
files.AddRange(additionalFiles); |
|
progress.TotalUnits = files.Count; |
|
} |
|
|
|
// The assembly-level attributes are a single file like any other: failing to decompile |
|
// them costs that file, not the export. |
|
IEnumerable<ProjectItemInfo> assemblyInfo; |
|
try |
|
{ |
|
assemblyInfo = WriteAssemblyInfo(ts, cancellationToken); |
|
} |
|
catch (Exception ex) when (!(ex is OperationCanceledException)) |
|
{ |
|
RecordError(ex as DecompilerException ?? new DecompilerException(module, "Error decompiling the module and assembly attributes", ex)); |
|
assemblyInfo = Enumerable.Empty<ProjectItemInfo>(); |
|
} |
|
|
|
return files.Select(f => f.Key).Except(missingFiles, Platform.FileNameComparer) |
|
.Select(f => new ProjectItemInfo("Compile", f)).Concat(assemblyInfo); |
|
|
|
string GetFileFileNameForHandle(TypeDefinitionHandle h) |
|
{ |
|
var type = metadata.GetTypeDefinition(h); |
|
string file = CleanUpFileName(metadata.GetString(type.Name), ".cs"); |
|
string ns = metadata.GetString(type.Namespace); |
|
if (string.IsNullOrEmpty(ns)) |
|
{ |
|
return file; |
|
} |
|
else |
|
{ |
|
string dir = Settings.UseNestedDirectoriesForNamespaces ? CleanUpPath(ns) : CleanUpDirectoryName(ns); |
|
if (directories.Add(dir)) |
|
{ |
|
var path = Path.Combine(TargetDirectory, dir); |
|
CreateDirectory(path); |
|
} |
|
return Path.Combine(dir, file); |
|
} |
|
} |
|
|
|
void ProcessFiles(List<IGrouping<string, TypeDefinitionHandle>> files) |
|
{ |
|
processedTypes.AddRange(files.SelectMany(f => f)); |
|
Parallel.ForEach( |
|
Partitioner.Create(files, loadBalance: true), |
|
new ParallelOptions { |
|
MaxDegreeOfParallelism = this.MaxDegreeOfParallelism, |
|
CancellationToken = cancellationToken |
|
}, |
|
delegate (IGrouping<string, TypeDefinitionHandle> file) { |
|
var declaredTypes = file.ToArray(); |
|
DecompilerEventSource.Log.ProjectFileStart(file.Key, declaredTypes.Length); |
|
// Everything that can fail for this one file - creating it included, which is |
|
// where a path too long for the file system surfaces - belongs inside the try. |
|
TextWriter? w = null; |
|
CSharpDecompiler? decompiler = null; |
|
try |
|
{ |
|
w = CreateFile(Path.Combine(TargetDirectory, file.Key)); |
|
decompiler = CreateDecompiler(ts); |
|
|
|
foreach (var partialType in partialTypes) |
|
{ |
|
decompiler.AddPartialTypeDefinition(partialType); |
|
} |
|
|
|
decompiler.CancellationToken = cancellationToken; |
|
var syntaxTree = decompiler.DecompileTypes(declaredTypes); |
|
|
|
foreach (var node in syntaxTree.Descendants) |
|
{ |
|
var td = (node.GetResolveResult() as TypeResolveResult)?.Type.GetDefinition(); |
|
if (td?.ParentModule != ts.MainModule) |
|
continue; |
|
while (td?.DeclaringTypeDefinition != null) |
|
{ |
|
td = td.DeclaringTypeDefinition; |
|
} |
|
if (td != null && td.MetadataToken is { IsNil: false } token && !processedTypes.Contains((TypeDefinitionHandle)token)) |
|
{ |
|
lock (workList) |
|
{ |
|
workList.Add((TypeDefinitionHandle)token); |
|
} |
|
} |
|
} |
|
|
|
// A member the output visitor cannot write is replaced by the error text |
|
// rather than truncating the file where it failed. |
|
var outputVisitor = new ErrorTolerantOutputVisitor(w, Settings.CSharpFormattingOptions); |
|
syntaxTree.AcceptVisitor(outputVisitor); |
|
foreach (var outputError in outputVisitor.Errors) |
|
{ |
|
RecordError(new DecompilerException(module, $"Error writing '{file.Key}'", outputError)); |
|
} |
|
} |
|
catch (Exception innerException) when (!(innerException is OperationCanceledException)) |
|
{ |
|
// Whatever the decompiler could not cope with here, the remaining files |
|
// are unaffected and the user still gets a complete project; the error |
|
// takes the place of the file's contents. |
|
RecordError(innerException as DecompilerException ?? new DecompilerException(module, $"Error decompiling for '{file.Key}'", innerException)); |
|
if (w == null) |
|
{ |
|
// Nothing was written, so nothing can carry the error text - and the |
|
// project must not claim a file that is not there. |
|
missingFiles.Add(file.Key); |
|
} |
|
WriteErrorComment(w, innerException); |
|
} |
|
finally |
|
{ |
|
foreach (var error in decompiler?.Errors ?? (IReadOnlyList<DecompilerException>)Array.Empty<DecompilerException>()) |
|
{ |
|
RecordError(error); |
|
} |
|
try |
|
{ |
|
w?.Dispose(); |
|
} |
|
catch (Exception ex) when (!(ex is OperationCanceledException)) |
|
{ |
|
// Dispose flushes: on a full disk this is where the write actually fails. |
|
RecordError(new DecompilerException(module, $"Error writing '{file.Key}'", ex)); |
|
} |
|
DecompilerEventSource.Log.ProjectFileStop(file.Key); |
|
} |
|
progress.Status = file.Key; |
|
Interlocked.Increment(ref progress.UnitsCompleted); |
|
progressReporter?.Report(progress); |
|
}); |
|
} |
|
} |
|
#endregion |
|
|
|
#region WriteResourceFilesInProject |
|
protected virtual IEnumerable<ProjectItemInfo> WriteResourceFilesInProject(MetadataFile module) |
|
{ |
|
foreach (var r in module.Resources.Where(r => r.ResourceType == ResourceType.Embedded)) |
|
{ |
|
List<ProjectItemInfo> items; |
|
try |
|
{ |
|
items = WriteResourceFileInProject(module, r).ToList(); |
|
} |
|
catch (Exception ex) when (!(ex is OperationCanceledException)) |
|
{ |
|
// One resource nobody can decode - a mangled .resources blob, a BAML stream the |
|
// decompiler chokes on - costs that resource, not the ones behind it. |
|
RecordError(ex as DecompilerException ?? new DecompilerException(module, $"Error writing resource '{r.Name}'", ex)); |
|
continue; |
|
} |
|
foreach (var item in items) |
|
{ |
|
yield return item; |
|
} |
|
} |
|
} |
|
|
|
IEnumerable<ProjectItemInfo> WriteResourceFileInProject(MetadataFile module, Resource r) |
|
{ |
|
Stream? stream = r.TryOpenStream(); |
|
if (stream == null) |
|
yield break; |
|
|
|
stream.Position = 0; |
|
|
|
if (r.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) |
|
{ |
|
bool decodedIntoIndividualFiles; |
|
var individualResources = new List<ProjectItemInfo>(); |
|
try |
|
{ |
|
var resourcesFile = new ResourcesFile(stream); |
|
if (resourcesFile.AllEntriesAreStreams()) |
|
{ |
|
foreach (var (name, value) in resourcesFile) |
|
{ |
|
string fileName = SanitizeFileName(name); |
|
string? dirName = Path.GetDirectoryName(fileName); |
|
if (!string.IsNullOrEmpty(dirName) && directories.Add(dirName)) |
|
{ |
|
CreateDirectory(Path.Combine(TargetDirectory, dirName)); |
|
} |
|
Stream entryStream = (Stream)value!; |
|
entryStream.Position = 0; |
|
try |
|
{ |
|
individualResources.AddRange( |
|
WriteResourceToFile(fileName, name, entryStream)); |
|
} |
|
catch (Exception ex) when (!(ex is OperationCanceledException)) |
|
{ |
|
// One entry nobody can decode - a BAML stream carrying characters XML |
|
// cannot represent, say - costs that entry, not every other entry |
|
// sharing the container with it. |
|
RecordError(ex as DecompilerException ?? new DecompilerException(module, $"Error writing resource '{name}'", ex)); |
|
} |
|
} |
|
decodedIntoIndividualFiles = true; |
|
} |
|
else |
|
{ |
|
decodedIntoIndividualFiles = false; |
|
} |
|
} |
|
catch (BadImageFormatException) |
|
{ |
|
decodedIntoIndividualFiles = false; |
|
} |
|
catch (EndOfStreamException) |
|
{ |
|
decodedIntoIndividualFiles = false; |
|
} |
|
if (decodedIntoIndividualFiles) |
|
{ |
|
foreach (var entry in individualResources) |
|
{ |
|
yield return entry; |
|
} |
|
} |
|
else |
|
{ |
|
stream.Position = 0; |
|
string fileName = GetFileNameForResource(r.Name); |
|
foreach (var entry in WriteResourceToFile(fileName, r.Name, stream)) |
|
{ |
|
yield return entry; |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
string fileName = GetFileNameForResource(r.Name); |
|
using (FileStream fs = new FileStream(Path.Combine(TargetDirectory, fileName), FileMode.Create, FileAccess.Write)) |
|
{ |
|
stream.Position = 0; |
|
stream.CopyTo(fs); |
|
} |
|
yield return new ProjectItemInfo("EmbeddedResource", fileName).With("LogicalName", r.Name); |
|
} |
|
} |
|
|
|
protected virtual IEnumerable<ProjectItemInfo> WriteResourceToFile(string fileName, string resourceName, Stream entryStream) |
|
{ |
|
if (fileName.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) |
|
{ |
|
string resx = Path.ChangeExtension(fileName, ".resx"); |
|
try |
|
{ |
|
using (FileStream fs = new FileStream(Path.Combine(TargetDirectory, resx), FileMode.Create, FileAccess.Write)) |
|
using (ResXResourceWriter writer = new ResXResourceWriter(fs)) |
|
{ |
|
foreach (var entry in new ResourcesFile(entryStream)) |
|
{ |
|
writer.AddResource(entry.Key, entry.Value); |
|
} |
|
} |
|
return new[] { new ProjectItemInfo("EmbeddedResource", resx).With("LogicalName", resourceName) }; |
|
} |
|
catch (BadImageFormatException) |
|
{ |
|
// if the .resources can't be decoded, just save them as-is |
|
} |
|
catch (EndOfStreamException) |
|
{ |
|
// if the .resources can't be decoded, just save them as-is |
|
} |
|
} |
|
using (FileStream fs = new FileStream(Path.Combine(TargetDirectory, fileName), FileMode.Create, FileAccess.Write)) |
|
{ |
|
entryStream.CopyTo(fs); |
|
} |
|
return new[] { new ProjectItemInfo("EmbeddedResource", fileName).With("LogicalName", resourceName) }; |
|
} |
|
|
|
string GetFileNameForResource(string fullName) |
|
{ |
|
// Clean up the name first and ensure the length does not exceed the maximum length |
|
// supported by the OS. |
|
fullName = SanitizeFileName(fullName); |
|
// The purpose of the below algorithm is to "maximize" the directory name and "minimize" the file name. |
|
// That is, a full name of the form "Namespace1.Namespace2{...}.NamespaceN.ResourceName" is split such that |
|
// the directory part Namespace1\Namespace2\... reuses as many existing directories as |
|
// possible, and only the remaining name parts are used as prefix for the filename. |
|
// This is not affected by the UseNestedDirectoriesForNamespaces setting. |
|
string[] splitName = fullName.Split('\\', '/'); |
|
string fileName = string.Join(".", splitName); |
|
string separator = Path.DirectorySeparatorChar.ToString(); |
|
for (int i = splitName.Length - 1; i > 0; i--) |
|
{ |
|
string ns = string.Join(separator, splitName, 0, i); |
|
if (directories.Contains(ns)) |
|
{ |
|
string name = string.Join(".", splitName, i, splitName.Length - i); |
|
fileName = Path.Combine(ns, name); |
|
break; |
|
} |
|
} |
|
return fileName; |
|
} |
|
#endregion |
|
|
|
#region WriteMiscellaneousFilesInProject |
|
protected virtual IEnumerable<ProjectItemInfo> WriteMiscellaneousFilesInProject(PEFile module) |
|
{ |
|
var resources = module.Reader.ReadWin32Resources(); |
|
if (resources == null) |
|
yield break; |
|
|
|
// Each file is written on its own, so the one that fails is the only one lost. |
|
foreach (var item in TryWrite(module, "app.ico", () => { |
|
byte[]? appIcon = CreateApplicationIcon(resources); |
|
if (appIcon == null) |
|
return null; |
|
File.WriteAllBytes(Path.Combine(TargetDirectory, "app.ico"), appIcon); |
|
return new ProjectItemInfo("ApplicationIcon", "app.ico"); |
|
})) |
|
{ |
|
yield return item; |
|
} |
|
|
|
foreach (var item in TryWrite(module, "app.manifest", () => { |
|
byte[]? appManifest = CreateApplicationManifest(resources); |
|
if (appManifest == null || IsDefaultApplicationManifest(appManifest)) |
|
return null; |
|
File.WriteAllBytes(Path.Combine(TargetDirectory, "app.manifest"), appManifest); |
|
return new ProjectItemInfo("ApplicationManifest", "app.manifest"); |
|
})) |
|
{ |
|
yield return item; |
|
} |
|
|
|
foreach (var item in TryWrite(module, "app.config", () => { |
|
var appConfig = module.FileName + ".config"; |
|
if (!File.Exists(appConfig)) |
|
return null; |
|
File.Copy(appConfig, Path.Combine(TargetDirectory, "app.config"), overwrite: true); |
|
return new ProjectItemInfo("ApplicationConfig", Path.GetFileName(appConfig)); |
|
})) |
|
{ |
|
yield return item; |
|
} |
|
} |
|
|
|
IEnumerable<ProjectItemInfo> TryWrite(MetadataFile module, string what, Func<ProjectItemInfo?> write) |
|
{ |
|
ProjectItemInfo? item; |
|
try |
|
{ |
|
item = write(); |
|
} |
|
catch (Exception ex) when (!(ex is OperationCanceledException)) |
|
{ |
|
RecordError(ex as DecompilerException ?? new DecompilerException(module, $"Error writing '{what}'", ex)); |
|
yield break; |
|
} |
|
if (item.HasValue) |
|
{ |
|
yield return item.Value; |
|
} |
|
} |
|
|
|
const int RT_ICON = 3; |
|
const int RT_GROUP_ICON = 14; |
|
|
|
unsafe static byte[]? CreateApplicationIcon(Win32ResourceDirectory resources) |
|
{ |
|
var iconGroup = resources.Find(new Win32ResourceName(RT_GROUP_ICON))?.FirstDirectory()?.FirstData()?.Data; |
|
if (iconGroup == null) |
|
return null; |
|
|
|
var iconDir = resources.Find(new Win32ResourceName(RT_ICON)); |
|
if (iconDir == null) |
|
return null; |
|
|
|
using var outStream = new MemoryStream(); |
|
using var writer = new BinaryWriter(outStream); |
|
fixed (byte* pIconGroupData = iconGroup) |
|
{ |
|
var pIconGroup = (GRPICONDIR*)pIconGroupData; |
|
writer.Write(pIconGroup->idReserved); |
|
writer.Write(pIconGroup->idType); |
|
writer.Write(pIconGroup->idCount); |
|
|
|
int iconCount = pIconGroup->idCount; |
|
uint offset = (2 * 3) + ((uint)iconCount * 0x10); |
|
for (int i = 0; i < iconCount; i++) |
|
{ |
|
var pIconEntry = pIconGroup->idEntries + i; |
|
writer.Write(pIconEntry->bWidth); |
|
writer.Write(pIconEntry->bHeight); |
|
writer.Write(pIconEntry->bColorCount); |
|
writer.Write(pIconEntry->bReserved); |
|
writer.Write(pIconEntry->wPlanes); |
|
writer.Write(pIconEntry->wBitCount); |
|
writer.Write(pIconEntry->dwBytesInRes); |
|
writer.Write(offset); |
|
offset += pIconEntry->dwBytesInRes; |
|
} |
|
|
|
for (int i = 0; i < iconCount; i++) |
|
{ |
|
var icon = iconDir.FindDirectory(new Win32ResourceName(pIconGroup->idEntries[i].nID))?.FirstData()?.Data; |
|
if (icon == null) |
|
return null; |
|
writer.Write(icon); |
|
} |
|
} |
|
|
|
return outStream.ToArray(); |
|
} |
|
|
|
[StructLayout(LayoutKind.Sequential, Pack = 2)] |
|
unsafe struct GRPICONDIR |
|
{ |
|
public ushort idReserved; |
|
public ushort idType; |
|
public ushort idCount; |
|
private fixed byte _idEntries[1]; |
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>")] |
|
public GRPICONDIRENTRY* idEntries { |
|
get { |
|
fixed (byte* p = _idEntries) |
|
return (GRPICONDIRENTRY*)p; |
|
} |
|
} |
|
}; |
|
|
|
[StructLayout(LayoutKind.Sequential, Pack = 2)] |
|
struct GRPICONDIRENTRY |
|
{ |
|
public byte bWidth; |
|
public byte bHeight; |
|
public byte bColorCount; |
|
public byte bReserved; |
|
public ushort wPlanes; |
|
public ushort wBitCount; |
|
public uint dwBytesInRes; |
|
public short nID; |
|
}; |
|
|
|
const int RT_MANIFEST = 24; |
|
|
|
unsafe static byte[]? CreateApplicationManifest(Win32ResourceDirectory resources) |
|
{ |
|
return resources.Find(new Win32ResourceName(RT_MANIFEST))?.FirstDirectory()?.FirstData()?.Data; |
|
} |
|
|
|
static bool IsDefaultApplicationManifest(byte[] appManifest) |
|
{ |
|
const string DEFAULT_APPMANIFEST = |
|
"<?xmlversion=\"1.0\"encoding=\"UTF-8\"standalone=\"yes\"?><assemblyxmlns=\"urn:schemas-microsoft-com" + |
|
":asm.v1\"manifestVersion=\"1.0\"><assemblyIdentityversion=\"1.0.0.0\"name=\"MyApplication.app\"/><tr" + |
|
"ustInfoxmlns=\"urn:schemas-microsoft-com:asm.v2\"><security><requestedPrivilegesxmlns=\"urn:schemas-" + |
|
"microsoft-com:asm.v3\"><requestedExecutionLevellevel=\"asInvoker\"uiAccess=\"false\"/></requestedPri" + |
|
"vileges></security></trustInfo></assembly>"; |
|
|
|
string s = CleanUpApplicationManifest(appManifest); |
|
return s == DEFAULT_APPMANIFEST; |
|
} |
|
|
|
static string CleanUpApplicationManifest(byte[] appManifest) |
|
{ |
|
bool bom = appManifest.Length >= 3 && appManifest[0] == 0xEF && appManifest[1] == 0xBB && appManifest[2] == 0xBF; |
|
string s = Encoding.UTF8.GetString(appManifest, bom ? 3 : 0, appManifest.Length - (bom ? 3 : 0)); |
|
var sb = new StringBuilder(s.Length); |
|
for (int i = 0; i < s.Length; i++) |
|
{ |
|
char c = s[i]; |
|
switch (c) |
|
{ |
|
case '\t': |
|
case '\n': |
|
case '\r': |
|
case ' ': |
|
continue; |
|
} |
|
sb.Append(c); |
|
} |
|
return sb.ToString(); |
|
} |
|
#endregion |
|
|
|
/// <summary> |
|
/// Cleans up a node name for use as a file name. |
|
/// </summary> |
|
public static string CleanUpFileName(string text, string? extension) |
|
{ |
|
if (string.IsNullOrEmpty(extension) || extension.StartsWith(".")) |
|
text = $"{text}{extension}"; |
|
else |
|
text = $"{text}.{extension}"; |
|
|
|
return CleanUpName(text, separateAtDots: false, treatAsFileName: !string.IsNullOrEmpty(extension), treatAsPath: false); |
|
} |
|
|
|
/// <summary> |
|
/// Removes invalid characters from file names and reduces their length, |
|
/// but keeps file extensions and path structure intact. |
|
/// </summary> |
|
public static string SanitizeFileName(string fileName) |
|
{ |
|
return CleanUpName(fileName, separateAtDots: false, treatAsFileName: true, treatAsPath: true); |
|
} |
|
|
|
/// <summary> |
|
/// Cleans up a node name for use as a file system name. If <paramref name="separateAtDots"/> is active, |
|
/// dots are seen as segment separators. Each segment is limited to maxSegmentLength characters. |
|
/// If <paramref name="treatAsFileName"/> is active, we check for file a extension and try to preserve it, |
|
/// if it's valid. |
|
/// </summary> |
|
static string CleanUpName(string text, bool separateAtDots, bool treatAsFileName, bool treatAsPath) |
|
{ |
|
string? extension = null; |
|
int currentSegmentLength = 0; |
|
// Extract extension from the end of the name, if valid |
|
if (treatAsFileName) |
|
{ |
|
// Check if input is a file name, i.e., has a valid extension |
|
// If yes, preserve extension and append it at the end. |
|
// But only, if the extension length does not exceed maxSegmentLength, |
|
// if that's the case we just give up and treat the extension no different |
|
// from the file name. |
|
int lastDot = text.LastIndexOf('.'); |
|
if (lastDot >= 0 && text.Length - lastDot < maxSegmentLength) |
|
{ |
|
string originalText = text; |
|
extension = text.Substring(lastDot); |
|
text = text.Remove(lastDot); |
|
foreach (var c in extension) |
|
{ |
|
if (!(char.IsLetterOrDigit(c) || c == '-' || c == '_' || c == '.')) |
|
{ |
|
// extension contains an invalid character, therefore cannot be a valid extension. |
|
extension = null; |
|
text = originalText; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
// Remove anything that could be confused with a rooted path. |
|
int pos = text.IndexOf(':'); |
|
if (pos > 0) |
|
text = text.Substring(0, pos); |
|
text = text.Trim(); |
|
// Remove generics |
|
pos = text.IndexOf('`'); |
|
if (pos > 0) |
|
{ |
|
text = text.Substring(0, pos).Trim(); |
|
} |
|
// Whitelist allowed characters, replace everything else: |
|
StringBuilder b = new StringBuilder(text.Length + (extension?.Length ?? 0)); |
|
bool countBytes = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); |
|
int segmentStart = 0; |
|
int baseNameEnd = -1; |
|
foreach (var c in text) |
|
{ |
|
if (char.IsLetterOrDigit(c) || c == '-' || c == '_') |
|
{ |
|
unsafe |
|
{ |
|
currentSegmentLength += countBytes ? Encoding.UTF8.GetByteCount(&c, 1) : 1; |
|
} |
|
// if the current segment exceeds maxSegmentLength characters, |
|
// skip until the end of the segment. |
|
if (currentSegmentLength <= maxSegmentLength) |
|
b.Append(c); |
|
} |
|
else if (c == '.' && b.Length > 0 && b[^1] != '.') |
|
{ |
|
currentSegmentLength++; |
|
if (separateAtDots) |
|
{ |
|
// The dot ends the current segment. |
|
EscapeReservedFileSystemName(b, segmentStart, baseNameEnd); |
|
b.Append('.'); |
|
segmentStart = b.Length; |
|
baseNameEnd = -1; |
|
// Reset length at end of segment. |
|
currentSegmentLength = 0; |
|
} |
|
else if (currentSegmentLength <= maxSegmentLength) |
|
{ |
|
// The first dot ends the segment's base name, the part Windows |
|
// device-name parsing looks at. |
|
if (baseNameEnd < 0) |
|
baseNameEnd = b.Length; |
|
b.Append('.'); // allow dot, but never two in a row |
|
} |
|
} |
|
else if (treatAsPath && (c is '/' or '\\') && currentSegmentLength > 0) |
|
{ |
|
// if we treat this as a file name, we've started a new segment |
|
EscapeReservedFileSystemName(b, segmentStart, baseNameEnd); |
|
b.Append(Path.DirectorySeparatorChar); |
|
segmentStart = b.Length; |
|
baseNameEnd = -1; |
|
currentSegmentLength = 0; |
|
} |
|
else |
|
{ |
|
if (char.IsHighSurrogate(c)) |
|
{ |
|
// only add one replacement character for surrogate pairs |
|
continue; |
|
} |
|
|
|
currentSegmentLength++; |
|
// if the current segment exceeds maxSegmentLength characters, |
|
// skip until the end of the segment. |
|
if (currentSegmentLength <= maxSegmentLength) |
|
b.Append('-'); |
|
} |
|
} |
|
if (b.Length == 0) |
|
b.Append('-'); |
|
EscapeReservedFileSystemName(b, segmentStart, baseNameEnd); |
|
string name = b.ToString(); |
|
if (extension != null) |
|
{ |
|
// make sure that adding the extension to the filename |
|
// does not exceed maxSegmentLength. |
|
// trim the name, if necessary. |
|
if (name.Length + extension.Length > maxSegmentLength) |
|
name = name.Remove(name.Length - extension.Length); |
|
name += extension; |
|
} |
|
|
|
if (name == ".") |
|
return "_"; |
|
else |
|
return name; |
|
} |
|
|
|
/// <summary> |
|
/// Cleans up a node name for use as a directory name. |
|
/// </summary> |
|
public static string CleanUpDirectoryName(string text) |
|
{ |
|
return CleanUpName(text, separateAtDots: false, treatAsFileName: false, treatAsPath: false); |
|
} |
|
|
|
public static string CleanUpPath(string text) |
|
{ |
|
return CleanUpName(text, separateAtDots: true, treatAsFileName: false, treatAsPath: true) |
|
.Replace('.', Path.DirectorySeparatorChar); |
|
} |
|
|
|
/// <summary> |
|
/// Appends an underscore to the segment [<paramref name="segmentStart"/>..) of |
|
/// <paramref name="b"/> if its base name (the part before <paramref name="baseNameEnd"/>, |
|
/// or the whole segment when there was no dot) is a reserved Windows device name |
|
/// (CON, PRN, AUX, NUL, COM1-9, LPT1-9): "con" becomes "con_" and "con.txt" becomes |
|
/// "con_.txt". Windows device-name parsing ignores everything after the first dot, so the |
|
/// underscore must be inserted before it, not appended at the end. |
|
/// </summary> |
|
static void EscapeReservedFileSystemName(StringBuilder b, int segmentStart, int baseNameEnd) |
|
{ |
|
if (baseNameEnd < 0) |
|
baseNameEnd = b.Length; |
|
int baseNameLength = baseNameEnd - segmentStart; |
|
// All reserved names are 3 or 4 characters long; checking the length first avoids |
|
// allocating a substring for segments that cannot be reserved anyway. |
|
if (baseNameLength is 3 or 4 && IsReservedFileSystemName(b.ToString(segmentStart, baseNameLength))) |
|
b.Insert(baseNameEnd, '_'); |
|
} |
|
|
|
static bool IsReservedFileSystemName(string name) |
|
{ |
|
switch (name.ToUpperInvariant()) |
|
{ |
|
case "AUX": |
|
case "COM1": |
|
case "COM2": |
|
case "COM3": |
|
case "COM4": |
|
case "COM5": |
|
case "COM6": |
|
case "COM7": |
|
case "COM8": |
|
case "COM9": |
|
case "CON": |
|
case "LPT1": |
|
case "LPT2": |
|
case "LPT3": |
|
case "LPT4": |
|
case "LPT5": |
|
case "LPT6": |
|
case "LPT7": |
|
case "LPT8": |
|
case "LPT9": |
|
case "NUL": |
|
case "PRN": |
|
return true; |
|
default: |
|
return false; |
|
} |
|
} |
|
|
|
public static bool CanUseSdkStyleProjectFormat(MetadataFile module) |
|
{ |
|
return TargetServices.DetectTargetFramework(module).Moniker != null; |
|
} |
|
} |
|
|
|
public record struct ProjectItemInfo(string ItemType, string FileName) |
|
{ |
|
public List<PartialTypeInfo>? PartialTypes { get; set; } = null; |
|
|
|
public Dictionary<string, string>? AdditionalProperties { get; set; } = null; |
|
|
|
public ProjectItemInfo With(string name, string value) |
|
{ |
|
AdditionalProperties ??= new Dictionary<string, string>(); |
|
AdditionalProperties.Add(name, value); |
|
return this; |
|
} |
|
|
|
public ProjectItemInfo With(IEnumerable<KeyValuePair<string, string>> pairs) |
|
{ |
|
AdditionalProperties ??= new Dictionary<string, string>(); |
|
AdditionalProperties.AddRange(pairs); |
|
return this; |
|
} |
|
} |
|
}
|
|
|