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.
350 lines
10 KiB
350 lines
10 KiB
// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team |
|
// |
|
// 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. |
|
|
|
#nullable enable |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.Diagnostics; |
|
using System.IO; |
|
using System.IO.Compression; |
|
using System.IO.MemoryMappedFiles; |
|
using System.Linq; |
|
using System.Reflection; |
|
using System.Threading.Tasks; |
|
|
|
using ICSharpCode.Decompiler; |
|
using ICSharpCode.Decompiler.Metadata; |
|
|
|
namespace ICSharpCode.ILSpyX |
|
{ |
|
/// <summary> |
|
/// NuGet package or .NET bundle: |
|
/// </summary> |
|
public class LoadedPackage |
|
{ |
|
public enum PackageKind |
|
{ |
|
Zip, |
|
Bundle, |
|
} |
|
|
|
/// <summary> |
|
/// Gets the LoadedAssembly instance representing this bundle. |
|
/// </summary> |
|
internal LoadedAssembly? LoadedAssembly { get; set; } |
|
|
|
public PackageKind Kind { get; } |
|
|
|
internal SingleFileBundle.Header BundleHeader { get; set; } |
|
|
|
/// <summary> |
|
/// List of all entries, including those in sub-directories within the package. |
|
/// </summary> |
|
public IReadOnlyList<PackageEntry> Entries { get; } |
|
|
|
internal PackageFolder RootFolder { get; } |
|
|
|
public LoadedPackage(PackageKind kind, IEnumerable<PackageEntry> entries) |
|
{ |
|
this.Kind = kind; |
|
this.Entries = entries.ToArray(); |
|
var topLevelEntries = new List<PackageEntry>(); |
|
var folders = new Dictionary<string, PackageFolder>(); |
|
var rootFolder = new PackageFolder(this, null, ""); |
|
folders.Add("", rootFolder); |
|
foreach (var entry in this.Entries) |
|
{ |
|
var (dirname, filename) = SplitName(entry.Name); |
|
GetFolder(dirname).Entries.Add(new FolderEntry(filename, entry)); |
|
} |
|
this.RootFolder = rootFolder; |
|
|
|
static (string, string) SplitName(string filename) |
|
{ |
|
int pos = filename.LastIndexOfAny(new char[] { '/', '\\' }); |
|
if (pos == -1) |
|
return ("", filename); // file in root |
|
else |
|
return (filename.Substring(0, pos), filename.Substring(pos + 1)); |
|
} |
|
|
|
PackageFolder GetFolder(string name) |
|
{ |
|
if (folders.TryGetValue(name, out var result)) |
|
return result; |
|
var (dirname, basename) = SplitName(name); |
|
PackageFolder parent = GetFolder(dirname); |
|
result = new PackageFolder(this, parent, basename); |
|
parent.Folders.Add(result); |
|
folders.Add(name, result); |
|
return result; |
|
} |
|
} |
|
|
|
public static LoadedPackage FromZipFile(string file) |
|
{ |
|
Debug.WriteLine($"LoadedPackage.FromZipFile({file})"); |
|
using var archive = ZipFile.OpenRead(file); |
|
return new LoadedPackage(PackageKind.Zip, |
|
archive.Entries.Select(entry => new ZipFileEntry(file, entry))); |
|
} |
|
|
|
/// <summary> |
|
/// Load a .NET single-file bundle. |
|
/// </summary> |
|
public static LoadedPackage? FromBundle(string fileName) |
|
{ |
|
using var memoryMappedFile = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); |
|
var view = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); |
|
try |
|
{ |
|
if (!SingleFileBundle.IsBundle(view, out long bundleHeaderOffset)) |
|
return null; |
|
var manifest = SingleFileBundle.ReadManifest(view, bundleHeaderOffset); |
|
var entries = manifest.Entries.Select(e => new BundleEntry(fileName, view, e)).ToList(); |
|
var result = new LoadedPackage(PackageKind.Bundle, entries); |
|
result.BundleHeader = manifest; |
|
view = null; // don't dispose the view, we're still using it in the bundle entries |
|
return result; |
|
} |
|
finally |
|
{ |
|
view?.Dispose(); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Entry inside a package folder. Effectively renames the entry. |
|
/// </summary> |
|
sealed class FolderEntry : PackageEntry |
|
{ |
|
readonly PackageEntry originalEntry; |
|
public override string Name { get; } |
|
|
|
public FolderEntry(string name, PackageEntry originalEntry) |
|
{ |
|
this.Name = name; |
|
this.originalEntry = originalEntry; |
|
} |
|
|
|
public override ManifestResourceAttributes Attributes => originalEntry.Attributes; |
|
public override string FullName => originalEntry.FullName; |
|
public override ResourceType ResourceType => originalEntry.ResourceType; |
|
public override Stream? TryOpenStream() => originalEntry.TryOpenStream(); |
|
public override long? TryGetLength() => originalEntry.TryGetLength(); |
|
} |
|
|
|
sealed class ZipFileEntry : PackageEntry |
|
{ |
|
readonly string zipFile; |
|
public override string Name { get; } |
|
public override string FullName => $"zip://{zipFile};{Name}"; |
|
|
|
public ZipFileEntry(string zipFile, ZipArchiveEntry entry) |
|
{ |
|
this.zipFile = zipFile; |
|
this.Name = entry.FullName; |
|
} |
|
|
|
public override Stream? TryOpenStream() |
|
{ |
|
Debug.WriteLine("Decompress " + Name); |
|
using var archive = ZipFile.OpenRead(zipFile); |
|
var entry = archive.GetEntry(Name); |
|
if (entry == null) |
|
return null; |
|
var memoryStream = new MemoryStream(); |
|
using (var s = entry.Open()) |
|
{ |
|
s.CopyTo(memoryStream); |
|
} |
|
memoryStream.Position = 0; |
|
return memoryStream; |
|
} |
|
|
|
public override long? TryGetLength() |
|
{ |
|
Debug.WriteLine("TryGetLength " + Name); |
|
using var archive = ZipFile.OpenRead(zipFile); |
|
var entry = archive.GetEntry(Name); |
|
if (entry == null) |
|
return null; |
|
return entry.Length; |
|
} |
|
} |
|
|
|
sealed class BundleEntry : PackageEntry |
|
{ |
|
readonly string bundleFile; |
|
readonly MemoryMappedViewAccessor view; |
|
readonly SingleFileBundle.Entry entry; |
|
|
|
public BundleEntry(string bundleFile, MemoryMappedViewAccessor view, SingleFileBundle.Entry entry) |
|
{ |
|
this.bundleFile = bundleFile; |
|
this.view = view; |
|
this.entry = entry; |
|
} |
|
|
|
public override string Name => entry.RelativePath; |
|
public override string FullName => $"bundle://{bundleFile};{Name}"; |
|
|
|
public override Stream TryOpenStream() |
|
{ |
|
Debug.WriteLine("Open bundle member " + Name); |
|
|
|
if (entry.CompressedSize == 0) |
|
{ |
|
return new UnmanagedMemoryStream(view.SafeMemoryMappedViewHandle, entry.Offset, entry.Size); |
|
} |
|
else |
|
{ |
|
Stream compressedStream = new UnmanagedMemoryStream(view.SafeMemoryMappedViewHandle, entry.Offset, entry.CompressedSize); |
|
using var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress); |
|
Stream decompressedStream = new MemoryStream((int)entry.Size); |
|
deflateStream.CopyTo(decompressedStream); |
|
if (decompressedStream.Length != entry.Size) |
|
{ |
|
throw new InvalidDataException($"Corrupted single-file entry '${entry.RelativePath}'. Declared decompressed size '${entry.Size}' is not the same as actual decompressed size '${decompressedStream.Length}'."); |
|
} |
|
|
|
decompressedStream.Seek(0, SeekOrigin.Begin); |
|
return decompressedStream; |
|
} |
|
} |
|
|
|
public override long? TryGetLength() |
|
{ |
|
return entry.Size; |
|
} |
|
} |
|
} |
|
|
|
public abstract class PackageEntry : Resource |
|
{ |
|
/// <summary> |
|
/// Gets the file name of the entry (may include path components, relative to the package root). |
|
/// </summary> |
|
public abstract override string Name { get; } |
|
|
|
/// <summary> |
|
/// Gets the full file name for the entry. |
|
/// </summary> |
|
public abstract string FullName { get; } |
|
} |
|
|
|
sealed class PackageFolder : IAssemblyResolver |
|
{ |
|
/// <summary> |
|
/// Gets the short name of the folder. |
|
/// </summary> |
|
public string Name { get; } |
|
|
|
readonly LoadedPackage package; |
|
readonly PackageFolder? parent; |
|
|
|
internal PackageFolder(LoadedPackage package, PackageFolder? parent, string name) |
|
{ |
|
this.package = package; |
|
this.parent = parent; |
|
this.Name = name; |
|
} |
|
|
|
public List<PackageFolder> Folders { get; } = new List<PackageFolder>(); |
|
public List<PackageEntry> Entries { get; } = new List<PackageEntry>(); |
|
|
|
public PEFile? Resolve(IAssemblyReference reference) |
|
{ |
|
var asm = ResolveFileName(reference.Name + ".dll"); |
|
if (asm != null) |
|
{ |
|
return asm.GetPEFileOrNull(); |
|
} |
|
return parent?.Resolve(reference); |
|
} |
|
|
|
public Task<PEFile?> ResolveAsync(IAssemblyReference reference) |
|
{ |
|
var asm = ResolveFileName(reference.Name + ".dll"); |
|
if (asm != null) |
|
{ |
|
return asm.GetPEFileOrNullAsync(); |
|
} |
|
if (parent != null) |
|
{ |
|
return parent.ResolveAsync(reference); |
|
} |
|
return Task.FromResult<PEFile?>(null); |
|
} |
|
|
|
public PEFile? ResolveModule(PEFile mainModule, string moduleName) |
|
{ |
|
var asm = ResolveFileName(moduleName + ".dll"); |
|
if (asm != null) |
|
{ |
|
return asm.GetPEFileOrNull(); |
|
} |
|
return parent?.ResolveModule(mainModule, moduleName); |
|
} |
|
|
|
public Task<PEFile?> ResolveModuleAsync(PEFile mainModule, string moduleName) |
|
{ |
|
var asm = ResolveFileName(moduleName + ".dll"); |
|
if (asm != null) |
|
{ |
|
return asm.GetPEFileOrNullAsync(); |
|
} |
|
if (parent != null) |
|
{ |
|
return parent.ResolveModuleAsync(mainModule, moduleName); |
|
} |
|
return Task.FromResult<PEFile?>(null); |
|
} |
|
|
|
readonly Dictionary<string, LoadedAssembly?> assemblies = new Dictionary<string, LoadedAssembly?>(StringComparer.OrdinalIgnoreCase); |
|
|
|
internal LoadedAssembly? ResolveFileName(string name) |
|
{ |
|
if (package.LoadedAssembly == null) |
|
return null; |
|
lock (assemblies) |
|
{ |
|
if (assemblies.TryGetValue(name, out var asm)) |
|
return asm; |
|
var entry = Entries.FirstOrDefault(e => string.Equals(name, e.Name, StringComparison.OrdinalIgnoreCase)); |
|
if (entry != null) |
|
{ |
|
asm = new LoadedAssembly( |
|
package.LoadedAssembly, entry.Name, |
|
assemblyResolver: this, |
|
stream: Task.Run(entry.TryOpenStream), |
|
applyWinRTProjections: package.LoadedAssembly.AssemblyList.ApplyWinRTProjections, |
|
useDebugSymbols: package.LoadedAssembly.AssemblyList.UseDebugSymbols |
|
); |
|
} |
|
else |
|
{ |
|
asm = null; |
|
} |
|
assemblies.Add(name, asm); |
|
return asm; |
|
} |
|
} |
|
} |
|
}
|
|
|