using System;
using System.IO;
using System.Text;
using MiscUtil.Conversion;
namespace MiscUtil.IO
{
///
/// Equivalent of System.IO.BinaryReader, but with either endianness, depending on
/// the EndianBitConverter it is constructed with. No data is buffered in the
/// reader; the client may seek within the stream at will.
///
public class EndianBinaryReader : IDisposable
{
#region Fields not directly related to properties
///
/// Whether or not this reader has been disposed yet.
///
bool disposed=false;
///
/// Decoder to use for string conversions.
///
Decoder decoder;
///
/// Buffer used for temporary storage before conversion into primitives
///
byte[] buffer = new byte[16];
///
/// Buffer used for temporary storage when reading a single character
///
char[] charBuffer = new char[1];
///
/// Minimum number of bytes used to encode a character
///
int minBytesPerChar;
#endregion
#region Constructors
///
/// Equivalent of System.IO.BinaryWriter, but with either endianness, depending on
/// the EndianBitConverter it is constructed with.
///
/// Converter to use when reading data
/// Stream to read data from
public EndianBinaryReader (EndianBitConverter bitConverter,
Stream stream) : this (bitConverter, stream, Encoding.UTF8)
{
}
///
/// Constructs a new binary reader with the given bit converter, reading
/// to the given stream, using the given encoding.
///
/// Converter to use when reading data
/// Stream to read data from
/// Encoding to use when reading character data
public EndianBinaryReader (EndianBitConverter bitConverter, Stream stream, Encoding encoding)
{
if (bitConverter==null)
{
throw new ArgumentNullException("bitConverter");
}
if (stream==null)
{
throw new ArgumentNullException("stream");
}
if (encoding==null)
{
throw new ArgumentNullException("encoding");
}
if (!stream.CanRead)
{
throw new ArgumentException("Stream isn't writable", "stream");
}
this.stream = stream;
this.bitConverter = bitConverter;
this.encoding = encoding;
this.decoder = encoding.GetDecoder();
this.minBytesPerChar = 1;
if (encoding is UnicodeEncoding)
{
minBytesPerChar = 2;
}
}
#endregion
#region Properties
EndianBitConverter bitConverter;
///
/// The bit converter used to read values from the stream
///
public EndianBitConverter BitConverter
{
get { return bitConverter; }
}
Encoding encoding;
///
/// The encoding used to read strings
///
public Encoding Encoding
{
get { return encoding; }
}
Stream stream;
///
/// Gets the underlying stream of the EndianBinaryReader.
///
public Stream BaseStream
{
get { return stream; }
}
#endregion
#region Public methods
///
/// Closes the reader, including the underlying stream..
///
public void Close()
{
Dispose();
}
///
/// Seeks within the stream.
///
/// Offset to seek to.
/// Origin of seek operation.
public void Seek (int offset, SeekOrigin origin)
{
CheckDisposed();
stream.Seek (offset, origin);
}
///
/// Reads a single byte from the stream.
///
/// The byte read
public byte ReadByte()
{
ReadInternal(buffer, 1);
return buffer[0];
}
///
/// Reads a single signed byte from the stream.
///
/// The byte read
public sbyte ReadSByte()
{
ReadInternal(buffer, 1);
return unchecked((sbyte)buffer[0]);
}
///
/// Reads a boolean from the stream. 1 byte is read.
///
/// The boolean read
public bool ReadBoolean()
{
ReadInternal(buffer, 1);
return bitConverter.ToBoolean(buffer, 0);
}
///
/// Reads a 16-bit signed integer from the stream, using the bit converter
/// for this reader. 2 bytes are read.
///
/// The 16-bit integer read
public short ReadInt16()
{
ReadInternal(buffer, 2);
return bitConverter.ToInt16(buffer, 0);
}
///
/// Reads a 32-bit signed integer from the stream, using the bit converter
/// for this reader. 4 bytes are read.
///
/// The 32-bit integer read
public int ReadInt32()
{
ReadInternal(buffer, 4);
return bitConverter.ToInt32(buffer, 0);
}
///
/// Reads a 64-bit signed integer from the stream, using the bit converter
/// for this reader. 8 bytes are read.
///
/// The 64-bit integer read
public long ReadInt64()
{
ReadInternal(buffer, 8);
return bitConverter.ToInt64(buffer, 0);
}
///
/// Reads a 16-bit unsigned integer from the stream, using the bit converter
/// for this reader. 2 bytes are read.
///
/// The 16-bit unsigned integer read
public ushort ReadUInt16()
{
ReadInternal(buffer, 2);
return bitConverter.ToUInt16(buffer, 0);
}
///
/// Reads a 32-bit unsigned integer from the stream, using the bit converter
/// for this reader. 4 bytes are read.
///
/// The 32-bit unsigned integer read
public uint ReadUInt32()
{
ReadInternal(buffer, 4);
return bitConverter.ToUInt32(buffer, 0);
}
///
/// Reads a 64-bit unsigned integer from the stream, using the bit converter
/// for this reader. 8 bytes are read.
///
/// The 64-bit unsigned integer read
public ulong ReadUInt64()
{
ReadInternal(buffer, 8);
return bitConverter.ToUInt64(buffer, 0);
}
///
/// Reads a single-precision floating-point value from the stream, using the bit converter
/// for this reader. 4 bytes are read.
///
/// The floating point value read
public float ReadSingle()
{
ReadInternal(buffer, 4);
return bitConverter.ToSingle(buffer, 0);
}
///
/// Reads a double-precision floating-point value from the stream, using the bit converter
/// for this reader. 8 bytes are read.
///
/// The floating point value read
public double ReadDouble()
{
ReadInternal(buffer, 8);
return bitConverter.ToDouble(buffer, 0);
}
///
/// Reads a decimal value from the stream, using the bit converter
/// for this reader. 16 bytes are read.
///
/// The decimal value read
public decimal ReadDecimal()
{
ReadInternal(buffer, 16);
return bitConverter.ToDecimal(buffer, 0);
}
///
/// Reads a single character from the stream, using the character encoding for
/// this reader. If no characters have been fully read by the time the stream ends,
/// -1 is returned.
///
/// The character read, or -1 for end of stream.
public int Read()
{
int charsRead = Read(charBuffer, 0, 1);
if (charsRead==0)
{
return -1;
}
else
{
return charBuffer[0];
}
}
///
/// Reads the specified number of characters into the given buffer, starting at
/// the given index.
///
/// The buffer to copy data into
/// The first index to copy data into
/// The number of characters to read
/// The number of characters actually read. This will only be less than
/// the requested number of characters if the end of the stream is reached.
///
public int Read(char[] data, int index, int count)
{
CheckDisposed();
if (buffer==null)
{
throw new ArgumentNullException("buffer");
}
if (index < 0)
{
throw new ArgumentOutOfRangeException("index");
}
if (count < 0)
{
throw new ArgumentOutOfRangeException("index");
}
if (count+index > data.Length)
{
throw new ArgumentException
("Not enough space in buffer for specified number of characters starting at specified index");
}
int read=0;
bool firstTime=true;
// Use the normal buffer if we're only reading a small amount, otherwise
// use at most 4K at a time.
byte[] byteBuffer = buffer;
if (byteBuffer.Length < count*minBytesPerChar)
{
byteBuffer = new byte[4096];
}
while (read < count)
{
int amountToRead;
// First time through we know we haven't previously read any data
if (firstTime)
{
amountToRead = count*minBytesPerChar;
firstTime=false;
}
// After that we can only assume we need to fully read "chars left -1" characters
// and a single byte of the character we may be in the middle of
else
{
amountToRead = ((count-read-1)*minBytesPerChar)+1;
}
if (amountToRead > byteBuffer.Length)
{
amountToRead = byteBuffer.Length;
}
int bytesRead = TryReadInternal(byteBuffer, amountToRead);
if (bytesRead==0)
{
return read;
}
int decoded = decoder.GetChars(byteBuffer, 0, bytesRead, data, index);
read += decoded;
index += decoded;
}
return read;
}
///
/// Reads the specified number of bytes into the given buffer, starting at
/// the given index.
///
/// The buffer to copy data into
/// The first index to copy data into
/// The number of bytes to read
/// The number of bytes actually read. This will only be less than
/// the requested number of bytes if the end of the stream is reached.
///
public int Read(byte[] buffer, int index, int count)
{
CheckDisposed();
if (buffer==null)
{
throw new ArgumentNullException("buffer");
}
if (index < 0)
{
throw new ArgumentOutOfRangeException("index");
}
if (count < 0)
{
throw new ArgumentOutOfRangeException("index");
}
if (count+index > buffer.Length)
{
throw new ArgumentException
("Not enough space in buffer for specified number of bytes starting at specified index");
}
int read=0;
while (count > 0)
{
int block = stream.Read(buffer, index, count);
if (block==0)
{
return read;
}
index += block;
read += block;
count -= block;
}
return read;
}
///
/// Reads the specified number of bytes, returning them in a new byte array.
/// If not enough bytes are available before the end of the stream, this
/// method will return what is available.
///
/// The number of bytes to read
/// The bytes read
public byte[] ReadBytes(int count)
{
CheckDisposed();
if (count < 0)
{
throw new ArgumentOutOfRangeException("count");
}
byte[] ret = new byte[count];
int index=0;
while (index < count)
{
int read = stream.Read(ret, index, count-index);
// Stream has finished half way through. That's fine, return what we've got.
if (read==0)
{
byte[] copy = new byte[index];
Buffer.BlockCopy(ret, 0, copy, 0, index);
return copy;
}
index += read;
}
return ret;
}
///
/// Reads the specified number of bytes, returning them in a new byte array.
/// If not enough bytes are available before the end of the stream, this
/// method will throw an IOException.
///
/// The number of bytes to read
/// The bytes read
public byte[] ReadBytesOrThrow(int count)
{
byte[] ret = new byte[count];
ReadInternal(ret, count);
return ret;
}
///
/// Reads a 7-bit encoded integer from the stream. This is stored with the least significant
/// information first, with 7 bits of information per byte of value, and the top
/// bit as a continuation flag. This method is not affected by the endianness
/// of the bit converter.
///
/// The 7-bit encoded integer read from the stream.
public int Read7BitEncodedInt()
{
CheckDisposed();
int ret=0;
for (int shift = 0; shift < 35; shift+=7)
{
int b = stream.ReadByte();
if (b==-1)
{
throw new EndOfStreamException();
}
ret = ret | ((b&0x7f) << shift);
if ((b & 0x80) == 0)
{
return ret;
}
}
// Still haven't seen a byte with the high bit unset? Dodgy data.
throw new IOException("Invalid 7-bit encoded integer in stream.");
}
///
/// Reads a 7-bit encoded integer from the stream. This is stored with the most significant
/// information first, with 7 bits of information per byte of value, and the top
/// bit as a continuation flag. This method is not affected by the endianness
/// of the bit converter.
///
/// The 7-bit encoded integer read from the stream.
public int ReadBigEndian7BitEncodedInt()
{
CheckDisposed();
int ret=0;
for (int i=0; i < 5; i++)
{
int b = stream.ReadByte();
if (b==-1)
{
throw new EndOfStreamException();
}
ret = (ret << 7) | (b&0x7f);
if ((b & 0x80) == 0)
{
return ret;
}
}
// Still haven't seen a byte with the high bit unset? Dodgy data.
throw new IOException("Invalid 7-bit encoded integer in stream.");
}
///
/// Reads a length-prefixed string from the stream, using the encoding for this reader.
/// A 7-bit encoded integer is first read, which specifies the number of bytes
/// to read from the stream. These bytes are then converted into a string with
/// the encoding for this reader.
///
/// The string read from the stream.
public string ReadString()
{
int bytesToRead = Read7BitEncodedInt();
byte[] data = new byte[bytesToRead];
ReadInternal(data, bytesToRead);
return encoding.GetString(data, 0, data.Length);
}
#endregion
#region Private methods
///
/// Checks whether or not the reader has been disposed, throwing an exception if so.
///
void CheckDisposed()
{
if (disposed)
{
throw new ObjectDisposedException("EndianBinaryReader");
}
}
///
/// Reads the given number of bytes from the stream, throwing an exception
/// if they can't all be read.
///
/// Buffer to read into
/// Number of bytes to read
void ReadInternal (byte[] data, int size)
{
CheckDisposed();
int index=0;
while (index < size)
{
int read = stream.Read(data, index, size-index);
if (read==0)
{
throw new EndOfStreamException
(String.Format("End of stream reached with {0} byte{1} left to read.", size-index,
size-index==1 ? "s" : ""));
}
index += read;
}
}
///
/// Reads the given number of bytes from the stream if possible, returning
/// the number of bytes actually read, which may be less than requested if
/// (and only if) the end of the stream is reached.
///
/// Buffer to read into
/// Number of bytes to read
/// Number of bytes actually read
int TryReadInternal (byte[] data, int size)
{
CheckDisposed();
int index=0;
while (index < size)
{
int read = stream.Read(data, index, size-index);
if (read==0)
{
return index;
}
index += read;
}
return index;
}
#endregion
#region IDisposable Members
///
/// Disposes of the underlying stream.
///
public void Dispose()
{
if (!disposed)
{
disposed = true;
((IDisposable)stream).Dispose();
}
}
#endregion
}
}