Changed license to Apache License 2.0
This commit is contained in:
parent
d3bf0be9d7
commit
9f36d71529
144 changed files with 32422 additions and 32544 deletions
File diff suppressed because it is too large
Load diff
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue