#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 {
/// Wraps a stream and exposes only a limited region of its data
public class PartialStream : Stream {
/// Initializes a new partial stream
///
/// Stream the wrapper will make a limited region accessible of
///
///
/// Start index in the stream which becomes the beginning for the wrapper
///
///
/// Length the wrapped stream should report and allow access to
///
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;
}
/// Whether data can be read from the stream
public override bool CanRead {
get { return this.stream.CanRead; }
}
/// Whether the stream supports seeking
public override bool CanSeek {
get { return this.stream.CanSeek; }
}
/// Whether data can be written into the stream
public override bool CanWrite {
get { return this.stream.CanWrite; }
}
///
/// Clears all buffers for this stream and causes any buffered data to be written
/// to the underlying device.
///
public override void Flush() {
this.stream.Flush();
}
/// Length of the stream in bytes
///
/// The wrapped stream does not support seeking
///
public override long Length {
get { return this.length; }
}
/// Absolute position of the file pointer within the stream
///
/// The wrapped stream does not support seeking
///
public override long Position {
get {
if(!this.stream.CanSeek) {
throw makeSeekNotSupportedException("seek");
}
return this.position;
}
set { moveFilePointer(value); }
}
///
/// Reads a sequence of bytes from the stream and advances the position of
/// the file pointer by the number of bytes read.
///
/// Buffer that will receive the data read from the stream
///
/// Offset in the buffer at which the stream will place the data read
///
/// Maximum number of bytes that will be read
///
/// The number of bytes that were actually read from the stream and written into
/// the provided buffer
///
///
/// The wrapped stream does not support reading
///
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;
}
/// Changes the position of the file pointer
///
/// Offset to move the file pointer by, relative to the position indicated by
/// the parameter.
///
///
/// Reference point relative to which the file pointer is placed
///
/// The new absolute position within the stream
public override long Seek(long offset, SeekOrigin origin) {
switch(origin) {
case SeekOrigin.Begin: {
return Position = offset;
}
case SeekOrigin.Current: {
return Position += offset;
}
case SeekOrigin.End: {
return Position = (Length + offset);
}
default: {
throw new ArgumentException("Invalid seek origin", "origin");
}
}
}
/// Changes the length of the stream
/// New length the stream shall have
///
/// Always, the stream chainer does not support the SetLength() operation
///
public override void SetLength(long value) {
throw new NotSupportedException("Resizing partial streams is not supported");
}
///
/// Writes a sequence of bytes to the stream and advances the position of
/// the file pointer by the number of bytes written.
///
///
/// Buffer containing the data that will be written to the stream
///
///
/// Offset in the buffer at which the data to be written starts
///
/// Number of bytes that will be written into the stream
///
/// The behavior of this method is as follows: If one or more chained streams
/// do not support seeking, all data is appended to the final stream in the
/// chain. Otherwise, writing will begin with the stream the current file pointer
/// offset falls into. If the end of that stream is reached, writing continues
/// in the next stream. On the last stream, writing more data into the stream
/// that it current size allows will enlarge the stream.
///
public override void Write(byte[] buffer, int offset, int count) {
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;
}
/// Stream being wrapped by the partial stream wrapper
public Stream CompleteStream {
get { return this.stream; }
}
/// Moves the file pointer
/// New position the file pointer will be moved to
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;
}
///
/// Constructs a NotSupportException for an error caused by the wrapped
/// stream having no seek support
///
/// Action that was tried to perform
/// The newly constructed NotSupportedException
private static NotSupportedException makeSeekNotSupportedException(string action) {
return new NotSupportedException(
string.Format(
"Can't {0}: the wrapped stream does not support seeking",
action
)
);
}
/// Streams that have been chained together
private Stream stream;
/// Start index of the partial stream in the wrapped stream
private long start;
/// Zero-based position of the partial stream's file pointer
///
/// If the stream does not support seeking, the position will simply be counted
/// up until it reaches .
///
private long position;
/// Length of the partial stream
private long length;
}
} // namespace Nuclex.Support.IO