-
Notifications
You must be signed in to change notification settings - Fork 0
ZAR Format
The .zar format is ZArchive 0.1.2 by Exzap (unknownbrackets/ZArchive). This page documents the on-disk layout as implemented by ZArchiveSharp, which is byte-identical to the reference implementation.
A .zar file stores a directory tree whose file contents are split into 64 KiB blocks, each independently compressed with zstd. The layout:
+---------------------------+
| Compressed data blocks | <- zstd frames, back to back
+---------------------------+
| Offset records | <- block size index
+---------------------------+
| Name table | <- deduplicated entry names
+---------------------------+
| File tree | <- directory entries
+---------------------------+
| Meta directory (empty) |
+---------------------------+
| Meta data (empty) |
+---------------------------+
| Footer (144 bytes) | <- section table + SHA-256
+---------------------------+
All integers are big-endian. Entry names are encoded in Windows-1252.
| Constant | Value | Meaning |
|---|---|---|
| Block size | 64 KiB (0x10000) |
Uncompressed block granularity |
| Entries per offset record | 16 | Blocks indexed per record |
| Invalid node | 0xFFFFFFFF |
Path-not-found handle |
| Root name offset | 0x7FFFFFFF |
Root dir has no name |
| Max file size | 2⁴⁸ − 1 | Per-file limit |
| Max name length | 0x7FFF |
15-bit length header |
| Footer magic | 0x169F52D6 |
Last u32 |
| Version | 0x61BF3A01 |
Second-to-last u32 |
File content is cut into 64 KiB blocks. Each block is compressed independently:
- Compressed: a standalone zstd frame (windowLog = 17), one frame per block
-
Raw: stored uncompressed when compression does not shrink the block (the reference
StoreBlockrule: compressed ≥ input → raw) - Padding: the final partial block is zero-padded to 64 KiB before compression
Repeat-offset history is frame-scoped — each block is an independent frame, so repcodes never carry across blocks.
Each record indexes 16 blocks (40 bytes on disk):
struct CompressionOffsetRecord // 40 bytes, BE
{
uint64 baseOffset; // output offset of first block in record
uint16 sizes[16]; // per-block (compressedSize - 1)
}
A stored sizes[i] of 0xFFFF-decoding logic applies: actual compressed size = sizes[i] + 1. Blocks are located by:
blockIndex -> record = blockIndex / 16, slot = blockIndex % 16
blockOffset = record.baseOffset + sum(sizes[0..slot-1] + 1)
The records form a contiguous table placed after the compressed data.
Deduplicated entry names, each stored as:
uint16 length; // up to 0x7FFF
byte chars[length]; // Windows-1252
Names longer than 0x7FFF characters are truncated on characters before encoding (not bytes) — a faithful port of a 0.1.2 quirk.
Windows-1252 decoding maps 0x80–0x9F through the CP1252 high table (undefined slots map to C1 controls); characters with no representation encode to ? (0x3F).
A flat array of 16-byte entries, serialized in preorder:
struct FileDirectoryEntry // 16 bytes, BE
{
uint32 nameOffsetAndTypeFlag; // bit 31: file flag; bits 0-30: name offset
uint32 field1;
uint32 field2;
uint32 field3;
}
| Field | Meaning |
|---|---|
field1 |
File offset low 32 bits |
field2 |
File size low 32 bits |
field3 |
bits 0–15: offset high 16; bits 16–31: size high 16 |
Offset and size are 48-bit values assembled from the split fields. Offsets are logical (uncompressed) input offsets.
| Field | Meaning |
|---|---|
field1 |
First child node index |
field2 |
Child count |
field3 |
Reserved (0) |
The root directory is entry 0 with the special name offset 0x7FFFFFFF.
144 bytes, placed at the end:
struct Footer // 144 bytes, BE
{
OffsetInfo compressedData; // 16 bytes: uint64 offset + uint64 size
OffsetInfo offsetRecords; // 16
OffsetInfo names; // 16
OffsetInfo fileTree; // 16
OffsetInfo metaDirectory; // 16 (empty in 0.1.2)
OffsetInfo metaData; // 16 (empty in 0.1.2)
byte integrityHash[32]; // SHA-256
uint64 totalSize; // including footer
uint32 version; // 0x61BF3A01
uint32 magic; // 0x169F52D6 <- magic/version at the END
}
The SHA-256 covers every output byte written before the footer, then the footer itself with the hash field zeroed. This detects truncation and most corruption; the reader rejects archives whose hash does not verify.
- Separators: both
/and\accepted on input; stored paths use/ - Lookup is case-insensitive (ASCII A–Z folding only)
- Name deduplication in the writer's table is case-sensitive (ordinal)
- Sort order mirrors the C++ comparator exactly, including its reversed-mismatch quirk (
(byte)c2 - (byte)c1) and "shorter string sorts after its prefix"
-
TryOpenreturnsnullon any validation failure — it never throws (mirrors the C++ open chain) - A 4 MiB LRU cache (64 × 64 KiB blocks) holds decompressed blocks
- Reads are thread-safe (single lock, like the C++ mutex)
- Data blocks carry no per-block checksums (same as native): flipped bytes may decode to different content instead of throwing. Truncations always fail the open.
| Limit | Value |
|---|---|
| File size | 2⁴⁸ − 1 bytes |
| Name length | 0x7FFF chars |
| Block count | ~2⁶⁴ blocks (offset records are 64-bit based) |
- ZArchiveSharp-written archives extract with the original
zarchive.exeand vice versa (tested both directions) - Byte-parity is at the writer API sequence level: filesystem enumeration order is OS-unspecified in C++, so
ZArchiveTool.Packsorts entries by default (deterministicOrder: true); passfalseto mirror native enumeration for parity runs
ZArchiveSharp
Getting Started
Core Concepts
API
Advanced
Links