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;