.NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform!
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.
 
 
 
 

185 lines
6.2 KiB

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Immutable;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.CompilerServices;
using System.Text;
namespace ICSharpCode.Decompiler
{
/// <summary>
/// Class for dealing with .NET 5 single-file bundles.
///
/// Based on code from Microsoft.NET.HostModel.
/// </summary>
public static class SingleFileBundle
{
/// <summary>
/// Check if the memory-mapped data is a single-file bundle
/// </summary>
public static unsafe bool IsBundle(MemoryMappedViewAccessor view, out long bundleHeaderOffset)
{
var buffer = view.SafeMemoryMappedViewHandle;
byte* ptr = null;
buffer.AcquirePointer(ref ptr);
try
{
return IsBundle(ptr, checked((long)buffer.ByteLength), out bundleHeaderOffset);
}
finally
{
buffer.ReleasePointer();
}
}
/// <summary>
/// The 32-byte bundle signature: SHA-256 of ".net core bundle".
/// </summary>
internal static ReadOnlySpan<byte> BundleSignature => new byte[] {
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
};
public static unsafe bool IsBundle(byte* data, long size, out long bundleHeaderOffset)
{
ReadOnlySpan<byte> bundleSignature = BundleSignature;
// 'end' is the last position at which a full signature still fits, so it is a valid
// candidate itself: a memory-mapped view reports the exact file length on Unix (Windows
// rounds it up to a page), and the signature may occupy the final bytes of the file.
byte* end = data + (size - bundleSignature.Length);
for (byte* ptr = data; ptr <= end; ptr++)
{
if (*ptr == 0x8b && bundleSignature.SequenceEqual(new ReadOnlySpan<byte>(ptr, bundleSignature.Length)))
{
// A genuine bundle stores the 8-byte header offset immediately before the
// signature, so the signature never appears within the first sizeof(long)
// bytes of the file. Without this guard, a crafted file with the signature at
// offset 0..7 reads before the start of the (page-aligned, memory-mapped)
// buffer, faulting on the preceding unmapped page.
if (ptr - data >= sizeof(long))
{
bundleHeaderOffset = Unsafe.ReadUnaligned<long>(ptr - sizeof(long));
if (bundleHeaderOffset > 0 && bundleHeaderOffset < size)
{
return true;
}
}
}
}
bundleHeaderOffset = 0;
return false;
}
public struct Header
{
public uint MajorVersion;
public uint MinorVersion;
public int FileCount;
public string BundleID;
// Fields introduced with v2:
public long DepsJsonOffset;
public long DepsJsonSize;
public long RuntimeConfigJsonOffset;
public long RuntimeConfigJsonSize;
public ulong Flags;
public ImmutableArray<Entry> Entries;
}
/// <summary>
/// FileType: Identifies the type of file embedded into the bundle.
///
/// The bundler differentiates a few kinds of files via the manifest,
/// with respect to the way in which they'll be used by the runtime.
/// </summary>
public enum FileType : byte
{
Unknown, // Type not determined.
Assembly, // IL and R2R Assemblies
NativeBinary, // NativeBinaries
DepsJson, // .deps.json configuration file
RuntimeConfigJson, // .runtimeconfig.json configuration file
Symbols // PDB Files
};
public struct Entry
{
public long Offset;
public long Size;
public long CompressedSize; // 0 if not compressed, otherwise the compressed size in the bundle
public FileType Type;
public string RelativePath; // Path of an embedded file, relative to the Bundle source-directory.
}
/// <summary>
/// Reads the manifest header from the memory mapping.
/// </summary>
public static Header ReadManifest(MemoryMappedViewAccessor view, long bundleHeaderOffset)
{
using var stream = view.AsStream();
stream.Seek(bundleHeaderOffset, SeekOrigin.Begin);
return ReadManifest(stream);
}
/// <summary>
/// Reads the manifest header from the stream.
/// </summary>
public static Header ReadManifest(Stream stream)
{
var header = new Header();
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
header.MajorVersion = reader.ReadUInt32();
header.MinorVersion = reader.ReadUInt32();
// Major versions 3, 4 and 5 were skipped to align bundle versioning with .NET versioning scheme
if (header.MajorVersion < 1 || header.MajorVersion > 6)
{
throw new InvalidDataException($"Unsupported manifest version: {header.MajorVersion}.{header.MinorVersion}");
}
header.FileCount = reader.ReadInt32();
// FileCount is used below to pre-size the entry array. Each entry occupies at least
// one byte in the stream, so a count larger than the bytes that remain cannot be
// honest; reject it instead of attempting a huge allocation for a crafted manifest.
long remainingBytes = stream.Length - stream.Position;
if (header.FileCount < 0 || header.FileCount > remainingBytes)
{
throw new InvalidDataException($"Invalid bundle manifest: FileCount {header.FileCount} exceeds available data.");
}
header.BundleID = reader.ReadString();
if (header.MajorVersion >= 2)
{
header.DepsJsonOffset = reader.ReadInt64();
header.DepsJsonSize = reader.ReadInt64();
header.RuntimeConfigJsonOffset = reader.ReadInt64();
header.RuntimeConfigJsonSize = reader.ReadInt64();
header.Flags = reader.ReadUInt64();
}
var entries = ImmutableArray.CreateBuilder<Entry>(header.FileCount);
for (int i = 0; i < header.FileCount; i++)
{
entries.Add(ReadEntry(reader, header.MajorVersion));
}
header.Entries = entries.MoveToImmutable();
return header;
}
private static Entry ReadEntry(BinaryReader reader, uint bundleMajorVersion)
{
Entry entry;
entry.Offset = reader.ReadInt64();
entry.Size = reader.ReadInt64();
entry.CompressedSize = bundleMajorVersion >= 6 ? reader.ReadInt64() : 0;
entry.Type = (FileType)reader.ReadByte();
entry.RelativePath = reader.ReadString();
return entry;
}
}
}