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.
164 lines
7.2 KiB
164 lines
7.2 KiB
#nullable enable |
|
// Copyright (c) 2026 Siegfried Pammer |
|
// |
|
// 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; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Text; |
|
|
|
namespace ICSharpCode.Decompiler.Util |
|
{ |
|
/// <summary> |
|
/// Reader for the <c>!AvaloniaResources</c> manifest resource that the Avalonia UI framework |
|
/// embeds in compiled assemblies. The blob packs every Avalonia resource (compiled XAML, |
|
/// images, ...) behind a single index, much like a <c>.resources</c> file packs a managed |
|
/// resource set. |
|
/// </summary> |
|
/// <remarks> |
|
/// Layout (all integers little-endian): |
|
/// <list type="bullet"> |
|
/// <item><description>Int32 indexLength: byte length of the index section that follows.</description></item> |
|
/// <item><description>Index section: Int32 version (must be 2), Int32 entryCount, then per entry a |
|
/// 7-bit length-prefixed UTF-8 path, Int32 offset and Int32 size.</description></item> |
|
/// <item><description>Data section: concatenated file bytes; each entry's offset is relative to the |
|
/// start of this section (i.e. to <c>indexLength + 4</c>).</description></item> |
|
/// </list> |
|
/// </remarks> |
|
public sealed class AvaloniaResourcesFile : IEnumerable<KeyValuePair<string, byte[]>> |
|
{ |
|
/// <summary> |
|
/// Name of the manifest resource that holds the packed Avalonia resources. |
|
/// </summary> |
|
public const string ResourceName = "!AvaloniaResources"; |
|
|
|
// Index format version written by Avalonia's AvaloniaResourcesIndexReaderWriter. Version 1 |
|
// was a legacy XML index that this reader does not support. |
|
const int BinaryCurrentVersion = 2; |
|
|
|
readonly List<KeyValuePair<string, byte[]>> entries = new(); |
|
|
|
/// <summary> |
|
/// Returns <see langword="true"/> if <paramref name="resourceName"/> is the manifest |
|
/// resource name Avalonia uses for its packed resources. |
|
/// </summary> |
|
public static bool IsAvaloniaResourcesEntry(string resourceName) |
|
=> resourceName == ResourceName; |
|
|
|
/// <summary> |
|
/// Parses the <c>!AvaloniaResources</c> blob read from <paramref name="stream"/>. |
|
/// </summary> |
|
/// <exception cref="BadImageFormatException">The blob is truncated, uses an unsupported |
|
/// index version, or contains an entry that points outside the data section.</exception> |
|
public AvaloniaResourcesFile(Stream stream) |
|
{ |
|
if (stream == null) |
|
throw new ArgumentNullException(nameof(stream)); |
|
|
|
byte[] blob; |
|
using (var buffer = new MemoryStream()) |
|
{ |
|
stream.CopyTo(buffer); |
|
blob = buffer.ToArray(); |
|
} |
|
|
|
try |
|
{ |
|
Parse(blob); |
|
} |
|
catch (EndOfStreamException ex) |
|
{ |
|
throw new BadImageFormatException("Truncated !AvaloniaResources index.", ex); |
|
} |
|
} |
|
|
|
void Parse(byte[] blob) |
|
{ |
|
using var reader = new BinaryReader(new MemoryStream(blob), Encoding.UTF8); |
|
|
|
int indexLength = reader.ReadInt32(); |
|
// The data section begins right after the 4-byte length prefix and the index. |
|
long baseOffset = 4L + indexLength; |
|
if (indexLength < 0 || baseOffset > blob.Length) |
|
throw new BadImageFormatException("Invalid !AvaloniaResources index length."); |
|
|
|
int version = reader.ReadInt32(); |
|
if (version != BinaryCurrentVersion) |
|
throw new BadImageFormatException($"Unsupported !AvaloniaResources index version {version}."); |
|
|
|
int count = reader.ReadInt32(); |
|
if (count < 0) |
|
throw new BadImageFormatException("Invalid !AvaloniaResources entry count."); |
|
|
|
// The count is attacker-controlled, so it must not be trusted as an allocation size: an |
|
// honest entry occupies at least a 1-byte path length prefix plus two Int32s, so the index |
|
// section cannot hold more than this many entries. A lying count is left to the per-entry |
|
// spill guard below instead of forcing a huge pre-allocation. |
|
const int MinEntrySize = 1 + 4 + 4; |
|
long maxEntries = (baseOffset - reader.BaseStream.Position) / MinEntrySize; |
|
entries.Capacity = (int)Math.Min(count, Math.Max(0, maxEntries)); |
|
|
|
// The build task deduplicates identical files, so many entries can legitimately alias one |
|
// (offset, size) range in the data section; those share a single buffer here rather than |
|
// each copying the bytes. Distinct ranges in an honest index are non-overlapping and thus |
|
// cannot total more than the data section they occupy, so the sum of copied bytes is |
|
// bounded by the data section length. Rejecting anything larger stops a crafted index from |
|
// amplifying a small file into a huge allocation via overlapping, per-entry-copied slices. |
|
long dataSectionLength = blob.Length - baseOffset; |
|
long copiedBytes = 0; |
|
var buffersByRange = new Dictionary<long, byte[]>(); |
|
for (int i = 0; i < count; i++) |
|
{ |
|
string path = GetPathRooted(reader.ReadString()); |
|
int offset = reader.ReadInt32(); |
|
int size = reader.ReadInt32(); |
|
|
|
// Every byte of every entry must come from the index section: an entry that runs |
|
// past the declared index end would otherwise silently consume data-section bytes. |
|
// This also rejects a count claiming more entries than the index holds. |
|
if (reader.BaseStream.Position > baseOffset) |
|
throw new BadImageFormatException("Malformed !AvaloniaResources index."); |
|
|
|
if (offset < 0 || size < 0 || baseOffset + offset + size > blob.Length) |
|
throw new BadImageFormatException($"!AvaloniaResources entry '{path}' points outside the data section."); |
|
|
|
long rangeKey = ((long)offset << 32) | (uint)size; |
|
if (!buffersByRange.TryGetValue(rangeKey, out var data)) |
|
{ |
|
copiedBytes += size; |
|
if (copiedBytes > dataSectionLength) |
|
throw new BadImageFormatException("!AvaloniaResources index references more data than the data section contains."); |
|
data = new byte[size]; |
|
Buffer.BlockCopy(blob, (int)baseOffset + offset, data, 0, size); |
|
buffersByRange[rangeKey] = data; |
|
} |
|
entries.Add(new KeyValuePair<string, byte[]>(path, data)); |
|
} |
|
} |
|
|
|
// Avalonia's AssetLoader keys resources by a path with a leading slash; a path stored |
|
// without one is rooted on read so the keys match what Avalonia itself resolves. |
|
static string GetPathRooted(string path) |
|
=> path.Length == 0 || path[0] != '/' ? "/" + path : path; |
|
|
|
public IEnumerator<KeyValuePair<string, byte[]>> GetEnumerator() => entries.GetEnumerator(); |
|
|
|
IEnumerator IEnumerable.GetEnumerator() => entries.GetEnumerator(); |
|
} |
|
}
|
|
|