From 66b4a762cfd2038824c237324d36e8e756e37c75 Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Wed, 22 Apr 2009 18:55:59 +0000 Subject: [PATCH] Added a class for chaining together multiple System.IO.Stream instances into a single stream; minor improvements to docs; improved an exception error message in the StringSegment class git-svn-id: file:///srv/devel/repo-conversion/nusu@131 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Support (Xbox 360).csproj | 4 + Nuclex.Support.csproj | 4 + Source/IO/ChainStream.Test.cs | 548 +++++++++++++++++++++++++++++++ Source/IO/ChainStream.cs | 460 ++++++++++++++++++++++++++ Source/Parsing/CommandLine.cs | 2 +- Source/StringSegment.cs | 2 +- 6 files changed, 1018 insertions(+), 2 deletions(-) create mode 100644 Source/IO/ChainStream.Test.cs create mode 100644 Source/IO/ChainStream.cs diff --git a/Nuclex.Support (Xbox 360).csproj b/Nuclex.Support (Xbox 360).csproj index 01d0ed6..2482a12 100644 --- a/Nuclex.Support (Xbox 360).csproj +++ b/Nuclex.Support (Xbox 360).csproj @@ -224,6 +224,10 @@ Shared.cs + + + ChainStream.cs + StringHelper.cs diff --git a/Nuclex.Support.csproj b/Nuclex.Support.csproj index 3661cbb..53ec96c 100644 --- a/Nuclex.Support.csproj +++ b/Nuclex.Support.csproj @@ -206,6 +206,10 @@ Shared.cs + + + ChainStream.cs + StringHelper.cs diff --git a/Source/IO/ChainStream.Test.cs b/Source/IO/ChainStream.Test.cs new file mode 100644 index 0000000..f1e9fc4 --- /dev/null +++ b/Source/IO/ChainStream.Test.cs @@ -0,0 +1,548 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2009 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +#if UNITTEST + +using System; +using System.IO; + +using NUnit.Framework; + +namespace Nuclex.Support.IO { + + /// Unit Test for the stream chainer + [TestFixture] + public class ChainStreamTest { + + #region class TestStream + + /// Testing stream that allows specific features to be disabled + private class TestStream : Stream { + + /// Initializes a new test stream + /// Stream that will be wrapped + /// Whether to allow reading from the stream + /// Whether to allow writing to the stream + /// Whether to allow seeking within the stream + public TestStream( + Stream wrappedStream, bool allowRead, bool allowWrite, bool allowSeek + ) { + this.stream = wrappedStream; + this.readAllowed = allowRead; + this.writeAllowed = allowWrite; + this.seekAllowed = allowSeek; + } + + /// Whether data can be read from the stream + public override bool CanRead { + get { return this.readAllowed; } + } + + /// Whether the stream supports seeking + public override bool CanSeek { + get { return this.seekAllowed; } + } + + /// Whether data can be written into the stream + public override bool CanWrite { + get { return this.writeAllowed; } + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + public override void Flush() { + ++this.flushCallCount; + this.stream.Flush(); + } + + /// Length of the stream in bytes + public override long Length { + get { + enforceSeekAllowed(); + return this.stream.Length; + } + } + + /// Absolute position of the file pointer within the stream + /// + /// At least one of the chained streams does not support seeking + /// + public override long Position { + get { + enforceSeekAllowed(); + return this.stream.Position; + } + set { + enforceSeekAllowed(); + this.stream.Position = value; + } + } + + /// + /// Reads a sequence of bytes from the stream and advances the position of + /// the file pointer by the number of bytes read. + /// + /// Buffer that will receive the data read from the stream + /// + /// Offset in the buffer at which the stream will place the data read + /// + /// Maximum number of bytes that will be read + /// + /// The number of bytes that were actually read from the stream and written into + /// the provided buffer + /// + public override int Read(byte[] buffer, int offset, int count) { + enforceReadAllowed(); + return this.stream.Read(buffer, offset, count); + } + + /// Changes the position of the file pointer + /// + /// Offset to move the file pointer by, relative to the position indicated by + /// the parameter. + /// + /// + /// Reference point relative to which the file pointer is placed + /// + /// The new absolute position within the stream + public override long Seek(long offset, SeekOrigin origin) { + enforceSeekAllowed(); + return this.stream.Seek(offset, origin); + } + + /// Changes the length of the stream + /// New length the stream shall have + public override void SetLength(long value) { + enforceSeekAllowed(); + this.stream.SetLength(value); + } + + /// + /// Writes a sequence of bytes to the stream and advances the position of + /// the file pointer by the number of bytes written. + /// + /// + /// Buffer containing the data that will be written to the stream + /// + /// + /// Offset in the buffer at which the data to be written starts + /// + /// Number of bytes that will be written into the stream + public override void Write(byte[] buffer, int offset, int count) { + enforceWriteAllowed(); + this.stream.Write(buffer, offset, count); + } + + /// Number of times the Flush() method has been called + public int FlushCallCount { + get { return this.flushCallCount; } + } + + /// Throws an exception if reading is not allowed + private void enforceReadAllowed() { + if(!this.readAllowed) { + throw new NotSupportedException("Reading has been disabled"); + } + } + + /// Throws an exception if writing is not allowed + private void enforceWriteAllowed() { + if(!this.writeAllowed) { + throw new NotSupportedException("Writing has been disabled"); + } + } + + /// Throws an exception if seeking is not allowed + private void enforceSeekAllowed() { + if(!this.seekAllowed) { + throw new NotSupportedException("Seeking has been disabled"); + } + } + + /// Stream being wrapped for testing + private Stream stream; + /// whether to allow reading from the wrapped stream + private bool readAllowed; + /// Whether to allow writing to the wrapped stream + private bool writeAllowed; + /// Whether to allow seeking within the wrapped stream + private bool seekAllowed; + /// Number of times the Flush() method has been called + private int flushCallCount; + + } + + #endregion // class TestStream + + /// + /// Tests whether the stream chainer correctly partitions a long write request + /// over its chained streams and appends any remaining data to the end of + /// the last chained stream. + /// + [Test] + public void TestPartitionedWrite() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + + byte[] testData = new byte[20]; + for(int index = 0; index < testData.Length; ++index) { + testData[index] = (byte)(index + 1); + } + + chainer.Position = 5; + chainer.Write(testData, 0, testData.Length); + + Assert.AreEqual( + new byte[] { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5 }, + ((MemoryStream)chainer.ChainedStreams[0]).ToArray() + ); + Assert.AreEqual( + new byte[] { 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }, + ((MemoryStream)chainer.ChainedStreams[1]).ToArray() + ); + } + + /// + /// Tests whether the stream chainer correctly partitions a long read request + /// over its chained streams. + /// + [Test] + public void TestPartitionedRead() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + + ((MemoryStream)chainer.ChainedStreams[0]).Write( + new byte[] { 1, 2, 3, 4, 5 }, 0, 5 + ); + ((MemoryStream)chainer.ChainedStreams[1]).Write( + new byte[] { 6, 7, 8, 9, 10 }, 0, 5 + ); + + chainer.Position = 3; + byte[] buffer = new byte[15]; + int bytesRead = chainer.Read(buffer, 0, 14); + + Assert.AreEqual(14, bytesRead); + Assert.AreEqual(new byte[] { 4, 5, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 0, 0, 0 }, buffer); + } + + /// + /// Tests whether the stream chainer can handle a stream resize + /// + [Test] + public void TestWriteAfterResize() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + + // The first stream has a size of 10 bytes, so this goes into the second stream + chainer.Position = 11; + chainer.Write(new byte[] { 12, 34 }, 0, 2); + + // Now we resize the first stream to 15 bytes, so this goes into the first stream + ((MemoryStream)chainer.ChainedStreams[0]).SetLength(15); + chainer.Write(new byte[] { 56, 78, 11, 22 }, 0, 4); + + Assert.AreEqual( + new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 78 }, + ((MemoryStream)chainer.ChainedStreams[0]).ToArray() + ); + Assert.AreEqual( + new byte[] { 11, 22, 34, 0, 0, 0, 0, 0, 0, 0 }, + ((MemoryStream)chainer.ChainedStreams[1]).ToArray() + ); + } + + /// + /// Tests writing to a stream chainer that contains an unseekable stream + /// + [Test] + public void TestWriteToUnseekableStream() { + MemoryStream firstStream = new MemoryStream(); + + // Now the second stream _does_ support seeking. If the stream chainer ignores + // that, it would overwrite data in the second stream. + MemoryStream secondStream = new MemoryStream(); + secondStream.Write(new byte[] { 0, 9, 8, 7, 6 }, 0, 5); + secondStream.Position = 0; + + TestStream testStream = new TestStream(firstStream, true, true, false); + ChainStream chainer = new ChainStream(new Stream[] { testStream, secondStream }); + + chainer.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5); + Assert.IsFalse(chainer.CanSeek); + Assert.AreEqual(0, firstStream.Length); + Assert.AreEqual(new byte[] { 0, 9, 8, 7, 6, 1, 2, 3, 4, 5 }, secondStream.ToArray()); + } + + /// + /// Tests reading from a stream chainer that contains an unseekable stream + /// + [Test] + public void TestReadFromUnseekableStream() { + MemoryStream firstStream = new MemoryStream(); + + // Now the second stream _does_ support seeking. If the stream chainer ignores + // that, it would overwrite data in the second stream. + MemoryStream secondStream = new MemoryStream(); + secondStream.Write(new byte[] { 0, 9, 8, 7, 6 }, 0, 5); + secondStream.Position = 3; + + TestStream testStream = new TestStream(firstStream, true, true, false); + ChainStream chainer = new ChainStream(new Stream[] { testStream, secondStream }); + + Assert.IsFalse(chainer.CanSeek); + + byte[] buffer = new byte[5]; + int readByteCount = chainer.Read(buffer, 0, 3); + + Assert.AreEqual(3, readByteCount); + Assert.AreEqual(new byte[] { 0, 9, 8, 0, 0 }, buffer); + + readByteCount = chainer.Read(buffer, 0, 3); + + Assert.AreEqual(2, readByteCount); + Assert.AreEqual(new byte[] { 7, 6, 8, 0, 0 }, buffer); + } + + /// + /// Tests reading from a stream chainer that contains an unreadable stream + /// + [Test, ExpectedException(typeof(NotSupportedException))] + public void TestThrowOnReadFromUnreadableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, false, true, true); + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + + chainer.Read(new byte[5], 0, 5); + } + + /// + /// Tests writing to a stream chainer that contains an unwriteable stream + /// + [Test, ExpectedException(typeof(NotSupportedException))] + public void TestThrowOnWriteToUnwriteableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, true, false, true); + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + + chainer.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5); + } + + /// + /// Verifies that the stream chainer throws an exception if the attempt is + /// made to change the length of the stream + /// + [Test, ExpectedException(typeof(NotSupportedException))] + public void TestThrowOnLengthChange() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + chainer.SetLength(123); + } + + /// + /// Verifies that the CanRead property is correctly determined by the stream chainer + /// + [Test] + public void TestCanRead() { + MemoryStream yesStream = new MemoryStream(); + TestStream noStream = new TestStream(yesStream, false, true, true); + + Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream }; + Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream }; + Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream }; + + Assert.IsTrue(new ChainStream(yesGroup).CanRead); + Assert.IsFalse(new ChainStream(partialGroup).CanRead); + Assert.IsFalse(new ChainStream(noGroup).CanRead); + } + + /// + /// Verifies that the CanRead property is correctly determined by the stream chainer + /// + [Test] + public void TestCanWrite() { + MemoryStream yesStream = new MemoryStream(); + TestStream noStream = new TestStream(yesStream, true, false, true); + + Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream }; + Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream }; + Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream }; + + Assert.IsTrue(new ChainStream(yesGroup).CanWrite); + Assert.IsFalse(new ChainStream(partialGroup).CanWrite); + Assert.IsFalse(new ChainStream(noGroup).CanWrite); + } + + /// + /// Verifies that the CanSeek property is correctly determined by the stream chainer + /// + [Test] + public void TestCanSeek() { + MemoryStream yesStream = new MemoryStream(); + TestStream noStream = new TestStream(yesStream, true, true, false); + + Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream }; + Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream }; + Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream }; + + Assert.IsTrue(new ChainStream(yesGroup).CanSeek); + Assert.IsFalse(new ChainStream(partialGroup).CanSeek); + Assert.IsFalse(new ChainStream(noGroup).CanSeek); + } + + /// + /// Tests whether an exception is thrown if the Seek() method is called on + /// a stream chainer with streams that do not support seeking + /// + [Test, ExpectedException(typeof(NotSupportedException))] + public void TestThrowOnSeekWithUnseekableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, true, true, false); + + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + chainer.Seek(123, SeekOrigin.Begin); + } + + /// + /// Tests whether an exception is thrown if the Position property is retrieved + /// on a stream chainer with streams that do not support seeking + /// + [Test, ExpectedException(typeof(NotSupportedException))] + public void TestThrowOnGetPositionWithUnseekableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, true, true, false); + + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + Assert.IsTrue(chainer.Position != chainer.Position); // ;-) + } + + /// + /// Tests whether an exception is thrown if the Position property is set + /// on a stream chainer with streams that do not support seeking + /// + [Test, ExpectedException(typeof(NotSupportedException))] + public void TestThrowOnSetPositionWithUnseekableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, true, true, false); + + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + chainer.Position = 123; + } + + /// + /// Tests whether an exception is thrown if the Length property is retrieved + /// on a stream chainer with streams that do not support seeking + /// + [Test, ExpectedException(typeof(NotSupportedException))] + public void TestThrowOnGetLengthWithUnseekableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, true, true, false); + + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + Assert.IsTrue(chainer.Length != chainer.Length); // ;-) + } + + /// + /// Tests whether the Seek() method of the stream chainer is working + /// + [Test] + public void TestSeeking() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + + Assert.AreEqual(7, chainer.Seek(-13, SeekOrigin.End)); + Assert.AreEqual(14, chainer.Seek(7, SeekOrigin.Current)); + Assert.AreEqual(11, chainer.Seek(11, SeekOrigin.Begin)); + } + + /// + /// Tests whether the stream behaves correctly if data is read from beyond its end + /// + [Test] + public void TestReadBeyondEndOfStream() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + chainer.Seek(10, SeekOrigin.End); + + // This is how the MemoryStream behaves: it returns 0 bytes. + int readByteCount = chainer.Read(new byte[1], 0, 1); + Assert.AreEqual(0, readByteCount); + } + + /// + /// Tests whether the Seek() method throws an exception if an invalid + /// reference point is provided + /// + [Test, ExpectedException(typeof(ArgumentException))] + public void TestThrowOnInvalidSeekReferencePoint() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + chainer.Seek(1, (SeekOrigin)12345); + } + + /// Verifies that the position property works correctly + [Test] + public void TestPositionChange() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + + chainer.Position = 7; + Assert.AreEqual(chainer.Position, 7); + chainer.Position = 14; + Assert.AreEqual(chainer.Position, 14); + } + + /// Tests the Flush() method of the stream chainer + [Test] + public void TestFlush() { + MemoryStream firstStream = new MemoryStream(); + TestStream firstTestStream = new TestStream(firstStream, true, true, true); + + MemoryStream secondStream = new MemoryStream(); + TestStream secondTestStream = new TestStream(secondStream, true, true, true); + + ChainStream chainer = new ChainStream( + new Stream[] { firstTestStream, secondTestStream } + ); + + Assert.AreEqual(0, firstTestStream.FlushCallCount); + Assert.AreEqual(0, secondTestStream.FlushCallCount); + + chainer.Flush(); + + Assert.AreEqual(1, firstTestStream.FlushCallCount); + Assert.AreEqual(1, secondTestStream.FlushCallCount); + } + + /// + /// Creates a stream chainer with two streams that each have a size of 10 bytes + /// + /// The new stream chainer with two chained 10-byte streams + private static ChainStream chainTwoStreamsOfTenBytes() { + MemoryStream firstStream = new MemoryStream(10); + MemoryStream secondStream = new MemoryStream(10); + + firstStream.SetLength(10); + secondStream.SetLength(10); + + return new ChainStream( + new Stream[] { firstStream, secondStream } + ); + } + + } + +} // namespace Nuclex.Support.IO + +#endif // UNITTEST diff --git a/Source/IO/ChainStream.cs b/Source/IO/ChainStream.cs new file mode 100644 index 0000000..e8bc1be --- /dev/null +++ b/Source/IO/ChainStream.cs @@ -0,0 +1,460 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2009 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace Nuclex.Support.IO { + + /// Chains a series of independent streams into a single stream + /// + /// + /// This class can be used to chain multiple independent streams into a single + /// stream that acts as if its chained streams were only one combined stream. + /// It is useful to avoid creating huge memory streams or temporary files when + /// you just need to prepend or append some data to a stream or if you need to + /// read a file that was split into several parts as if it was a single file. + /// + /// + /// It is not recommended to change the size of any chained stream after it + /// has become part of a stream chainer, though the stream chainer will do its + /// best to cope with the changes as they occur. Increasing the length of a + /// chained stream is generally not an issue for streams that support seeking, + /// but reducing the length might invalidate the stream chainer's file pointer, + /// resulting in an IOException when Read() or Write() is next called. + /// + /// + public class ChainStream : Stream { + + /// Initializes a new stream chainer + /// Array of streams that will be chained together + public ChainStream(Stream[] streams) { + this.streams = (Stream[])streams.Clone(); + + determineCapabilities(); + } + + /// Whether data can be read from the stream + public override bool CanRead { + get { return this.allStreamsCanRead; } + } + + /// Whether the stream supports seeking + public override bool CanSeek { + get { return this.allStreamsCanSeek; } + } + + /// Whether data can be written into the stream + public override bool CanWrite { + get { return this.allStreamsCanWrite; } + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + public override void Flush() { + for(int index = 0; index < this.streams.Length; ++index) { + this.streams[index].Flush(); + } + } + + /// Length of the stream in bytes + /// + /// At least one of the chained streams does not support seeking + /// + public override long Length { + get { + if(!this.allStreamsCanSeek) { + throw makeSeekNotSupportedException("determine length"); + } + + // Sum up the length of all chained streams + long length = 0; + for(int index = 0; index < this.streams.Length; ++index) { + length += this.streams[index].Length; + } + + return length; + } + } + + /// Absolute position of the file pointer within the stream + /// + /// At least one of the chained streams does not support seeking + /// + public override long Position { + get { + if(!this.allStreamsCanSeek) { + throw makeSeekNotSupportedException("seek"); + } + + return this.position; + } + set { moveFilePointer(value); } + } + + /// + /// Reads a sequence of bytes from the stream and advances the position of + /// the file pointer by the number of bytes read. + /// + /// Buffer that will receive the data read from the stream + /// + /// Offset in the buffer at which the stream will place the data read + /// + /// Maximum number of bytes that will be read + /// + /// The number of bytes that were actually read from the stream and written into + /// the provided buffer + /// + /// + /// The chained stream at the current position does not support reading + /// + public override int Read(byte[] buffer, int offset, int count) { + if(!this.allStreamsCanRead) { + throw new NotSupportedException( + "Can't read: at least one of the chained streams doesn't support reading" + ); + } + + int totalBytesRead = 0; + int lastStreamIndex = this.streams.Length - 1; + + if(this.CanSeek) { + + // Find out from which stream and at which position we need to begin reading + int streamIndex; + long streamOffset; + findStreamIndexAndOffset(this.position, out streamIndex, out streamOffset); + + // Try to read from the stream our current file pointer falls into. If more + // data was requested than the stream contains, read each stream to its end + // until we either have enough data or run out of streams. + while(count > 0) { + Stream currentStream = this.streams[streamIndex]; + + // Read up to count bytes from the current stream. Count is decreased each + // time we successfully get data and holds the number of bytes remaining + // to be read + long maximumBytes = Math.Min(count, currentStream.Length - streamOffset); + currentStream.Position = streamOffset; + int bytesRead = currentStream.Read(buffer, offset, (int)maximumBytes); + + // Accumulate the total number of bytes we read for the return value + totalBytesRead += bytesRead; + + // If the stream returned partial data, stop here. Also, if this was the + // last stream we queried, this is as far as we can go. + if((bytesRead < maximumBytes) || (streamIndex == lastStreamIndex)) { + break; + } + + // Move on to the next stream in the chain + ++streamIndex; + streamOffset = 0; + count -= bytesRead; + offset += bytesRead; + } + + this.position += totalBytesRead; + + } else { + + // Try to read from the active read stream. If the end of the active read + // stream is reached, switch to the next stream in the chain until we have + // no more streams left to read from + while(this.activeReadStreamIndex <= lastStreamIndex) { + + // Try to read from the stream. The stream can either return any amount + // of data > 0 if there's still data left ot be read or 0 if the end of + // the stream was reached + Stream activeStream = this.streams[this.activeReadStreamIndex]; + if(activeStream.CanSeek) { + activeStream.Position = this.activeReadStreamPosition; + } + totalBytesRead = activeStream.Read(buffer, offset, count); + + // If we got any data, we're done, exit the loop + if(totalBytesRead != 0) { + break; + } else { // Otherwise, go to the next stream in the chain + this.activeReadStreamPosition = 0; + ++this.activeReadStreamIndex; + } + } + + this.activeReadStreamPosition += totalBytesRead; + + } + + return totalBytesRead; + } + + /// Changes the position of the file pointer + /// + /// Offset to move the file pointer by, relative to the position indicated by + /// the parameter. + /// + /// + /// Reference point relative to which the file pointer is placed + /// + /// The new absolute position within the stream + public override long Seek(long offset, SeekOrigin origin) { + switch(origin) { + case SeekOrigin.Begin: { + return Position = offset; + } + case SeekOrigin.Current: { + return Position += offset; + } + case SeekOrigin.End: { + return Position = (Length + offset); + } + default: { + throw new ArgumentException("Invalid seek origin", "origin"); + } + } + } + + /// Changes the length of the stream + /// New length the stream shall have + /// + /// Always, the stream chainer does not support the SetLength() operation + /// + public override void SetLength(long value) { + throw new NotSupportedException("Resizing chained streams is not supported"); + } + + /// + /// Writes a sequence of bytes to the stream and advances the position of + /// the file pointer by the number of bytes written. + /// + /// + /// Buffer containing the data that will be written to the stream + /// + /// + /// Offset in the buffer at which the data to be written starts + /// + /// Number of bytes that will be written into the stream + /// + /// The behavior of this method is as follows: If one or more chained streams + /// do not support seeking, all data is appended to the final stream in the + /// chain. Otherwise, writing will begin with the stream the current file pointer + /// offset falls into. If the end of that stream is reached, writing continues + /// in the next stream. On the last stream, writing more data into the stream + /// that it current size allows will enlarge the stream. + /// + public override void Write(byte[] buffer, int offset, int count) { + if(!this.allStreamsCanWrite) { + throw new NotSupportedException( + "Can't write: at least one of the chained streams doesn't support writing" + ); + } + + int remaining = count; + + // If seeking is supported, we can write into the mid of the stream, + // if the user so desires + if(this.allStreamsCanSeek) { + + // Find out in which stream and at which position we need to begin writing + int streamIndex; + long streamOffset; + findStreamIndexAndOffset(this.position, out streamIndex, out streamOffset); + + // Write data into the streams, switching over to the next stream if data is + // too large to fit into the current stream, until all data is spent. + int lastStreamIndex = this.streams.Length - 1; + while(remaining > 0) { + Stream currentStream = this.streams[streamIndex]; + + // If this is the last stream, just write. If the data is larger than the last + // stream's remaining bytes, it will append to that stream, enlarging it. + if(streamIndex == lastStreamIndex) { + + // Write all remaining data into the last stream + currentStream.Position = streamOffset; + currentStream.Write(buffer, offset, remaining); + remaining = 0; + + } else { // We're writing into a stream that's followed by another stream + + // Find out how much data we can put into the current stream without + // enlarging it (if seeking is supported, so is the Length property) + long currentStreamRemaining = currentStream.Length - streamOffset; + int bytesToWrite = (int)Math.Min((long)remaining, currentStreamRemaining); + + // Write all data that can fit into the current stream + currentStream.Position = streamOffset; + currentStream.Write(buffer, offset, bytesToWrite); + + // Adjust the offsets and count for the next stream + offset += bytesToWrite; + remaining -= bytesToWrite; + streamOffset = 0; + ++streamIndex; + + } + } + + } else { // Seeking not supported, append everything to the last stream + Stream lastStream = this.streams[this.streams.Length - 1]; + if(lastStream.CanSeek) { + lastStream.Seek(0, SeekOrigin.End); + } + lastStream.Write(buffer, offset, remaining); + } + + this.position += count; + } + + /// Streams being combined by the stream chainer + public Stream[] ChainedStreams { + get { return this.streams; } + } + + /// Moves the file pointer + /// New position the file pointer will be moved to + private void moveFilePointer(long position) { + if(!this.allStreamsCanSeek) { + throw makeSeekNotSupportedException("seek"); + } + + // Seemingly, it is okay to move the file pointer beyond the end of + // the stream until you try to Read() or Write() + this.position = position; + } + + /// + /// Finds the stream index and local offset for an absolute position within + /// the combined streams. + /// + /// Absolute position within the combined streams + /// + /// Index of the stream the overall position falls into + /// + /// + /// Local position within the stream indicated by + /// + private void findStreamIndexAndOffset( + long overallPosition, out int streamIndex, out long streamPosition + ) { + Debug.Assert( + this.allStreamsCanSeek, "Call to findStreamIndexAndOffset() but no seek support" + ); + + // In case the position is beyond the stream's end, this is what we will + // return to the caller + streamIndex = (this.streams.Length - 1); + + // Search until we have found the stream the position must lie in + for(int index = 0; index < this.streams.Length; ++index) { + long streamLength = this.streams[index].Length; + + if(overallPosition < streamLength) { + streamIndex = index; + break; + } + + overallPosition -= streamLength; + } + + // The overall position will have been decreased by each skipped stream's length, + // so it should now contain the local position for the final stream we checked. + streamPosition = overallPosition; + } + + /// Determines the capabilities of the chained streams + /// + /// + /// Theoretically, it would be possible to create a stream chainer that supported + /// writing only when the file pointer was on a chained stream with write support, + /// that could seek within the beginning of the stream until the first chained + /// stream with no seek capability was encountered and so on. + /// + /// + /// However, the interface of the Stream class requires us to make a definitive + /// statement as to whether the Stream supports seeking, reading and writing. + /// We can't return "maybe" or "mostly" in CanSeek, so the only sane choice that + /// doesn't violate the Stream interface is to implement these capabilities as + /// all or nothing - either all streams support a feature, or the stream chainer + /// will report the feature as unsupported. + /// + /// + private void determineCapabilities() { + this.allStreamsCanSeek = true; + this.allStreamsCanRead = true; + this.allStreamsCanWrite = true; + + for(int index = 0; index < this.streams.Length; ++index) { + this.allStreamsCanSeek &= this.streams[index].CanSeek; + this.allStreamsCanRead &= this.streams[index].CanRead; + this.allStreamsCanWrite &= this.streams[index].CanWrite; + } + } + + /// + /// Constructs a NotSupportException for an error caused by one of the chained + /// streams having no seek support + /// + /// Action that was tried to perform + /// The newly constructed NotSupportedException + private static NotSupportedException makeSeekNotSupportedException(string action) { + return new NotSupportedException( + string.Format( + "Can't {0}: at least one of the chained streams does not support seeking", + action + ) + ); + } + + /// Streams that have been chained together + private Stream[] streams; + /// Current position of the overall file pointer + private long position; + /// Stream we're currently reading from if seeking is not supported + /// + /// If seeking is not supported, the stream chainer will read from each stream + /// until the end was reached + /// sequentially + /// + private int activeReadStreamIndex; + /// Position in the current read stream if seeking is not supported + /// + /// If there is a mix of streams supporting seeking and not supporting seeking, we + /// need to keep track of the read index for those streams that do. If, for example, + /// the last stream is written to and read from in succession, the file pointer + /// of that stream would have been moved to the end by the write attempt, skipping + /// data that should have been read in the following read attempt. + /// + private long activeReadStreamPosition; + + /// Whether all of the chained streams support seeking + private bool allStreamsCanSeek; + /// Whether all of the chained streams support reading + private bool allStreamsCanRead; + /// Whether all of the chained streams support writing + private bool allStreamsCanWrite; + + } + +} // namespace Nuclex.Support.IO diff --git a/Source/Parsing/CommandLine.cs b/Source/Parsing/CommandLine.cs index 54deb7e..51f7d9e 100644 --- a/Source/Parsing/CommandLine.cs +++ b/Source/Parsing/CommandLine.cs @@ -58,7 +58,7 @@ namespace Nuclex.Support.Parsing { /// /// Argument /// - /// Either an option or a loose value (see below) that being specified on + /// Either an option or a loose value (see below) being specified on /// the command line /// /// diff --git a/Source/StringSegment.cs b/Source/StringSegment.cs index 5a84724..361adfb 100644 --- a/Source/StringSegment.cs +++ b/Source/StringSegment.cs @@ -50,7 +50,7 @@ namespace Nuclex.Support { /// String is null public StringSegment(string text) { if(text == null) { // questionable, but matches behavior of ArraySegment class - throw new ArgumentNullException("text"); + throw new ArgumentNullException("text", "Text must not be null"); } this.text = text;