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 } }