using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Globalization; namespace Kaitai { /// /// The base Kaitai stream which exposes an API for the Kaitai Struct framework. /// It's based off a BinaryReader, which is a little-endian reader. /// public partial class KaitaiStream : BinaryReader { #region Constructors public KaitaiStream(Stream stream) : base(stream) { } /// /// Creates a KaitaiStream backed by a file (RO) /// public KaitaiStream(string file) : base(File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) { } /// ///Creates a KaitaiStream backed by a byte buffer /// public KaitaiStream(byte[] bytes) : base(new MemoryStream(bytes)) { } private ulong Bits = 0; private int BitsLeft = 0; static readonly bool IsLittleEndian = BitConverter.IsLittleEndian; #endregion #region Stream positioning /// /// Check if the stream position is at the end of the stream /// public bool IsEof { get { return BaseStream.Position >= BaseStream.Length && BitsLeft == 0; } } /// /// Seek to a specific position from the beginning of the stream /// /// The position to seek to public void Seek(long position) { BaseStream.Seek(position, SeekOrigin.Begin); } /// /// Get the current position in the stream /// public long Pos { get { return BaseStream.Position; } } /// /// Get the total length of the stream (ie. file size) /// public long Size { get { return BaseStream.Length; } } #endregion #region Integer types #region Signed /// /// Read a signed byte from the stream /// /// public sbyte ReadS1() { return ReadSByte(); } #region Big-endian /// /// Read a signed short from the stream (big endian) /// /// public short ReadS2be() { return BitConverter.ToInt16(ReadBytesNormalisedBigEndian(2), 0); } /// /// Read a signed int from the stream (big endian) /// /// public int ReadS4be() { return BitConverter.ToInt32(ReadBytesNormalisedBigEndian(4), 0); } /// /// Read a signed long from the stream (big endian) /// /// public long ReadS8be() { return BitConverter.ToInt64(ReadBytesNormalisedBigEndian(8), 0); } #endregion #region Little-endian /// /// Read a signed short from the stream (little endian) /// /// public short ReadS2le() { return BitConverter.ToInt16(ReadBytesNormalisedLittleEndian(2), 0); } /// /// Read a signed int from the stream (little endian) /// /// public int ReadS4le() { return BitConverter.ToInt32(ReadBytesNormalisedLittleEndian(4), 0); } /// /// Read a signed long from the stream (little endian) /// /// public long ReadS8le() { return BitConverter.ToInt64(ReadBytesNormalisedLittleEndian(8), 0); } #endregion #endregion #region Unsigned /// /// Read an unsigned byte from the stream /// /// public byte ReadU1() { return ReadByte(); } #region Big-endian /// /// Read an unsigned short from the stream (big endian) /// /// public ushort ReadU2be() { return BitConverter.ToUInt16(ReadBytesNormalisedBigEndian(2), 0); } /// /// Read an unsigned int from the stream (big endian) /// /// public uint ReadU4be() { return BitConverter.ToUInt32(ReadBytesNormalisedBigEndian(4), 0); } /// /// Read an unsigned long from the stream (big endian) /// /// public ulong ReadU8be() { return BitConverter.ToUInt64(ReadBytesNormalisedBigEndian(8), 0); } #endregion #region Little-endian /// /// Read an unsigned short from the stream (little endian) /// /// public ushort ReadU2le() { return BitConverter.ToUInt16(ReadBytesNormalisedLittleEndian(2), 0); } /// /// Read an unsigned int from the stream (little endian) /// /// public uint ReadU4le() { return BitConverter.ToUInt32(ReadBytesNormalisedLittleEndian(4), 0); } /// /// Read an unsigned long from the stream (little endian) /// /// public ulong ReadU8le() { return BitConverter.ToUInt64(ReadBytesNormalisedLittleEndian(8), 0); } #endregion #endregion #endregion #region Floating point types #region Big-endian /// /// Read a single-precision floating point value from the stream (big endian) /// /// public float ReadF4be() { return BitConverter.ToSingle(ReadBytesNormalisedBigEndian(4), 0); } /// /// Read a double-precision floating point value from the stream (big endian) /// /// public double ReadF8be() { return BitConverter.ToDouble(ReadBytesNormalisedBigEndian(8), 0); } #endregion #region Little-endian /// /// Read a single-precision floating point value from the stream (little endian) /// /// public float ReadF4le() { return BitConverter.ToSingle(ReadBytesNormalisedLittleEndian(4), 0); } /// /// Read a double-precision floating point value from the stream (little endian) /// /// public double ReadF8le() { return BitConverter.ToDouble(ReadBytesNormalisedLittleEndian(8), 0); } #endregion #endregion #region Unaligned bit values public void AlignToByte() { Bits = 0; BitsLeft = 0; } /// /// Read a n-bit integer in a big-endian manner from the stream /// /// public ulong ReadBitsIntBe(int n) { int bitsNeeded = n - BitsLeft; if (bitsNeeded > 0) { // 1 bit => 1 byte // 8 bits => 1 byte // 9 bits => 2 bytes int bytesNeeded = ((bitsNeeded - 1) / 8) + 1; byte[] buf = ReadBytes(bytesNeeded); for (int i = 0; i < buf.Length; i++) { Bits <<= 8; Bits |= buf[i]; BitsLeft += 8; } } // raw mask with required number of 1s, starting from lowest bit ulong mask = GetMaskOnes(n); // shift "bits" to align the highest bits with the mask & derive reading result int shiftBits = BitsLeft - n; ulong res = (Bits >> shiftBits) & mask; // clear top bits that we've just read => AND with 1s BitsLeft -= n; mask = GetMaskOnes(BitsLeft); Bits &= mask; return res; } [Obsolete("use ReadBitsIntBe instead")] public ulong ReadBitsInt(int n) { return ReadBitsIntBe(n); } /// /// Read a n-bit integer in a little-endian manner from the stream /// /// public ulong ReadBitsIntLe(int n) { int bitsNeeded = n - BitsLeft; if (bitsNeeded > 0) { // 1 bit => 1 byte // 8 bits => 1 byte // 9 bits => 2 bytes int bytesNeeded = ((bitsNeeded - 1) / 8) + 1; byte[] buf = ReadBytes(bytesNeeded); for (int i = 0; i < buf.Length; i++) { ulong v = (ulong)((ulong)buf[i] << BitsLeft); Bits |= v; BitsLeft += 8; } } // raw mask with required number of 1s, starting from lowest bit ulong mask = GetMaskOnes(n); // derive reading result ulong res = (Bits & mask); // remove bottom bits that we've just read by shifting Bits >>= n; BitsLeft -= n; return res; } private static ulong GetMaskOnes(int n) { return n == 64 ? 0xffffffffffffffffUL : (1UL << n) - 1; } #endregion #region Byte arrays /// /// Read a fixed number of bytes from the stream /// /// The number of bytes to read /// public byte[] ReadBytes(long count) { if (count < 0 || count > Int32.MaxValue) throw new ArgumentOutOfRangeException("requested " + count + " bytes, while only non-negative int32 amount of bytes possible"); byte[] bytes = base.ReadBytes((int) count); if (bytes.Length < count) throw new EndOfStreamException("requested " + count + " bytes, but got only " + bytes.Length + " bytes"); return bytes; } /// /// Read a fixed number of bytes from the stream /// /// The number of bytes to read /// public byte[] ReadBytes(ulong count) { if (count > Int32.MaxValue) throw new ArgumentOutOfRangeException("requested " + count + " bytes, while only non-negative int32 amount of bytes possible"); byte[] bytes = base.ReadBytes((int)count); if (bytes.Length < (int)count) throw new EndOfStreamException("requested " + count + " bytes, but got only " + bytes.Length + " bytes"); return bytes; } /// /// Read bytes from the stream in little endian format and convert them to the endianness of the current platform /// /// The number of bytes to read /// An array of bytes that matches the endianness of the current platform protected byte[] ReadBytesNormalisedLittleEndian(int count) { byte[] bytes = ReadBytes(count); if (!IsLittleEndian) Array.Reverse(bytes); return bytes; } /// /// Read bytes from the stream in big endian format and convert them to the endianness of the current platform /// /// The number of bytes to read /// An array of bytes that matches the endianness of the current platform protected byte[] ReadBytesNormalisedBigEndian(int count) { byte[] bytes = ReadBytes(count); if (IsLittleEndian) Array.Reverse(bytes); return bytes; } /// /// Read all the remaining bytes from the stream until the end is reached /// /// public byte[] ReadBytesFull() { return ReadBytes(BaseStream.Length - BaseStream.Position); } /// /// Read a terminated string from the stream /// /// The string terminator value /// True to include the terminator in the returned string /// True to consume the terminator byte before returning /// True to throw an error when the EOS was reached before the terminator /// public byte[] ReadBytesTerm(byte terminator, bool includeTerminator, bool consumeTerminator, bool eosError) { List bytes = new List(); while (true) { if (IsEof) { if (eosError) throw new EndOfStreamException(string.Format("End of stream reached, but no terminator `{0}` found", terminator)); break; } byte b = ReadByte(); if (b == terminator) { if (includeTerminator) bytes.Add(b); if (!consumeTerminator) Seek(Pos - 1); break; } bytes.Add(b); } return bytes.ToArray(); } /// /// Read a specific set of bytes and assert that they are the same as an expected result /// /// The expected result /// [Obsolete("use explicit \"if\" using ByteArrayCompare method instead")] public byte[] EnsureFixedContents(byte[] expected) { byte[] bytes = ReadBytes(expected.Length); if (bytes.Length != expected.Length) { throw new Exception(string.Format("Expected bytes: {0} ({1} bytes), Instead got: {2} ({3} bytes)", Convert.ToBase64String(expected), expected.Length, Convert.ToBase64String(bytes), bytes.Length)); } for (int i = 0; i < bytes.Length; i++) { if (bytes[i] != expected[i]) { throw new Exception(string.Format("Expected bytes: {0} ({1} bytes), Instead got: {2} ({3} bytes)", Convert.ToBase64String(expected), expected.Length, Convert.ToBase64String(bytes), bytes.Length)); } } return bytes; } public static byte[] BytesStripRight(byte[] src, byte padByte) { int newLen = src.Length; while (newLen > 0 && src[newLen - 1] == padByte) newLen--; byte[] dst = new byte[newLen]; Array.Copy(src, dst, newLen); return dst; } public static byte[] BytesTerminate(byte[] src, byte terminator, bool includeTerminator) { int newLen = 0; int maxLen = src.Length; while (newLen < maxLen && src[newLen] != terminator) newLen++; if (includeTerminator && newLen < maxLen) newLen++; byte[] dst = new byte[newLen]; Array.Copy(src, dst, newLen); return dst; } #endregion #region Byte array processing /// /// Performs XOR processing with given data, XORing every byte of the input with a single value. /// /// The data toe process /// The key value to XOR with /// Processed data public byte[] ProcessXor(byte[] value, int key) { byte[] result = new byte[value.Length]; for (int i = 0; i < value.Length; i++) { result[i] = (byte)(value[i] ^ key); } return result; } /// /// Performs XOR processing with given data, XORing every byte of the input with a key /// array, repeating from the beginning of the key array if necessary /// /// The data toe process /// The key array to XOR with /// Processed data public byte[] ProcessXor(byte[] value, byte[] key) { int keyLen = key.Length; byte[] result = new byte[value.Length]; for (int i = 0, j = 0; i < value.Length; i++, j = (j + 1) % keyLen) { result[i] = (byte)(value[i] ^ key[j]); } return result; } /// /// Performs a circular left rotation shift for a given buffer by a given amount of bits. /// Pass a negative amount to rotate right. /// /// The data to rotate /// The number of bytes to rotate by /// /// public byte[] ProcessRotateLeft(byte[] data, int amount, int groupSize) { if (amount > 7 || amount < -7) throw new ArgumentException("Rotation of more than 7 cannot be performed.", "amount"); if (amount < 0) amount += 8; // Rotation of -2 is the same as rotation of +6 byte[] r = new byte[data.Length]; switch (groupSize) { case 1: for (int i = 0; i < data.Length; i++) { byte bits = data[i]; // http://stackoverflow.com/a/812039 r[i] = (byte) ((bits << amount) | (bits >> (8 - amount))); } break; default: throw new NotImplementedException(string.Format("Unable to rotate a group of {0} bytes yet", groupSize)); } return r; } /// /// Inflates a deflated zlib byte stream /// /// The data to deflate /// The deflated result public byte[] ProcessZlib(byte[] data) { // See RFC 1950 (https://tools.ietf.org/html/rfc1950) // zlib adds a header to DEFLATE streams - usually 2 bytes, // but can be 6 bytes if FDICT is set. // There's also 4 checksum bytes at the end of the stream. byte zlibCmf = data[0]; if ((zlibCmf & 0x0F) != 0x08) throw new NotSupportedException("Only the DEFLATE algorithm is supported for zlib data."); const int zlibFooter = 4; int zlibHeader = 2; // If the FDICT bit (0x20) is 1, then the 4-byte dictionary is included in the header, we need to skip it byte zlibFlg = data[1]; if ((zlibFlg & 0x20) == 0x20) zlibHeader += 4; using (MemoryStream ms = new MemoryStream(data, zlibHeader, data.Length - (zlibHeader + zlibFooter))) { using (DeflateStream ds = new DeflateStream(ms, CompressionMode.Decompress)) { using (MemoryStream target = new MemoryStream()) { byte[] buffer = new byte[32768]; CopyTo(target, ds); return target.ToArray(); } } } } #endregion #region Misc utility methods /// /// Performs modulo operation between two integers. /// /// /// This method is required because C# lacks a "true" modulo /// operator, the % operator rather being the "remainder" /// operator. We want mod operations to always be positive. /// /// The value to be divided /// The value to divide by. Must be greater than zero. /// The result of the modulo opertion. Will always be positive. public static int Mod(int a, int b) { if (b <= 0) throw new ArgumentException("Divisor of mod operation must be greater than zero.", "b"); int r = a % b; if (r < 0) r += b; return r; } /// /// Performs modulo operation between two integers. /// /// /// This method is required because C# lacks a "true" modulo /// operator, the % operator rather being the "remainder" /// operator. We want mod operations to always be positive. /// /// The value to be divided /// The value to divide by. Must be greater than zero. /// The result of the modulo opertion. Will always be positive. public static long Mod(long a, long b) { if (b <= 0) throw new ArgumentException("Divisor of mod operation must be greater than zero.", "b"); long r = a % b; if (r < 0) r += b; return r; } /// /// Compares two byte arrays in lexicographical order. /// /// negative number if a is less than b, 0 if a is equal to b, positive number if a is greater than b. /// First byte array to compare /// Second byte array to compare. public static int ByteArrayCompare(byte[] a, byte[] b) { if (a == b) return 0; int al = a.Length; int bl = b.Length; int minLen = al < bl ? al : bl; for (int i = 0; i < minLen; i++) { int cmp = a[i] - b[i]; if (cmp != 0) return cmp; } // Reached the end of at least one of the arrays if (al == bl) { return 0; } else { return al - bl; } } /// /// Reverses the string, Unicode-aware. /// /// taken from here public static string StringReverse(string s) { TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(s); List elements = new List(); while (enumerator.MoveNext()) elements.Add(enumerator.GetTextElement()); elements.Reverse(); return string.Concat(elements); } #endregion #region Compatibility methods /// /// Replaces Stream.CopyTo from System.Linq for backwards compatibility. /// /// Stream to copy data from. /// Stream to copy data to. private static void CopyTo(Stream source, Stream destination) { byte[] buffer = new byte[32768]; int read; while ((read = source.Read(buffer, 0, buffer.Length)) > 0) { destination.Write(buffer, 0, read); } } #endregion } }