diff --git a/MetadataExtractor/Formats/Avi/AviRiffHandler.cs b/MetadataExtractor/Formats/Avi/AviRiffHandler.cs index 03ab69f97..8c0fe74fd 100644 --- a/MetadataExtractor/Formats/Avi/AviRiffHandler.cs +++ b/MetadataExtractor/Formats/Avi/AviRiffHandler.cs @@ -52,7 +52,7 @@ public void ProcessChunk(string fourCc, byte[] payload) { case "strh": { - var reader = new ByteArrayReader(payload, isMotorolaByteOrder: false); + var reader = new BufferReader(payload, isBigEndian: false); var directory = GetOrCreateAviDirectory(); try @@ -101,8 +101,13 @@ public void ProcessChunk(string fourCc, byte[] payload) { var directory = GetOrCreateAviDirectory(); - var reader = new ByteArrayReader(payload, isMotorolaByteOrder: false); - try + var reader = new BufferReader(payload, isBigEndian: false); + + if (payload.Length < 40) + { + directory.AddError("Insufficient bytes for AviRiff chunk 'avih'"); + } + else { //int dwMicroSecPerFrame = reader.GetInt32(0); //int dwMaxBytesPerSec = reader.GetInt32(4); @@ -120,21 +125,20 @@ public void ProcessChunk(string fourCc, byte[] payload) directory.Set(AviDirectory.TagHeight, dwHeight); directory.Set(AviDirectory.TagStreams, dwStreams); } - catch (IOException e) - { - directory.AddError("Exception reading AviRiff chunk 'avih' : " + e.Message); - } break; } case "IDIT": { - var reader = new ByteArrayReader(payload); - var str = reader.GetString(0, payload.Length, Encoding.ASCII); - if (str.Length == 26 && str.EndsWith("\n\0", StringComparison.Ordinal)) + string str; + if (payload.Length is 26 && payload.AsSpan().EndsWith("\n\0"u8)) { // ?0A 00? "New Line" + padded to nearest WORD boundary - str = str.Substring(0, 24); + str = Encoding.ASCII.GetString(payload.AsSpan(0, 24)); + } + else + { + str = Encoding.ASCII.GetString(payload); } GetOrCreateAviDirectory().Set(AviDirectory.TagDateTimeOriginal, str); break; diff --git a/MetadataExtractor/Formats/Exif/ExifDescriptorBase.cs b/MetadataExtractor/Formats/Exif/ExifDescriptorBase.cs index 64650f58c..10d998425 100644 --- a/MetadataExtractor/Formats/Exif/ExifDescriptorBase.cs +++ b/MetadataExtractor/Formats/Exif/ExifDescriptorBase.cs @@ -575,7 +575,7 @@ public abstract class ExifDescriptorBase(T directory) return ret; } - IndexedReader reader = new ByteArrayReader(values); + var reader = new BufferReader(values, isBigEndian: true); // first two values should be read as 16-bits (2 bytes) var item0 = reader.GetInt16(0); @@ -588,7 +588,7 @@ public abstract class ExifDescriptorBase(T directory) if (end > values.Length) // sanity check in case of byte order problems; calculated 'end' should be <= length of the values { // try swapping byte order (I have seen this order different than in EXIF) - reader = reader.WithByteOrder(!reader.IsMotorolaByteOrder); + reader = new BufferReader(values, isBigEndian: !reader.IsBigEndian); item0 = reader.GetInt16(0); item1 = reader.GetInt16(2); diff --git a/MetadataExtractor/Formats/Exif/makernotes/PanasonicMakernoteDescriptor.cs b/MetadataExtractor/Formats/Exif/makernotes/PanasonicMakernoteDescriptor.cs index 5bfe18c24..1110775e6 100644 --- a/MetadataExtractor/Formats/Exif/makernotes/PanasonicMakernoteDescriptor.cs +++ b/MetadataExtractor/Formats/Exif/makernotes/PanasonicMakernoteDescriptor.cs @@ -167,29 +167,25 @@ public sealed class PanasonicMakernoteDescriptor(PanasonicMakernoteDirectory dir if (values is null) return null; - IndexedReader reader = new ByteArrayReader(values); - - try - { - int val1 = reader.GetUInt16(0); - int val2 = reader.GetUInt16(2); - if (val1 == -1 && val2 == 1) - return "Slim Low"; - if (val1 == -3 && val2 == 2) - return "Slim High"; - if (val1 == 0 && val2 == 0) - return "Off"; - if (val1 == 1 && val2 == 1) - return "Stretch Low"; - if (val1 == 3 && val2 == 2) - return "Stretch High"; - - return "Unknown (" + val1 + " " + val2 + ")"; - } - catch (IOException) + if (values.Length < 2 + 2) { return null; } + + var reader = new BufferReader(values, isBigEndian: true); + + int val1 = reader.GetUInt16(0); + int val2 = reader.GetUInt16(2); + + return (val1, val2) switch + { + (-1, 1) => "Slim Low", + (-3, 2) => "Slim High", + (0, 0) => "Off", + (1, 1) => "Stretch Low", + (3, 2) => "Stretch High", + _ => $"Unknown ({val1} {val2})" + }; } public string? GetIntelligentExposureDescription() diff --git a/MetadataExtractor/Formats/Icc/IccDescriptor.cs b/MetadataExtractor/Formats/Icc/IccDescriptor.cs index 3f19c3a00..0157b5a2d 100644 --- a/MetadataExtractor/Formats/Icc/IccDescriptor.cs +++ b/MetadataExtractor/Formats/Icc/IccDescriptor.cs @@ -48,7 +48,7 @@ private enum IccTagType if (bytes is null) return Directory.GetString(tagType); - var reader = new ByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); var iccTagType = (IccTagType)reader.GetInt32(0); @@ -71,6 +71,10 @@ private enum IccTagType case IccTagType.Desc: { var stringLength = reader.GetInt32(8); + + if (stringLength < 0 || stringLength > bytes.Length) + return null; + return Encoding.UTF8.GetString(bytes, 12, stringLength - 1); } diff --git a/MetadataExtractor/Formats/Photoshop/PhotoshopDescriptor.cs b/MetadataExtractor/Formats/Photoshop/PhotoshopDescriptor.cs index e2674893a..bb8b1fb35 100644 --- a/MetadataExtractor/Formats/Photoshop/PhotoshopDescriptor.cs +++ b/MetadataExtractor/Formats/Photoshop/PhotoshopDescriptor.cs @@ -49,97 +49,88 @@ public sealed class PhotoshopDescriptor(PhotoshopDirectory directory) public string? GetJpegQualityString() { - try - { - var b = Directory.GetByteArray(PhotoshopDirectory.TagJpegQuality); - if (b is null) - return Directory.GetString(PhotoshopDirectory.TagJpegQuality); + var b = Directory.GetByteArray(PhotoshopDirectory.TagJpegQuality); - var reader = new ByteArrayReader(b); + if (b is null) + return Directory.GetString(PhotoshopDirectory.TagJpegQuality); - int q = reader.GetUInt16(0); - int f = reader.GetUInt16(2); - int s = reader.GetUInt16(4); - - var q1 = q is >= 0xFFFD and <= 0xFFFF - ? q - 0xFFFC - : q <= 8 - ? q + 4 - : q; - string quality = q switch - { - 0xFFFD or 0xFFFE or 0xFFFF or 0 => "Low", - 1 or 2 or 3 => "Medium", - 4 or 5 => "High", - 6 or 7 or 8 => "Maximum", - _ => "Unknown" - }; - var format = f switch - { - 0x0000 => "Standard", - 0x0001 => "Optimised", - 0x0101 => "Progressive", - _ => $"Unknown (0x{f:X4})" - }; - var scans = s is >= 1 and <= 3 - ? (s + 2).ToString() - : $"Unknown (0x{s:X4})"; - - return $"{q1} ({quality}), {format} format, {scans} scans"; - } - catch + if (b.Length < 2 + 2 + 2) { return null; } + + var reader = new BufferReader(b, isBigEndian: true); + + int q = reader.GetUInt16(0); + int f = reader.GetUInt16(2); + int s = reader.GetUInt16(4); + + var q1 = q is >= 0xFFFD and <= 0xFFFF + ? q - 0xFFFC + : q <= 8 + ? q + 4 + : q; + string quality = q switch + { + 0xFFFD or 0xFFFE or 0xFFFF or 0 => "Low", + 1 or 2 or 3 => "Medium", + 4 or 5 => "High", + 6 or 7 or 8 => "Maximum", + _ => "Unknown" + }; + var format = f switch + { + 0x0000 => "Standard", + 0x0001 => "Optimised", + 0x0101 => "Progressive", + _ => $"Unknown (0x{f:X4})" + }; + var scans = s is >= 1 and <= 3 + ? (s + 2).ToString() + : $"Unknown (0x{s:X4})"; + + return $"{q1} ({quality}), {format} format, {scans} scans"; } public string? GetPixelAspectRatioString() { - try - { - var bytes = Directory.GetByteArray(PhotoshopDirectory.TagPixelAspectRatio); + var bytes = Directory.GetByteArray(PhotoshopDirectory.TagPixelAspectRatio); - if (bytes is null) - return null; + if (bytes is null) + return null; - var reader = new ByteArrayReader(bytes); - var d = reader.GetDouble64(4); - return d.ToString("0.0##"); - } - catch - { + if (bytes.Length < 4 + 8) return null; - } + + var reader = new BufferReader(bytes, isBigEndian: true); + var d = reader.GetDouble64(4); + return d.ToString("0.0##"); } public string? GetPrintScaleDescription() { - try - { - var bytes = Directory.GetByteArray(PhotoshopDirectory.TagPrintScale); + var bytes = Directory.GetByteArray(PhotoshopDirectory.TagPrintScale); - if (bytes is null) - return null; + if (bytes is null) + return null; - var reader = new ByteArrayReader(bytes); - var style = reader.GetInt32(0); - var locX = reader.GetFloat32(2); - var locY = reader.GetFloat32(6); - var scale = reader.GetFloat32(10); + if (bytes.Length < 14) + return null; - return style switch - { - 0 => $"Centered, Scale {scale:0.0##}", - 1 => "Size to fit", - 2 => $"User defined, X:{locX} Y:{locY}, Scale:{scale:0.0##}", - _ => $"Unknown {style:X4}, X:{locX} Y:{locY}, Scale:{scale:0.0##}", - }; - } - catch + var reader = new BufferReader(bytes, isBigEndian: true); + var style = reader.GetInt16(0); + var locX = reader.GetFloat32(2); + var locY = reader.GetFloat32(6); + var scale = reader.GetFloat32(10); + + return style switch { - return null; - } + 0 => $"Centered, Scale {scale:0.0##}", + 1 => "Size to fit", + 2 => $"User defined, X:{locX} Y:{locY}, Scale:{scale:0.0##}", + _ => $"Unknown {style:X4}, X:{locX} Y:{locY}, Scale:{scale:0.0##}" + }; } public string? GetResolutionInfoDescription() @@ -151,7 +142,7 @@ public sealed class PhotoshopDescriptor(PhotoshopDirectory directory) if (bytes is null) return null; - var reader = new ByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); var resX = reader.GetS15Fixed16(0); var resY = reader.GetS15Fixed16(8); @@ -174,7 +165,7 @@ public sealed class PhotoshopDescriptor(PhotoshopDirectory directory) if (bytes is null) return null; - var reader = new ByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); var pos = 0; var ver = reader.GetInt32(0); @@ -207,7 +198,7 @@ public sealed class PhotoshopDescriptor(PhotoshopDirectory directory) if (bytes is null) return null; - var reader = new ByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); var nameLength = reader.GetInt32(20); var name = reader.GetString(24, nameLength * 2, Encoding.BigEndianUnicode); @@ -223,29 +214,27 @@ public sealed class PhotoshopDescriptor(PhotoshopDirectory directory) public string? GetThumbnailDescription(int tagType) { - try - { - var v = Directory.GetByteArray(tagType); + var v = Directory.GetByteArray(tagType); - if (v is null) - return null; + if (v is null) + return null; - var reader = new ByteArrayReader(v); - var format = reader.GetInt32(0); - var width = reader.GetInt32(4); - var height = reader.GetInt32(8); - // skip WidthBytes - var totalSize = reader.GetInt32(16); - var compSize = reader.GetInt32(20); - var bpp = reader.GetInt32(24); - // skip Number of planes - - return $"{(format == 1 ? "JpegRGB" : "RawRGB")}, {width}x{height}, Decomp {totalSize} bytes, {bpp} bpp, {compSize} bytes"; - } - catch + if (v.Length < 28) { return null; } + + var reader = new BufferReader(v, isBigEndian: true); + var format = reader.GetInt32(0); + var width = reader.GetInt32(4); + var height = reader.GetInt32(8); + // skip WidthBytes + var totalSize = reader.GetInt32(16); + var compSize = reader.GetInt32(20); + var bpp = reader.GetInt32(24); + // skip Number of planes + + return $"{(format == 1 ? "JpegRGB" : "RawRGB")}, {width}x{height}, Decomp {totalSize} bytes, {bpp} bpp, {compSize} bytes"; } private string? GetBooleanString(int tag) @@ -265,16 +254,12 @@ public sealed class PhotoshopDescriptor(PhotoshopDirectory directory) if (bytes is null) return null; - var reader = new ByteArrayReader(bytes); - - try - { - return $"{reader.GetInt32(0)}"; - } - catch - { + if (bytes.Length < 4) return null; - } + + var reader = new BufferReader(bytes, isBigEndian: true); + + return reader.GetInt32().ToString(); } private string? GetSimpleString(int tagType) @@ -319,8 +304,8 @@ public sealed class PhotoshopDescriptor(PhotoshopDirectory directory) var bytes = Directory.GetByteArray(tagType); if (bytes is null) return null; - var reader = new ByteArrayReader(bytes); - int length = (int)(reader.Length - reader.GetByte((int)reader.Length - 1) - 1) / 26; + var reader = new BufferReader(bytes, isBigEndian: true); + int length = (bytes.Length - reader.GetByte(bytes.Length - 1) - 1) / 26; string? fillRecord = null; @@ -431,8 +416,8 @@ public sealed class PhotoshopDescriptor(PhotoshopDirectory directory) paths.Add(oSubpath); // Extract name (previously appended to end of byte array) - int nameLength = reader.GetByte((int)reader.Length - 1); - var name = reader.GetString((int)reader.Length - nameLength - 1, nameLength, Encoding.ASCII); + int nameLength = reader.GetByte(bytes.Length - 1); + var name = reader.GetString(bytes.Length - nameLength - 1, nameLength, Encoding.ASCII); // Build description var str = new StringBuilder(); diff --git a/MetadataExtractor/Formats/Png/PngMetadataReader.cs b/MetadataExtractor/Formats/Png/PngMetadataReader.cs index 85f31beb4..1a967806d 100644 --- a/MetadataExtractor/Formats/Png/PngMetadataReader.cs +++ b/MetadataExtractor/Formats/Png/PngMetadataReader.cs @@ -84,7 +84,6 @@ public static IReadOnlyList ReadMetadata(Stream stream) /// For more guidance: http://www.w3.org/TR/PNG-Decoders.html#D.Text-chunk-processing /// private static readonly Encoding _latin1Encoding = Encoding.GetEncoding("ISO-8859-1"); - private static readonly Encoding _utf8Encoding = Encoding.UTF8; /// /// @@ -148,7 +147,7 @@ private static IEnumerable ProcessChunk(PngChunk chunk) } else if (chunkType == PngChunkType.iCCP) { - var reader = new SequentialByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); var profileName = reader.GetNullTerminatedStringValue(maxLengthBytes: 79); var directory = new PngDirectory(PngChunkType.iCCP); directory.Set(PngDirectory.TagIccProfileName, profileName); @@ -198,7 +197,7 @@ private static IEnumerable ProcessChunk(PngChunk chunk) } else if (chunkType == PngChunkType.tEXt) { - var reader = new SequentialByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); var keyword = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding); var bytesLeft = bytes.Length - keyword.Length - 1; var value = reader.GetNullTerminatedStringValue(bytesLeft, _latin1Encoding); @@ -210,7 +209,7 @@ private static IEnumerable ProcessChunk(PngChunk chunk) } else if (chunkType == PngChunkType.zTXt) { - var reader = new SequentialByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); var keyword = reader.GetNullTerminatedStringValue(maxLengthBytes: 79).ToString(_latin1Encoding); var compressionMethod = reader.GetSByte(); @@ -242,9 +241,9 @@ private static IEnumerable ProcessChunk(PngChunk chunk) } else if (chunkType == PngChunkType.iTXt) { - var reader = new SequentialByteArrayReader(bytes); + var reader = new BufferReader(bytes, isBigEndian: true); var keywordStringValue = reader.GetNullTerminatedStringValue(maxLengthBytes: 79); - var keyword = keywordStringValue.ToString(_utf8Encoding); + var keyword = keywordStringValue.ToString(Encoding.UTF8); var compressionFlag = reader.GetSByte(); var compressionMethod = reader.GetSByte(); @@ -454,7 +453,7 @@ static PngDirectory ReadTextDirectory(string keyword, byte[] textBytes, PngChunk if (pngChunkType == PngChunkType.iTXt) { - encoding = _utf8Encoding; + encoding = Encoding.UTF8; } var textPairs = new[] { new KeyValuePair(keyword, new StringValue(textBytes, encoding)) }; diff --git a/MetadataExtractor/Formats/WebP/WebpRiffHandler.cs b/MetadataExtractor/Formats/WebP/WebpRiffHandler.cs index 42f259f45..2410d6faa 100644 --- a/MetadataExtractor/Formats/WebP/WebpRiffHandler.cs +++ b/MetadataExtractor/Formats/WebP/WebpRiffHandler.cs @@ -70,43 +70,29 @@ public void ProcessChunk(string fourCc, byte[] payload) if (payload.Length != 10) break; - string? error = null; - var reader = new ByteArrayReader(payload, isMotorolaByteOrder: false); - var isAnimation = false; - var hasAlpha = false; - var widthMinusOne = -1; - var heightMinusOne = -1; - try - { - // Flags - // bit 0: has fragments - // bit 1: is animation - // bit 2: has XMP - // bit 3: has Exif - // bit 4: has alpha - // big 5: has ICC - isAnimation = reader.GetBit(1); - hasAlpha = reader.GetBit(4); - - // Image size - widthMinusOne = reader.GetInt24(4); - heightMinusOne = reader.GetInt24(7); - } - catch (IOException e) - { - error = "Exception reading WebpRiff chunk 'VP8X' : " + e.Message; - } + var reader = new BufferReader(payload, isBigEndian: false); + + // Flags + // bit 0: has fragments + // bit 1: is animation + // bit 2: has XMP + // bit 3: has Exif + // bit 4: has alpha + // big 5: has ICC + bool isAnimation = reader.GetBit(1); + bool hasAlpha = reader.GetBit(4); + + // Image size + int widthMinusOne = reader.GetInt24(4); + int heightMinusOne = reader.GetInt24(7); var directory = new WebPDirectory(); - if (error is null) - { - directory.Set(WebPDirectory.TagImageWidth, widthMinusOne + 1); - directory.Set(WebPDirectory.TagImageHeight, heightMinusOne + 1); - directory.Set(WebPDirectory.TagHasAlpha, hasAlpha); - directory.Set(WebPDirectory.TagIsAnimation, isAnimation); - } - else - directory.AddError(error); + + directory.Set(WebPDirectory.TagImageWidth, widthMinusOne + 1); + directory.Set(WebPDirectory.TagImageHeight, heightMinusOne + 1); + directory.Set(WebPDirectory.TagHasAlpha, hasAlpha); + directory.Set(WebPDirectory.TagIsAnimation, isAnimation); + _directories.Add(directory); break; } @@ -115,31 +101,22 @@ public void ProcessChunk(string fourCc, byte[] payload) if (payload.Length < 5) break; - var reader = new ByteArrayReader(payload, isMotorolaByteOrder: false); + var reader = new BufferReader(payload, isBigEndian: false); string? error = null; - var widthMinusOne = -1; - var heightMinusOne = -1; - try - { - // https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#2_riff_header - - // Expect the signature byte - if (reader.GetByte(0) != 0x2F) - break; - var b1 = reader.GetByte(1); - var b2 = reader.GetByte(2); - var b3 = reader.GetByte(3); - var b4 = reader.GetByte(4); - // 14 bits for width - widthMinusOne = (b2 & 0x3F) << 8 | b1; - // 14 bits for height - heightMinusOne = (b4 & 0x0F) << 10 | b3 << 2 | (b2 & 0xC0) >> 6; - } - catch (IOException e) - { - error = "Exception reading WebpRiff chunk 'VP8L' : " + e.Message; - } + // https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#2_riff_header + + // Expect the signature byte + if (reader.GetByte(0) != 0x2F) + break; + var b1 = reader.GetByte(1); + var b2 = reader.GetByte(2); + var b3 = reader.GetByte(3); + var b4 = reader.GetByte(4); + // 14 bits for width + int widthMinusOne = (b2 & 0x3F) << 8 | b1; + // 14 bits for height + int heightMinusOne = (b4 & 0x0F) << 10 | b3 << 2 | (b2 & 0xC0) >> 6; var directory = new WebPDirectory(); if (error is null) @@ -157,28 +134,21 @@ public void ProcessChunk(string fourCc, byte[] payload) if (payload.Length < 10) break; - var reader = new ByteArrayReader(payload, isMotorolaByteOrder: false); + var reader = new BufferReader(payload, isBigEndian: false); string? error = null; - var width = 0; - var height = 0; - try - { - // https://tools.ietf.org/html/rfc6386#section-9.1 - // https://github.com/webmproject/libwebp/blob/master/src/enc/syntax.c#L115 - - // Expect the signature bytes - if (reader.GetByte(3) != 0x9D || - reader.GetByte(4) != 0x01 || - reader.GetByte(5) != 0x2A) - break; - width = reader.GetUInt16(6); - height = reader.GetUInt16(8); - } - catch (IOException e) - { - error = "Exception reading WebpRiff chunk 'VP8' : " + e.Message; - } + + // https://tools.ietf.org/html/rfc6386#section-9.1 + // https://github.com/webmproject/libwebp/blob/master/src/enc/syntax.c#L115 + + // Expect the signature bytes + if (reader.GetByte(3) != 0x9D || + reader.GetByte(4) != 0x01 || + reader.GetByte(5) != 0x2A) + break; + + var width = reader.GetUInt16(6); + var height = reader.GetUInt16(8); var directory = new WebPDirectory(); if (error is null) diff --git a/MetadataExtractor/IO/BufferReader.Indexed.cs b/MetadataExtractor/IO/BufferReader.Indexed.cs new file mode 100644 index 000000000..4b803a315 --- /dev/null +++ b/MetadataExtractor/IO/BufferReader.Indexed.cs @@ -0,0 +1,223 @@ +// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Buffers; +using System.Buffers.Binary; + +namespace MetadataExtractor.IO; + +internal ref partial struct BufferReader +{ + public readonly bool GetBit(int index) + { + var byteIndex = index / 8; + var bitIndex = index % 8; + + return ((GetByte(byteIndex) >> bitIndex) & 1) == 1; + } + + public readonly void GetBytes(int index, scoped Span bytes) + { + ValidateIndex(index, bytes.Length); + + _bytes.Slice(index, bytes.Length).CopyTo(bytes); + } + + public readonly byte GetByte(int index) + { + ValidateIndex(index, 1); + + return _bytes[index]; + } + + public readonly short GetInt16(int index) + { + ValidateIndex(index, 2); + + var bytes = _bytes.Slice(index, 2); + + return _isBigEndian + ? BinaryPrimitives.ReadInt16BigEndian(bytes) + : BinaryPrimitives.ReadInt16LittleEndian(bytes); + } + + public readonly ushort GetUInt16(int index) + { + ValidateIndex(index, 2); + + var bytes = _bytes.Slice(index, 2); + + return _isBigEndian + ? BinaryPrimitives.ReadUInt16BigEndian(bytes) + : BinaryPrimitives.ReadUInt16LittleEndian(bytes); + } + + public readonly int GetInt24(int index) + { + Span bytes = stackalloc byte[3]; + + GetBytes(index, bytes); + + if (_isBigEndian) + { + return + bytes[0] << 16 | + bytes[1] << 8 | + bytes[2]; + } + else + { + return + bytes[2] << 16 | + bytes[1] << 8 | + bytes[0]; + } + } + + public readonly int GetInt32(int index) + { + ValidateIndex(index, 4); + + var bytes = _bytes.Slice(index, 4); + + return _isBigEndian + ? BinaryPrimitives.ReadInt32BigEndian(bytes) + : BinaryPrimitives.ReadInt32LittleEndian(bytes); + } + + public readonly uint GetUInt32(int index) + { + ValidateIndex(index, 4); + + var bytes = _bytes.Slice(index, 4); + + return _isBigEndian + ? BinaryPrimitives.ReadUInt32BigEndian(bytes) + : BinaryPrimitives.ReadUInt32LittleEndian(bytes); + } + + public readonly float GetS15Fixed16(int index) + { + ValidateIndex(index, 4); + + ReadOnlySpan bytes = _bytes.Slice(index, 4); + + if (_isBigEndian) + { + float res = bytes[0] << 8 | bytes[1]; + var d = bytes[2] << 8 | bytes[3]; + return (float)(res + d / 65536.0); + } + else + { + // this particular branch is untested + var d = bytes[1] << 8 | bytes[0]; + float res = bytes[3] << 8 | bytes[2]; + return (float)(res + d / 65536.0); + } + } + + public readonly long GetInt64(int index) + { + ValidateIndex(index, 8); + + var bytes = _bytes.Slice(index, 8); + + return _isBigEndian + ? BinaryPrimitives.ReadInt64BigEndian(bytes) + : BinaryPrimitives.ReadInt64LittleEndian(bytes); + } + + /// + public readonly float GetFloat32(int index) + { +#if NET462 || NETSTANDARD1_3 + return BitConverter.ToSingle(BitConverter.GetBytes(GetInt32(index)), 0); +#else + Span bytes = stackalloc byte[4]; + + GetBytes(index, bytes); + +#if NET8_0_OR_GREATER + return _isBigEndian + ? BinaryPrimitives.ReadSingleBigEndian(bytes) + : BinaryPrimitives.ReadSingleLittleEndian(bytes); +#else + if (_isBigEndian) + { + bytes.Reverse(); + } + + return BitConverter.ToSingle(bytes); +#endif +#endif + } + + public readonly double GetDouble64(int index) + { +#if NET462 || NETSTANDARD1_3 + return BitConverter.Int64BitsToDouble(GetInt64(index)); +#else + Span bytes = stackalloc byte[8]; + + GetBytes(index, bytes); + +#if NET8_0_OR_GREATER + return _isBigEndian + ? BinaryPrimitives.ReadDoubleBigEndian(bytes) + : BinaryPrimitives.ReadDoubleLittleEndian(bytes); +#else + if (_isBigEndian) + { + bytes.Reverse(); + } + + return BitConverter.ToDouble(bytes); +#endif +#endif + } + + public readonly string GetString(int index, int bytesRequested, Encoding encoding) + { + // This check is important on .NET Framework + if (bytesRequested is 0) + { + return ""; + } + else if (bytesRequested < 256) + { + Span bytes = stackalloc byte[bytesRequested]; + + GetBytes(index, bytes); + + return encoding.GetString(bytes); + } + else + { + byte[] bytes = ArrayPool.Shared.Rent(bytesRequested); + + Span span = bytes.AsSpan().Slice(0, bytesRequested); + + GetBytes(index, span); + + var s = encoding.GetString(span); + + ArrayPool.Shared.Return(bytes); + + return s; + } + } + + private readonly void ValidateIndex(int index, int bytesRequested) + { + if (!IsValidIndex(index, bytesRequested)) + throw new BufferBoundsException(index, bytesRequested, _bytes.Length); + } + + private readonly bool IsValidIndex(int index, int bytesRequested) + { + return + bytesRequested >= 0 && + index >= 0 && + index + (long)bytesRequested - 1L < _bytes.Length; + } +} diff --git a/MetadataExtractor/IO/BufferReader.Sequential.cs b/MetadataExtractor/IO/BufferReader.Sequential.cs new file mode 100644 index 000000000..1231664c3 --- /dev/null +++ b/MetadataExtractor/IO/BufferReader.Sequential.cs @@ -0,0 +1,171 @@ +// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Buffers.Binary; + +namespace MetadataExtractor.IO; + +internal ref partial struct BufferReader +{ + public byte GetByte() + { + if (_position >= _bytes.Length) + throw new IOException("End of data reached."); + + return _bytes[_position++]; + } + + public void GetBytes(scoped Span bytes) + { + var buffer = Advance(bytes.Length); + buffer.CopyTo(bytes); + } + + public byte[] GetBytes(int count) + { + var buffer = Advance(count); + var bytes = new byte[count]; + + buffer.CopyTo(bytes); + return bytes; + } + + private ReadOnlySpan Advance(int count) + { + Debug.Assert(count >= 0, "count must be zero or greater"); + + if (_position + count > _bytes.Length) + throw new IOException("End of data reached."); + + var span = _bytes.Slice(_position, count); + + _position += count; + + return span; + } + + public void Skip(int count) + { + Debug.Assert(count >= 0, "count must be zero or greater"); + + if (_position + count > _bytes.Length) + throw new IOException("End of data reached."); + + _position += count; + } + + public sbyte GetSByte() + { + return unchecked((sbyte)_bytes[_position++]); + } + + public ushort GetUInt16() + { + var bytes = Advance(2); + + return _isBigEndian + ? BinaryPrimitives.ReadUInt16BigEndian(bytes) + : BinaryPrimitives.ReadUInt16LittleEndian(bytes); + } + + public short GetInt16() + { + var bytes = Advance(2); + + return _isBigEndian + ? BinaryPrimitives.ReadInt16BigEndian(bytes) + : BinaryPrimitives.ReadInt16LittleEndian(bytes); + } + + public uint GetUInt32() + { + var bytes = Advance(4); + + return _isBigEndian + ? BinaryPrimitives.ReadUInt32BigEndian(bytes) + : BinaryPrimitives.ReadUInt32LittleEndian(bytes); + } + + public int GetInt32() + { + var bytes = Advance(4); + + return _isBigEndian + ? BinaryPrimitives.ReadInt32BigEndian(bytes) + : BinaryPrimitives.ReadInt32LittleEndian(bytes); + } + + public long GetInt64() + { + var bytes = Advance(8); + + return _isBigEndian + ? BinaryPrimitives.ReadInt64BigEndian(bytes) + : BinaryPrimitives.ReadInt64LittleEndian(bytes); + } + + public ulong GetUInt64() + { + var bytes = Advance(8); + + return _isBigEndian + ? BinaryPrimitives.ReadUInt64BigEndian(bytes) + : BinaryPrimitives.ReadUInt64LittleEndian(bytes); + } + + public string GetString(int bytesRequested, Encoding encoding) + { + // This check is important on .NET Framework + if (bytesRequested is 0) + return ""; + + Span bytes = bytesRequested <= 256 + ? stackalloc byte[bytesRequested] + : new byte[bytesRequested]; + + GetBytes(bytes); + + return encoding.GetString(bytes); + } + + public StringValue GetNullTerminatedStringValue(int maxLengthBytes, Encoding? encoding = null, bool moveToMaxLength = false) + { + var bytes = GetNullTerminatedBytes(maxLengthBytes, moveToMaxLength); + + return new StringValue(bytes, encoding); + } + + public byte[] GetNullTerminatedBytes(int maxLengthBytes, bool moveToMaxLength = false) + { + // The number of non-null bytes + int length; + + byte[] buffer; + + if (moveToMaxLength) + { + buffer = GetBytes(maxLengthBytes); + length = Array.IndexOf(buffer, (byte)'\0') switch + { + -1 => maxLengthBytes, + int i => i + }; + } + else + { + buffer = new byte[maxLengthBytes]; + length = 0; + + while (length < buffer.Length && (buffer[length] = GetByte()) != 0) + length++; + } + + if (length == 0) + return []; + if (length == maxLengthBytes) + return buffer; + var bytes = new byte[length]; + if (length > 0) + Array.Copy(buffer, bytes, length); + return bytes; + } +} diff --git a/MetadataExtractor/IO/BufferReader.cs b/MetadataExtractor/IO/BufferReader.cs index d5143003d..658e0f616 100644 --- a/MetadataExtractor/IO/BufferReader.cs +++ b/MetadataExtractor/IO/BufferReader.cs @@ -1,138 +1,40 @@ // Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -using System.Buffers.Binary; - namespace MetadataExtractor.IO; -internal ref struct BufferReader(ReadOnlySpan bytes, bool isBigEndian) +/// +/// Stack-based reader for decoding values from byte spans. +/// Supports sequential and indexed access. +/// Supports little-endian and big-endian values. +/// +/// The byte buffer to decode from. +/// The byte ordering to use for multi-byte values. +internal ref partial struct BufferReader(ReadOnlySpan bytes, bool isBigEndian) { private readonly ReadOnlySpan _bytes = bytes; private readonly bool _isBigEndian = isBigEndian; private int _position = 0; + /// + /// Gets the number of bytes remaining in the buffer from the current + /// until the end of the buffer. + /// + /// + /// This value only makes sense when performing sequential access. + /// public readonly int Available => _bytes.Length - _position; + /// + /// Gets the current position in the buffer. The next value will be read from this position. + /// + /// + /// Only applies to sequential access. Indexed access does not update this value. + /// public readonly int Position => _position; - public byte GetByte() - { - if (_position >= _bytes.Length) - throw new IOException("End of data reached."); - - return _bytes[_position++]; - } - - public void GetBytes(scoped Span bytes) - { - var buffer = Advance(bytes.Length); - buffer.CopyTo(bytes); - } - - public byte[] GetBytes(int count) - { - var buffer = Advance(count); - var bytes = new byte[count]; - - buffer.CopyTo(bytes); - return bytes; - } - - private ReadOnlySpan Advance(int count) - { - Debug.Assert(count >= 0, "count must be zero or greater"); - - if (_position + count > _bytes.Length) - throw new IOException("End of data reached."); - - var span = _bytes.Slice(_position, count); - - _position += count; - - return span; - } - - public void Skip(int count) - { - Debug.Assert(count >= 0, "count must be zero or greater"); - - if (_position + count > _bytes.Length) - throw new IOException("End of data reached."); - - _position += count; - } - - public sbyte GetSByte() - { - return unchecked((sbyte)_bytes[_position++]); - } - - public ushort GetUInt16() - { - var bytes = Advance(2); - - return _isBigEndian - ? BinaryPrimitives.ReadUInt16BigEndian(bytes) - : BinaryPrimitives.ReadUInt16LittleEndian(bytes); - } - - public short GetInt16() - { - var bytes = Advance(2); - - return _isBigEndian - ? BinaryPrimitives.ReadInt16BigEndian(bytes) - : BinaryPrimitives.ReadInt16LittleEndian(bytes); - } - - public uint GetUInt32() - { - var bytes = Advance(4); - - return _isBigEndian - ? BinaryPrimitives.ReadUInt32BigEndian(bytes) - : BinaryPrimitives.ReadUInt32LittleEndian(bytes); - } - - public int GetInt32() - { - var bytes = Advance(4); - - return _isBigEndian - ? BinaryPrimitives.ReadInt32BigEndian(bytes) - : BinaryPrimitives.ReadInt32LittleEndian(bytes); - } - - public long GetInt64() - { - var bytes = Advance(8); - - return _isBigEndian - ? BinaryPrimitives.ReadInt64BigEndian(bytes) - : BinaryPrimitives.ReadInt64LittleEndian(bytes); - } - - public ulong GetUInt64() - { - var bytes = Advance(8); - - return _isBigEndian - ? BinaryPrimitives.ReadUInt64BigEndian(bytes) - : BinaryPrimitives.ReadUInt64LittleEndian(bytes); - } - - public string GetString(int bytesRequested, Encoding encoding) - { - // This check is important on .NET Framework - if (bytesRequested is 0) - return ""; - - Span bytes = bytesRequested <= 256 - ? stackalloc byte[bytesRequested] - : new byte[bytesRequested]; - - GetBytes(bytes); - - return encoding.GetString(bytes); - } + /// + /// Gets the byte ordering this reader uses for multi-byte values. + /// + public readonly bool IsBigEndian => _isBigEndian; }