Changed license to Apache License 2.0

This commit is contained in:
Markus Ewald 2024-06-13 18:36:21 +02:00 committed by cygon
parent d3bf0be9d7
commit 9f36d71529
144 changed files with 32422 additions and 32544 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,459 +1,458 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 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.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(params 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.allStreamsCanSeek) {
// 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
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
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(params 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.allStreamsCanSeek) {
// 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

File diff suppressed because it is too large Load diff

View file

@ -1,261 +1,260 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 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.IO;
namespace Nuclex.Support.IO {
/// <summary>Wraps a stream and exposes only a limited region of its data</summary>
public class PartialStream : Stream {
/// <summary>Initializes a new partial stream</summary>
/// <param name="stream">
/// Stream the wrapper will make a limited region accessible of
/// </param>
/// <param name="start">
/// Start index in the stream which becomes the beginning for the wrapper
/// </param>
/// <param name="length">
/// Length the wrapped stream should report and allow access to
/// </param>
public PartialStream(Stream stream, long start, long length) {
if(start < 0) {
throw new ArgumentException("Start index must not be less than 0", "start");
}
if(stream.CanSeek) {
if(start + length > stream.Length) {
throw new ArgumentException(
"Partial stream exceeds end of full stream", "length"
);
}
} else {
if(start != 0) {
throw new ArgumentException(
"The only valid start for unseekable streams is 0", "start"
);
}
}
this.stream = stream;
this.start = start;
this.length = length;
}
/// <summary>Whether data can be read from the stream</summary>
public override bool CanRead {
get { return this.stream.CanRead; }
}
/// <summary>Whether the stream supports seeking</summary>
public override bool CanSeek {
get { return this.stream.CanSeek; }
}
/// <summary>Whether data can be written into the stream</summary>
public override bool CanWrite {
get { return this.stream.CanWrite; }
}
/// <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.stream.Flush();
}
/// <summary>Length of the stream in bytes</summary>
/// <exception cref="NotSupportedException">
/// The wrapped stream does not support seeking
/// </exception>
public override long Length {
get { return this.length; }
}
/// <summary>Absolute position of the file pointer within the stream</summary>
/// <exception cref="NotSupportedException">
/// The wrapped stream does not support seeking
/// </exception>
public override long Position {
get {
if(!this.stream.CanSeek) {
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 wrapped stream does not support reading
/// </exception>
public override int Read(byte[] buffer, int offset, int count) {
if(!this.stream.CanRead) {
throw new NotSupportedException(
"Can't read: the wrapped stream doesn't support reading"
);
}
long remaining = this.length - this.position;
int bytesToRead = (int)Math.Min(count, remaining);
if(this.stream.CanSeek) {
this.stream.Position = this.position + this.start;
}
int bytesRead = this.stream.Read(buffer, offset, bytesToRead);
this.position += bytesRead;
return bytesRead;
}
/// <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 partial 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) {
long remaining = this.length - this.position;
if(count > remaining) {
throw new NotSupportedException(
"Cannot extend the length of the partial stream"
);
}
if(this.stream.CanSeek) {
this.stream.Position = this.position + this.start;
}
this.stream.Write(buffer, offset, count);
this.position += count;
}
/// <summary>Stream being wrapped by the partial stream wrapper</summary>
public Stream CompleteStream {
get { return this.stream; }
}
/// <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.stream.CanSeek) {
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>
/// Constructs a NotSupportException for an error caused by the wrapped
/// stream 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}: the wrapped stream does not support seeking",
action
)
);
}
/// <summary>Streams that have been chained together</summary>
private Stream stream;
/// <summary>Start index of the partial stream in the wrapped stream</summary>
private long start;
/// <summary>Zero-based position of the partial stream's file pointer</summary>
/// <remarks>
/// If the stream does not support seeking, the position will simply be counted
/// up until it reaches <see cref="PartialStream.length" />.
/// </remarks>
private long position;
/// <summary>Length of the partial stream</summary>
private long length;
}
} // namespace Nuclex.Support.IO
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.IO;
namespace Nuclex.Support.IO {
/// <summary>Wraps a stream and exposes only a limited region of its data</summary>
public class PartialStream : Stream {
/// <summary>Initializes a new partial stream</summary>
/// <param name="stream">
/// Stream the wrapper will make a limited region accessible of
/// </param>
/// <param name="start">
/// Start index in the stream which becomes the beginning for the wrapper
/// </param>
/// <param name="length">
/// Length the wrapped stream should report and allow access to
/// </param>
public PartialStream(Stream stream, long start, long length) {
if(start < 0) {
throw new ArgumentException("Start index must not be less than 0", "start");
}
if(stream.CanSeek) {
if(start + length > stream.Length) {
throw new ArgumentException(
"Partial stream exceeds end of full stream", "length"
);
}
} else {
if(start != 0) {
throw new ArgumentException(
"The only valid start for unseekable streams is 0", "start"
);
}
}
this.stream = stream;
this.start = start;
this.length = length;
}
/// <summary>Whether data can be read from the stream</summary>
public override bool CanRead {
get { return this.stream.CanRead; }
}
/// <summary>Whether the stream supports seeking</summary>
public override bool CanSeek {
get { return this.stream.CanSeek; }
}
/// <summary>Whether data can be written into the stream</summary>
public override bool CanWrite {
get { return this.stream.CanWrite; }
}
/// <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.stream.Flush();
}
/// <summary>Length of the stream in bytes</summary>
/// <exception cref="NotSupportedException">
/// The wrapped stream does not support seeking
/// </exception>
public override long Length {
get { return this.length; }
}
/// <summary>Absolute position of the file pointer within the stream</summary>
/// <exception cref="NotSupportedException">
/// The wrapped stream does not support seeking
/// </exception>
public override long Position {
get {
if(!this.stream.CanSeek) {
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 wrapped stream does not support reading
/// </exception>
public override int Read(byte[] buffer, int offset, int count) {
if(!this.stream.CanRead) {
throw new NotSupportedException(
"Can't read: the wrapped stream doesn't support reading"
);
}
long remaining = this.length - this.position;
int bytesToRead = (int)Math.Min(count, remaining);
if(this.stream.CanSeek) {
this.stream.Position = this.position + this.start;
}
int bytesRead = this.stream.Read(buffer, offset, bytesToRead);
this.position += bytesRead;
return bytesRead;
}
/// <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 partial 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) {
long remaining = this.length - this.position;
if(count > remaining) {
throw new NotSupportedException(
"Cannot extend the length of the partial stream"
);
}
if(this.stream.CanSeek) {
this.stream.Position = this.position + this.start;
}
this.stream.Write(buffer, offset, count);
this.position += count;
}
/// <summary>Stream being wrapped by the partial stream wrapper</summary>
public Stream CompleteStream {
get { return this.stream; }
}
/// <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.stream.CanSeek) {
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>
/// Constructs a NotSupportException for an error caused by the wrapped
/// stream 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}: the wrapped stream does not support seeking",
action
)
);
}
/// <summary>Streams that have been chained together</summary>
private Stream stream;
/// <summary>Start index of the partial stream in the wrapped stream</summary>
private long start;
/// <summary>Zero-based position of the partial stream's file pointer</summary>
/// <remarks>
/// If the stream does not support seeking, the position will simply be counted
/// up until it reaches <see cref="PartialStream.length" />.
/// </remarks>
private long position;
/// <summary>Length of the partial stream</summary>
private long length;
}
} // namespace Nuclex.Support.IO

View file

@ -1,330 +1,329 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 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.IO;
#if UNITTEST
using NUnit.Framework;
namespace Nuclex.Support.IO {
/// <summary>Unit Test for the ring buffer class</summary>
[TestFixture]
internal class RingMemoryStreamTest {
/// <summary>Prepares some test data for the units test methods</summary>
[TestFixtureSetUp]
public void Setup() {
this.testBytes = new byte[20];
for(int i = 0; i < 20; ++i)
this.testBytes[i] = (byte)i;
}
/// <summary>
/// Ensures that the ring buffer blocks write attempts that would exceed its capacity
/// </summary>
[Test]
public void TestWriteTooLargeChunk() {
Assert.Throws<OverflowException>(
delegate() { new RingMemoryStream(10).Write(this.testBytes, 0, 11); }
);
}
/// <summary>
/// Ensures that the ring buffer still accepts write attempts that would fill the
/// entire buffer in one go.
/// </summary>
[Test]
public void TestWriteBarelyFittingChunk() {
new RingMemoryStream(10).Write(this.testBytes, 0, 10);
}
/// <summary>
/// Ensures that the ring buffer correctly manages write attempts that have to
/// be split at the end of the ring buffer
/// </summary>
[Test]
public void TestWriteSplitBlock() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 8);
testRing.Read(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 7);
byte[] actual = new byte[10];
testRing.Read(actual, 0, 10);
Assert.AreEqual(new byte[] { 5, 6, 7, 0, 1, 2, 3, 4, 5, 6 }, actual);
}
/// <summary>
/// Ensures that the ring buffer correctly manages write attempts that write into
/// the gap after the ring buffer's data has become split
/// </summary>
[Test]
public void TestWriteSplitAndLinearBlock() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 8);
testRing.Read(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 2);
byte[] actual = new byte[10];
testRing.Read(actual, 0, 10);
Assert.AreEqual(new byte[] { 5, 6, 7, 0, 1, 2, 3, 4, 0, 1 }, actual);
}
/// <summary>
/// Ensures that the ring buffer still detects write that would exceed its capacity
/// if they write into the gap after the ring buffer's data has become split
/// </summary>
[Test]
public void TestWriteSplitAndLinearTooLargeBlock() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 8);
testRing.Read(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 5);
Assert.Throws<OverflowException>(
delegate() { testRing.Write(this.testBytes, 0, 3); }
);
}
/// <summary>Tests whether the ring buffer correctly handles fragmentation</summary>
[Test]
public void TestSplitBlockWrappedRead() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 10);
testRing.Read(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 5);
byte[] actual = new byte[10];
testRing.Read(actual, 0, 10);
Assert.AreEqual(new byte[] { 5, 6, 7, 8, 9, 0, 1, 2, 3, 4 }, actual);
}
/// <summary>Tests whether the ring buffer correctly handles fragmentation</summary>
[Test]
public void TestSplitBlockLinearRead() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 10);
testRing.Read(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 5);
byte[] actual = new byte[5];
testRing.Read(actual, 0, 5);
Assert.AreEqual(new byte[] { 5, 6, 7, 8, 9 }, actual);
}
/// <summary>
/// Tests whether the ring buffer correctly returns partial data if more
/// data is requested than is contained in it.
/// </summary>
[Test]
public void TestEndOfStream() {
byte[] tempBytes = new byte[10];
RingMemoryStream testRing = new RingMemoryStream(10);
Assert.AreEqual(0, testRing.Read(tempBytes, 0, 5));
testRing.Write(this.testBytes, 0, 5);
Assert.AreEqual(5, testRing.Read(tempBytes, 0, 10));
testRing.Write(this.testBytes, 0, 6);
testRing.Read(tempBytes, 0, 5);
testRing.Write(this.testBytes, 0, 9);
Assert.AreEqual(10, testRing.Read(tempBytes, 0, 20));
}
/// <summary>
/// Validates that the ring buffer can extend its capacity without loosing data
/// </summary>
[Test]
public void TestCapacityIncrease() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 10);
testRing.Capacity = 20;
byte[] actual = new byte[10];
testRing.Read(actual, 0, 10);
Assert.AreEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, actual);
}
/// <summary>
/// Validates that the ring buffer can reduce its capacity without loosing data
/// </summary>
[Test]
public void TestCapacityDecrease() {
RingMemoryStream testRing = new RingMemoryStream(20);
testRing.Write(this.testBytes, 0, 10);
testRing.Capacity = 10;
byte[] actual = new byte[10];
testRing.Read(actual, 0, 10);
Assert.AreEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, actual);
}
/// <summary>
/// Checks that an exception is thrown when the ring buffer's capacity is
/// reduced so much it would have to give up some of its contained data
/// </summary>
[Test]
public void TestCapacityDecreaseException() {
RingMemoryStream testRing = new RingMemoryStream(20);
testRing.Write(this.testBytes, 0, 20);
Assert.Throws<ArgumentOutOfRangeException>(
delegate() { testRing.Capacity = 10; }
);
}
/// <summary>Tests whether the Capacity property returns the current capacity</summary>
[Test]
public void TestCapacity() {
RingMemoryStream testRing = new RingMemoryStream(123);
Assert.AreEqual(123, testRing.Capacity);
}
/// <summary>Ensures that the CanRead property returns true</summary>
[Test]
public void TestCanRead() {
Assert.IsTrue(new RingMemoryStream(10).CanRead);
}
/// <summary>Ensures that the CanSeek property returns false</summary>
[Test]
public void TestCanSeek() {
Assert.IsFalse(new RingMemoryStream(10).CanSeek);
}
/// <summary>Ensures that the CanWrite property returns true</summary>
[Test]
public void TestCanWrite() {
Assert.IsTrue(new RingMemoryStream(10).CanWrite);
}
/// <summary>
/// Tests whether the auto reset feature works (resets the buffer pointer to the
/// left end of the buffer when it gets empty; mainly a performance feature).
/// </summary>
[Test]
public void TestAutoReset() {
byte[] tempBytes = new byte[10];
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 8);
testRing.Read(tempBytes, 0, 2);
testRing.Read(tempBytes, 0, 2);
testRing.Read(tempBytes, 0, 1);
testRing.Read(tempBytes, 0, 1);
Assert.AreEqual(2, testRing.Length);
}
/// <summary>
/// Verifies that an exception is thrown when the Position property of the ring
/// memory stream is used to retrieve the current file pointer position
/// </summary>
[Test]
public void TestThrowOnRetrievePosition() {
Assert.Throws<NotSupportedException>(
delegate() { Console.WriteLine(new RingMemoryStream(10).Position); }
);
}
/// <summary>
/// Verifies that an exception is thrown when the Position property of the ring
/// memory stream is used to modify the current file pointer position
/// </summary>
[Test]
public void TestThrowOnAssignPosition() {
Assert.Throws<NotSupportedException>(
delegate() { new RingMemoryStream(10).Position = 0; }
);
}
/// <summary>
/// Verifies that an exception is thrown when the Seek() method of the ring memory
/// stream is attempted to be used
/// </summary>
[Test]
public void TestThrowOnSeek() {
Assert.Throws<NotSupportedException>(
delegate() { new RingMemoryStream(10).Seek(0, SeekOrigin.Begin); }
);
}
/// <summary>
/// Verifies that an exception is thrown when the SetLength() method of the ring
/// memory stream is attempted to be used
/// </summary>
[Test]
public void TestThrowOnSetLength() {
Assert.Throws<NotSupportedException>(
delegate() { new RingMemoryStream(10).SetLength(10); }
);
}
/// <summary>
/// Tests the Flush() method of the ring memory stream, which is either a dummy
/// implementation or has no side effects
/// </summary>
[Test]
public void TestFlush() {
new RingMemoryStream(10).Flush();
}
/// <summary>
/// Tests whether the length property is updated in accordance to the data written
/// into the ring memory stream
/// </summary>
[Test]
public void TestLengthOnLinearBlock() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(new byte[10], 0, 10);
Assert.AreEqual(10, testRing.Length);
}
/// <summary>
/// Tests whether the length property is updated in accordance to the data written
/// into the ring memory stream when the data is split within the stream
/// </summary>
[Test]
public void TestLengthOnSplitBlock() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(new byte[10], 0, 10);
testRing.Read(new byte[5], 0, 5);
testRing.Write(new byte[5], 0, 5);
Assert.AreEqual(10, testRing.Length);
}
/// <summary>Test data for the ring buffer unit tests</summary>
private byte[] testBytes;
}
} // namespace Nuclex.Support.IO
#endif // UNITTEST
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.IO;
#if UNITTEST
using NUnit.Framework;
namespace Nuclex.Support.IO {
/// <summary>Unit Test for the ring buffer class</summary>
[TestFixture]
internal class RingMemoryStreamTest {
/// <summary>Prepares some test data for the units test methods</summary>
[TestFixtureSetUp]
public void Setup() {
this.testBytes = new byte[20];
for(int i = 0; i < 20; ++i)
this.testBytes[i] = (byte)i;
}
/// <summary>
/// Ensures that the ring buffer blocks write attempts that would exceed its capacity
/// </summary>
[Test]
public void TestWriteTooLargeChunk() {
Assert.Throws<OverflowException>(
delegate() { new RingMemoryStream(10).Write(this.testBytes, 0, 11); }
);
}
/// <summary>
/// Ensures that the ring buffer still accepts write attempts that would fill the
/// entire buffer in one go.
/// </summary>
[Test]
public void TestWriteBarelyFittingChunk() {
new RingMemoryStream(10).Write(this.testBytes, 0, 10);
}
/// <summary>
/// Ensures that the ring buffer correctly manages write attempts that have to
/// be split at the end of the ring buffer
/// </summary>
[Test]
public void TestWriteSplitBlock() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 8);
testRing.Read(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 7);
byte[] actual = new byte[10];
testRing.Read(actual, 0, 10);
Assert.AreEqual(new byte[] { 5, 6, 7, 0, 1, 2, 3, 4, 5, 6 }, actual);
}
/// <summary>
/// Ensures that the ring buffer correctly manages write attempts that write into
/// the gap after the ring buffer's data has become split
/// </summary>
[Test]
public void TestWriteSplitAndLinearBlock() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 8);
testRing.Read(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 2);
byte[] actual = new byte[10];
testRing.Read(actual, 0, 10);
Assert.AreEqual(new byte[] { 5, 6, 7, 0, 1, 2, 3, 4, 0, 1 }, actual);
}
/// <summary>
/// Ensures that the ring buffer still detects write that would exceed its capacity
/// if they write into the gap after the ring buffer's data has become split
/// </summary>
[Test]
public void TestWriteSplitAndLinearTooLargeBlock() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 8);
testRing.Read(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 5);
Assert.Throws<OverflowException>(
delegate() { testRing.Write(this.testBytes, 0, 3); }
);
}
/// <summary>Tests whether the ring buffer correctly handles fragmentation</summary>
[Test]
public void TestSplitBlockWrappedRead() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 10);
testRing.Read(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 5);
byte[] actual = new byte[10];
testRing.Read(actual, 0, 10);
Assert.AreEqual(new byte[] { 5, 6, 7, 8, 9, 0, 1, 2, 3, 4 }, actual);
}
/// <summary>Tests whether the ring buffer correctly handles fragmentation</summary>
[Test]
public void TestSplitBlockLinearRead() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 10);
testRing.Read(this.testBytes, 0, 5);
testRing.Write(this.testBytes, 0, 5);
byte[] actual = new byte[5];
testRing.Read(actual, 0, 5);
Assert.AreEqual(new byte[] { 5, 6, 7, 8, 9 }, actual);
}
/// <summary>
/// Tests whether the ring buffer correctly returns partial data if more
/// data is requested than is contained in it.
/// </summary>
[Test]
public void TestEndOfStream() {
byte[] tempBytes = new byte[10];
RingMemoryStream testRing = new RingMemoryStream(10);
Assert.AreEqual(0, testRing.Read(tempBytes, 0, 5));
testRing.Write(this.testBytes, 0, 5);
Assert.AreEqual(5, testRing.Read(tempBytes, 0, 10));
testRing.Write(this.testBytes, 0, 6);
testRing.Read(tempBytes, 0, 5);
testRing.Write(this.testBytes, 0, 9);
Assert.AreEqual(10, testRing.Read(tempBytes, 0, 20));
}
/// <summary>
/// Validates that the ring buffer can extend its capacity without loosing data
/// </summary>
[Test]
public void TestCapacityIncrease() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 10);
testRing.Capacity = 20;
byte[] actual = new byte[10];
testRing.Read(actual, 0, 10);
Assert.AreEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, actual);
}
/// <summary>
/// Validates that the ring buffer can reduce its capacity without loosing data
/// </summary>
[Test]
public void TestCapacityDecrease() {
RingMemoryStream testRing = new RingMemoryStream(20);
testRing.Write(this.testBytes, 0, 10);
testRing.Capacity = 10;
byte[] actual = new byte[10];
testRing.Read(actual, 0, 10);
Assert.AreEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, actual);
}
/// <summary>
/// Checks that an exception is thrown when the ring buffer's capacity is
/// reduced so much it would have to give up some of its contained data
/// </summary>
[Test]
public void TestCapacityDecreaseException() {
RingMemoryStream testRing = new RingMemoryStream(20);
testRing.Write(this.testBytes, 0, 20);
Assert.Throws<ArgumentOutOfRangeException>(
delegate() { testRing.Capacity = 10; }
);
}
/// <summary>Tests whether the Capacity property returns the current capacity</summary>
[Test]
public void TestCapacity() {
RingMemoryStream testRing = new RingMemoryStream(123);
Assert.AreEqual(123, testRing.Capacity);
}
/// <summary>Ensures that the CanRead property returns true</summary>
[Test]
public void TestCanRead() {
Assert.IsTrue(new RingMemoryStream(10).CanRead);
}
/// <summary>Ensures that the CanSeek property returns false</summary>
[Test]
public void TestCanSeek() {
Assert.IsFalse(new RingMemoryStream(10).CanSeek);
}
/// <summary>Ensures that the CanWrite property returns true</summary>
[Test]
public void TestCanWrite() {
Assert.IsTrue(new RingMemoryStream(10).CanWrite);
}
/// <summary>
/// Tests whether the auto reset feature works (resets the buffer pointer to the
/// left end of the buffer when it gets empty; mainly a performance feature).
/// </summary>
[Test]
public void TestAutoReset() {
byte[] tempBytes = new byte[10];
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(this.testBytes, 0, 8);
testRing.Read(tempBytes, 0, 2);
testRing.Read(tempBytes, 0, 2);
testRing.Read(tempBytes, 0, 1);
testRing.Read(tempBytes, 0, 1);
Assert.AreEqual(2, testRing.Length);
}
/// <summary>
/// Verifies that an exception is thrown when the Position property of the ring
/// memory stream is used to retrieve the current file pointer position
/// </summary>
[Test]
public void TestThrowOnRetrievePosition() {
Assert.Throws<NotSupportedException>(
delegate() { Console.WriteLine(new RingMemoryStream(10).Position); }
);
}
/// <summary>
/// Verifies that an exception is thrown when the Position property of the ring
/// memory stream is used to modify the current file pointer position
/// </summary>
[Test]
public void TestThrowOnAssignPosition() {
Assert.Throws<NotSupportedException>(
delegate() { new RingMemoryStream(10).Position = 0; }
);
}
/// <summary>
/// Verifies that an exception is thrown when the Seek() method of the ring memory
/// stream is attempted to be used
/// </summary>
[Test]
public void TestThrowOnSeek() {
Assert.Throws<NotSupportedException>(
delegate() { new RingMemoryStream(10).Seek(0, SeekOrigin.Begin); }
);
}
/// <summary>
/// Verifies that an exception is thrown when the SetLength() method of the ring
/// memory stream is attempted to be used
/// </summary>
[Test]
public void TestThrowOnSetLength() {
Assert.Throws<NotSupportedException>(
delegate() { new RingMemoryStream(10).SetLength(10); }
);
}
/// <summary>
/// Tests the Flush() method of the ring memory stream, which is either a dummy
/// implementation or has no side effects
/// </summary>
[Test]
public void TestFlush() {
new RingMemoryStream(10).Flush();
}
/// <summary>
/// Tests whether the length property is updated in accordance to the data written
/// into the ring memory stream
/// </summary>
[Test]
public void TestLengthOnLinearBlock() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(new byte[10], 0, 10);
Assert.AreEqual(10, testRing.Length);
}
/// <summary>
/// Tests whether the length property is updated in accordance to the data written
/// into the ring memory stream when the data is split within the stream
/// </summary>
[Test]
public void TestLengthOnSplitBlock() {
RingMemoryStream testRing = new RingMemoryStream(10);
testRing.Write(new byte[10], 0, 10);
testRing.Read(new byte[5], 0, 5);
testRing.Write(new byte[5], 0, 5);
Assert.AreEqual(10, testRing.Length);
}
/// <summary>Test data for the ring buffer unit tests</summary>
private byte[] testBytes;
}
} // namespace Nuclex.Support.IO
#endif // UNITTEST

View file

@ -1,256 +1,255 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 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.IO;
namespace Nuclex.Support.IO {
/// <summary>Specialized memory stream for ring buffers</summary>
/// <remarks>
/// This ring buffer class is specialized for binary data and tries to achieve
/// optimal efficiency when storing and retrieving chunks of several bytes
/// at once. Typical use cases include audio and network buffers where one party
/// is responsible for refilling the buffer at regular intervals while the other
/// constantly streams data out of it.
/// </remarks>
public class RingMemoryStream : Stream {
/// <summary>Initializes a new ring memory stream</summary>
/// <param name="capacity">Maximum capacity of the stream</param>
public RingMemoryStream(int capacity) {
this.ringBuffer = new MemoryStream(capacity);
this.ringBuffer.SetLength(capacity);
this.empty = true;
}
/// <summary>Maximum amount of data that will fit into the ring memory stream</summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the new capacity is too small for the data already contained
/// in the ring buffer.
/// </exception>
public long Capacity {
get { return this.ringBuffer.Length; }
set {
int length = (int)Length;
if(value < length) {
throw new ArgumentOutOfRangeException(
"New capacity is less than the stream's current length"
);
}
// This could be done in a more efficient manner than just replacing
// the entire buffer, but since this operation will probably be only called
// once during the lifetime of the application, if at all, I don't see
// the need to optimize it...
MemoryStream newBuffer = new MemoryStream((int)value);
newBuffer.SetLength(value);
if(length > 0) {
Read(newBuffer.GetBuffer(), 0, length);
}
this.ringBuffer.Close(); // Equals dispose of the old buffer
this.ringBuffer = newBuffer;
this.startIndex = 0;
this.endIndex = length;
}
}
/// <summary>Whether it's possible to read from this stream</summary>
public override bool CanRead { get { return true; } }
/// <summary>Whether this stream supports random access</summary>
public override bool CanSeek { get { return false; } }
/// <summary>Whether it's possible to write into this stream</summary>
public override bool CanWrite { get { return true; } }
/// <summary>Flushes the buffers and writes down unsaved data</summary>
public override void Flush() { }
/// <summary>Current length of the stream</summary>
public override long Length {
get {
if((this.endIndex > this.startIndex) || this.empty) {
return this.endIndex - this.startIndex;
} else {
return this.ringBuffer.Length - this.startIndex + this.endIndex;
}
}
}
/// <summary>Current cursor position within the stream</summary>
/// <exception cref="NotSupportedException">Always</exception>
public override long Position {
get { throw new NotSupportedException("The ring buffer does not support seeking"); }
set { throw new NotSupportedException("The ring buffer does not support seeking"); }
}
/// <summary>Reads data from the beginning of the stream</summary>
/// <param name="buffer">Buffer in which to store the data</param>
/// <param name="offset">Starting index at which to begin writing the buffer</param>
/// <param name="count">Number of bytes to read from the stream</param>
/// <returns>Die Number of bytes actually read</returns>
public override int Read(byte[] buffer, int offset, int count) {
// The end index lies behind the start index (usual case), so the
// ring memory is not fragmented. Example: |-----<#######>-----|
if((this.startIndex < this.endIndex) || this.empty) {
// The Stream interface requires us to return less than the requested
// number of bytes if we don't have enough data
count = Math.Min(count, this.endIndex - this.startIndex);
if(count > 0) {
this.ringBuffer.Position = this.startIndex;
this.ringBuffer.Read(buffer, offset, count);
this.startIndex += count;
if(this.startIndex == this.endIndex) {
setEmpty();
}
}
} else { // The end index lies in front of the start index
// With the end before the start index, the data in the ring memory
// stream is fragmented. Example: |#####>-------<#####|
int linearAvailable = (int)this.ringBuffer.Length - this.startIndex;
// Will this read process cross the end of the ring buffer, requiring us to
// read the data in 2 steps?
if(count > linearAvailable) {
// The Stream interface requires us to return less than the requested
// number of bytes if we don't have enough data
count = Math.Min(count, linearAvailable + this.endIndex);
this.ringBuffer.Position = this.startIndex;
this.ringBuffer.Read(buffer, offset, linearAvailable);
this.ringBuffer.Position = 0;
this.startIndex = count - linearAvailable;
this.ringBuffer.Read(buffer, offset + linearAvailable, this.startIndex);
} else { // Nope, the amount of requested data can be read in one piece
this.ringBuffer.Position = this.startIndex;
this.ringBuffer.Read(buffer, offset, count);
this.startIndex += count;
}
// If we consumed the entire ring buffer, set the empty flag and move
// the indexes back to zero for better performance
if(this.startIndex == this.endIndex) {
setEmpty();
}
}
return count;
}
/// <summary>Appends data to the end of the stream</summary>
/// <param name="buffer">Buffer containing the data to append</param>
/// <param name="offset">Starting index of the data in the buffer</param>
/// <param name="count">Number of bytes to write to the stream</param>
/// <exception cref="OverflowException">When the ring buffer is full</exception>
public override void Write(byte[] buffer, int offset, int count) {
// The end index lies behind the start index (usual case), so the
// unused buffer space is fragmented. Example: |-----<#######>-----|
if((this.startIndex < this.endIndex) || this.empty) {
int linearAvailable = (int)(this.ringBuffer.Length - this.endIndex);
// If the data to be written would cross the ring memory stream's end,
// we have to check that there's enough space at the beginning of the
// stream to contain the remainder of the data.
if(count > linearAvailable) {
if(count > (linearAvailable + this.startIndex))
throw new OverflowException("Data does not fit in buffer");
this.ringBuffer.Position = this.endIndex;
this.ringBuffer.Write(buffer, offset, linearAvailable);
this.ringBuffer.Position = 0;
this.endIndex = count - linearAvailable;
this.ringBuffer.Write(buffer, offset + linearAvailable, this.endIndex);
} else { // All data can be appended at the current stream position
this.ringBuffer.Position = this.endIndex;
this.ringBuffer.Write(buffer, offset, count);
this.endIndex += count;
}
this.empty = false;
} else { // The end index lies before the start index
// The ring memory stream has been fragmented. This means the gap into which
// we are about to write is not fragmented. Example: |#####>-------<#####|
if(count > (this.startIndex - this.endIndex))
throw new OverflowException("Data does not fit in buffer");
// Because the gap isn't fragmented, we can be sure that a single
// write call will suffice.
this.ringBuffer.Position = this.endIndex;
this.ringBuffer.Write(buffer, offset, count);
this.endIndex += count;
}
}
/// <summary>Jumps to the specified location within the stream</summary>
/// <param name="offset">Position to jump to</param>
/// <param name="origin">Origin towards which to interpret the offset</param>
/// <returns>The new offset within the stream</returns>
/// <exception cref="NotSupportedException">Always</exception>
public override long Seek(long offset, SeekOrigin origin) {
throw new NotSupportedException("The ring buffer does not support seeking");
}
/// <summary>Changes the length of the stream</summary>
/// <param name="value">New length to resize the stream to</param>
/// <exception cref="NotSupportedException">Always</exception>
public override void SetLength(long value) {
throw new NotSupportedException("This operation is not supported");
}
/// <summary>Resets the stream to its empty state</summary>
private void setEmpty() {
this.empty = true;
this.startIndex = 0;
this.endIndex = 0;
}
/// <summary>Internal stream containing the ring buffer data</summary>
private MemoryStream ringBuffer;
/// <summary>Start index of the data within the ring buffer</summary>
private int startIndex;
/// <summary>End index of the data within the ring buffer</summary>
private int endIndex;
/// <summary>Whether the ring buffer is empty</summary>
/// <remarks>
/// This field is required to differentiate between the ring buffer being
/// filled to the limit and being totally empty, because in both cases,
/// the start index and the end index will be the same.
/// </remarks>
private bool empty;
}
} // namespace Nuclex.Support.IO
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.IO;
namespace Nuclex.Support.IO {
/// <summary>Specialized memory stream for ring buffers</summary>
/// <remarks>
/// This ring buffer class is specialized for binary data and tries to achieve
/// optimal efficiency when storing and retrieving chunks of several bytes
/// at once. Typical use cases include audio and network buffers where one party
/// is responsible for refilling the buffer at regular intervals while the other
/// constantly streams data out of it.
/// </remarks>
public class RingMemoryStream : Stream {
/// <summary>Initializes a new ring memory stream</summary>
/// <param name="capacity">Maximum capacity of the stream</param>
public RingMemoryStream(int capacity) {
this.ringBuffer = new MemoryStream(capacity);
this.ringBuffer.SetLength(capacity);
this.empty = true;
}
/// <summary>Maximum amount of data that will fit into the ring memory stream</summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the new capacity is too small for the data already contained
/// in the ring buffer.
/// </exception>
public long Capacity {
get { return this.ringBuffer.Length; }
set {
int length = (int)Length;
if(value < length) {
throw new ArgumentOutOfRangeException(
"New capacity is less than the stream's current length"
);
}
// This could be done in a more efficient manner than just replacing
// the entire buffer, but since this operation will probably be only called
// once during the lifetime of the application, if at all, I don't see
// the need to optimize it...
MemoryStream newBuffer = new MemoryStream((int)value);
newBuffer.SetLength(value);
if(length > 0) {
Read(newBuffer.GetBuffer(), 0, length);
}
this.ringBuffer.Close(); // Equals dispose of the old buffer
this.ringBuffer = newBuffer;
this.startIndex = 0;
this.endIndex = length;
}
}
/// <summary>Whether it's possible to read from this stream</summary>
public override bool CanRead { get { return true; } }
/// <summary>Whether this stream supports random access</summary>
public override bool CanSeek { get { return false; } }
/// <summary>Whether it's possible to write into this stream</summary>
public override bool CanWrite { get { return true; } }
/// <summary>Flushes the buffers and writes down unsaved data</summary>
public override void Flush() { }
/// <summary>Current length of the stream</summary>
public override long Length {
get {
if((this.endIndex > this.startIndex) || this.empty) {
return this.endIndex - this.startIndex;
} else {
return this.ringBuffer.Length - this.startIndex + this.endIndex;
}
}
}
/// <summary>Current cursor position within the stream</summary>
/// <exception cref="NotSupportedException">Always</exception>
public override long Position {
get { throw new NotSupportedException("The ring buffer does not support seeking"); }
set { throw new NotSupportedException("The ring buffer does not support seeking"); }
}
/// <summary>Reads data from the beginning of the stream</summary>
/// <param name="buffer">Buffer in which to store the data</param>
/// <param name="offset">Starting index at which to begin writing the buffer</param>
/// <param name="count">Number of bytes to read from the stream</param>
/// <returns>Die Number of bytes actually read</returns>
public override int Read(byte[] buffer, int offset, int count) {
// The end index lies behind the start index (usual case), so the
// ring memory is not fragmented. Example: |-----<#######>-----|
if((this.startIndex < this.endIndex) || this.empty) {
// The Stream interface requires us to return less than the requested
// number of bytes if we don't have enough data
count = Math.Min(count, this.endIndex - this.startIndex);
if(count > 0) {
this.ringBuffer.Position = this.startIndex;
this.ringBuffer.Read(buffer, offset, count);
this.startIndex += count;
if(this.startIndex == this.endIndex) {
setEmpty();
}
}
} else { // The end index lies in front of the start index
// With the end before the start index, the data in the ring memory
// stream is fragmented. Example: |#####>-------<#####|
int linearAvailable = (int)this.ringBuffer.Length - this.startIndex;
// Will this read process cross the end of the ring buffer, requiring us to
// read the data in 2 steps?
if(count > linearAvailable) {
// The Stream interface requires us to return less than the requested
// number of bytes if we don't have enough data
count = Math.Min(count, linearAvailable + this.endIndex);
this.ringBuffer.Position = this.startIndex;
this.ringBuffer.Read(buffer, offset, linearAvailable);
this.ringBuffer.Position = 0;
this.startIndex = count - linearAvailable;
this.ringBuffer.Read(buffer, offset + linearAvailable, this.startIndex);
} else { // Nope, the amount of requested data can be read in one piece
this.ringBuffer.Position = this.startIndex;
this.ringBuffer.Read(buffer, offset, count);
this.startIndex += count;
}
// If we consumed the entire ring buffer, set the empty flag and move
// the indexes back to zero for better performance
if(this.startIndex == this.endIndex) {
setEmpty();
}
}
return count;
}
/// <summary>Appends data to the end of the stream</summary>
/// <param name="buffer">Buffer containing the data to append</param>
/// <param name="offset">Starting index of the data in the buffer</param>
/// <param name="count">Number of bytes to write to the stream</param>
/// <exception cref="OverflowException">When the ring buffer is full</exception>
public override void Write(byte[] buffer, int offset, int count) {
// The end index lies behind the start index (usual case), so the
// unused buffer space is fragmented. Example: |-----<#######>-----|
if((this.startIndex < this.endIndex) || this.empty) {
int linearAvailable = (int)(this.ringBuffer.Length - this.endIndex);
// If the data to be written would cross the ring memory stream's end,
// we have to check that there's enough space at the beginning of the
// stream to contain the remainder of the data.
if(count > linearAvailable) {
if(count > (linearAvailable + this.startIndex))
throw new OverflowException("Data does not fit in buffer");
this.ringBuffer.Position = this.endIndex;
this.ringBuffer.Write(buffer, offset, linearAvailable);
this.ringBuffer.Position = 0;
this.endIndex = count - linearAvailable;
this.ringBuffer.Write(buffer, offset + linearAvailable, this.endIndex);
} else { // All data can be appended at the current stream position
this.ringBuffer.Position = this.endIndex;
this.ringBuffer.Write(buffer, offset, count);
this.endIndex += count;
}
this.empty = false;
} else { // The end index lies before the start index
// The ring memory stream has been fragmented. This means the gap into which
// we are about to write is not fragmented. Example: |#####>-------<#####|
if(count > (this.startIndex - this.endIndex))
throw new OverflowException("Data does not fit in buffer");
// Because the gap isn't fragmented, we can be sure that a single
// write call will suffice.
this.ringBuffer.Position = this.endIndex;
this.ringBuffer.Write(buffer, offset, count);
this.endIndex += count;
}
}
/// <summary>Jumps to the specified location within the stream</summary>
/// <param name="offset">Position to jump to</param>
/// <param name="origin">Origin towards which to interpret the offset</param>
/// <returns>The new offset within the stream</returns>
/// <exception cref="NotSupportedException">Always</exception>
public override long Seek(long offset, SeekOrigin origin) {
throw new NotSupportedException("The ring buffer does not support seeking");
}
/// <summary>Changes the length of the stream</summary>
/// <param name="value">New length to resize the stream to</param>
/// <exception cref="NotSupportedException">Always</exception>
public override void SetLength(long value) {
throw new NotSupportedException("This operation is not supported");
}
/// <summary>Resets the stream to its empty state</summary>
private void setEmpty() {
this.empty = true;
this.startIndex = 0;
this.endIndex = 0;
}
/// <summary>Internal stream containing the ring buffer data</summary>
private MemoryStream ringBuffer;
/// <summary>Start index of the data within the ring buffer</summary>
private int startIndex;
/// <summary>End index of the data within the ring buffer</summary>
private int endIndex;
/// <summary>Whether the ring buffer is empty</summary>
/// <remarks>
/// This field is required to differentiate between the ring buffer being
/// filled to the limit and being totally empty, because in both cases,
/// the start index and the end index will be the same.
/// </remarks>
private bool empty;
}
} // namespace Nuclex.Support.IO