ZArchive support (#75)

* ZArchive support

* Fix offset record format

* Simplfiy Extensions

* Delete unused writers and test data

* Rework reader

* Fix build
This commit is contained in:
Deterous
2026-04-02 15:18:47 +09:00
committed by GitHub
parent 4035d9db86
commit 5bb8557555
23 changed files with 1292 additions and 0 deletions

View File

@@ -90,6 +90,7 @@ Below is a list of all existing namespaces with the `SabreTools.Data.Models` pre
| `XDVDFS` | Xbox DVD Filesystem (XISO) |
| `XZ` | xz archive |
| `XZP` | XBox Package File |
| `ZArchive` | ZArchive (ZAR) |
| `ZSTD` | ZSTD archive |
## Notable Information Sources

View File

@@ -0,0 +1,50 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <summary>
/// Represents a single ZAR archive
/// Most fields are Big Endian
/// </summary>
/// <see href="https://github.com/Exzap/ZArchive/"/>
public class Archive
{
/// <summary>
/// Zstd compressed file data, from 65536-byte blocks of the original files
/// Blocks are stored uncompressed if ZStd does not decrease the size
/// Due to the file size, this field is not usually filled in but remains here for completeness
/// </summary>
public byte[]? CompressedData { get; set; }
/// <summary>
/// Padding bytes to be added after compressed blocks to ensure 8-byte alignment
/// Padding bytes are all NULL (0x00)
/// </summary>
public byte[]? Padding { get; set; }
/// <summary>
/// Records containing the offsets and block sizes of each group of blocks
/// This allows the reader to jump to any 65536-byte boundary in the uncompressed stream.
/// </summary>
public OffsetRecord[] OffsetRecords { get; set; } = [];
/// <summary>
/// UTF-8 strings, prepended by string lengths
/// </summary>
public NameTable NameTable { get; set; } = new();
/// <summary>
/// Serialized file tree structure using a queue of nodes
/// </summary>
public FileDirectoryEntry[] FileTree { get; set; } = [];
/// <summary>
/// Section for custom key-value pairs and properties
/// </summary>
public Metadata? Metadata { get; set; }
/// <summary>
/// Archive footer containing the offsets and sizes of all other sections
/// Ends with a SHA256 hash/size of the entire archive, and magic bytes
/// </summary>
public Footer Footer { get; set; } = new();
}
}

View File

@@ -0,0 +1,68 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <see href="https://github.com/Exzap/ZArchive/"/>
public static class Constants
{
/// <summary>
/// Number of compressed blocks referred to by a record
/// </summary>
public const int BlockSize = 64 * 1024;
/// <summary>
/// Number of compressed blocks referred to by a record
/// </summary>
public const int BlocksPerOffsetRecord = 16;
/// <summary>
/// Number of bytes stored in an offset record
/// </summary>
public const int OffsetRecordSize = sizeof(ulong) + sizeof(ushort) * BlocksPerOffsetRecord;
/// <summary>
/// Number of bytes stored in a file/directory entry
/// </summary>
public const int FileDirectoryEntrySize = 16;
/// <summary>
/// Number of bytes stored in the footer
/// 6 OffsetInfo fields,
/// </summary>
public const int FooterSize = 144;
/// <summary>
/// NameOffsetAndTypeFlag value for the root node in the FileTree
/// </summary>
public const uint RootNode = 0x7FFFFFFF;
/// <summary>
/// Mask for the NameOffsetAndTypeFlag value when checking if it is a file
/// </summary>
public const uint FileFlag = 0x80000000;
/// <summary>
/// Maximum size of the Offset Records section
/// </summary>
public const ulong MaxOffsetRecordsSize = 0xFFFFFFFF;
/// <summary>
/// Maximum size of the Offset Records section
/// </summary>
public const ulong MaxNameTableSize = 0x7FFFFFFF;
/// <summary>
/// Maximum size of the File Tree section
/// </summary>
public const ulong MaxFileTreeSize = 0x7FFFFFFF;
/// <summary>
/// ZArchive magic bytes at end of file
/// </summary>
public static readonly byte[] MagicBytes = [0x16, 0x9F, 0x52, 0xD6];
/// <summary>
/// ZArchive version field that acts as an extended magic immediately before final 4 magic bytes
/// Currently only version 1 is implemented, any future version bytes are not suppported yet
/// </summary>
public static readonly byte[] Version1Bytes = [0x61, 0xBF, 0x3A, 0x01];
}
}

View File

@@ -0,0 +1,26 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <summary>
/// Node in the FileTree representing a directory
/// </summary>
/// <see href="https://github.com/Exzap/ZArchive/"/>
public sealed class DirectoryEntry : FileDirectoryEntry
{
/// <summary>
/// Starting index of the directory node
/// </summary>
/// <remarks>Big-endian</remarks>
public uint NodeStartIndex { get; set; }
/// <summary>
/// Number of
/// </summary>
/// <remarks>Big-endian</remarks>
public uint Count { get; set; }
/// <summary>
/// Reserved field
/// </summary>
public uint Reserved { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <summary>
/// Node in the FileTree
/// Represents either a file or a directory
/// </summary>
/// <see href="https://github.com/Exzap/ZArchive/"/>
public abstract class FileDirectoryEntry
{
/// <summary>
/// MSB is the type flag, 0 is Directory, 1 is File
/// Remaining 31 bits are the offset in the NameTable
/// </summary>
/// <remarks>Big-endian</remarks>
public uint NameOffsetAndTypeFlag { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <summary>
/// Node in the FileTree representing a file
/// </summary>
/// <see href="https://github.com/Exzap/ZArchive/"/>
public sealed class FileEntry : FileDirectoryEntry
{
/// <summary>
/// Lowest 8 bits of the file's offset
/// </summary>
/// <remarks>Big-endian</remarks>
public uint FileOffsetLow { get; set; }
/// <summary>
/// Lowest 8 bits of the file's size
/// </summary>
/// <remarks>Big-endian</remarks>
public uint FileSizeLow { get; set; }
/// <summary>
/// Highest 4 bits of the file's size
/// </summary>
/// <remarks>Big-endian</remarks>
public ushort FileSizeHigh { get; set; }
/// <summary>
/// Highest 4 bits of the file's offset
/// </summary>
/// <remarks>Big-endian</remarks>
public ushort FileOffsetHigh { get; set; }
}
}

View File

@@ -0,0 +1,60 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <summary>
/// Footer data stored at the end of a ZArchive file
/// </summary>
/// <see href="https://github.com/Exzap/ZArchive/"/>
public class Footer
{
/// <summary>
/// Size and offset values for the CompressedData section
/// </summary>
public OffsetInfo SectionCompressedData { get; set; } = new();
/// <summary>
/// Size and offset values for the OffsetRecords section
/// </summary>
public OffsetInfo SectionOffsetRecords { get; set; } = new();
/// <summary>
/// Size and offset values for the NameTable section
/// </summary>
public OffsetInfo SectionNameTable { get; set; } = new();
/// <summary>
/// Size and offset values for the FileFree section
/// </summary>
public OffsetInfo SectionFileTree { get; set; } = new();
/// <summary>
/// Size and offset values for the MetaDirectory section
/// </summary>
public OffsetInfo SectionMetaDirectory { get; set; } = new();
/// <summary>
/// Size and offset values for the MetaData section
/// </summary>
public OffsetInfo SectionMetaData { get; set; } = new();
/// <summary>
/// SHA-256 hash of the ZArchive file prior the footer
/// </summary>
public byte[] IntegrityHash { get; set; } = new byte[32];
/// <summary>
/// Size of the entire ZArchive file
/// </summary>
/// <remarks>Big-endian</remarks>
public ulong Size { get; set; }
/// <summary>
/// Version indicator, also acts as extended magic
/// </summary>
public byte[] Version { get; set; } = new byte[4];
/// <summary>
/// Magic bytes to indicate ZArchive file
/// </summary>
public byte[] Magic { get; set; } = new byte[4];
}
}

View File

@@ -0,0 +1,11 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <summary>
/// ZARchive section for Meta Data and Meta Directories
/// </summary>
/// <see href="https://github.com/Exzap/ZArchive/"/>
public class Metadata
{
// Not yet implemented in the ZArchive standard, should be empty
}
}

View File

@@ -0,0 +1,27 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <summary>
/// Filename entry in the NameTable
/// </summary>
/// <see href="https://github.com/Exzap/ZArchive/"/>
public class NameEntry
{
/// <summary>
/// Filename length, with MSB set to 0 for filenames less than 127 long
/// NodeLengthShort and NodeLengthLong fields are exclusive, and one must be present
/// </summary>
public byte? NodeLengthShort { get; set; }
/// <summary>
/// Filename length, with prefix byte's MSB set to 1 for filenames greater than 127 long
/// NodeLengthShort and NodeLengthLong fields are exclusive, and one must be present
/// </summary>
public ushort? NodeLengthLong { get; set; }
/// <summary>
/// UTF-8 encoded file name
/// </summary>
/// <remarks>Maximum length of 2^15 - 1 bytes</remarks>
public byte[] NodeName { get; set; } = [];
}
}

View File

@@ -0,0 +1,20 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <summary>
/// UTF-8 strings, prepended by string lengths
/// </summary>
/// <see href="https://github.com/Exzap/ZArchive/"/>
public class NameTable
{
/// <summary>
/// List of filename entries
/// </summary>
public NameEntry[] NameEntries { get; set; } = [];
/// <summary>
/// Virtual field, to cache the offsets of each name entry in the name table
/// Used for referencing the name entry from an offset into the name table
/// </summary>
public uint[] NameTableOffsets { get; set; } = [];
}
}

View File

@@ -0,0 +1,19 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <summary>
/// Offset and size values of a ZArchive section, stored in the Footer
/// </summary>
/// <see href="https://github.com/Exzap/ZArchive/"/>
public class OffsetInfo
{
/// <summary>
/// Base offset value for the section in bytes
/// </summary>
public ulong Offset { get; set; }
/// <summary>
/// Total size of the section in bytes
/// </summary>
public ulong Size { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
namespace SabreTools.Data.Models.ZArchive
{
/// <summary>
/// Location and size properties of compressed blocks of the file data
/// </summary>
/// <see href="https://github.com/Exzap/ZArchive/"/>
public class OffsetRecord
{
/// <summary>
/// Base offset of compressed blocks
/// </summary>
public ulong Offset { get; set; }
/// <summary>
/// Sizes of each compressed block in this record
/// </summary>
public ushort[] Size { get; set; } = new ushort[Constants.BlocksPerOffsetRecord];
}
}