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
This commit is contained in:
Markus Ewald 2009-04-22 18:55:59 +00:00
parent a2331b95c1
commit 66b4a762cf
6 changed files with 1018 additions and 2 deletions

View File

@ -224,6 +224,10 @@
<Compile Include="Source\Shared.Test.cs">
<DependentUpon>Shared.cs</DependentUpon>
</Compile>
<Compile Include="Source\IO\ChainStream.cs" />
<Compile Include="Source\IO\ChainStream.Test.cs">
<DependentUpon>ChainStream.cs</DependentUpon>
</Compile>
<Compile Include="Source\StringHelper.cs" />
<Compile Include="Source\StringHelper.Test.cs">
<DependentUpon>StringHelper.cs</DependentUpon>

View File

@ -206,6 +206,10 @@
<Compile Include="Source\Shared.Test.cs">
<DependentUpon>Shared.cs</DependentUpon>
</Compile>
<Compile Include="Source\IO\ChainStream.cs" />
<Compile Include="Source\IO\ChainStream.Test.cs">
<DependentUpon>ChainStream.cs</DependentUpon>
</Compile>
<Compile Include="Source\StringHelper.cs" />
<Compile Include="Source\StringHelper.Test.cs">
<DependentUpon>StringHelper.cs</DependentUpon>

View File

@ -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 {
/// <summary>Unit Test for the stream chainer</summary>
[TestFixture]
public class ChainStreamTest {
#region class TestStream
/// <summary>Testing stream that allows specific features to be disabled</summary>
private class TestStream : Stream {
/// <summary>Initializes a new test stream</summary>
/// <param name="wrappedStream">Stream that will be wrapped</param>
/// <param name="allowRead">Whether to allow reading from the stream</param>
/// <param name="allowWrite">Whether to allow writing to the stream</param>
/// <param name="allowSeek">Whether to allow seeking within the stream</param>
public TestStream(
Stream wrappedStream, bool allowRead, bool allowWrite, bool allowSeek
) {
this.stream = wrappedStream;
this.readAllowed = allowRead;
this.writeAllowed = allowWrite;
this.seekAllowed = allowSeek;
}
/// <summary>Whether data can be read from the stream</summary>
public override bool CanRead {
get { return this.readAllowed; }
}
/// <summary>Whether the stream supports seeking</summary>
public override bool CanSeek {
get { return this.seekAllowed; }
}
/// <summary>Whether data can be written into the stream</summary>
public override bool CanWrite {
get { return this.writeAllowed; }
}
/// <summary>
/// Clears all buffers for this stream and causes any buffered data to be written
/// to the underlying device.
/// </summary>
public override void Flush() {
++this.flushCallCount;
this.stream.Flush();
}
/// <summary>Length of the stream in bytes</summary>
public override long Length {
get {
enforceSeekAllowed();
return this.stream.Length;
}
}
/// <summary>Absolute position of the file pointer within the stream</summary>
/// <exception cref="NotSupportedException">
/// At least one of the chained streams does not support seeking
/// </exception>
public override long Position {
get {
enforceSeekAllowed();
return this.stream.Position;
}
set {
enforceSeekAllowed();
this.stream.Position = value;
}
}
/// <summary>
/// Reads a sequence of bytes from the stream and advances the position of
/// the file pointer by the number of bytes read.
/// </summary>
/// <param name="buffer">Buffer that will receive the data read from the stream</param>
/// <param name="offset">
/// Offset in the buffer at which the stream will place the data read
/// </param>
/// <param name="count">Maximum number of bytes that will be read</param>
/// <returns>
/// The number of bytes that were actually read from the stream and written into
/// the provided buffer
/// </returns>
public override int Read(byte[] buffer, int offset, int count) {
enforceReadAllowed();
return this.stream.Read(buffer, offset, count);
}
/// <summary>Changes the position of the file pointer</summary>
/// <param name="offset">
/// Offset to move the file pointer by, relative to the position indicated by
/// the <paramref name="origin" /> parameter.
/// </param>
/// <param name="origin">
/// Reference point relative to which the file pointer is placed
/// </param>
/// <returns>The new absolute position within the stream</returns>
public override long Seek(long offset, SeekOrigin origin) {
enforceSeekAllowed();
return this.stream.Seek(offset, origin);
}
/// <summary>Changes the length of the stream</summary>
/// <param name="value">New length the stream shall have</param>
public override void SetLength(long value) {
enforceSeekAllowed();
this.stream.SetLength(value);
}
/// <summary>
/// Writes a sequence of bytes to the stream and advances the position of
/// the file pointer by the number of bytes written.
/// </summary>
/// <param name="buffer">
/// Buffer containing the data that will be written to the stream
/// </param>
/// <param name="offset">
/// Offset in the buffer at which the data to be written starts
/// </param>
/// <param name="count">Number of bytes that will be written into the stream</param>
public override void Write(byte[] buffer, int offset, int count) {
enforceWriteAllowed();
this.stream.Write(buffer, offset, count);
}
/// <summary>Number of times the Flush() method has been called</summary>
public int FlushCallCount {
get { return this.flushCallCount; }
}
/// <summary>Throws an exception if reading is not allowed</summary>
private void enforceReadAllowed() {
if(!this.readAllowed) {
throw new NotSupportedException("Reading has been disabled");
}
}
/// <summary>Throws an exception if writing is not allowed</summary>
private void enforceWriteAllowed() {
if(!this.writeAllowed) {
throw new NotSupportedException("Writing has been disabled");
}
}
/// <summary>Throws an exception if seeking is not allowed</summary>
private void enforceSeekAllowed() {
if(!this.seekAllowed) {
throw new NotSupportedException("Seeking has been disabled");
}
}
/// <summary>Stream being wrapped for testing</summary>
private Stream stream;
/// <summary>whether to allow reading from the wrapped stream</summary>
private bool readAllowed;
/// <summary>Whether to allow writing to the wrapped stream</summary>
private bool writeAllowed;
/// <summary>Whether to allow seeking within the wrapped stream</summary>
private bool seekAllowed;
/// <summary>Number of times the Flush() method has been called</summary>
private int flushCallCount;
}
#endregion // class TestStream
/// <summary>
/// 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.
/// </summary>
[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()
);
}
/// <summary>
/// Tests whether the stream chainer correctly partitions a long read request
/// over its chained streams.
/// </summary>
[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);
}
/// <summary>
/// Tests whether the stream chainer can handle a stream resize
/// </summary>
[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()
);
}
/// <summary>
/// Tests writing to a stream chainer that contains an unseekable stream
/// </summary>
[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());
}
/// <summary>
/// Tests reading from a stream chainer that contains an unseekable stream
/// </summary>
[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);
}
/// <summary>
/// Tests reading from a stream chainer that contains an unreadable stream
/// </summary>
[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);
}
/// <summary>
/// Tests writing to a stream chainer that contains an unwriteable stream
/// </summary>
[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);
}
/// <summary>
/// Verifies that the stream chainer throws an exception if the attempt is
/// made to change the length of the stream
/// </summary>
[Test, ExpectedException(typeof(NotSupportedException))]
public void TestThrowOnLengthChange() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
chainer.SetLength(123);
}
/// <summary>
/// Verifies that the CanRead property is correctly determined by the stream chainer
/// </summary>
[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);
}
/// <summary>
/// Verifies that the CanRead property is correctly determined by the stream chainer
/// </summary>
[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);
}
/// <summary>
/// Verifies that the CanSeek property is correctly determined by the stream chainer
/// </summary>
[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);
}
/// <summary>
/// Tests whether an exception is thrown if the Seek() method is called on
/// a stream chainer with streams that do not support seeking
/// </summary>
[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);
}
/// <summary>
/// Tests whether an exception is thrown if the Position property is retrieved
/// on a stream chainer with streams that do not support seeking
/// </summary>
[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); // ;-)
}
/// <summary>
/// Tests whether an exception is thrown if the Position property is set
/// on a stream chainer with streams that do not support seeking
/// </summary>
[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;
}
/// <summary>
/// Tests whether an exception is thrown if the Length property is retrieved
/// on a stream chainer with streams that do not support seeking
/// </summary>
[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); // ;-)
}
/// <summary>
/// Tests whether the Seek() method of the stream chainer is working
/// </summary>
[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));
}
/// <summary>
/// Tests whether the stream behaves correctly if data is read from beyond its end
/// </summary>
[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);
}
/// <summary>
/// Tests whether the Seek() method throws an exception if an invalid
/// reference point is provided
/// </summary>
[Test, ExpectedException(typeof(ArgumentException))]
public void TestThrowOnInvalidSeekReferencePoint() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
chainer.Seek(1, (SeekOrigin)12345);
}
/// <summary>Verifies that the position property works correctly</summary>
[Test]
public void TestPositionChange() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
chainer.Position = 7;
Assert.AreEqual(chainer.Position, 7);
chainer.Position = 14;
Assert.AreEqual(chainer.Position, 14);
}
/// <summary>Tests the Flush() method of the stream chainer</summary>
[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);
}
/// <summary>
/// Creates a stream chainer with two streams that each have a size of 10 bytes
/// </summary>
/// <returns>The new stream chainer with two chained 10-byte streams</returns>
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

460
Source/IO/ChainStream.cs Normal file
View File

@ -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 {
/// <summary>Chains a series of independent streams into a single stream</summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public class ChainStream : Stream {
/// <summary>Initializes a new stream chainer</summary>
/// <param name="streams">Array of streams that will be chained together</param>
public ChainStream(Stream[] streams) {
this.streams = (Stream[])streams.Clone();
determineCapabilities();
}
/// <summary>Whether data can be read from the stream</summary>
public override bool CanRead {
get { return this.allStreamsCanRead; }
}
/// <summary>Whether the stream supports seeking</summary>
public override bool CanSeek {
get { return this.allStreamsCanSeek; }
}
/// <summary>Whether data can be written into the stream</summary>
public override bool CanWrite {
get { return this.allStreamsCanWrite; }
}
/// <summary>
/// Clears all buffers for this stream and causes any buffered data to be written
/// to the underlying device.
/// </summary>
public override void Flush() {
for(int index = 0; index < this.streams.Length; ++index) {
this.streams[index].Flush();
}
}
/// <summary>Length of the stream in bytes</summary>
/// <exception cref="NotSupportedException">
/// At least one of the chained streams does not support seeking
/// </exception>
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;
}
}
/// <summary>Absolute position of the file pointer within the stream</summary>
/// <exception cref="NotSupportedException">
/// At least one of the chained streams does not support seeking
/// </exception>
public override long Position {
get {
if(!this.allStreamsCanSeek) {
throw makeSeekNotSupportedException("seek");
}
return this.position;
}
set { moveFilePointer(value); }
}
/// <summary>
/// Reads a sequence of bytes from the stream and advances the position of
/// the file pointer by the number of bytes read.
/// </summary>
/// <param name="buffer">Buffer that will receive the data read from the stream</param>
/// <param name="offset">
/// Offset in the buffer at which the stream will place the data read
/// </param>
/// <param name="count">Maximum number of bytes that will be read</param>
/// <returns>
/// The number of bytes that were actually read from the stream and written into
/// the provided buffer
/// </returns>
/// <exception cref="NotSupportedException">
/// The chained stream at the current position does not support reading
/// </exception>
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;
}
/// <summary>Changes the position of the file pointer</summary>
/// <param name="offset">
/// Offset to move the file pointer by, relative to the position indicated by
/// the <paramref name="origin" /> parameter.
/// </param>
/// <param name="origin">
/// Reference point relative to which the file pointer is placed
/// </param>
/// <returns>The new absolute position within the stream</returns>
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");
}
}
}
/// <summary>Changes the length of the stream</summary>
/// <param name="value">New length the stream shall have</param>
/// <exception cref="NotSupportedException">
/// Always, the stream chainer does not support the SetLength() operation
/// </exception>
public override void SetLength(long value) {
throw new NotSupportedException("Resizing chained streams is not supported");
}
/// <summary>
/// Writes a sequence of bytes to the stream and advances the position of
/// the file pointer by the number of bytes written.
/// </summary>
/// <param name="buffer">
/// Buffer containing the data that will be written to the stream
/// </param>
/// <param name="offset">
/// Offset in the buffer at which the data to be written starts
/// </param>
/// <param name="count">Number of bytes that will be written into the stream</param>
/// <remarks>
/// 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.
/// </remarks>
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;
}
/// <summary>Streams being combined by the stream chainer</summary>
public Stream[] ChainedStreams {
get { return this.streams; }
}
/// <summary>Moves the file pointer</summary>
/// <param name="position">New position the file pointer will be moved to</param>
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;
}
/// <summary>
/// Finds the stream index and local offset for an absolute position within
/// the combined streams.
/// </summary>
/// <param name="overallPosition">Absolute position within the combined streams</param>
/// <param name="streamIndex">
/// Index of the stream the overall position falls into
/// </param>
/// <param name="streamPosition">
/// Local position within the stream indicated by <paramref name="streamIndex" />
/// </param>
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;
}
/// <summary>Determines the capabilities of the chained streams</summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
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;
}
}
/// <summary>
/// Constructs a NotSupportException for an error caused by one of the chained
/// streams having no seek support
/// </summary>
/// <param name="action">Action that was tried to perform</param>
/// <returns>The newly constructed NotSupportedException</returns>
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
)
);
}
/// <summary>Streams that have been chained together</summary>
private Stream[] streams;
/// <summary>Current position of the overall file pointer</summary>
private long position;
/// <summary>Stream we're currently reading from if seeking is not supported</summary>
/// <remarks>
/// If seeking is not supported, the stream chainer will read from each stream
/// until the end was reached
/// sequentially
/// </remarks>
private int activeReadStreamIndex;
/// <summary>Position in the current read stream if seeking is not supported</summary>
/// <remarks>
/// 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.
/// </remarks>
private long activeReadStreamPosition;
/// <summary>Whether all of the chained streams support seeking</summary>
private bool allStreamsCanSeek;
/// <summary>Whether all of the chained streams support reading</summary>
private bool allStreamsCanRead;
/// <summary>Whether all of the chained streams support writing</summary>
private bool allStreamsCanWrite;
}
} // namespace Nuclex.Support.IO

View File

@ -58,7 +58,7 @@ namespace Nuclex.Support.Parsing {
/// <item>
/// <term>Argument</term>
/// <description>
/// 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
/// </description>
/// </item>

View File

@ -50,7 +50,7 @@ namespace Nuclex.Support {
/// <exception cref="System.ArgumentNullException">String is null</exception>
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;