diff --git a/SabreTools.Serialization.Test/Readers/XZTests.cs b/SabreTools.Serialization.Test/Readers/XZTests.cs new file mode 100644 index 00000000..2969f606 --- /dev/null +++ b/SabreTools.Serialization.Test/Readers/XZTests.cs @@ -0,0 +1,73 @@ +using System.IO; +using System.Linq; +using SabreTools.Serialization.Readers; +using Xunit; + +namespace SabreTools.Serialization.Test.Readers +{ + public class XZTests + { + [Fact] + public void NullArray_Null() + { + byte[]? data = null; + int offset = 0; + var deserializer = new XZ(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void EmptyArray_Null() + { + byte[]? data = []; + int offset = 0; + var deserializer = new XZ(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void InvalidArray_Null() + { + byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)]; + int offset = 0; + var deserializer = new XZ(); + + var actual = deserializer.Deserialize(data, offset); + Assert.Null(actual); + } + + [Fact] + public void NullStream_Null() + { + Stream? data = null; + var deserializer = new XZ(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void EmptyStream_Null() + { + Stream? data = new MemoryStream([]); + var deserializer = new XZ(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + + [Fact] + public void InvalidStream_Null() + { + Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]); + var deserializer = new XZ(); + + var actual = deserializer.Deserialize(data); + Assert.Null(actual); + } + } +} \ No newline at end of file diff --git a/SabreTools.Serialization/Extensions/XZ.cs b/SabreTools.Serialization/Extensions/XZ.cs index 485399f8..2330ce86 100644 --- a/SabreTools.Serialization/Extensions/XZ.cs +++ b/SabreTools.Serialization/Extensions/XZ.cs @@ -9,10 +9,12 @@ namespace SabreTools.Data.Extensions /// /// Value to decode /// Maximum number of bytes to parse + /// Number of bytes parsed /// UInt64 representing the decoded integer /// - public static ulong DecodeVariableLength(this byte[] value, int maxSize) + public static ulong DecodeVariableLength(this byte[] value, int maxSize, out int length) { + length = 0; if (maxSize <= 0) return 0; @@ -30,6 +32,7 @@ namespace SabreTools.Data.Extensions output |= (ulong)(value[i] & 0x7F) << (i * 7); } + length = i; return output; } diff --git a/SabreTools.Serialization/Models/XZ/Record.cs b/SabreTools.Serialization/Models/XZ/Record.cs index 64b60c3c..b4c8e381 100644 --- a/SabreTools.Serialization/Models/XZ/Record.cs +++ b/SabreTools.Serialization/Models/XZ/Record.cs @@ -12,6 +12,6 @@ namespace SabreTools.Data.Models.XZ /// Uncompressed size of the block /// /// Stored as a variable-length integer - public ulong NumberOfRecords { get; set; } + public ulong UncompressedSize { get; set; } } } diff --git a/SabreTools.Serialization/Readers/XZ.cs b/SabreTools.Serialization/Readers/XZ.cs new file mode 100644 index 00000000..3fab8d0b --- /dev/null +++ b/SabreTools.Serialization/Readers/XZ.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.IO; +using SabreTools.Data.Extensions; +using SabreTools.Data.Models.XZ; +using SabreTools.IO.Extensions; +using static SabreTools.Data.Models.XZ.Constants; + +namespace SabreTools.Serialization.Readers +{ + public class XZ : BaseBinaryReader + { + /// + public override Archive? Deserialize(Stream? data) + { + // If the data is invalid + if (data == null || !data.CanRead) + return null; + + try + { + // Cache the current offset + long initialOffset = data.Position; + + // Create a new archive to fill + var archive = new Archive(); + + #region Header + + // Try to parse the header + var header = ParseHeader(data); + if (!header.Signature.EqualsExactly(HeaderSignatureBytes)) + return null; + + // Set the stream header + archive.Header = header; + + #endregion + + #region Blocks and Index + + // Create the block array + var blocks = new List(); + + // Try to parse the blocks + while (data.Position < data.Length) + { + // Peek at the first byte in the block + byte peek = data.ReadByteValue(); + data.Seek(-1, SeekOrigin.Current); + + // Blocks have values from 0x01-0x0F, index is 0x00 + if (peek == 0x00) + break; + + // Try to parse the block + var block = ParseBlock(data, header.Flags); + blocks.Add(block); + } + + // Set the blocks + archive.Blocks = [.. blocks]; + + // Try to parse the index + var index = ParseIndex(data); + if (index.IndexIndicator != 0x00) + return null; + + // Set the index + archive.Index = index; + + #endregion + + #region Footer + + // Try to parse the footer + var footer = ParseFooter(data); + if (!footer.Signature.EqualsExactly(FooterSignatureBytes)) + return null; + + // Set the footer + archive.Footer = footer; + + #endregion + + return archive; + } + catch + { + // Ignore the actual error + return null; + } + } + + /// + /// Parse a Stream into a Header + /// + /// Stream to parse + /// Filled Header on success, null on error + public static Header ParseHeader(Stream data) + { + var obj = new Header(); + + obj.Signature = data.ReadBytes(6); + obj.Flags = (HeaderFlags)data.ReadUInt16LittleEndian(); + obj.Crc32 = data.ReadUInt32LittleEndian(); + + return obj; + } + + /// + /// Parse a Stream into a Block + /// + /// Stream to parse + /// HeaderFlags to for determining the check value + /// Filled Block on success, null on error + public static Block ParseBlock(Stream data, HeaderFlags headerFlags) + { + // Cache the current offset + long currentOffset = data.Position; + + var obj = new Block(); + + obj.HeaderSize = data.ReadByteValue(); + int realHeaderSize = (obj.HeaderSize + 1) * 4; + obj.Flags = (BlockFlags)data.ReadByteValue(); + +#if NET20 || NET35 + if ((obj.Flags & BlockFlags.CompressedSize) != 0) +#else + if (obj.Flags.HasFlag(BlockFlags.CompressedSize)) +#endif + obj.CompressedSize = ParseVariableLength(data); + +#if NET20 || NET35 + if ((obj.Flags & BlockFlags.UncompressedSize) != 0) +#else + if (obj.Flags.HasFlag(BlockFlags.UncompressedSize)) +#endif + obj.UncompressedSize = ParseVariableLength(data); + + // Determine the number of filters to read + int filterCount = ((byte)obj.Flags & 0x03) + 1; + + // Try to parse the filters + obj.FilterFlags = new FilterFlag[filterCount]; + for (int i = 0; i < obj.FilterFlags.Length; i++) + { + obj.FilterFlags[i] = ParseFilterFlag(data); + } + + // Parse the padding as needed + int paddingLength = realHeaderSize - (int)(data.Position - currentOffset); + if (paddingLength >= 0) + obj.HeaderPadding = data.ReadBytes(paddingLength); + + obj.Crc32 = data.ReadUInt32LittleEndian(); + + // TODO: How to handle large blocks? + if ((int)obj.UncompressedSize >= 0) + obj.CompressedData = data.ReadBytes((int)obj.UncompressedSize); + + // Parse the padding as needed + paddingLength = (int)obj.UncompressedSize % 4; + if (paddingLength >= 0) + obj.BlockPadding = data.ReadBytes(paddingLength); + + // Read the Check as needed + if (headerFlags == HeaderFlags.Crc32) + obj.Check = data.ReadBytes(4); + else if (headerFlags == HeaderFlags.Crc64) + obj.Check = data.ReadBytes(8); + else if (headerFlags == HeaderFlags.Sha256) + obj.Check = data.ReadBytes(32); + + return obj; + } + + /// + /// Parse a Stream into a FilterFlag + /// + /// Stream to parse + /// Filled FilterFlag on success, null on error + public static FilterFlag ParseFilterFlag(Stream data) + { + var obj = new FilterFlag(); + + obj.FilterID = ParseVariableLength(data); + obj.SizeOfProperties = ParseVariableLength(data); + obj.Properties = data.ReadBytes((int)obj.SizeOfProperties); + + return obj; + } + + /// + /// Parse a Stream into a Index + /// + /// Stream to parse + /// Filled Index on success, null on error + public static Data.Models.XZ.Index ParseIndex(Stream data) + { + // Cache the current offset + long currentOffset = data.Position; + + var obj = new Data.Models.XZ.Index(); + + obj.IndexIndicator = data.ReadByteValue(); + obj.NumberOfRecords = ParseVariableLength(data); + + obj.Records = new Record[obj.NumberOfRecords]; + for (int i = 0; i < obj.Records.Length; i++) + { + obj.Records[i] = ParseRecord(data); + } + + // Parse the padding as needed + int paddingLength = (int)(data.Position - currentOffset) % 4; + if (paddingLength >= 0) + obj.Padding = data.ReadBytes(paddingLength); + + obj.Crc32 = data.ReadUInt32LittleEndian(); + + return obj; + } + + /// + /// Parse a Stream into a Record + /// + /// Stream to parse + /// Filled Record on success, null on error + public static Record ParseRecord(Stream data) + { + var obj = new Record(); + + obj.UnpaddedSize = ParseVariableLength(data); + obj.UncompressedSize = ParseVariableLength(data); + + return obj; + } + + /// + /// Parse a Stream into a Footer + /// + /// Stream to parse + /// Filled Footer on success, null on error + public static Footer ParseFooter(Stream data) + { + var obj = new Footer(); + + obj.Crc32 = data.ReadUInt32LittleEndian(); + obj.BackwardSize = data.ReadUInt32LittleEndian(); + obj.Flags = (HeaderFlags)data.ReadByteValue(); + obj.Signature = data.ReadBytes(2); + + return obj; + } + + /// + /// Parse a variable-length number from the stream + /// + /// Stream to parse + /// Decoded variable-length value + private static ulong ParseVariableLength(Stream data) + { + // Cache the current offset + long currentOffset = data.Position; + + // Read up to 9 bytes for decoding + int byteCount = (int)Math.Min(data.Length - data.Position, 9); + byte[] encoded = data.ReadBytes(byteCount); + + // Attempt to decode the value + ulong output = encoded.DecodeVariableLength(byteCount, out int length); + + // Seek the actual length processed and return + data.Seek(currentOffset + length, SeekOrigin.Begin); + return output; + } + } +}