From 2281ceaad7dff2b8d6ccebadc992c758c6b964c2 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Fri, 7 Oct 2016 22:54:58 +0100 Subject: [PATCH] Simplify TIFF processing by 'shifting' base of indexed reader, rather than passing TIFF header offsets around everywhere. --- .../IO/IndexedReaderTestBase.cs | 31 +++++++++ MetadataExtractor/Formats/Exif/ExifReader.cs | 6 +- .../Formats/Exif/ExifTiffHandler.cs | 63 ++++++++++--------- .../Formats/Tiff/DirectoryTiffHandler.cs | 4 +- .../Formats/Tiff/ITiffHandler.cs | 4 +- MetadataExtractor/Formats/Tiff/TiffReader.cs | 51 ++++++++------- MetadataExtractor/IO/ByteArrayReader.cs | 23 +++++-- .../IO/IndexedCapturingReader.cs | 41 ++++++++++-- MetadataExtractor/IO/IndexedReader.cs | 4 ++ MetadataExtractor/IO/IndexedSeekingReader.cs | 48 ++++++++------ 10 files changed, 183 insertions(+), 92 deletions(-) diff --git a/MetadataExtractor.Tests/IO/IndexedReaderTestBase.cs b/MetadataExtractor.Tests/IO/IndexedReaderTestBase.cs index 070c174a6..8720956a5 100644 --- a/MetadataExtractor.Tests/IO/IndexedReaderTestBase.cs +++ b/MetadataExtractor.Tests/IO/IndexedReaderTestBase.cs @@ -318,5 +318,36 @@ public void GetByteEof() reader.GetByte(0); Assert.Throws(() => reader.GetByte(1)); } + + [Fact] + public void WithShiftedBaseOffset() + { + var reader = CreateReader(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + reader.IsMotorolaByteOrder = false; + + Assert.Equal(10, reader.Length); + Assert.Equal(0, reader.GetByte(0)); + Assert.Equal(1, reader.GetByte(1)); + Assert.Equal(new byte[] { 0, 1 }, reader.GetBytes(0, 2)); + Assert.Equal(4, reader.ToUnshiftedOffset(4)); + + reader = reader.WithShiftedBaseOffset(2); + + Assert.False(reader.IsMotorolaByteOrder); + Assert.Equal(8, reader.Length); + Assert.Equal(2, reader.GetByte(0)); + Assert.Equal(3, reader.GetByte(1)); + Assert.Equal(new byte[] { 2, 3 }, reader.GetBytes(0, 2)); + Assert.Equal(6, reader.ToUnshiftedOffset(4)); + + reader = reader.WithShiftedBaseOffset(2); + + Assert.False(reader.IsMotorolaByteOrder); + Assert.Equal(6, reader.Length); + Assert.Equal(4, reader.GetByte(0)); + Assert.Equal(5, reader.GetByte(1)); + Assert.Equal(new byte[] { 4, 5 }, reader.GetBytes(0, 2)); + Assert.Equal(8, reader.ToUnshiftedOffset(4)); + } } } diff --git a/MetadataExtractor/Formats/Exif/ExifReader.cs b/MetadataExtractor/Formats/Exif/ExifReader.cs index d89f4654b..fb3902d16 100644 --- a/MetadataExtractor/Formats/Exif/ExifReader.cs +++ b/MetadataExtractor/Formats/Exif/ExifReader.cs @@ -59,7 +59,7 @@ public sealed class ExifReader : IJpegSegmentMetadataReader { return segments .Where(segment => segment.Bytes.Length >= JpegSegmentPreamble.Length && Encoding.UTF8.GetString(segment.Bytes, 0, JpegSegmentPreamble.Length) == JpegSegmentPreamble) - .SelectMany(segment => Extract(new ByteArrayReader(segment.Bytes), JpegSegmentPreamble.Length)) + .SelectMany(segment => Extract(new ByteArrayReader(segment.Bytes, baseOffset: JpegSegmentPreamble.Length))) .ToList(); } @@ -73,14 +73,14 @@ public sealed class ExifReader : IJpegSegmentMetadataReader #else IReadOnlyList #endif - Extract([NotNull] IndexedReader reader, int readerOffset = 0) + Extract([NotNull] IndexedReader reader) { var directories = new List(); try { // Read the TIFF-formatted Exif data - TiffReader.ProcessTiff(reader, new ExifTiffHandler(directories, StoreThumbnailBytes), readerOffset); + TiffReader.ProcessTiff(reader, new ExifTiffHandler(directories, StoreThumbnailBytes)); } catch (TiffProcessingException e) { diff --git a/MetadataExtractor/Formats/Exif/ExifTiffHandler.cs b/MetadataExtractor/Formats/Exif/ExifTiffHandler.cs index f7bfb011c..30f49909c 100644 --- a/MetadataExtractor/Formats/Exif/ExifTiffHandler.cs +++ b/MetadataExtractor/Formats/Exif/ExifTiffHandler.cs @@ -24,6 +24,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -143,13 +144,11 @@ public override bool HasFollowerIfd() return false; } - public override bool CustomProcessTag(int tagOffset, ICollection processedIfdOffsets, int tiffHeaderOffset, IndexedReader reader, int tagId, int byteCount) + public override bool CustomProcessTag(int tagOffset, ICollection processedIfdOffsets, IndexedReader reader, int tagId, int byteCount) { // Custom processing for the Makernote tag if (tagId == ExifDirectoryBase.TagMakernote && CurrentDirectory is ExifSubIfdDirectory) - { - return ProcessMakernote(tagOffset, processedIfdOffsets, tiffHeaderOffset, reader); - } + return ProcessMakernote(tagOffset, processedIfdOffsets, reader); // Custom processing for embedded IPTC data if (tagId == ExifDirectoryBase.TagIptcNaa && CurrentDirectory is ExifIfd0Directory) @@ -190,7 +189,7 @@ public override bool TryCustomProcessFormat(int tagId, TiffDataFormatCode format return false; } - public override void Completed(IndexedReader reader, int tiffHeaderOffset) + public override void Completed(IndexedReader reader) { if (_storeThumbnailBytes) { @@ -205,7 +204,7 @@ public override void Completed(IndexedReader reader, int tiffHeaderOffset) { try { - var thumbnailData = reader.GetBytes(tiffHeaderOffset + offset, length); + var thumbnailData = reader.GetBytes(/*tiffHeaderOffset +*/ offset, length); thumbnailDirectory.ThumbnailData = thumbnailData; } catch (IOException ex) @@ -218,8 +217,10 @@ public override void Completed(IndexedReader reader, int tiffHeaderOffset) } /// - private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection processedIfdOffsets, int tiffHeaderOffset, [NotNull] IndexedReader reader) + private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection processedIfdOffsets, [NotNull] IndexedReader reader) { + Debug.Assert(makernoteOffset >= 0, "makernoteOffset >= 0"); + var cameraMake = Directories.OfType().FirstOrDefault()?.GetString(ExifDirectoryBase.TagMake); var firstTwoChars = reader.GetString(makernoteOffset, 2, Encoding.UTF8); @@ -241,7 +242,7 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr // Olympus Makernote // Epson and Agfa use Olympus makernote standard: http://www.ozhiker.com/electronics/pjmt/jpeg_info/ PushDirectory(new OlympusMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 8); } else if (string.Equals("OLYMPUS\0II", firstTenChars, StringComparison.Ordinal)) { @@ -249,14 +250,14 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr // Note that data is relative to the beginning of the makernote // http://exiv2.org/makernote.html PushDirectory(new OlympusMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, makernoteOffset); + TiffReader.ProcessIfd(this, reader.WithShiftedBaseOffset(makernoteOffset), processedIfdOffsets, 12); } else if (cameraMake != null && cameraMake.StartsWith("MINOLTA", StringComparison.OrdinalIgnoreCase)) { // Cases seen with the model starting with MINOLTA in capitals seem to have a valid Olympus makernote // area that commences immediately. PushDirectory(new OlympusMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset); } else if (cameraMake != null && cameraMake.TrimStart().StartsWith("NIKON", StringComparison.OrdinalIgnoreCase)) { @@ -275,14 +276,14 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr * :0010: 00 08 00 1E 00 01 00 07-00 00 00 04 30 32 30 30 ............0200 */ PushDirectory(new NikonType1MakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 8); break; } case 2: { PushDirectory(new NikonType2MakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 18, makernoteOffset + 10); + TiffReader.ProcessIfd(this, reader.WithShiftedBaseOffset(makernoteOffset + 10), processedIfdOffsets, 8); break; } @@ -297,14 +298,14 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr { // The IFD begins with the first Makernote byte (no ASCII name). This occurs with CoolPix 775, E990 and D1 models. PushDirectory(new NikonType2MakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset); } } else if (string.Equals("SONY CAM", firstEightChars, StringComparison.Ordinal) || string.Equals("SONY DSC", firstEightChars, StringComparison.Ordinal)) { PushDirectory(new SonyType1MakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 12); } else if (string.Equals("SEMC MS\u0000\u0000\u0000\u0000\u0000", firstTwelveChars, StringComparison.Ordinal)) { @@ -312,13 +313,13 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr reader.IsMotorolaByteOrder = true; // skip 12 byte header + 2 for "MM" + 6 PushDirectory(new SonyType6MakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 20, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 20); } else if (string.Equals("SIGMA\u0000\u0000\u0000", firstEightChars, StringComparison.Ordinal) || string.Equals("FOVEON\u0000\u0000", firstEightChars, StringComparison.Ordinal)) { PushDirectory(new SigmaMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 10, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 10); } else if (string.Equals("KDK", firstThreeChars, StringComparison.Ordinal)) { @@ -330,19 +331,19 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr else if ("CANON".Equals(cameraMake, StringComparison.OrdinalIgnoreCase)) { PushDirectory(new CanonMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset); } else if (cameraMake != null && cameraMake.StartsWith("CASIO", StringComparison.OrdinalIgnoreCase)) { if (string.Equals("QVC\u0000\u0000\u0000", firstSixChars, StringComparison.Ordinal)) { PushDirectory(new CasioType2MakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 6, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 6); } else { PushDirectory(new CasioType1MakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset); } } else if (string.Equals("FUJIFILM", firstEightChars, StringComparison.Ordinal) || @@ -350,18 +351,20 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr { // Note that this also applies to certain Leica cameras, such as the Digilux-4.3 reader.IsMotorolaByteOrder = false; + // the 4 bytes after "FUJIFILM" in the makernote point to the start of the makernote // IFD, though the offset is relative to the start of the makernote, not the TIFF // header (like everywhere else) - var ifdStart = makernoteOffset + reader.GetInt32(makernoteOffset + 8); + var makernoteReader = reader.WithShiftedBaseOffset(makernoteOffset); + var ifdStart = makernoteReader.GetInt32(8); PushDirectory(new FujifilmMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, ifdStart, makernoteOffset); + TiffReader.ProcessIfd(this, reader.WithShiftedBaseOffset(makernoteOffset), processedIfdOffsets, ifdStart); } else if (string.Equals("KYOCERA", firstSevenChars, StringComparison.Ordinal)) { // http://www.ozhiker.com/electronics/pjmt/jpeg_info/kyocera_mn.html PushDirectory(new KyoceraMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 22, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 22); } else if (string.Equals("LEICA", firstFiveChars, StringComparison.Ordinal)) { @@ -369,13 +372,13 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr if (string.Equals("Leica Camera AG", cameraMake, StringComparison.Ordinal)) { PushDirectory(new LeicaMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 8); } else if (string.Equals("LEICA", cameraMake, StringComparison.Ordinal)) { // Some Leica cameras use Panasonic makernote tags PushDirectory(new PanasonicMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 8); } else { @@ -388,7 +391,7 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr // Offsets are relative to the start of the TIFF header at the beginning of the EXIF segment // more information here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html PushDirectory(new PanasonicMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, tiffHeaderOffset); + TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 12); } else if (string.Equals("AOC\u0000", firstFourChars, StringComparison.Ordinal)) { @@ -398,7 +401,7 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr // Observed for: // - Pentax ist D PushDirectory(new CasioType2MakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 6, makernoteOffset); + TiffReader.ProcessIfd(this, reader.WithShiftedBaseOffset(makernoteOffset), processedIfdOffsets, 6); } else if (cameraMake != null && (cameraMake.StartsWith("PENTAX", StringComparison.OrdinalIgnoreCase) || cameraMake.StartsWith("ASAHI", StringComparison.OrdinalIgnoreCase))) { @@ -409,7 +412,7 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr // - PENTAX Optio 330 // - PENTAX Optio 430 PushDirectory(new PentaxMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset, makernoteOffset); + TiffReader.ProcessIfd(this, reader.WithShiftedBaseOffset(makernoteOffset), processedIfdOffsets, 0); } // else if ("KC" == firstTwoChars || "MINOL" == firstFiveChars || "MLY" == firstThreeChars || "+M+M+M+M" == firstEightChars) // { @@ -421,7 +424,7 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr else if (string.Equals("SANYO\x0\x1\x0", firstEightChars, StringComparison.Ordinal)) { PushDirectory(new SanyoMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset); + TiffReader.ProcessIfd(this, reader.WithShiftedBaseOffset(makernoteOffset), processedIfdOffsets, 8); } else if (cameraMake != null && cameraMake.StartsWith("RICOH", StringComparison.OrdinalIgnoreCase)) { @@ -440,7 +443,7 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr // Always in Motorola byte order reader.IsMotorolaByteOrder = true; PushDirectory(new RicohMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset); + TiffReader.ProcessIfd(this, reader.WithShiftedBaseOffset(makernoteOffset), processedIfdOffsets, 8); } } else if (string.Equals(firstTenChars, "Apple iOS\0", StringComparison.Ordinal)) @@ -449,7 +452,7 @@ private bool ProcessMakernote(int makernoteOffset, [NotNull] ICollection pr var orderBefore = reader.IsMotorolaByteOrder; reader.IsMotorolaByteOrder = true; PushDirectory(new AppleMakernoteDirectory()); - TiffReader.ProcessIfd(this, reader, processedIfdOffsets, makernoteOffset + 14, makernoteOffset); + TiffReader.ProcessIfd(this, reader.WithShiftedBaseOffset(makernoteOffset), processedIfdOffsets, 14); reader.IsMotorolaByteOrder = orderBefore; } else if (string.Equals("RECONYX", cameraMake, StringComparison.OrdinalIgnoreCase) || diff --git a/MetadataExtractor/Formats/Tiff/DirectoryTiffHandler.cs b/MetadataExtractor/Formats/Tiff/DirectoryTiffHandler.cs index a877c22e2..0bd999523 100644 --- a/MetadataExtractor/Formats/Tiff/DirectoryTiffHandler.cs +++ b/MetadataExtractor/Formats/Tiff/DirectoryTiffHandler.cs @@ -83,9 +83,9 @@ protected void PushDirectory([NotNull] Directory directory) public void SetInt32U(int tagId, uint int32U) => CurrentDirectory.Set(tagId, int32U); public void SetInt32UArray(int tagId, uint[] array) => CurrentDirectory.Set(tagId, array); - public abstract void Completed(IndexedReader reader, int tiffHeaderOffset); + public abstract void Completed(IndexedReader reader); - public abstract bool CustomProcessTag(int tagOffset, ICollection processedIfdOffsets, int tiffHeaderOffset, IndexedReader reader, int tagId, int byteCount); + public abstract bool CustomProcessTag(int tagOffset, ICollection processedIfdOffsets, IndexedReader reader, int tagId, int byteCount); public abstract bool TryCustomProcessFormat(int tagId, TiffDataFormatCode formatCode, uint componentCount, out long byteCount); diff --git a/MetadataExtractor/Formats/Tiff/ITiffHandler.cs b/MetadataExtractor/Formats/Tiff/ITiffHandler.cs index 2c4ffd6b2..f0bdc0d7a 100644 --- a/MetadataExtractor/Formats/Tiff/ITiffHandler.cs +++ b/MetadataExtractor/Formats/Tiff/ITiffHandler.cs @@ -52,10 +52,10 @@ public interface ITiffHandler void EndingIfd(); - void Completed([NotNull] IndexedReader reader, int tiffHeaderOffset); + void Completed([NotNull] IndexedReader reader); /// - bool CustomProcessTag(int tagOffset, [NotNull] ICollection processedIfdOffsets, int tiffHeaderOffset, [NotNull] IndexedReader reader, int tagId, int byteCount); + bool CustomProcessTag(int tagOffset, [NotNull] ICollection processedIfdOffsets, [NotNull] IndexedReader reader, int tagId, int byteCount); bool TryCustomProcessFormat(int tagId, TiffDataFormatCode formatCode, uint componentCount, out long byteCount); diff --git a/MetadataExtractor/Formats/Tiff/TiffReader.cs b/MetadataExtractor/Formats/Tiff/TiffReader.cs index 95b4417a8..e7722c68b 100644 --- a/MetadataExtractor/Formats/Tiff/TiffReader.cs +++ b/MetadataExtractor/Formats/Tiff/TiffReader.cs @@ -37,14 +37,14 @@ public static class TiffReader /// Processes a TIFF data sequence. /// the from which the data should be read /// the that will coordinate processing and accept read values - /// the offset within reader at which the TIFF header starts /// if an error occurred during the processing of TIFF data that could not be ignored or recovered from /// an error occurred while accessing the required data /// - public static void ProcessTiff([NotNull] IndexedReader reader, [NotNull] ITiffHandler handler, int tiffHeaderOffset = 0) + public static void ProcessTiff([NotNull] IndexedReader reader, [NotNull] ITiffHandler handler) { // Read byte order. - switch (reader.GetInt16(tiffHeaderOffset)) + var byteOrder = reader.GetInt16(0); + switch (byteOrder) { case 0x4d4d: // MM reader.IsMotorolaByteOrder = true; @@ -53,14 +53,14 @@ public static void ProcessTiff([NotNull] IndexedReader reader, [NotNull] ITiffHa reader.IsMotorolaByteOrder = false; break; default: - throw new TiffProcessingException("Unclear distinction between Motorola/Intel byte ordering: " + reader.GetInt16(tiffHeaderOffset)); + throw new TiffProcessingException("Unclear distinction between Motorola/Intel byte ordering: " + reader.GetInt16(0)); } // Check the next two values for correctness. - int tiffMarker = reader.GetUInt16(2 + tiffHeaderOffset); + int tiffMarker = reader.GetUInt16(2); handler.SetTiffMarker(tiffMarker); - var firstIfdOffset = reader.GetInt32(4 + tiffHeaderOffset) + tiffHeaderOffset; + var firstIfdOffset = reader.GetInt32(4); // David Ekholm sent a digital camera image that has this problem // TODO calling Length should be avoided as it causes IndexedCapturingReader to read to the end of the stream @@ -68,14 +68,14 @@ public static void ProcessTiff([NotNull] IndexedReader reader, [NotNull] ITiffHa { handler.Warn("First IFD offset is beyond the end of the TIFF data segment -- trying default offset"); // First directory normally starts immediately after the offset bytes, so try that - firstIfdOffset = tiffHeaderOffset + 2 + 2 + 4; + firstIfdOffset = 2 + 2 + 4; } var processedIfdOffsets = new HashSet(); - ProcessIfd(handler, reader, processedIfdOffsets, firstIfdOffset, tiffHeaderOffset); + ProcessIfd(handler, reader, processedIfdOffsets, firstIfdOffset); - handler.Completed(reader, tiffHeaderOffset); + handler.Completed(reader); } /// Processes a TIFF IFD. @@ -94,21 +94,24 @@ public static void ProcessTiff([NotNull] IndexedReader reader, [NotNull] ITiffHa /// /// the that will coordinate processing and accept read values /// the from which the data should be read - /// the set of visited IFD offsets, to avoid revisiting the same IFD in an endless loop + /// the set of visited IFD offsets, to avoid revisiting the same IFD in an endless loop /// the offset within reader at which the IFD data starts - /// the offset within reader at which the TIFF header starts /// an error occurred while accessing the required data - public static void ProcessIfd([NotNull] ITiffHandler handler, [NotNull] IndexedReader reader, [NotNull] ICollection processedIfdOffsets, int ifdOffset, int tiffHeaderOffset) + public static void ProcessIfd([NotNull] ITiffHandler handler, [NotNull] IndexedReader reader, [NotNull] ICollection processedGlobalIfdOffsets, int ifdOffset) { bool? resetByteOrder = null; try { - // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist - if (processedIfdOffsets.Contains(ifdOffset)) + // Check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist. + // Note that we track these offsets in the global frame, not the reader's local frame. + var globalIfdOffset = reader.ToUnshiftedOffset(ifdOffset); + if (processedGlobalIfdOffsets.Contains(globalIfdOffset)) return; - // remember that we've visited this directory so that we don't visit it again later - processedIfdOffsets.Add(ifdOffset); + // Remember that we've visited this directory so that we don't visit it again later + processedGlobalIfdOffsets.Add(globalIfdOffset); + + // Validate IFD offset if (ifdOffset >= reader.Length || ifdOffset < 0) { handler.Error("Ignored IFD marked to start outside data segment"); @@ -181,14 +184,13 @@ public static void ProcessIfd([NotNull] ITiffHandler handler, [NotNull] IndexedR if (byteCount > 4) { // If it's bigger than 4 bytes, the dir entry contains an offset. - var offsetVal = reader.GetInt32(tagOffset + 8); - if (offsetVal + byteCount > reader.Length) + tagValueOffset = reader.GetUInt32(tagOffset + 8); + if (tagValueOffset + byteCount > reader.Length) { // Bogus pointer offset and / or byteCount value handler.Error("Illegal TIFF tag pointer offset"); continue; } - tagValueOffset = tiffHeaderOffset + offsetVal; } else { @@ -219,14 +221,14 @@ public static void ProcessIfd([NotNull] ITiffHandler handler, [NotNull] IndexedR if (handler.TryEnterSubIfd(tagId)) { isIfdPointer = true; - var subDirOffset = tiffHeaderOffset + reader.GetUInt32((int)(tagValueOffset + i*4)); - ProcessIfd(handler, reader, processedIfdOffsets, (int)subDirOffset, tiffHeaderOffset); + var subDirOffset = reader.GetUInt32((int)(tagValueOffset + i*4)); + ProcessIfd(handler, reader, processedGlobalIfdOffsets, (int)subDirOffset); } } } // If it wasn't an IFD pointer, allow custom tag processing to occur - if (!isIfdPointer && !handler.CustomProcessTag((int)tagValueOffset, processedIfdOffsets, tiffHeaderOffset, reader, tagId, (int)byteCount)) + if (!isIfdPointer && !handler.CustomProcessTag((int)tagValueOffset, processedGlobalIfdOffsets, reader, tagId, (int)byteCount)) { // If no custom processing occurred, process the tag in the standard fashion ProcessTag(handler, tagId, (int)tagValueOffset, (int)componentCount, formatCode, reader); @@ -238,12 +240,9 @@ public static void ProcessIfd([NotNull] ITiffHandler handler, [NotNull] IndexedR var nextIfdOffset = reader.GetInt32(finalTagOffset); if (nextIfdOffset != 0) { - nextIfdOffset += tiffHeaderOffset; - if (nextIfdOffset >= reader.Length) { // Last 4 bytes of IFD reference another IFD with an address that is out of bounds - // Note this could have been caused by jhead 1.3 cropping too much return; } else if (nextIfdOffset < ifdOffset) @@ -254,7 +253,7 @@ public static void ProcessIfd([NotNull] ITiffHandler handler, [NotNull] IndexedR } if (handler.HasFollowerIfd()) - ProcessIfd(handler, reader, processedIfdOffsets, nextIfdOffset, tiffHeaderOffset); + ProcessIfd(handler, reader, processedGlobalIfdOffsets, nextIfdOffset); } } finally diff --git a/MetadataExtractor/IO/ByteArrayReader.cs b/MetadataExtractor/IO/ByteArrayReader.cs index f6347402a..a7e759c1a 100644 --- a/MetadataExtractor/IO/ByteArrayReader.cs +++ b/MetadataExtractor/IO/ByteArrayReader.cs @@ -39,32 +39,43 @@ public class ByteArrayReader : IndexedReader { [NotNull] private readonly byte[] _buffer; + private readonly int _baseOffset; - public ByteArrayReader([NotNull] byte[] buffer) + public ByteArrayReader([NotNull] byte[] buffer, int baseOffset = 0) { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); + if (baseOffset < 0) + throw new ArgumentOutOfRangeException(nameof(baseOffset), "Must be zero or greater."); _buffer = buffer; + _baseOffset = baseOffset; } - public override long Length => _buffer.Length; + public override IndexedReader WithShiftedBaseOffset(int shift) => shift == 0 ? this : new ByteArrayReader(_buffer, _baseOffset + shift) { IsMotorolaByteOrder = IsMotorolaByteOrder }; + + public override int ToUnshiftedOffset(int localOffset) => localOffset + _baseOffset; + + public override long Length => _buffer.Length - _baseOffset; public override byte GetByte(int index) { ValidateIndex(index, 1); - return _buffer[index]; + return _buffer[index + _baseOffset]; } protected override void ValidateIndex(int index, int bytesRequested) { if (!IsValidIndex(index, bytesRequested)) - throw new BufferBoundsException(index, bytesRequested, _buffer.Length); + throw new BufferBoundsException(ToUnshiftedOffset(index), bytesRequested, _buffer.Length); } protected override bool IsValidIndex(int index, int bytesRequested) { - return bytesRequested >= 0 && index >= 0 && index + (long)bytesRequested - 1L < _buffer.Length; + return + bytesRequested >= 0 && + index >= 0 && + index + (long)bytesRequested - 1L < Length; } public override byte[] GetBytes(int index, int count) @@ -72,7 +83,7 @@ public override byte[] GetBytes(int index, int count) ValidateIndex(index, count); var bytes = new byte[count]; - Array.Copy(_buffer, index, bytes, 0, count); + Array.Copy(_buffer, index + _baseOffset, bytes, 0, count); return bytes; } } diff --git a/MetadataExtractor/IO/IndexedCapturingReader.cs b/MetadataExtractor/IO/IndexedCapturingReader.cs index ea7d0d5bf..841854ee3 100644 --- a/MetadataExtractor/IO/IndexedCapturingReader.cs +++ b/MetadataExtractor/IO/IndexedCapturingReader.cs @@ -78,15 +78,15 @@ protected override void ValidateIndex(int index, int bytesRequested) if (!IsValidIndex(index, bytesRequested)) { if (index < 0) - throw new BufferBoundsException($"Attempt to read from buffer using a negative index ({index})."); + throw new BufferBoundsException($"Attempt to read from buffer using a negative index ({index})"); if (bytesRequested < 0) - throw new BufferBoundsException("Number of requested bytes must be zero or greater."); + throw new BufferBoundsException("Number of requested bytes must be zero or greater"); if ((long)index + bytesRequested - 1 > int.MaxValue) - throw new BufferBoundsException($"Number of requested bytes summed with starting index exceed maximum range of signed 32 bit integers (requested index: {index}, requested count: {bytesRequested})."); + throw new BufferBoundsException($"Number of requested bytes summed with starting index exceed maximum range of signed 32 bit integers (requested index: {index}, requested count: {bytesRequested})"); Debug.Assert(_isStreamFinished); // TODO test that can continue using an instance of this type after this exception - throw new BufferBoundsException(index, bytesRequested, _streamLength); + throw new BufferBoundsException(ToUnshiftedOffset(index), bytesRequested, _streamLength); } } @@ -140,6 +140,8 @@ protected override bool IsValidIndex(int index, int bytesRequested) return true; } + public override int ToUnshiftedOffset(int localOffset) => localOffset; + public override byte GetByte(int index) { ValidateIndex(index, 1); @@ -171,5 +173,36 @@ public override byte[] GetBytes(int index, int count) } return bytes; } + + public override IndexedReader WithShiftedBaseOffset(int shift) => shift == 0 ? (IndexedReader)this : new ShiftedIndexedCapturingReader(this, shift) { IsMotorolaByteOrder = IsMotorolaByteOrder }; + + private sealed class ShiftedIndexedCapturingReader : IndexedReader + { + private readonly IndexedCapturingReader _baseReader; + private readonly int _baseOffset; + + public ShiftedIndexedCapturingReader(IndexedCapturingReader baseReader, int baseOffset) + { + if (baseOffset < 0) + throw new ArgumentOutOfRangeException(nameof(baseOffset), "Must be zero or greater."); + + _baseReader = baseReader; + _baseOffset = baseOffset; + } + + public override IndexedReader WithShiftedBaseOffset(int shift) => shift == 0 ? this : new ShiftedIndexedCapturingReader(_baseReader, _baseOffset + shift) { IsMotorolaByteOrder = IsMotorolaByteOrder }; + + public override int ToUnshiftedOffset(int localOffset) => localOffset + _baseOffset; + + public override byte GetByte(int index) => _baseReader.GetByte(_baseOffset + index); + + public override byte[] GetBytes(int index, int count) => _baseReader.GetBytes(_baseOffset + index, count); + + protected override void ValidateIndex(int index, int bytesRequested) => _baseReader.ValidateIndex(index + _baseOffset, bytesRequested); + + protected override bool IsValidIndex(int index, int bytesRequested) => _baseReader.IsValidIndex(index + _baseOffset, bytesRequested); + + public override long Length => _baseReader.Length - _baseOffset; + } } } diff --git a/MetadataExtractor/IO/IndexedReader.cs b/MetadataExtractor/IO/IndexedReader.cs index 3b49407c3..d6d5b9272 100644 --- a/MetadataExtractor/IO/IndexedReader.cs +++ b/MetadataExtractor/IO/IndexedReader.cs @@ -53,6 +53,10 @@ public abstract class IndexedReader /// public bool IsMotorolaByteOrder { set; get; } = true; + public abstract IndexedReader WithShiftedBaseOffset(int shift); + + public abstract int ToUnshiftedOffset(int localOffset); + /// Gets the byte value at the specified byte index. /// /// Implementations must validate by calling . diff --git a/MetadataExtractor/IO/IndexedSeekingReader.cs b/MetadataExtractor/IO/IndexedSeekingReader.cs index 61c0c5a16..c397c9b23 100644 --- a/MetadataExtractor/IO/IndexedSeekingReader.cs +++ b/MetadataExtractor/IO/IndexedSeekingReader.cs @@ -23,7 +23,6 @@ #endregion using System; -using System.Diagnostics; using System.IO; using JetBrains.Annotations; @@ -38,47 +37,58 @@ public class IndexedSeekingReader : IndexedReader [NotNull] private readonly Stream _stream; - private int _currentIndex; + private readonly int _baseOffset; - public IndexedSeekingReader([NotNull] Stream stream) + public IndexedSeekingReader([NotNull] Stream stream, int baseOffset = 0) { if (stream == null) throw new ArgumentNullException(nameof(stream)); if (!stream.CanSeek) throw new ArgumentException("Must be capable of seeking.", nameof(stream)); + if (baseOffset < 0) + throw new ArgumentOutOfRangeException(nameof(baseOffset), "Must be zero or greater."); + + var actualLength = stream.Length; + var availableLength = actualLength - baseOffset; + + if (availableLength < 0) + throw new ArgumentOutOfRangeException(nameof(baseOffset), "Cannot be greater than the stream's length."); _stream = stream; - Length = _stream.Length; + _baseOffset = baseOffset; + Length = availableLength; } + public override IndexedReader WithShiftedBaseOffset(int shift) => shift == 0 ? this : new IndexedSeekingReader(_stream, _baseOffset + shift) { IsMotorolaByteOrder = IsMotorolaByteOrder }; + + public override int ToUnshiftedOffset(int localOffset) => localOffset + _baseOffset; + public override long Length { get; } - /// public override byte GetByte(int index) { ValidateIndex(index, 1); - if (index != _currentIndex) + + if (index + _baseOffset != _stream.Position) Seek(index); var b = _stream.ReadByte(); + if (b < 0) throw new BufferBoundsException("Unexpected end of file encountered."); - Debug.Assert(b <= 0xff); - _currentIndex++; return unchecked((byte)b); } - /// public override byte[] GetBytes(int index, int count) { ValidateIndex(index, count); - if (index != _currentIndex) + + if (index + _baseOffset != _stream.Position) Seek(index); var bytes = new byte[count]; var bytesRead = _stream.Read(bytes, 0, count); - _currentIndex += bytesRead; if (bytesRead != count) throw new BufferBoundsException("Unexpected end of file encountered."); @@ -86,27 +96,27 @@ public override byte[] GetBytes(int index, int count) return bytes; } - /// private void Seek(int index) { - if (index == _currentIndex) + var streamIndex = index + _baseOffset; + if (streamIndex == _stream.Position) return; - _stream.Seek(index, SeekOrigin.Begin); - _currentIndex = index; + _stream.Seek(streamIndex, SeekOrigin.Begin); } - /// protected override bool IsValidIndex(int index, int bytesRequested) { - return bytesRequested >= 0 && index >= 0 && index + (long)bytesRequested - 1L < Length; + return + bytesRequested >= 0 && + index >= 0 && + index + (long)bytesRequested - 1L < Length; } - /// protected override void ValidateIndex(int index, int bytesRequested) { if (!IsValidIndex(index, bytesRequested)) - throw new BufferBoundsException(index, bytesRequested, Length); + throw new BufferBoundsException(ToUnshiftedOffset(index), bytesRequested, _stream.Length); } } }