diff --git a/MetadataExtractor/Formats/Apple/BplistReader.cs b/MetadataExtractor/Formats/Apple/BplistReader.cs index 6f13eb3cb..e38221228 100644 --- a/MetadataExtractor/Formats/Apple/BplistReader.cs +++ b/MetadataExtractor/Formats/Apple/BplistReader.cs @@ -1,5 +1,7 @@ // 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; + namespace MetadataExtractor.Formats.Apple; /// @@ -120,7 +122,7 @@ object HandleInt(byte marker) Dictionary HandleDict(byte count) { - var keyRefs = new byte[count]; + var keyRefs = ArrayPool.Shared.Rent(count); for (int j = 0; j < count; j++) { @@ -134,6 +136,8 @@ Dictionary HandleDict(byte count) map.Add(keyRefs[j], reader.GetByte()); } + ArrayPool.Shared.Return(keyRefs); + return map; } diff --git a/MetadataExtractor/Formats/Flir/FlirReader.cs b/MetadataExtractor/Formats/Flir/FlirReader.cs index 529590953..bcd961461 100644 --- a/MetadataExtractor/Formats/Flir/FlirReader.cs +++ b/MetadataExtractor/Formats/Flir/FlirReader.cs @@ -1,5 +1,6 @@ // 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 MetadataExtractor.Formats.Jpeg; using static MetadataExtractor.Formats.Flir.FlirCameraInfoDirectory; @@ -35,26 +36,35 @@ public IEnumerable ReadJpegSegments(IEnumerable segments if (length == 0) return []; - var buffer = new byte[length]; - using var merged = new MemoryStream(buffer); + byte[] buffer = ArrayPool.Shared.Rent(length); - foreach (var segment in segments) + try { - // Skip segments not starting with the required preamble - if (segment.Span.StartsWith(preamble)) + using var merged = new MemoryStream(buffer); + + foreach (var segment in segments) { - merged.Write(segment.Bytes, preambleLength, segment.Bytes.Length - preambleLength); + // Skip segments not starting with the required preamble + if (segment.Span.StartsWith(preamble)) + { + merged.Write(segment.Bytes, preambleLength, segment.Bytes.Length - preambleLength); + } } - } - return Extract(new ByteArrayReader(buffer)); + return Extract(new ByteArrayReader(buffer)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } } public IEnumerable Extract(IndexedReader reader) { - var header = reader.GetUInt32(0); + Span header = stackalloc byte[4]; + reader.GetBytes(0, header); - if (header != 0x46464600) + if (!header.SequenceEqual("FFF\0"u8)) { var flirHeaderDirectory = new FlirHeaderDirectory(); flirHeaderDirectory.AddError("Unexpected FFF header bytes."); diff --git a/MetadataExtractor/Formats/Gif/GifReader.cs b/MetadataExtractor/Formats/Gif/GifReader.cs index 3b354afad..bd4818924 100644 --- a/MetadataExtractor/Formats/Gif/GifReader.cs +++ b/MetadataExtractor/Formats/Gif/GifReader.cs @@ -1,5 +1,6 @@ // 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 MetadataExtractor.Formats.Icc; using MetadataExtractor.Formats.Xmp; @@ -234,45 +235,42 @@ private static GifCommentDirectory ReadCommentBlock(SequentialReader reader, byt if (blockSizeBytes != 11) return new ErrorDirectory($"Invalid GIF application extension block size. Expected 11, got {blockSizeBytes}."); - var extensionType = reader.GetString(blockSizeBytes, Encoding.UTF8); + Span extensionType = stackalloc byte[11]; - switch (extensionType) + reader.GetBytes(extensionType); + + if (extensionType.SequenceEqual("XMP DataXMP"u8)) { - case "XMP DataXMP": - { - // XMP data extension - var xmpBytes = GatherBytes(reader); - int xmpLength = xmpBytes.Length - 257; // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF - // Only extract valid blocks - return xmpLength > 0 - ? new XmpReader().Extract(xmpBytes, 0, xmpBytes.Length - 257) - : null; - } - case "ICCRGBG1012": - { - // ICC profile extension - var iccBytes = GatherBytes(reader, reader.GetByte()); - return iccBytes.Length != 0 - ? new IccReader().Extract(new ByteArrayReader(iccBytes)) - : null; - } - case "NETSCAPE2.0": - { - reader.Skip(2); - // Netscape's animated GIF extension - // Iteration count (0 means infinite) - var iterationCount = reader.GetUInt16(); - // Skip terminator - reader.Skip(1); - var animationDirectory = new GifAnimationDirectory(); - animationDirectory.Set(GifAnimationDirectory.TagIterationCount, iterationCount); - return animationDirectory; - } - default: - { - SkipBlocks(reader); - return null; - } + // XMP data extension + var xmpBytes = GatherXmpBytes(reader); + return xmpBytes is not null + ? new XmpReader().Extract(xmpBytes) + : null; + } + else if (extensionType.SequenceEqual("ICCRGBG1012"u8)) + { + // ICC profile extension + var iccBytes = GatherBytes(reader, reader.GetByte()); + return iccBytes.Length != 0 + ? new IccReader().Extract(new ByteArrayReader(iccBytes)) + : null; + } + else if (extensionType.SequenceEqual("NETSCAPE2.0"u8)) + { + reader.Skip(2); + // Netscape's animated GIF extension + // Iteration count (0 means infinite) + var iterationCount = reader.GetUInt16(); + // Skip terminator + reader.Skip(1); + var animationDirectory = new GifAnimationDirectory(); + animationDirectory.Set(GifAnimationDirectory.TagIterationCount, iterationCount); + return animationDirectory; + } + else + { + SkipBlocks(reader); + return null; } } @@ -329,36 +327,58 @@ private static GifImageDirectory ReadImageBlock(SequentialReader reader) #region Utility methods - private static byte[] GatherBytes(SequentialReader reader) + private static byte[]? GatherXmpBytes(SequentialReader reader) { - var bytes = new MemoryStream(); - var buffer = new byte[257]; + // GatherXmpBytes differs from GatherBytes in that this method includes the "length" + // bytes in its output. + + var stream = new MemoryStream(); + var buffer = ArrayPool.Shared.Rent(byte.MaxValue + 1); while (true) { - var b = reader.GetByte(); - if (b == 0) - return bytes.ToArray(); - buffer[0] = b; - reader.GetBytes(buffer, 1, b); - bytes.Write(buffer, 0, b + 1); + var len = reader.GetByte(); + if (len == 0) + break; + buffer[0] = len; + reader.GetBytes(buffer, offset: 1, count: len); + stream.Write(buffer, 0, len + 1); } + + ArrayPool.Shared.Return(buffer); + + // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF + int xmpLength = checked((int)stream.Length) - 257; + + if (xmpLength <= 0) + { + return null; + } + + stream.SetLength(xmpLength); + + return stream.ToArray(); } - private static byte[] GatherBytes(SequentialReader reader, int firstLength) + private static byte[] GatherBytes(SequentialReader reader, byte firstLength) { - var buffer = new MemoryStream(); + var stream = new MemoryStream(); + var buffer = ArrayPool.Shared.Rent(byte.MaxValue); var length = firstLength; while (length > 0) { - buffer.Write(reader.GetBytes(length), 0, length); + reader.GetBytes(buffer.AsSpan().Slice(0, length)); + + stream.Write(buffer, 0, length); length = reader.GetByte(); } - return buffer.ToArray(); + ArrayPool.Shared.Return(buffer); + + return stream.ToArray(); } private static void SkipBlocks(SequentialReader reader) diff --git a/MetadataExtractor/Formats/Icc/IccReader.cs b/MetadataExtractor/Formats/Icc/IccReader.cs index 5632a326e..7757b2a40 100644 --- a/MetadataExtractor/Formats/Icc/IccReader.cs +++ b/MetadataExtractor/Formats/Icc/IccReader.cs @@ -1,5 +1,6 @@ // 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 MetadataExtractor.Formats.Jpeg; namespace MetadataExtractor.Formats.Icc @@ -37,14 +38,14 @@ public IEnumerable ReadJpegSegments(IEnumerable segments byte[] buffer; if (iccSegments.Count == 1) { - buffer = new byte[iccSegments[0].Bytes.Length - JpegSegmentPreambleLength]; + buffer = ArrayPool.Shared.Rent(iccSegments[0].Bytes.Length - JpegSegmentPreambleLength); Array.Copy(iccSegments[0].Bytes, JpegSegmentPreambleLength, buffer, 0, iccSegments[0].Bytes.Length - JpegSegmentPreambleLength); } else { // Concatenate all buffers var totalLength = iccSegments.Sum(s => s.Bytes.Length - JpegSegmentPreambleLength); - buffer = new byte[totalLength]; + buffer = ArrayPool.Shared.Rent(totalLength); for (int i = 0, pos = 0; i < iccSegments.Count; i++) { var segment = iccSegments[i]; @@ -53,7 +54,11 @@ public IEnumerable ReadJpegSegments(IEnumerable segments } } - return new Directory[] { Extract(new ByteArrayReader(buffer)) }; + Directory directory = Extract(new ByteArrayReader(buffer)); + + ArrayPool.Shared.Return(buffer); + + return [directory]; } public IccDirectory Extract(IndexedReader reader) diff --git a/MetadataExtractor/Formats/Raf/RafMetadataReader.cs b/MetadataExtractor/Formats/Raf/RafMetadataReader.cs index 6d1e111af..9e2aa631f 100644 --- a/MetadataExtractor/Formats/Raf/RafMetadataReader.cs +++ b/MetadataExtractor/Formats/Raf/RafMetadataReader.cs @@ -1,5 +1,6 @@ // 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 MetadataExtractor.Formats.Jpeg; namespace MetadataExtractor.Formats.Raf @@ -14,7 +15,8 @@ public static IReadOnlyList ReadMetadata(Stream stream) if (!stream.CanSeek) throw new ArgumentException("Must support seek", nameof(stream)); - var data = new byte[512]; + var data = ArrayPool.Shared.Rent(512); + var bytesRead = stream.Read(data, 0, 512); if (bytesRead == 0) @@ -32,6 +34,8 @@ public static IReadOnlyList ReadMetadata(Stream stream) } } + ArrayPool.Shared.Return(data); + return JpegMetadataReader.ReadMetadata(stream); } } diff --git a/MetadataExtractor/Formats/Tga/TgaExtensionReader.cs b/MetadataExtractor/Formats/Tga/TgaExtensionReader.cs index 1bae3faae..9e07b6b9d 100644 --- a/MetadataExtractor/Formats/Tga/TgaExtensionReader.cs +++ b/MetadataExtractor/Formats/Tga/TgaExtensionReader.cs @@ -1,5 +1,7 @@ // 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; + namespace MetadataExtractor.Formats.Tga { /// Reads TGA image file extension area. @@ -55,12 +57,7 @@ protected override void Populate(Stream stream, int offset, TgaExtensionDirector string GetString(int length) { - var buffer = new byte[length]; - reader.GetBytes(buffer, 0, length); - int i = 0; - while (i < buffer.Length && buffer[i] != '\0') - ++i; - return Encoding.ASCII.GetString(buffer, 0, i).TrimEnd(); + return reader.GetNullTerminatedString(length, Encoding.ASCII, moveToMaxLength: true).TrimEnd(); } bool TryGetDateTime(out DateTime dateTime) diff --git a/MetadataExtractor/IO/IndexedReader.cs b/MetadataExtractor/IO/IndexedReader.cs index 1ad977d2f..29450c1ac 100644 --- a/MetadataExtractor/IO/IndexedReader.cs +++ b/MetadataExtractor/IO/IndexedReader.cs @@ -1,6 +1,7 @@ // 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 @@ -321,13 +322,31 @@ public 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]; - Span bytes = bytesRequested < 256 ? stackalloc byte[bytesRequested] : new byte[bytesRequested]; + GetBytes(index, bytes); - GetBytes(index, bytes); + return encoding.GetString(bytes); + } + else + { + byte[] bytes = ArrayPool.Shared.Rent(bytesRequested); + + Span span = bytes.AsSpan().Slice(0, bytesRequested); + + GetBytes(index, span); - return encoding.GetString(bytes); + var s = encoding.GetString(span); + + ArrayPool.Shared.Return(bytes); + + return s; + } } /// diff --git a/MetadataExtractor/IO/SequentialReader.cs b/MetadataExtractor/IO/SequentialReader.cs index d16888206..1098a2b08 100644 --- a/MetadataExtractor/IO/SequentialReader.cs +++ b/MetadataExtractor/IO/SequentialReader.cs @@ -1,5 +1,6 @@ // 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 @@ -222,13 +223,31 @@ public string GetString(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]; - Span bytes = bytesRequested < 256 ? stackalloc byte[bytesRequested] : new byte[bytesRequested]; + GetBytes(bytes); - GetBytes(bytes); + return encoding.GetString(bytes); + } + else + { + byte[] bytes = ArrayPool.Shared.Rent(bytesRequested); + + Span span = bytes.AsSpan().Slice(0, bytesRequested); + + GetBytes(span); - return encoding.GetString(bytes); + var s = encoding.GetString(span); + + ArrayPool.Shared.Return(bytes); + + return s; + } } public StringValue GetStringValue(int bytesRequested, Encoding? encoding = null) diff --git a/MetadataExtractor/Util/FileTypeDetector.cs b/MetadataExtractor/Util/FileTypeDetector.cs index 870dc6bc9..fc76042e1 100644 --- a/MetadataExtractor/Util/FileTypeDetector.cs +++ b/MetadataExtractor/Util/FileTypeDetector.cs @@ -1,5 +1,6 @@ // 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 MetadataExtractor.Formats.Mpeg; using MetadataExtractor.Formats.QuickTime; using MetadataExtractor.Formats.Riff; @@ -78,31 +79,39 @@ public static FileType DetectFileType(Stream stream) if (!stream.CanSeek) throw new ArgumentException("Must support seek", nameof(stream)); - var bytes = new byte[_bytesNeeded]; - var bytesRead = stream.Read(bytes, 0, bytes.Length); + var bytes = ArrayPool.Shared.Rent(_bytesNeeded); - if (bytesRead == 0) - return FileType.Unknown; + try + { + var bytesRead = stream.Read(bytes, 0, bytes.Length); - stream.Seek(-bytesRead, SeekOrigin.Current); + if (bytesRead == 0) + return FileType.Unknown; - var fileType = _root.Find(bytes); + stream.Seek(-bytesRead, SeekOrigin.Current); - if (fileType == FileType.Unknown) - { - foreach (var fixedChecker in _fixedCheckers) + var fileType = _root.Find(bytes); + + if (fileType == FileType.Unknown) { - if (bytesRead >= fixedChecker.ByteCount) + foreach (var fixedChecker in _fixedCheckers) { - fileType = fixedChecker.CheckType(bytes); + if (bytesRead >= fixedChecker.ByteCount) + { + fileType = fixedChecker.CheckType(bytes); - if (fileType != FileType.Unknown) - return fileType; + if (fileType != FileType.Unknown) + return fileType; + } } } - } - return fileType; + return fileType; + } + finally + { + ArrayPool.Shared.Return(bytes); + } } } }