diff --git a/Kaitai.Struct.Runtime.Async/Interface/IKaitaiAsyncStream.cs b/Kaitai.Struct.Runtime.Async/Interface/IKaitaiAsyncStream.cs new file mode 100644 index 0000000..8000ad5 --- /dev/null +++ b/Kaitai.Struct.Runtime.Async/Interface/IKaitaiAsyncStream.cs @@ -0,0 +1,161 @@ +using System.Threading.Tasks; + +namespace Kaitai.Async +{ + public interface IKaitaiAsyncStream : IKaitaiStreamBase + { + /// + /// Seek to a specific position from the beginning of the stream + /// + /// The position to seek to + Task SeekAsync(long position); + + /// + /// Read a signed byte from the stream + /// + /// + Task ReadS1Async(); + + /// + /// Read a signed short from the stream (big endian) + /// + /// + Task ReadS2beAsync(); + + /// + /// Read a signed int from the stream (big endian) + /// + /// + Task ReadS4beAsync(); + + /// + /// Read a signed long from the stream (big endian) + /// + /// + Task ReadS8beAsync(); + + /// + /// Read a signed short from the stream (little endian) + /// + /// + Task ReadS2leAsync(); + + /// + /// Read a signed int from the stream (little endian) + /// + /// + Task ReadS4leAsync(); + + /// + /// Read a signed long from the stream (little endian) + /// + /// + Task ReadS8leAsync(); + + /// + /// Read an unsigned byte from the stream + /// + /// + Task ReadU1Async(); + + /// + /// Read an unsigned short from the stream (big endian) + /// + /// + Task ReadU2beAsync(); + + /// + /// Read an unsigned int from the stream (big endian) + /// + /// + Task ReadU4beAsync(); + + /// + /// Read an unsigned long from the stream (big endian) + /// + /// + Task ReadU8beAsync(); + + /// + /// Read an unsigned short from the stream (little endian) + /// + /// + Task ReadU2leAsync(); + + /// + /// Read an unsigned int from the stream (little endian) + /// + /// + Task ReadU4leAsync(); + + /// + /// Read an unsigned long from the stream (little endian) + /// + /// + Task ReadU8leAsync(); + + /// + /// Read a single-precision floating point value from the stream (big endian) + /// + /// + Task ReadF4beAsync(); + + /// + /// Read a double-precision floating point value from the stream (big endian) + /// + /// + Task ReadF8beAsync(); + + /// + /// Read a single-precision floating point value from the stream (little endian) + /// + /// + Task ReadF4leAsync(); + + /// + /// Read a double-precision floating point value from the stream (little endian) + /// + /// + Task ReadF8leAsync(); + + Task ReadBitsIntAsync(int n); + Task ReadBitsIntLeAsync(int n); + + /// + /// Read a fixed number of bytes from the stream + /// + /// The number of bytes to read + /// + Task ReadBytesAsync(long count); + + /// + /// Read a fixed number of bytes from the stream + /// + /// The number of bytes to read + /// + Task ReadBytesAsync(ulong count); + + /// + /// Read all the remaining bytes from the stream until the end is reached + /// + /// + Task ReadBytesFullAsync(); + + /// + /// 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 + /// + Task ReadBytesTermAsync(byte terminator, bool includeTerminator, bool consumeTerminator, bool eosError); + + /// + /// Read a specific set of bytes and assert that they are the same as an expected result + /// + /// The expected result + /// + Task EnsureFixedContentsAsync(byte[] expected); + } +} diff --git a/Kaitai.Struct.Runtime.Async/Kaitai.Struct.Runtime.Async.csproj b/Kaitai.Struct.Runtime.Async/Kaitai.Struct.Runtime.Async.csproj index cb63190..848a9b2 100644 --- a/Kaitai.Struct.Runtime.Async/Kaitai.Struct.Runtime.Async.csproj +++ b/Kaitai.Struct.Runtime.Async/Kaitai.Struct.Runtime.Async.csproj @@ -1,7 +1,16 @@ - + netcoreapp3.1 + Kaitai.Async + + + + + + + + diff --git a/Kaitai.Struct.Runtime.Async/KaitaiAsyncStruct.cs b/Kaitai.Struct.Runtime.Async/KaitaiAsyncStruct.cs new file mode 100644 index 0000000..7fb9253 --- /dev/null +++ b/Kaitai.Struct.Runtime.Async/KaitaiAsyncStruct.cs @@ -0,0 +1,15 @@ +namespace Kaitai.Async +{ + public abstract class KaitaiAsyncStruct + { + protected readonly KaitaiAsyncStream m_io; + + protected KaitaiAsyncStruct(KaitaiAsyncStream kaitaiStream) + { + m_io = kaitaiStream; + } + + // ReSharper disable once InconsistentNaming + public KaitaiAsyncStream M_Io => m_io; + } +} \ No newline at end of file diff --git a/Kaitai.Struct.Runtime.Async/KaitaiStreamAsync.cs b/Kaitai.Struct.Runtime.Async/KaitaiStreamAsync.cs new file mode 100644 index 0000000..dc4e37d --- /dev/null +++ b/Kaitai.Struct.Runtime.Async/KaitaiStreamAsync.cs @@ -0,0 +1,424 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; +using Overby.Extensions.AsyncBinaryReaderWriter; + +namespace Kaitai.Async +{ + public partial class KaitaiAsyncStream : KaitaiStreamBase, IKaitaiAsyncStream + { + protected Stream BaseStream; + + private ulong _bits = 0; + private int _bitsLeft = 0; + private AsyncBinaryReader _asyncBinaryReader; + + protected AsyncBinaryReader AsyncBinaryReader + { + get => _asyncBinaryReader ?? (_asyncBinaryReader = new AsyncBinaryReader(BaseStream)); + set => _asyncBinaryReader = value; + } + + #region Constructors + + public KaitaiAsyncStream(Stream stream) + { + BaseStream = stream; + } + + /// + /// Creates a IKaitaiAsyncStream backed by a file (RO) + /// + public KaitaiAsyncStream(string file) : this(File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + } + + /// + ///Creates a IKaitaiAsyncStream backed by a byte buffer + /// + public KaitaiAsyncStream(byte[] bytes) : this(new MemoryStream(bytes)) + { + } + + #endregion + + #region Stream positioning + + /// + /// Check if the stream position is at the end of the stream + /// + public override bool IsEof => BaseStream.Position >= BaseStream.Length && _bitsLeft == 0; + + /// + /// Seek to a specific position from the beginning of the stream + /// + /// The position to seek to + /// .NET Stream API does not support SeekAsync, therefore, consumer needs to define his own implementation. + public virtual Task SeekAsync(long position) + { + BaseStream.Seek(position, SeekOrigin.Begin); + return Task.CompletedTask; + } + + /// + /// Get the current position in the stream + /// + public override long Pos => BaseStream.Position; + + /// + /// Get the total length of the stream (ie. file size) + /// + public override long Size => BaseStream.Length; + + #endregion + + #region Integer types + + #region Signed + + /// + /// Read a signed byte from the stream + /// + /// + public async Task ReadS1Async() => await AsyncBinaryReader.ReadSByteAsync(); + + #region Big-endian + + /// + /// Read a signed short from the stream (big endian) + /// + /// + public async Task ReadS2beAsync() => BitConverter.ToInt16(await ReadBytesNormalisedBigEndianAsync(2), 0); + + /// + /// Read a signed int from the stream (big endian) + /// + /// + public async Task ReadS4beAsync() => BitConverter.ToInt32(await ReadBytesNormalisedBigEndianAsync(4), 0); + + /// + /// Read a signed long from the stream (big endian) + /// + /// + public async Task ReadS8beAsync() => BitConverter.ToInt64(await ReadBytesNormalisedBigEndianAsync(8), 0); + + #endregion + + #region Little-endian + + /// + /// Read a signed short from the stream (little endian) + /// + /// + public async Task ReadS2leAsync() => BitConverter.ToInt16(await ReadBytesNormalisedLittleEndianAsync(2), 0); + + /// + /// Read a signed int from the stream (little endian) + /// + /// + public async Task ReadS4leAsync() => BitConverter.ToInt32(await ReadBytesNormalisedLittleEndianAsync(4), 0); + + /// + /// Read a signed long from the stream (little endian) + /// + /// + public async Task ReadS8leAsync() => BitConverter.ToInt64(await ReadBytesNormalisedLittleEndianAsync(8), 0); + + #endregion + + #endregion + + #region Unsigned + + /// + /// Read an unsigned byte from the stream + /// + /// + public async Task ReadU1Async() => await AsyncBinaryReader.ReadByteAsync(); + + #region Big-endian + + /// + /// Read an unsigned short from the stream (big endian) + /// + /// + public async Task ReadU2beAsync() => BitConverter.ToUInt16(await ReadBytesNormalisedBigEndianAsync(2), 0); + + /// + /// Read an unsigned int from the stream (big endian) + /// + /// + public async Task ReadU4beAsync() => BitConverter.ToUInt32(await ReadBytesNormalisedBigEndianAsync(4), 0); + + /// + /// Read an unsigned long from the stream (big endian) + /// + /// + public async Task ReadU8beAsync() => BitConverter.ToUInt64(await ReadBytesNormalisedBigEndianAsync(8), 0); + + #endregion + + #region Little-endian + + /// + /// Read an unsigned short from the stream (little endian) + /// + /// + public async Task ReadU2leAsync() => BitConverter.ToUInt16(await ReadBytesNormalisedLittleEndianAsync(2), 0); + + /// + /// Read an unsigned int from the stream (little endian) + /// + /// + public async Task ReadU4leAsync() => BitConverter.ToUInt32(await ReadBytesNormalisedLittleEndianAsync(4), 0); + + /// + /// Read an unsigned long from the stream (little endian) + /// + /// + public async Task ReadU8leAsync() => BitConverter.ToUInt64(await ReadBytesNormalisedLittleEndianAsync(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 async Task ReadF4beAsync() => BitConverter.ToSingle(await ReadBytesNormalisedBigEndianAsync(4), 0); + + /// + /// Read a double-precision floating point value from the stream (big endian) + /// + /// + public async Task ReadF8beAsync() => BitConverter.ToDouble(await ReadBytesNormalisedBigEndianAsync(8), 0); + + #endregion + + #region Little-endian + + /// + /// Read a single-precision floating point value from the stream (little endian) + /// + /// + public async Task ReadF4leAsync() => BitConverter.ToSingle(await ReadBytesNormalisedLittleEndianAsync(4), 0); + + /// + /// Read a double-precision floating point value from the stream (little endian) + /// + /// + public async Task ReadF8leAsync() => BitConverter.ToDouble(await ReadBytesNormalisedLittleEndianAsync(8), 0); + + #endregion + + #endregion + + #region Unaligned bit values + + public override void AlignToByte() + { + _bits = 0; + _bitsLeft = 0; + } + + public async Task ReadBitsIntAsync(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 = await ReadBytesAsync(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 mask to align with highest bits available in "bits" + int shiftBits = _bitsLeft - n; + mask = mask << shiftBits; + // derive reading result + ulong res = (_bits & mask) >> shiftBits; + // clear top bits that we've just read => AND with 1s + _bitsLeft -= n; + mask = GetMaskOnes(_bitsLeft); + _bits &= mask; + + return res; + } + + //Method ported from algorithm specified @ issue#155 + public async Task ReadBitsIntLeAsync(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 = await ReadBytesAsync(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; + } + + #endregion + + #region Byte arrays + + /// + /// Read a fixed number of bytes from the stream + /// + /// The number of bytes to read + /// + public async Task ReadBytesAsync(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 = await AsyncBinaryReader.ReadBytesAsync((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 async Task ReadBytesAsync(ulong count) + { + if (count > Int32.MaxValue) + throw new ArgumentOutOfRangeException("requested " + count + " bytes, while only non-negative int32 amount of bytes possible"); + byte[] bytes = await AsyncBinaryReader.ReadBytesAsync((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 async Task ReadBytesNormalisedLittleEndianAsync(int count) + { + byte[] bytes = await ReadBytesAsync(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 async Task ReadBytesNormalisedBigEndianAsync(int count) + { + byte[] bytes = await ReadBytesAsync(count); + if (IsLittleEndian) Array.Reverse(bytes); + return bytes; + } + + /// + /// Read all the remaining bytes from the stream until the end is reached + /// + /// + /// //TODO Handle asynchronously, BaseStream.Length is troublesome + public virtual async Task ReadBytesFullAsync() => await ReadBytesAsync(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 async Task ReadBytesTermAsync(byte terminator, bool includeTerminator, bool consumeTerminator, bool eosError) + { + List bytes = new List(); + while (true) + { + if (IsEof) + { + if (eosError) throw new EndOfStreamException( + $"End of stream reached, but no terminator `{terminator}` found"); + break; + } + + byte b = await AsyncBinaryReader.ReadByteAsync(); + if (b == terminator) + { + if (includeTerminator) bytes.Add(b); + if (!consumeTerminator) await SeekAsync(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 + /// + public async Task EnsureFixedContentsAsync(byte[] expected) + { + byte[] bytes = await ReadBytesAsync(expected.Length); + + if (bytes.Length != expected.Length) + { + throw new Exception( + $"Expected bytes: {Convert.ToBase64String(expected)} ({expected.Length} bytes), Instead got: {Convert.ToBase64String(bytes)} ({bytes.Length} bytes)"); + } + for (int i = 0; i < bytes.Length; i++) + { + if (bytes[i] != expected[i]) + { + throw new Exception( + $"Expected bytes: {Convert.ToBase64String(expected)} ({expected.Length} bytes), Instead got: {Convert.ToBase64String(bytes)} ({bytes.Length} bytes)"); + } + } + + return bytes; + } + + #endregion + } +} diff --git a/Kaitai.Struct.Runtime/Exception/ValidationFailedError.cs b/Kaitai.Struct.Runtime/Exception/ValidationFailedError.cs index 4ac5992..373af24 100644 --- a/Kaitai.Struct.Runtime/Exception/ValidationFailedError.cs +++ b/Kaitai.Struct.Runtime/Exception/ValidationFailedError.cs @@ -8,9 +8,9 @@ namespace Kaitai /// public class ValidationFailedError : KaitaiStructError { - protected KaitaiStream io; + protected IKaitaiStreamBase io; - public ValidationFailedError(string msg, KaitaiStream io, string srcPath) + public ValidationFailedError(string msg, IKaitaiStreamBase io, string srcPath) : base($"at pos {io.Pos}: validation failed: {msg}", srcPath) { this.io = io; diff --git a/Kaitai.Struct.Runtime/Exception/ValidationNotEqualError.cs b/Kaitai.Struct.Runtime/Exception/ValidationNotEqualError.cs index c7eb300..e97520e 100644 --- a/Kaitai.Struct.Runtime/Exception/ValidationNotEqualError.cs +++ b/Kaitai.Struct.Runtime/Exception/ValidationNotEqualError.cs @@ -12,7 +12,7 @@ public class ValidationNotEqualError : ValidationFailedError protected Object expected; - public ValidationNotEqualError(byte[] expected, byte[] actual, KaitaiStream io, string srcPath) + public ValidationNotEqualError(byte[] expected, byte[] actual, IKaitaiStreamBase io, string srcPath) : base($"not equal, expected {ByteArrayToHex(expected)}, but got {ByteArrayToHex(actual)}", io, srcPath) { @@ -20,7 +20,7 @@ public ValidationNotEqualError(byte[] expected, byte[] actual, KaitaiStream io, this.actual = actual; } - public ValidationNotEqualError(Object expected, Object actual, KaitaiStream io, string srcPath) + public ValidationNotEqualError(Object expected, Object actual, IKaitaiStreamBase io, string srcPath) : base($"not equal, expected {expected}, but got {actual}", io, srcPath) { this.expected = expected; diff --git a/Kaitai.Struct.sln.DotSettings b/Kaitai.Struct.sln.DotSettings new file mode 100644 index 0000000..72e13e3 --- /dev/null +++ b/Kaitai.Struct.sln.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file