// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
namespace Microsoft.DotNet.Cli.Resources.Internal;
/// <summary>
/// Reads the default binary <c>.resources</c> format (version 2) directly over a caller-owned
/// <see cref="ReadOnlyMemory{T}"/> or a memory-mapped file, without allocating strings or byte
/// arrays.
/// </summary>
/// <remarks>
/// <para>
/// Unlike <see cref="System.Resources.ResourceReader"/>, this type never materializes managed
/// values: name lookups return an index, type, and byte length; value bytes are copied into a
/// caller-supplied <see cref="Span{T}"/> or <see cref="Stream"/>. The name-hash and name-position
/// index tables are read in place from the memory, so construction, lookup, and reads allocate
/// nothing. This mirrors the runtime's <see cref="System.IO.UnmanagedMemoryStream"/> fast path but
/// works over any <see cref="ReadOnlyMemory{T}"/> - a managed array or a memory-mapped view.
/// </para>
/// <para>
/// The reader is immutable over its memory, so lookups are pure and require no locking. Only string
/// resources, the primitive types, <c>byte[]</c>, and <see cref="Stream"/> values are exposed as
/// raw bytes; serialized user types are reported by type code and type name only. No value is ever
/// decoded or deserialized, so the reader triggers no reflection and is trim- and AOT-safe.
/// </para>
/// <para>
/// Values are presented as content only - the on-disk length prefixes of strings, byte arrays, and
/// streams are stripped, so <see cref="ResourceLocation.ByteLength"/> and
/// <see cref="TryGetResourceData(int, Span{byte}, out int)"/> yield exactly the value bytes.
/// </para>
/// </remarks>
internal sealed class RawResourceReader : DisposableBase, IStringResourceReader
{
// On-disk layout of a default-format (version 2) .resources file. All integers are little-endian
// and "7-bit int" is a LEB128-style length prefix. The reader validates and indexes this in place
// over _resources; it never copies or decodes the data.
//
// ResourceManager header
// int32 magic (0xBEEFCACE)
// int32 ResourceManager header version
// int32 byteCountToSkip
// byte[] reader / resource-set type names (skipped: byteCountToSkip bytes)
// ------------------------------------------------------------------------------------
// RuntimeResourceSet header
// int32 version (== 2)
// int32 numResources
// int32 numTypes
// entry[] type-name table: numTypes x [7-bit len][UTF-8] (_typeNamesOffset)
// ------------------------------------------------------------------------------------
// pad to the next 8-byte boundary ('P','A','D' filler)
// ------------------------------------------------------------------------------------
// Index
// int32[] nameHashes[numResources] (sorted ascending) (_nameHashesOffset)
// int32[] namePositions[numResources] (parallel to hashes) (_namePositionsOffset)
// int32 dataSectionOffset
// ------------------------------------------------------------------------------------
// Name section (_nameSectionOffset)
// per resource, at _nameSectionOffset + namePositions[i]:
// [7-bit byteLen][UTF-16LE name][int32 dataPosition]
// ------------------------------------------------------------------------------------
// Data section (_dataSectionOffset)
// per resource, at _dataSectionOffset + dataPosition:
// [7-bit ResourceTypeCode][value], where value is:
// String : [7-bit byteLen][UTF-8 bytes]
// ByteArray / Stream : [int32 byteLen][bytes]
// primitives : fixed-size little-endian
// user types (>= 0x40): serialized payload (exposed by type name only)
//
// .resources magic number and the only supported RuntimeResourceSet format version.
private const int MagicNumber = unchecked((int)0xBEEFCACE);
private const int SupportedVersion = 2;
private readonly ReadOnlyMemory<byte> _resources;
private readonly IDisposable? _owned;
private readonly int _numResources;
private readonly int _numTypes;
// Absolute offsets into _resources, all computed once at construction.
private readonly int _typeNamesOffset; // start of the type-name string table
private readonly int _nameHashesOffset; // int32[_numResources], sorted ascending
private readonly int _namePositionsOffset; // int32[_numResources], parallel to the hashes
private readonly int _nameSectionOffset; // start of the name section
private readonly int _dataSectionOffset; // start of the data section
/// <summary>
/// Initializes a new instance of the <see cref="RawResourceReader"/> class over the given
/// <c>.resources</c> content.
/// </summary>
/// <param name="resources">
/// The complete bytes of a default-format version 2 <c>.resources</c> file. The memory is not
/// copied; the caller owns its lifetime and must keep it valid for the life of the reader.
/// </param>
/// <exception cref="ArgumentException">The data is not a <c>.resources</c> file (bad magic number).</exception>
/// <exception cref="NotSupportedException">
/// The file is not format version 2 (for example a legacy version 1 file), or the current
/// architecture is big-endian.
/// </exception>
/// <exception cref="BadImageFormatException">The file header is malformed or truncated.</exception>
public RawResourceReader(ReadOnlyMemory<byte> resources)
: this(resources, owned: null)
{
}
private RawResourceReader(ReadOnlyMemory<byte> resources, IDisposable? owned)
{
if (!BitConverter.IsLittleEndian)
{
throw new NotSupportedException("RawResourceReader is only supported on little-endian architectures.");
}
_owned = owned;
_resources = resources;
SpanReader<byte> reader = new(resources.Span);
if (!reader.TryReadInt32LittleEndian(out int magic) || magic != MagicNumber)
{
throw new ArgumentException(
"The data is not a valid .resources file (bad magic number).",
nameof(resources));
}
if (!reader.TryReadInt32LittleEndian(out int resourceManagerHeaderVersion)
|| resourceManagerHeaderVersion < 0
|| !reader.TryReadInt32LittleEndian(out int byteCountToSkip)
// Skip the rest of the ResourceManager header (reader and set type names). We parse the
// format structurally and do not validate the declared reader type, so files written for the
// System.Resources.Extensions reader are read up to their common primitive/string content.
|| !reader.TryAdvance(byteCountToSkip))
{
ThrowBadImageFormatException("The file is not a valid .resources file (bad header).");
}
if (!reader.TryReadInt32LittleEndian(out int version) || version != SupportedVersion)
{
throw new NotSupportedException(
$"Unsupported .resources format version {version}; only version {SupportedVersion} is supported.");
}
if (!reader.TryReadInt32LittleEndian(out _numResources)
|| _numResources < 0
|| !reader.TryReadInt32LittleEndian(out _numTypes)
|| _numTypes < 0)
{
ThrowBadImageFormatException("The file is not a valid .resources file (bad resource or type count).");
}
// Type-name table: _numTypes length-prefixed UTF-8 strings. Remember the start so user type
// names can be read on demand; skip past it here without allocating.
_typeNamesOffset = reader.Position;
for (int i = 0; i < _numTypes; i++)
{
if (!reader.TryRead7BitEncodedInt32(out int typeNameLength)
|| typeNameLength < 0
|| !reader.TryAdvance(typeNameLength))
{
ThrowBadImageFormatException("The file is not a valid .resources file (bad type name).");
}
}
// The name-hash array is aligned to 8 bytes (the writer pads with 'P','A','D').
if (!reader.TryAdvance((8 - (reader.Position & 7)) & 7))
{
ThrowBadImageFormatException("The file is not a valid .resources file (bad alignment).");
}
_nameHashesOffset = reader.Position;
// The two int32 index arrays and the data-section offset are fixed size; validate they fit.
long indexBytes = ((long)_numResources * 8) + 4;
if (indexBytes > reader.Unread.Length)
{
ThrowBadImageFormatException("The file is not a valid .resources file (truncated index).");
}
_namePositionsOffset = _nameHashesOffset + (_numResources * 4);
reader.Position = _namePositionsOffset + (_numResources * 4);
if (!reader.TryReadInt32LittleEndian(out _dataSectionOffset))
{
ThrowBadImageFormatException("The file is not a valid .resources file (bad data section).");
}
_nameSectionOffset = reader.Position;
if (_dataSectionOffset < _nameSectionOffset || _dataSectionOffset > reader.Length)
{
ThrowBadImageFormatException("The file is not a valid .resources file (bad data section).");
}
}
/// <summary>
/// Creates a reader over memory and transfers ownership of <paramref name="owned"/> to it.
/// </summary>
/// <param name="resources">The resource image.</param>
/// <param name="owned">The object that keeps the image valid and releases it.</param>
/// <returns>A reader that owns the backing object.</returns>
internal static RawResourceReader CreateOwned(
ReadOnlyMemory<byte> resources,
IDisposable owned)
{
ArgumentNullException.ThrowIfNull(owned);
try
{
return new(resources, owned);
}
catch
{
owned.Dispose();
throw;
}
}
/// <summary>
/// Creates a <see cref="RawResourceReader"/> that memory-maps the file at <paramref name="path"/>.
/// </summary>
/// <remarks>
/// <para>
/// The file is mapped, not copied, so no array is allocated for its contents. The returned
/// reader owns the mapping and releases it when <see cref="Dispose"/> is called; do not use the
/// reader after disposing it.
/// </para>
/// </remarks>
/// <param name="path">The path of a <c>.resources</c> file.</param>
/// <returns>A reader that owns the file mapping.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
public static RawResourceReader CreateFromFile(string path)
{
ArgumentNullException.ThrowIfNull(path);
MappedMemoryManager resource = MappedMemoryManager.CreateFromFile(path);
try
{
return new RawResourceReader(resource.Memory, resource);
}
catch
{
((IDisposable)resource).Dispose();
throw;
}
}
/// <summary>
/// Releases the memory mapping owned by this reader, if any. Disposal is thread-safe and
/// idempotent; after it, member access throws <see cref="ObjectDisposedException"/>. A reader
/// constructed directly over a <see cref="ReadOnlyMemory{T}"/> owns no mapping to release.
/// </summary>
/// <param name="disposing"><see langword="true"/> when called from <see cref="DisposableBase.Dispose()"/>.</param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
_owned?.Dispose();
}
}
/// <summary>
/// The number of resources in the file.
/// </summary>
public int ResourceCount
{
get
{
ObjectDisposedException.ThrowIf(Disposed, this);
return _numResources;
}
}
/// <summary>
/// Finds a resource by name.
/// </summary>
/// <param name="name">The resource name to look up (ordinal, case-sensitive).</param>
/// <param name="location">On success, the located resource.</param>
/// <returns><see langword="true"/> if a resource with the given name exists.</returns>
/// <exception cref="BadImageFormatException">The file is malformed.</exception>
public bool TryFindResource(ReadOnlySpan<char> name, out ResourceLocation location)
{
ObjectDisposedException.ThrowIf(Disposed, this);
location = default;
if (_numResources == 0)
{
return false;
}
int hash = ResourceNameHash.Compute(name);
// The header is validated as little-endian at construction, so the sorted int32 hash array can
// be reinterpreted in place and searched directly - no per-probe reads.
ReadOnlySpan<byte> resources = _resources.Span;
ReadOnlySpan<int> hashes = MemoryMarshal.Cast<byte, int>(resources.Slice(_nameHashesOffset, _numResources * 4));
// Binary search the sorted hash array.
int lo = 0;
int hi = _numResources - 1;
int index = -1;
while (lo <= hi)
{
int mid = (int)(((uint)lo + (uint)hi) >> 1);
int currentHash = hashes[mid];
if (currentHash == hash)
{
index = mid;
break;
}
if (currentHash < hash)
{
lo = mid + 1;
}
else
{
hi = mid - 1;
}
}
if (index < 0)
{
return false;
}
// Hashes can collide; expand to the full run of equal hashes and compare names.
int runStart = index;
while (runStart > 0 && hashes[runStart - 1] == hash)
{
runStart--;
}
int runEnd = index;
while (runEnd < _numResources - 1 && hashes[runEnd + 1] == hash)
{
runEnd++;
}
SpanReader<byte> reader = new(resources);
for (int i = runStart; i <= runEnd; i++)
{
reader.Position = _nameSectionOffset + GetNamePosition(i);
if (!reader.TryRead7BitEncodedInt32(out int nameByteLength)
|| nameByteLength < 0
|| (nameByteLength & 1) != 0)
{
ThrowBadImageFormatException("A resource name is corrupted.");
}
if (nameByteLength != (long)name.Length * 2)
{
continue;
}
if (!reader.TryRead(nameByteLength, out ReadOnlySpan<byte> nameBytes))
{
ThrowBadImageFormatException("A resource name is corrupted.");
}
// Names are stored UTF-16LE and the reader requires a little-endian architecture, so the
// bytes can be reinterpreted as chars and compared directly.
if (MemoryMarshal.Cast<byte, char>(nameBytes).SequenceEqual(name))
{
if (!reader.TryReadInt32LittleEndian(out int dataPosition))
{
ThrowBadImageFormatException("A resource name is corrupted.");
}
location = BuildLocation(i, dataPosition);
return true;
}
}
return false;
}
/// <inheritdoc cref="TryFindResource(ReadOnlySpan{char}, out ResourceLocation)"/>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
public bool TryFindResource(string name, out ResourceLocation location)
{
ArgumentNullException.ThrowIfNull(name);
return TryFindResource(name.AsSpan(), out location);
}
/// <summary>
/// Gets the location (type and byte length) of the resource at <paramref name="index"/>.
/// </summary>
/// <param name="index">A resource index in the range <c>[0, <see cref="ResourceCount"/>)</c>.</param>
/// <returns>The location of the resource at <paramref name="index"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
/// <exception cref="BadImageFormatException">The file is malformed.</exception>
public ResourceLocation GetLocation(int index)
{
ObjectDisposedException.ThrowIf(Disposed, this);
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _numResources);
return BuildLocation(index, ReadDataPosition(index));
}
/// <inheritdoc/>
ResourceTypeCode IStringResourceReader.GetResourceTypeCode(int index)
{
ObjectDisposedException.ThrowIf(Disposed, this);
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _numResources);
SpanReader<byte> reader = new(_resources.Span)
{
Position = GetDataOffset(ReadDataPosition(index))
};
if (!reader.TryRead7BitEncodedInt32(out int typeCodeValue) || typeCodeValue < 0)
{
ThrowBadImageFormatException("A resource type code is corrupted.");
}
return ResourceTypeCodeValidator.Validate(typeCodeValue, _numTypes);
}
/// <inheritdoc/>
string IStringResourceReader.GetResourceName(int index)
{
ObjectDisposedException.ThrowIf(Disposed, this);
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _numResources);
SpanReader<byte> reader = new(_resources.Span)
{
Position = _nameSectionOffset + GetNamePosition(index)
};
if (!reader.TryRead7BitEncodedInt32(out int byteLength)
|| byteLength < 0
|| (byteLength & 1) != 0)
{
ThrowBadImageFormatException("A resource name is corrupted.");
}
if (!reader.TryRead(byteLength, out ReadOnlySpan<byte> nameBytes))
{
ThrowBadImageFormatException("A resource name is corrupted.");
}
return MemoryMarshal.Cast<byte, char>(nameBytes).ToString();
}
/// <inheritdoc/>
string IStringResourceReader.GetString(int index)
{
ResourceLocation location = GetLocation(index);
return GetString(location);
}
/// <inheritdoc/>
string? IStringResourceReader.Lookup(string name)
{
if (!TryFindResource(name, out ResourceLocation location)
|| location.TypeCode != ResourceTypeCode.String)
{
return null;
}
return GetString(location);
}
/// <summary>
/// Copies the raw value content of the resource at <paramref name="index"/> into
/// <paramref name="destination"/>.
/// </summary>
/// <param name="index">A resource index in the range <c>[0, <see cref="ResourceCount"/>)</c>.</param>
/// <param name="destination">The span to copy the value bytes into.</param>
/// <param name="bytesWritten">On success, the number of bytes written.</param>
/// <returns>
/// <see langword="false"/> if the resource is a user type (no bytes are exposed) or if
/// <paramref name="destination"/> is too small; otherwise <see langword="true"/>.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
public bool TryGetResourceData(int index, Span<byte> destination, out int bytesWritten)
{
ResourceLocation location = GetLocation(index);
if (location.IsUserType || destination.Length < location.ByteLength)
{
bytesWritten = 0;
return false;
}
_resources.Span.Slice(location.ContentOffset, location.ByteLength).CopyTo(destination);
bytesWritten = location.ByteLength;
return true;
}
/// <summary>
/// Copies the raw value content of the resource at <paramref name="index"/> into
/// <paramref name="destination"/>.
/// </summary>
/// <param name="index">A resource index in the range <c>[0, <see cref="ResourceCount"/>)</c>.</param>
/// <param name="destination">The stream to write the value bytes to.</param>
/// <returns><see langword="false"/> if the resource is a user type; otherwise <see langword="true"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="destination"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
public bool TryCopyResourceData(int index, Stream destination)
{
ArgumentNullException.ThrowIfNull(destination);
ResourceLocation location = GetLocation(index);
if (location.IsUserType)
{
return false;
}
ReadOnlyMemory<byte> content = _resources.Slice(location.ContentOffset, location.ByteLength);
destination.Write(content.Span);
return true;
}
/// <summary>
/// Decodes the string at <paramref name="location"/> directly from the indexed resource image.
/// </summary>
/// <param name="location">The string resource location returned by this reader.</param>
/// <returns>The decoded string.</returns>
internal string GetString(ResourceLocation location)
{
ObjectDisposedException.ThrowIf(Disposed, this);
if (location.TypeCode != ResourceTypeCode.String)
{
throw new InvalidOperationException("The resource is not a string.");
}
return Encoding.UTF8.GetString(
_resources.Span.Slice(location.ContentOffset, location.ByteLength));
}
/// <summary>
/// Copies the name of the resource at <paramref name="index"/> into <paramref name="destination"/>.
/// </summary>
/// <param name="index">A resource index in the range <c>[0, <see cref="ResourceCount"/>)</c>.</param>
/// <param name="destination">The span to copy the name characters into.</param>
/// <param name="charsWritten">On success, the number of characters written.</param>
/// <returns>
/// <see langword="false"/> if <paramref name="destination"/> is too small; otherwise <see langword="true"/>.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
/// <exception cref="BadImageFormatException">The file is malformed.</exception>
public bool TryGetResourceName(int index, Span<char> destination, out int charsWritten)
{
ObjectDisposedException.ThrowIf(Disposed, this);
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _numResources);
SpanReader<byte> reader = new(_resources.Span) { Position = _nameSectionOffset + GetNamePosition(index) };
if (!reader.TryRead7BitEncodedInt32(out int nameByteLength)
|| nameByteLength < 0
|| (nameByteLength & 1) != 0)
{
ThrowBadImageFormatException("A resource name is corrupted.");
}
// Read the name bytes before checking the destination size so a length that runs past the end
// of the file is rejected as corruption here, rather than returning false and driving the caller
// to grow its buffer toward the claimed (potentially unbounded) length.
if (!reader.TryRead(nameByteLength, out ReadOnlySpan<byte> nameBytes))
{
ThrowBadImageFormatException("A resource name is corrupted.");
}
int charCount = nameByteLength / 2;
if (destination.Length < charCount)
{
charsWritten = 0;
return false;
}
// Names are stored UTF-16LE and the reader requires a little-endian architecture, so the bytes
// can be reinterpreted as chars and copied directly.
MemoryMarshal.Cast<byte, char>(nameBytes).CopyTo(destination);
charsWritten = charCount;
return true;
}
/// <summary>
/// Copies the declared type name of the user type at <paramref name="index"/> into
/// <paramref name="destination"/>.
/// </summary>
/// <param name="index">A resource index in the range <c>[0, <see cref="ResourceCount"/>)</c>.</param>
/// <param name="destination">The span to copy the type name characters into.</param>
/// <param name="charsWritten">On success, the number of characters written.</param>
/// <returns>
/// <see langword="false"/> if the resource is not a user type or if <paramref name="destination"/>
/// is too small; otherwise <see langword="true"/>.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
/// <exception cref="BadImageFormatException">The file is malformed.</exception>
public bool TryGetUserTypeName(int index, Span<char> destination, out int charsWritten)
{
ResourceLocation location = GetLocation(index);
if (!location.IsUserType)
{
charsWritten = 0;
return false;
}
int typeIndex = (int)location.TypeCode - (int)ResourceTypeCode.StartOfUserTypes;
if (typeIndex < 0 || typeIndex >= _numTypes)
{
throw new BadImageFormatException("A user type index is out of range.");
}
SpanReader<byte> reader = new(_resources.Span) { Position = _typeNamesOffset };
for (int i = 0; i < typeIndex; i++)
{
if (!reader.TryRead7BitEncodedInt32(out int skipLength)
|| skipLength < 0
|| !reader.TryAdvance(skipLength))
{
ThrowBadImageFormatException("A user type name is corrupted.");
}
}
if (!reader.TryRead7BitEncodedInt32(out int typeNameLength) || typeNameLength < 0)
{
ThrowBadImageFormatException("A user type name is corrupted.");
}
if (!reader.TryRead(typeNameLength, out ReadOnlySpan<byte> typeNameBytes))
{
ThrowBadImageFormatException("A user type name is corrupted.");
}
return Encoding.UTF8.TryGetChars(typeNameBytes, destination, out charsWritten);
}
// Reads the data-section virtual offset stored after the name for a resource index.
private int ReadDataPosition(int index)
{
SpanReader<byte> reader = new(_resources.Span) { Position = _nameSectionOffset + GetNamePosition(index) };
if (!reader.TryRead7BitEncodedInt32(out int nameByteLength)
|| nameByteLength < 0
|| (nameByteLength & 1) != 0
|| !reader.TryAdvance(nameByteLength))
{
ThrowBadImageFormatException("A resource name is corrupted.");
}
if (!reader.TryReadInt32LittleEndian(out int dataPosition))
{
ThrowBadImageFormatException("A resource name is corrupted.");
}
return dataPosition;
}
// Reads a resource's type code and computes its content offset and length.
private ResourceLocation BuildLocation(int index, int dataPosition)
{
SpanReader<byte> reader = new(_resources.Span) { Position = GetDataOffset(dataPosition) };
if (!reader.TryRead7BitEncodedInt32(out int typeCodeValue))
{
ThrowBadImageFormatException("A resource type code is corrupted.");
}
ResourceTypeCode typeCode = ResourceTypeCodeValidator.Validate(typeCodeValue, _numTypes);
if (typeCodeValue >= (int)ResourceTypeCode.StartOfUserTypes)
{
// Serialized user type: report the code and type name only, no value bytes.
return new ResourceLocation(index, typeCode, byteLength: 0, contentOffset: reader.Position);
}
int contentLength;
switch (typeCode)
{
case ResourceTypeCode.Null:
contentLength = 0;
break;
case ResourceTypeCode.Boolean:
case ResourceTypeCode.Byte:
case ResourceTypeCode.SByte:
contentLength = 1;
break;
case ResourceTypeCode.Char:
case ResourceTypeCode.Int16:
case ResourceTypeCode.UInt16:
contentLength = 2;
break;
case ResourceTypeCode.Int32:
case ResourceTypeCode.UInt32:
case ResourceTypeCode.Single:
contentLength = 4;
break;
case ResourceTypeCode.Int64:
case ResourceTypeCode.UInt64:
case ResourceTypeCode.Double:
case ResourceTypeCode.DateTime:
case ResourceTypeCode.TimeSpan:
contentLength = 8;
break;
case ResourceTypeCode.Decimal:
contentLength = 16;
break;
case ResourceTypeCode.String:
if (!reader.TryRead7BitEncodedInt32(out contentLength))
{
ThrowBadImageFormatException("A resource value length is out of range.");
}
break;
case ResourceTypeCode.ByteArray:
case ResourceTypeCode.Stream:
if (!reader.TryReadInt32LittleEndian(out contentLength))
{
ThrowBadImageFormatException("A resource value length is out of range.");
}
break;
default:
throw new BadImageFormatException($"Unsupported resource type code 0x{typeCodeValue:X}.");
}
int contentOffset = reader.Position;
if (contentLength < 0 || contentOffset > _resources.Length - contentLength)
{
ThrowBadImageFormatException("A resource value length is out of range.");
}
return new ResourceLocation(index, typeCode, contentLength, contentOffset);
}
private int GetDataOffset(int dataPosition)
{
if (dataPosition < 0 || dataPosition >= _resources.Length - _dataSectionOffset)
{
throw new BadImageFormatException("A resource data offset is out of range.");
}
return _dataSectionOffset + dataPosition;
}
private int GetNamePosition(int index)
{
SpanReader<byte> reader = new(_resources.Span) { Position = _namePositionsOffset + (index * 4) };
if (!reader.TryReadInt32LittleEndian(out int position)
|| position < 0
|| position > _dataSectionOffset - _nameSectionOffset)
{
ThrowBadImageFormatException("A resource name offset is out of range.");
}
return position;
}
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
private static void ThrowBadImageFormatException(string message) => throw new BadImageFormatException(message);
}