From d773963d7408f3c234743ee6387e27a0c7dc5849 Mon Sep 17 00:00:00 2001 From: Andrew Wilkinson Date: Sun, 21 May 2017 21:54:28 +0100 Subject: [PATCH] Add support for Deflate (and OldDeflate) compression --- .../Compression/DeflateTiffCompression.cs | 60 ++++ .../Tiff/Compression/TiffCompressionType.cs | 5 + .../Formats/Tiff/TiffDecoderCore.cs | 10 + .../Formats/Tiff/Utils/SubStream.cs | 177 ++++++++++ .../Formats/Tiff/Utils/TiffUtils.cs | 10 + .../DeflateTiffCompressionTests.cs | 48 +++ .../Formats/Tiff/TiffDecoderImageTests.cs | 8 +- .../Formats/Tiff/Utils/SubStreamTests.cs | 327 ++++++++++++++++++ 8 files changed, 641 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/Compression/DeflateTiffCompression.cs create mode 100644 src/ImageSharp/Formats/Tiff/Utils/SubStream.cs create mode 100644 tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs create mode 100644 tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Utils/SubStreamTests.cs diff --git a/src/ImageSharp/Formats/Tiff/Compression/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/DeflateTiffCompression.cs new file mode 100644 index 0000000000..1af8362a02 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/DeflateTiffCompression.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Formats.Tiff +{ + using System; + using System.Buffers; + using System.IO; + using System.IO.Compression; + using System.Runtime.CompilerServices; + + /// + /// Class to handle cases where TIFF image data is compressed using Deflate compression. + /// + /// + /// Note that the 'OldDeflate' compression type is identical to the 'Deflate' compression type. + /// + internal static class DeflateTiffCompression + { + /// + /// Decompresses image data into the supplied buffer. + /// + /// The to read image data from. + /// The number of bytes to read from the input stream. + /// The output buffer for uncompressed data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Decompress(Stream stream, int byteCount, byte[] buffer) + { + // Read the 'zlib' header information + int cmf = stream.ReadByte(); + int flag = stream.ReadByte(); + + if ((cmf & 0x0f) != 8) + { + throw new Exception($"Bad compression method for ZLIB header: cmf={cmf}"); + } + + // If the 'fdict' flag is set then we should skip the next four bytes + bool fdict = (flag & 32) != 0; + + if (fdict) + { + stream.ReadByte(); + stream.ReadByte(); + stream.ReadByte(); + stream.ReadByte(); + } + + // The subsequent data is the Deflate compressed data (except for the last four bytes of checksum) + int headerLength = fdict ? 10 : 6; + SubStream subStream = new SubStream(stream, byteCount - headerLength); + using (DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress, true)) + { + deflateStream.ReadFull(buffer); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs index 6f9ce8f870..6108194c41 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs @@ -19,5 +19,10 @@ namespace ImageSharp.Formats.Tiff /// Image data is compressed using PackBits compression. /// PackBits = 1, + + /// + /// Image data is compressed using Deflate compression. + /// + Deflate = 2, } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 7419f17590..e7c98cad7d 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -268,6 +268,13 @@ namespace ImageSharp.Formats break; } + case TiffCompression.Deflate: + case TiffCompression.OldDeflate: + { + this.CompressionType = TiffCompressionType.Deflate; + break; + } + default: { throw new NotSupportedException("The specified TIFF compression format is not supported."); @@ -482,6 +489,9 @@ namespace ImageSharp.Formats case TiffCompressionType.PackBits: PackBitsTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer); break; + case TiffCompressionType.Deflate: + DeflateTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer); + break; default: throw new InvalidOperationException(); } diff --git a/src/ImageSharp/Formats/Tiff/Utils/SubStream.cs b/src/ImageSharp/Formats/Tiff/Utils/SubStream.cs new file mode 100644 index 0000000000..3bab41edb1 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Utils/SubStream.cs @@ -0,0 +1,177 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// +namespace ImageSharp.Formats.Tiff +{ + using System; + using System.IO; + + /// + /// Utility class to encapsulate a sub-portion of another . + /// + /// + /// Note that disposing of the does not dispose the underlying + /// . + /// + internal class SubStream : Stream + { + private Stream innerStream; + private long offset; + private long endOffset; + private long length; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying to wrap. + /// The length of the sub-stream. + /// + /// Note that calling the sub-stream with start from the current offset of the + /// underlying + /// + public SubStream(Stream innerStream, long length) + { + this.innerStream = innerStream; + this.offset = this.innerStream.Position; + this.endOffset = this.offset + length; + this.length = length; + } + + /// + /// Initializes a new instance of the class. + /// + /// The underlying to wrap. + /// The offset of the sub-stream within the underlying . + /// The length of the sub-stream. + /// + /// Note that calling the constructor will immediately move the underlying + /// to the specified offset. + /// + public SubStream(Stream innerStream, long offset, long length) + { + this.innerStream = innerStream; + this.offset = offset; + this.endOffset = offset + length; + this.length = length; + + innerStream.Seek(offset, SeekOrigin.Begin); + } + + /// + public override bool CanRead + { + get + { + return true; + } + } + + /// + public override bool CanWrite + { + get + { + return false; + } + } + + /// + public override bool CanSeek + { + get + { + return this.innerStream.CanSeek; + } + } + + /// + public override long Length + { + get + { + return this.length; + } + } + + /// + public override long Position + { + get + { + return this.innerStream.Position - this.offset; + } + + set + { + this.Seek(value, SeekOrigin.Begin); + } + } + + /// + public override void Flush() + { + throw new NotSupportedException(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + long bytesRemaining = this.endOffset - this.innerStream.Position; + + if (bytesRemaining < count) + { + count = (int)bytesRemaining; + } + + return this.innerStream.Read(buffer, offset, count); + } + + /// + public override int ReadByte() + { + if (this.innerStream.Position < this.endOffset) + { + return this.innerStream.ReadByte(); + } + else + { + return -1; + } + } + + /// + public override void Write(byte[] array, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + public override void WriteByte(byte value) + { + throw new NotSupportedException(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Current: + return this.innerStream.Seek(offset, SeekOrigin.Current) - this.offset; + case SeekOrigin.Begin: + return this.innerStream.Seek(this.offset + offset, SeekOrigin.Begin) - this.offset; + case SeekOrigin.End: + return this.innerStream.Seek(this.endOffset - offset, SeekOrigin.Begin) - this.offset; + default: + throw new ArgumentException("Invalid seek origin."); + } + } + + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index 64e3527453..59b2491054 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -34,5 +34,15 @@ namespace ImageSharp.Formats.Tiff count -= bytesRead; } } + + /// + /// Reads all bytes from the input stream into a buffer until the end of stream or the buffer is full. + /// + /// The stream to read from. + /// A buffer to store the retrieved data. + public static void ReadFull(this Stream stream, byte[] buffer) + { + ReadFull(stream, buffer, buffer.Length); + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs new file mode 100644 index 0000000000..e637008806 --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/DeflateTiffCompressionTests.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System.IO; + using Xunit; + + using ImageSharp.Formats; + using ImageSharp.Formats.Tiff; + + public class DeflateTiffCompressionTests + { + [Theory] + [InlineData(new byte[] { })] + [InlineData(new byte[] { 42 })] // One byte + [InlineData(new byte[] { 42, 16, 128, 53, 96, 218, 7, 64, 3, 4, 97 })] // Random bytes + [InlineData(new byte[] { 1, 2, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 3, 4 })] // Repeated bytes + [InlineData(new byte[] { 1, 2, 42, 53, 42, 53, 42, 53, 42, 53, 42, 53, 3, 4 })] // Repeated sequence + public void Decompress_ReadsData(byte[] data) + { + using (Stream stream = CreateCompressedStream(data)) + { + byte[] buffer = new byte[data.Length]; + + DeflateTiffCompression.Decompress(stream, data.Length, buffer); + + Assert.Equal(data, buffer); + } + } + + private static Stream CreateCompressedStream(byte[] data) + { + Stream compressedStream = new MemoryStream(); + + using (Stream uncompressedStream = new MemoryStream(data), + deflateStream = new ZlibDeflateStream(compressedStream, 6)) + { + uncompressedStream.CopyTo(deflateStream); + } + + compressedStream.Seek(0, SeekOrigin.Begin); + return compressedStream; + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs index 693ddfea1a..6eef305ff2 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs @@ -125,6 +125,10 @@ namespace ImageSharp.Tests [InlineData(true, TiffCompression.None, TiffCompressionType.None)] [InlineData(false, TiffCompression.PackBits, TiffCompressionType.PackBits)] [InlineData(true, TiffCompression.PackBits, TiffCompressionType.PackBits)] + [InlineData(false, TiffCompression.Deflate, TiffCompressionType.Deflate)] + [InlineData(true, TiffCompression.Deflate, TiffCompressionType.Deflate)] + [InlineData(false, TiffCompression.OldDeflate, TiffCompressionType.Deflate)] + [InlineData(true, TiffCompression.OldDeflate, TiffCompressionType.Deflate)] public void ReadImageFormat_DeterminesCorrectCompressionImplementation(bool isLittleEndian, ushort compression, int compressionType) { Stream stream = CreateTiffGenIfd() @@ -142,23 +146,19 @@ namespace ImageSharp.Tests [InlineData(false, TiffCompression.Ccitt1D)] [InlineData(false, TiffCompression.CcittGroup3Fax)] [InlineData(false, TiffCompression.CcittGroup4Fax)] - [InlineData(false, TiffCompression.Deflate)] [InlineData(false, TiffCompression.ItuTRecT43)] [InlineData(false, TiffCompression.ItuTRecT82)] [InlineData(false, TiffCompression.Jpeg)] [InlineData(false, TiffCompression.Lzw)] - [InlineData(false, TiffCompression.OldDeflate)] [InlineData(false, TiffCompression.OldJpeg)] [InlineData(false, 999)] [InlineData(true, TiffCompression.Ccitt1D)] [InlineData(true, TiffCompression.CcittGroup3Fax)] [InlineData(true, TiffCompression.CcittGroup4Fax)] - [InlineData(true, TiffCompression.Deflate)] [InlineData(true, TiffCompression.ItuTRecT43)] [InlineData(true, TiffCompression.ItuTRecT82)] [InlineData(true, TiffCompression.Jpeg)] [InlineData(true, TiffCompression.Lzw)] - [InlineData(true, TiffCompression.OldDeflate)] [InlineData(true, TiffCompression.OldJpeg)] [InlineData(true, 999)] public void ReadImageFormat_ThrowsExceptionForUnsupportedCompression(bool isLittleEndian, ushort compression) diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Utils/SubStreamTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Utils/SubStreamTests.cs new file mode 100644 index 0000000000..b57a77c74c --- /dev/null +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Utils/SubStreamTests.cs @@ -0,0 +1,327 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Tests +{ + using System; + using System.IO; + using Xunit; + + using ImageSharp.Formats.Tiff; + + public class SubStreamTests + { + [Fact] + public void Constructor_PositionsStreamCorrectly_WithSpecifiedOffset() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + innerStream.Position = 2; + + SubStream stream = new SubStream(innerStream, 4, 6); + + Assert.Equal(0, stream.Position); + Assert.Equal(6, stream.Length); + Assert.Equal(4, innerStream.Position); + } + + [Fact] + public void Constructor_PositionsStreamCorrectly_WithCurrentOffset() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + innerStream.Position = 2; + + SubStream stream = new SubStream(innerStream, 6); + + Assert.Equal(0, stream.Position); + Assert.Equal(6, stream.Length); + Assert.Equal(2, innerStream.Position); + } + + [Fact] + public void CanRead_ReturnsTrue() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + Assert.True(stream.CanRead); + } + + [Fact] + public void CanWrite_ReturnsFalse() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + Assert.False(stream.CanWrite); + } + + [Fact] + public void CanSeek_ReturnsTrue() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + Assert.True(stream.CanSeek); + } + + [Fact] + public void Length_ReturnsTheConstrainedLength() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + Assert.Equal(6, stream.Length); + } + + [Fact] + public void Position_ReturnsZeroBeforeReading() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + Assert.Equal(0, stream.Position); + Assert.Equal(2, innerStream.Position); + } + + [Fact] + public void Position_ReturnsPositionAfterReading() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Read(new byte[2], 0, 2); + + Assert.Equal(2, stream.Position); + Assert.Equal(4, innerStream.Position); + } + + [Fact] + public void Position_ReturnsPositionAfterReadingTwice() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Read(new byte[2], 0, 2); + stream.Read(new byte[2], 0, 2); + + Assert.Equal(4, stream.Position); + Assert.Equal(6, innerStream.Position); + } + + [Fact] + public void Position_SettingPropertySeeksToNewPosition() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Position = 3; + + Assert.Equal(3, stream.Position); + Assert.Equal(5, innerStream.Position); + } + + [Fact] + public void Flush_ThrowsNotSupportedException() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + Assert.Throws(() => stream.Flush()); + } + + [Fact] + public void Read_Reads_FromStartOfSubStream() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + byte[] buffer = new byte[3]; + var result = stream.Read(buffer, 0, 3); + + Assert.Equal(new byte[] { 3, 4, 5 }, buffer); + Assert.Equal(3, result); + } + + [Theory] + [InlineData(2, SeekOrigin.Begin)] + [InlineData(1, SeekOrigin.Current)] + [InlineData(4, SeekOrigin.End)] + public void Read_Reads_FromMiddleOfSubStream(long offset, SeekOrigin origin) + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Position = 1; + stream.Seek(offset, origin); + byte[] buffer = new byte[3]; + var result = stream.Read(buffer, 0, 3); + + Assert.Equal(new byte[] { 5, 6, 7 }, buffer); + Assert.Equal(3, result); + } + + [Theory] + [InlineData(3, SeekOrigin.Begin)] + [InlineData(2, SeekOrigin.Current)] + [InlineData(3, SeekOrigin.End)] + public void Read_Reads_FromEndOfSubStream(long offset, SeekOrigin origin) + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Position = 1; + stream.Seek(offset, origin); + byte[] buffer = new byte[3]; + var result = stream.Read(buffer, 0, 3); + + Assert.Equal(new byte[] { 6, 7, 8 }, buffer); + Assert.Equal(3, result); + } + + [Theory] + [InlineData(4, SeekOrigin.Begin)] + [InlineData(3, SeekOrigin.Current)] + [InlineData(2, SeekOrigin.End)] + public void Read_Reads_FromBeyondEndOfSubStream(long offset, SeekOrigin origin) + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Position = 1; + stream.Seek(offset, origin); + byte[] buffer = new byte[3]; + var result = stream.Read(buffer, 0, 3); + + Assert.Equal(new byte[] { 7, 8, 0 }, buffer); + Assert.Equal(2, result); + } + + [Fact] + public void ReadByte_Reads_FromStartOfSubStream() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + var result = stream.ReadByte(); + + Assert.Equal(3, result); + } + + [Fact] + public void ReadByte_Reads_FromMiddleOfSubStream() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Position = 3; + var result = stream.ReadByte(); + + Assert.Equal(6, result); + } + + [Fact] + public void ReadByte_Reads_FromEndOfSubStream() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Position = 5; + var result = stream.ReadByte(); + + Assert.Equal(8, result); + } + + [Fact] + public void ReadByte_Reads_FromBeyondEndOfSubStream() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Position = 5; + stream.ReadByte(); + var result = stream.ReadByte(); + + Assert.Equal(-1, result); + } + + [Fact] + public void Write_ThrowsNotSupportedException() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + Assert.Throws(() => stream.Write(new byte[] { 1, 2 }, 0, 2)); + } + + [Fact] + public void WriteByte_ThrowsNotSupportedException() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + Assert.Throws(() => stream.WriteByte(42)); + } + + [Fact] + public void Seek_MovesToNewPosition_FromBegin() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Position = 1; + long result = stream.Seek(2, SeekOrigin.Begin); + + Assert.Equal(2, result); + Assert.Equal(2, stream.Position); + Assert.Equal(4, innerStream.Position); + } + + [Fact] + public void Seek_MovesToNewPosition_FromCurrent() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Position = 1; + long result = stream.Seek(2, SeekOrigin.Current); + + Assert.Equal(3, result); + Assert.Equal(3, stream.Position); + Assert.Equal(5, innerStream.Position); + } + + [Fact] + public void Seek_MovesToNewPosition_FromEnd() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + stream.Position = 1; + long result = stream.Seek(2, SeekOrigin.End); + + Assert.Equal(4, result); + Assert.Equal(4, stream.Position); + Assert.Equal(6, innerStream.Position); + } + + [Fact] + public void Seek_ThrowsException_WithInvalidOrigin() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + var e = Assert.Throws(() => stream.Seek(2, (SeekOrigin)99)); + Assert.Equal("Invalid seek origin.", e.Message); + } + + public void SetLength_ThrowsNotSupportedException() + { + Stream innerStream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + SubStream stream = new SubStream(innerStream, 2, 6); + + Assert.Throws(() => stream.SetLength(5)); + } + } +} \ No newline at end of file