From bfb206a06d6867f893089243352032c48fabd11a Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 29 Sep 2025 22:56:53 -0400 Subject: [PATCH] Add XZ models --- SabreTools.Serialization/Models/README.MD | 1 + SabreTools.Serialization/Models/XZ/Archive.cs | 22 ++++- SabreTools.Serialization/Models/XZ/Block.cs | 80 +++++++++++++++++++ .../Models/XZ/Constants.cs | 4 +- SabreTools.Serialization/Models/XZ/Enums.cs | 47 +++++++++++ .../Models/XZ/FilterFlags.cs | 23 ++++++ SabreTools.Serialization/Models/XZ/Footer.cs | 39 +++++++++ SabreTools.Serialization/Models/XZ/Header.cs | 27 +++++++ SabreTools.Serialization/Models/XZ/Index.cs | 36 +++++++++ SabreTools.Serialization/Models/XZ/Record.cs | 17 ++++ SabreTools.Serialization/WrapperFactory.cs | 2 +- .../Wrappers/NewExecutable.Extraction.cs | 2 +- .../Wrappers/PortableExecutable.Extraction.cs | 4 +- 13 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 SabreTools.Serialization/Models/XZ/Block.cs create mode 100644 SabreTools.Serialization/Models/XZ/Enums.cs create mode 100644 SabreTools.Serialization/Models/XZ/FilterFlags.cs create mode 100644 SabreTools.Serialization/Models/XZ/Footer.cs create mode 100644 SabreTools.Serialization/Models/XZ/Header.cs create mode 100644 SabreTools.Serialization/Models/XZ/Index.cs create mode 100644 SabreTools.Serialization/Models/XZ/Record.cs diff --git a/SabreTools.Serialization/Models/README.MD b/SabreTools.Serialization/Models/README.MD index 81edc149..1a582ab3 100644 --- a/SabreTools.Serialization/Models/README.MD +++ b/SabreTools.Serialization/Models/README.MD @@ -45,6 +45,7 @@ Not all of this information was able to be gathered directly from the files in q | [Technical Committee T10](https://www.t10.org/) | PIC | | [The Go tools for Windows + Assembler](https://www.godevtool.com/) | PortableExecutable | | [The Whole Half-Life](https://twhl.info/wiki/page/Specification:_WAD3) | WAD3 | +| [Tukaani](https://tukaani.org/xz/format.html) | XZ | | [Unshield](https://github.com/twogood/unshield) | InstallShieldCabinet | | [unshieldv3](https://github.com/wfr/unshieldv3) | InstallShieldArchiveV3 | | [Valve Developer Community](https://developer.valvesoftware.com/wiki/Main_Page) | BSP, VPK | diff --git a/SabreTools.Serialization/Models/XZ/Archive.cs b/SabreTools.Serialization/Models/XZ/Archive.cs index 0d27eacf..1ae389ec 100644 --- a/SabreTools.Serialization/Models/XZ/Archive.cs +++ b/SabreTools.Serialization/Models/XZ/Archive.cs @@ -1,10 +1,26 @@ namespace SabreTools.Data.Models.XZ { - /// - /// This is a placeholder model for future work - /// + /// public class Archive { + /// + /// Pre-blocks header + /// + public Header? Header { get; set; } + /// + /// Sequence of 0 or more blocks + /// + public Block[]? Blocks { get; set; } + + /// + /// Index structure + /// + public Index? Index { get; set; } + + /// + /// Post-blocks footer + /// + public Footer? Footer { get; set; } } } diff --git a/SabreTools.Serialization/Models/XZ/Block.cs b/SabreTools.Serialization/Models/XZ/Block.cs new file mode 100644 index 00000000..590efefc --- /dev/null +++ b/SabreTools.Serialization/Models/XZ/Block.cs @@ -0,0 +1,80 @@ +namespace SabreTools.Data.Models.XZ +{ + /// + /// Represents a single compressed block in the stream + /// + public class Block + { + /// + /// Size of the header + /// + /// + /// The real header size can be calculated by the following: + /// (HeaderSize + 1) * 4 + /// + public byte HeaderSize { get; set; } + + /// + /// The Block Flags field is a bit field + /// + public BlockFlags Flags { get; set; } + + /// + /// Size of the compressed data + /// Present if is set. + /// + /// Stored as a variable-length integer + public byte[]? CompressedSize { get; set; } + + /// + /// Size of the block after decompression + /// Present if is set. + /// + /// Stored as a variable-length integer + public byte[]? UncompressedSize { get; set; } + + /// + /// List of filter flags + /// + /// + /// The number of filter flags is given by the first two + /// bits of + /// + public FilterFlag[]? FilterFlags { get; set; } + + /// + /// This field contains as many null byte as it is needed to make + /// the Block Header have the size specified in Block Header Size. + /// + public byte[]? HeaderPadding { get; set; } + + /// + /// The CRC32 is calculated over everything in the Block Header + /// field except the CRC32 field itself. It is stored as an + /// unsigned 32-bit little endian integer. + /// + public uint Crc32 { get; set; } + + /// + /// The format of Compressed Data depends on Block Flags and List + /// of Filter Flags + /// + public byte[]? CompressedData { get; set; } + + /// + /// Block Padding MUST contain 0-3 null bytes to make the size of + /// the Block a multiple of four bytes. This can be needed when + /// the size of Compressed Data is not a multiple of four. + /// + public byte[]? BlockPadding { get; set; } + + /// + /// The type and size of the Check field depends on which bits + /// are set in the Stream Flags field. + /// + /// The Check, when used, is calculated from the original + /// uncompressed data. + /// + public byte[]? Check { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/XZ/Constants.cs b/SabreTools.Serialization/Models/XZ/Constants.cs index df70fe3d..d8d0c5b9 100644 --- a/SabreTools.Serialization/Models/XZ/Constants.cs +++ b/SabreTools.Serialization/Models/XZ/Constants.cs @@ -2,6 +2,8 @@ namespace SabreTools.Data.Models.XZ { public static class Constants { - public static readonly byte[] SignatureBytes = [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]; + public static readonly byte[] HeaderSignatureBytes = [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]; + + public static readonly byte[] FooterSignatureBytes = [0x59, 0x5A]; } } \ No newline at end of file diff --git a/SabreTools.Serialization/Models/XZ/Enums.cs b/SabreTools.Serialization/Models/XZ/Enums.cs new file mode 100644 index 00000000..16b1f3e2 --- /dev/null +++ b/SabreTools.Serialization/Models/XZ/Enums.cs @@ -0,0 +1,47 @@ +using System; + +namespace SabreTools.Data.Models.XZ +{ + [Flags] + public enum BlockFlags : byte + { + #region Bits 0-1 - Number of filters + + OneFilter = 0x00, + TwoFilters = 0x01, + ThreeFiltrs = 0x02, + FourFilters = 0x03, + + #endregion + + /// + /// Compressed size field present + /// + CompressedSize = 0x40, + + /// + /// Uncompressed size field present + /// + UncompressedSize = 0x80, + } + + public enum HeaderFlags : ushort + { + None = 0x0000, + Crc32 = 0x0001, + Reserved0x02 = 0x0002, + Reserved0x03 = 0x0003, + Crc64 = 0x0004, + Reserved0x05 = 0x0005, + Reserved0x06 = 0x0006, + Reserved0x07 = 0x0007, + Reserved0x08 = 0x0008, + Reserved0x09 = 0x0009, + Sha256 = 0x000A, + Reserved0x0B = 0x000B, + Reserved0x0C = 0x000C, + Reserved0x0D = 0x000D, + Reserved0x0E = 0x000E, + Reserved0x0F = 0x000F, + } +} diff --git a/SabreTools.Serialization/Models/XZ/FilterFlags.cs b/SabreTools.Serialization/Models/XZ/FilterFlags.cs new file mode 100644 index 00000000..e6216884 --- /dev/null +++ b/SabreTools.Serialization/Models/XZ/FilterFlags.cs @@ -0,0 +1,23 @@ +namespace SabreTools.Data.Models.XZ +{ + public class FilterFlag + { + /// + /// Filter ID + /// + /// Stored as a variable-length integer + public byte[]? FilterID { get; set; } + + /// + /// Filter ID + /// + /// Stored as a variable-length integer + public byte[]? SizeOfProperties { get; set; } + + /// + /// Properties of the filter whose length is given by + /// + /// + public byte[]? Properties { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/XZ/Footer.cs b/SabreTools.Serialization/Models/XZ/Footer.cs new file mode 100644 index 00000000..0ef86d18 --- /dev/null +++ b/SabreTools.Serialization/Models/XZ/Footer.cs @@ -0,0 +1,39 @@ +namespace SabreTools.Data.Models.XZ +{ + /// + /// Represents the post-block data in the stream + /// + public class Footer + { + /// + /// The CRC32 is calculated from the Backward Size and Stream Flags + /// fields. It is stored as an unsigned 32-bit little endian + /// integer. + /// + public uint Crc32 { get; set; } + + /// + /// Backward Size is stored as a 32-bit little endian integer, + /// which indicates the size of the Index field as multiple of + /// four bytes, minimum value being four bytes. + /// + /// + /// The real index size can be calculated by the following: + /// (BackwardSize + 1) * 4 + /// + public uint BackwardSize { get; set; } + + /// + /// This is a copy of the Stream Flags field from the Stream + /// Header. The information stored to Stream Flags is needed + /// when parsing the Stream backwards. + /// + public HeaderFlags Flags { get; set; } + + /// + /// Header magic number ("YZ") + /// + /// 2 bytes + public byte[]? Signature { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/XZ/Header.cs b/SabreTools.Serialization/Models/XZ/Header.cs new file mode 100644 index 00000000..40eb935a --- /dev/null +++ b/SabreTools.Serialization/Models/XZ/Header.cs @@ -0,0 +1,27 @@ +namespace SabreTools.Data.Models.XZ +{ + /// + /// Represents the pre-block data in the stream + /// + public class Header + { + /// + /// Header magic number (0xFD, '7', 'z', 'X', 'Z', 0x00) + /// + /// 6 bytes + public byte[]? Signature { get; set; } + + /// + /// The first byte of Stream Flags is always a null byte. In the + /// future, this byte may be used to indicate a new Stream version + /// or other Stream properties. + /// + public HeaderFlags Flags { get; set; } + + /// + /// The CRC32 is calculated from the Stream Flags field. It is + /// stored as an unsigned 32-bit little endian integer. + /// + public uint Crc32 { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/XZ/Index.cs b/SabreTools.Serialization/Models/XZ/Index.cs new file mode 100644 index 00000000..094f1480 --- /dev/null +++ b/SabreTools.Serialization/Models/XZ/Index.cs @@ -0,0 +1,36 @@ +namespace SabreTools.Data.Models.XZ +{ + public class Index + { + /// + /// The value of Index Indicator is always 0x00 + /// + public byte IndexIndicator { get; set; } + + /// + /// This field indicates how many Records there are in the List + /// of Records field, and thus how many Blocks there are in the + /// Stream + /// + /// Stored as a variable-length integer + public byte[]? NumberOfRecords { get; set; } + + /// + /// One record per block + /// + public Record[]? Records { get; set; } + + /// + /// This field MUST contain 0-3 null bytes to pad the Index to + /// a multiple of four bytes. + /// + public byte[]? Padding { get; set; } + + /// + /// The CRC32 is calculated over everything in the Index field + /// except the CRC32 field itself. The CRC32 is stored as an + /// unsigned 32-bit little endian integer. + /// + public uint Crc32 { get; set; } + } +} diff --git a/SabreTools.Serialization/Models/XZ/Record.cs b/SabreTools.Serialization/Models/XZ/Record.cs new file mode 100644 index 00000000..1f3aab4b --- /dev/null +++ b/SabreTools.Serialization/Models/XZ/Record.cs @@ -0,0 +1,17 @@ +namespace SabreTools.Data.Models.XZ +{ + public class Record + { + /// + /// Unpadded size of the block + /// + /// Stored as a variable-length integer + public byte UnpaddedSize { get; set; } + + /// + /// Uncompressed size of the block + /// + /// Stored as a variable-length integer + public byte[]? NumberOfRecords { get; set; } + } +} diff --git a/SabreTools.Serialization/WrapperFactory.cs b/SabreTools.Serialization/WrapperFactory.cs index 3cf1dd0f..16cd18c7 100644 --- a/SabreTools.Serialization/WrapperFactory.cs +++ b/SabreTools.Serialization/WrapperFactory.cs @@ -788,7 +788,7 @@ namespace SabreTools.Serialization #region XZ - if (magic.StartsWith(Data.Models.XZ.Constants.SignatureBytes)) + if (magic.StartsWith(Data.Models.XZ.Constants.HeaderSignatureBytes)) return WrapperType.XZ; if (extension.Equals("xz", StringComparison.OrdinalIgnoreCase)) diff --git a/SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs b/SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs index dbb2147a..a3cf7f93 100644 --- a/SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs +++ b/SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs @@ -132,7 +132,7 @@ namespace SabreTools.Serialization.Wrappers extension = "xml"; break; } - else if (overlaySample.StartsWith(Data.Models.XZ.Constants.SignatureBytes)) + else if (overlaySample.StartsWith(Data.Models.XZ.Constants.HeaderSignatureBytes)) { extension = "xz"; break; diff --git a/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs b/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs index a1f09d89..de0f8a9e 100644 --- a/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs +++ b/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs @@ -271,7 +271,7 @@ namespace SabreTools.Serialization.Wrappers extension = "xml"; break; } - else if (overlaySample.StartsWith(Data.Models.XZ.Constants.SignatureBytes)) + else if (overlaySample.StartsWith(Data.Models.XZ.Constants.HeaderSignatureBytes)) { extension = "xz"; break; @@ -474,7 +474,7 @@ namespace SabreTools.Serialization.Wrappers extension = "xml"; break; } - else if (resourceSample.StartsWith(Data.Models.XZ.Constants.SignatureBytes)) + else if (resourceSample.StartsWith(Data.Models.XZ.Constants.HeaderSignatureBytes)) { extension = "xz"; break;