| File: Resources\StreamStringResourceReader.cs | Web Access |
| Project: Microsoft.DotNet.Cli.CoreUtils.csproj (Microsoft.DotNet.Cli.CoreUtils) |
// 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; using System.Threading; namespace Microsoft.DotNet.Cli.Resources.Internal; /// <summary> /// Reads an indexed version-2 resource table directly from an owned seekable stream. /// </summary> internal sealed class StreamStringResourceReader : DisposableBase, IStringResourceReader { private const int MagicNumber = unchecked((int)0xBEEFCACE); private const int SupportedVersion = 2; private readonly BinaryReader _reader; private readonly Lock _lock = new(); private readonly long _resourceOffset; private readonly int[] _nameHashes; private readonly int[] _namePositions; private readonly int _typeCount; private readonly long _nameSectionOffset; private readonly long _dataSectionOffset; /// <summary> /// Initializes a reader over the current position of <paramref name="stream"/> and takes /// ownership of the stream. /// </summary> /// <param name="stream">The readable, seekable resource stream.</param> internal StreamStringResourceReader(Stream stream) { ArgumentNullException.ThrowIfNull(stream); BinaryReader? reader = null; try { if (!stream.CanRead) { throw new ArgumentException("The resource stream must be readable.", nameof(stream)); } if (!stream.CanSeek) { throw new NotSupportedException("The resource stream must be seekable."); } reader = new(stream, Encoding.UTF8); _reader = reader; _resourceOffset = stream.Position; if (_reader.ReadInt32() != MagicNumber) { throw new ArgumentException("The stream is not a valid .resources file.", nameof(stream)); } int resourceManagerHeaderVersion = _reader.ReadInt32(); int byteCountToSkip = _reader.ReadInt32(); if (resourceManagerHeaderVersion < 0 || byteCountToSkip < 0) { throw new BadImageFormatException("The resource header is invalid."); } stream.Seek(byteCountToSkip, SeekOrigin.Current); int version = _reader.ReadInt32(); if (version != SupportedVersion) { throw new NotSupportedException( $"Unsupported .resources format version {version}; only version {SupportedVersion} is supported."); } ResourceCount = _reader.ReadInt32(); _typeCount = _reader.ReadInt32(); if (ResourceCount < 0 || _typeCount < 0) { throw new BadImageFormatException("The resource or type count is invalid."); } for (int i = 0; i < _typeCount; i++) { SkipLengthPrefixedBytes(); } long relativePosition = stream.Position - _resourceOffset; stream.Seek((8 - (relativePosition & 7)) & 7, SeekOrigin.Current); long indexByteLength = ((long)ResourceCount * 2 + 1) * sizeof(int); if (indexByteLength > stream.Length - stream.Position) { throw new BadImageFormatException("The resource index is truncated."); } _nameHashes = new int[ResourceCount]; for (int i = 0; i < _nameHashes.Length; i++) { _nameHashes[i] = _reader.ReadInt32(); } _namePositions = new int[ResourceCount]; for (int i = 0; i < _namePositions.Length; i++) { int position = _reader.ReadInt32(); if (position < 0) { throw new BadImageFormatException("A resource name offset is invalid."); } _namePositions[i] = position; } int dataSectionOffset = _reader.ReadInt32(); if (dataSectionOffset < 0) { throw new BadImageFormatException("The resource data section offset is invalid."); } _nameSectionOffset = stream.Position; _dataSectionOffset = checked(_resourceOffset + dataSectionOffset); if (_dataSectionOffset < _nameSectionOffset || _dataSectionOffset > stream.Length) { throw new BadImageFormatException("The resource data section is invalid."); } long nameSectionLength = _dataSectionOffset - _nameSectionOffset; for (int i = 0; i < _namePositions.Length; i++) { if (_namePositions[i] >= nameSectionLength) { throw new BadImageFormatException("A resource name offset is invalid."); } } } catch (EndOfStreamException exception) { DisposeFailedConstruction(reader, stream); throw new BadImageFormatException("The resource stream is truncated.", exception); } catch { DisposeFailedConstruction(reader, stream); throw; } } /// <inheritdoc/> public int ResourceCount { get; } private static void DisposeFailedConstruction(BinaryReader? reader, Stream stream) { if (reader is null) { stream.Dispose(); } else { reader.Dispose(); } } /// <inheritdoc/> public ResourceTypeCode GetResourceTypeCode(int index) { ObjectDisposedException.ThrowIf(Disposed, this); lock (_lock) { int dataPosition = ReadDataPosition(index); _reader.BaseStream.Position = checked(_dataSectionOffset + dataPosition); return ReadResourceTypeCode(); } } /// <inheritdoc/> public string GetResourceName(int index) { ObjectDisposedException.ThrowIf(Disposed, this); lock (_lock) { ArgumentOutOfRangeException.ThrowIfNegative(index); ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, ResourceCount); _reader.BaseStream.Position = checked(_nameSectionOffset + _namePositions[index]); int byteLength = Read7BitEncodedInt32(); if (byteLength < 0 || (byteLength & 1) != 0) { throw new BadImageFormatException("A resource name is invalid."); } if (byteLength > _reader.BaseStream.Length - _reader.BaseStream.Position) { throw new BadImageFormatException("A resource name is truncated."); } byte[] bytes = _reader.ReadBytes(byteLength); if (bytes.Length != byteLength) { throw new BadImageFormatException("A resource name is truncated."); } return Encoding.Unicode.GetString(bytes); } } /// <inheritdoc/> public string GetString(int index) { ObjectDisposedException.ThrowIf(Disposed, this); lock (_lock) { int dataPosition = ReadDataPosition(index); _reader.BaseStream.Position = checked(_dataSectionOffset + dataPosition); if (ReadResourceTypeCode() != ResourceTypeCode.String) { throw new InvalidOperationException("The resource is not a string."); } return _reader.ReadString(); } } /// <inheritdoc/> public string? Lookup(string name) { ObjectDisposedException.ThrowIf(Disposed, this); ArgumentNullException.ThrowIfNull(name); lock (_lock) { int hash = ResourceNameHash.Compute(name); int index = Array.BinarySearch(_nameHashes, hash); if (index < 0) { return null; } int first = index; while (first > 0 && _nameHashes[first - 1] == hash) { first--; } int last = index; while (last < ResourceCount - 1 && _nameHashes[last + 1] == hash) { last++; } for (int i = first; i <= last; i++) { _reader.BaseStream.Position = checked(_nameSectionOffset + _namePositions[i]); int byteLength = Read7BitEncodedInt32(); if (byteLength < 0 || (byteLength & 1) != 0) { throw new BadImageFormatException("A resource name is invalid."); } if (byteLength != checked(name.Length * sizeof(char)) || !NameEquals(name)) { continue; } int dataPosition = _reader.ReadInt32(); if (dataPosition < 0 || dataPosition >= _reader.BaseStream.Length - _dataSectionOffset) { throw new BadImageFormatException("A resource data offset is invalid."); } _reader.BaseStream.Position = _dataSectionOffset + dataPosition; ResourceTypeCode typeCode = ReadResourceTypeCode(); if (typeCode != ResourceTypeCode.String) { return null; } return _reader.ReadString(); } return null; } } /// <inheritdoc/> protected override void Dispose(bool disposing) => _reader.Dispose(); private int ReadDataPosition(int index) { ArgumentOutOfRangeException.ThrowIfNegative(index); ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, ResourceCount); _reader.BaseStream.Position = checked(_nameSectionOffset + _namePositions[index]); SkipLengthPrefixedBytes(); int dataPosition = _reader.ReadInt32(); if (dataPosition < 0 || dataPosition >= _reader.BaseStream.Length - _dataSectionOffset) { throw new BadImageFormatException("A resource data offset is invalid."); } return dataPosition; } private ResourceTypeCode ReadResourceTypeCode() { int value = Read7BitEncodedInt32(); return value < 0 ? throw new BadImageFormatException("A resource type code is invalid.") : ResourceTypeCodeValidator.Validate(value, _typeCount); } private bool NameEquals(string name) { for (int i = 0; i < name.Length; i++) { if (_reader.ReadUInt16() != name[i]) { return false; } } return true; } private void SkipLengthPrefixedBytes() { int length = Read7BitEncodedInt32(); if (length < 0) { throw new BadImageFormatException("A length-prefixed value is invalid."); } _reader.BaseStream.Seek(length, SeekOrigin.Current); } private int Read7BitEncodedInt32() { uint result = 0; const int MaxBytesWithoutOverflow = 4; for (int shift = 0; shift < MaxBytesWithoutOverflow * 7; shift += 7) { byte value = _reader.ReadByte(); result |= (uint)(value & 0x7F) << shift; if (value <= 0x7F) { return (int)result; } } byte last = _reader.ReadByte(); if (last > 0x0F) { throw new BadImageFormatException("A 7-bit encoded integer is invalid."); } result |= (uint)last << (MaxBytesWithoutOverflow * 7); return (int)result; } }