1. Introduction
This section is non-normative.
The Aqueous archive format stores a directory tree, or a single file, as a sequence of blocks. This section gives an overview of how the parts of the format fit together; each paragraph points to the chapter defining the part it describes.
An archive is a header, a sequence of blocks and a footer (§ 3 Container). Every block starts with its type and its size, so a decompressor can skip any block without understanding it, and the type tells whether the block can be ignored when it is not understood (§ 3.1.1 Block types). The footer points back to the last metadata block, so that a decompressor can start reading from the end of the archive.
Blocks are grouped into segments, each ending with a metadata block (§ 4 Segments and entries). The metadata block names the entries of the segment, i.e. directories, files, symbolic links and other nodes, and points each file to the block holding its content. A later segment adds, replaces and removes entries of earlier segments, so that an archive can be extended by appending a segment without rewriting what precedes it. An archive without any metadata block simply decompresses into a single unnamed file, as a gzip file does.
The content of a file is given by a single leaf block. The simplest leaf block is a data block storing the content itself, optionally compressed with Zstandard and a dictionary shared by many small blocks (§ 5 Stored data). A formula block is a leaf block that describes the content instead, as a small stack machine program over non-leaf blocks following it (§ 6 Formulas). The program takes slices of them, concatenates them, patches them, and applies WebAssembly modules to them to undo a transformation made by the compressor (§ 7 Modules). A leaf block and the non-leaf blocks following it form a parcel, which is typically all that a decompressor reads to extract a single file, and the formula block serves as an index for random access within it.
An overlay archive describes a directory tree as changes to an existing one, called its base (§ 8 Overlay archives and imports). Import blocks read byte ranges of files in the base, which formulas can patch, so that an overlay archive works as an update or a delta of the base.
The whole archive may be encrypted with a passphrase using age [AGE] (§ 9 Encrypted archives). Checksums cover every block except, optionally, leaf data blocks, and except shorthand blocks, which name registered dictionaries and modules by their hashes (§ 4.2.2 Registered dictionaries and modules). Mirror blocks hold copies of the blocks whose damage would affect many files (§ 10 Integrity and recovery). Metadata that is not necessary to read the files, such as timestamps, comments and permissions, is encoded in CBOR and may be ignored by a decompressor (§ 11 Optional metadata, § 12 Extraction).
2. Conventions
2.1. Expressions
All expressions are interpreted according to the standard arithmetics, with the following extensions:
-
a / b equals floor(a ÷ b).
-
a % b, where b > 0, is a non-negative modulo of a divided by b, and equals a - (a / b) × b.
-
a << b equals a × 2b.
-
a >> b equals a / 2b.
-
XX16 denotes a hexadecimal integer.
-
(a, b, …) denotes an ordered array of values.
-
(XX YY ZZ)16 is a shorthand for (XX16, YY16, ZZ16).
-
a || b concatenates two arrays a and b, treating any non-array value as a singleton array (a) or (b).
-
For array a, an equals a || an-1 when n > 0. a0 equals an empty array ().
-
[a, b] denotes a closed interval between a and b, both inclusive.
-
[a, b) denotes a half-open interval between a (inclusive) and b (exclusive).
All expressions result in an integer between -(264 - 1) and 264 - 1, or an array of those integers with the number of elements not exceeding 264 - 1. Note that subexpressions still may exceed this range.
2.2. Types
Types are a set of values and associated encoding scheme. A type or a structure is implicitly-sized when its byte length is only available through the context. Unless stated as being implicitly-sized, all types have the defined byte length out of the context.
u8 is an unsigned 8-bit integer, occupying 1 byte. Its range is [0, 28).
u16le is an unsigned 16-bit integer, occupying 2 bytes, stored in little-endian byte order (least significant byte first). Its range is [0, 216).
u32le is an unsigned 32-bit integer, occupying 4 bytes, stored in little-endian byte order. Its range is [0, 232).
u64le is an unsigned 64-bit integer, occupying 8 bytes, stored in little-endian byte order. Its range is [0, 264).
u(n), where n is an integer less than 264, is the smallest integer type that can represent n if n > 0, and a zero-sized type if n = 0. In other words,
-
If n = 0, u(n) is zero-sized.
-
If n ∈ [1, 28), u(n) is the same as u8.
-
If n ∈ [28, 216), u(n) is the same as u16le.
-
If n ∈ [216, 232), u(n) is the same as u32le.
-
If n ∈ [232, 264), u(n) is the same as u64le.
leb128 is an unsigned 64-bit integer encoded using the variable number of bytes with the LEB128 encoding. Each byte, except the last, has its most significant bit (MSB) set to 1, indicating that more bytes follow. The remaining 7 bits of each byte are used to store the integer’s value. The least significant 7 bits of the first byte are the least significant bits of the integer, and so on. The last byte has its MSB set to 0. Its range is [0, 264): an encoding MUST be at most 10 bytes long, and MUST NOT encode a value of 264 or more.
a[n], where n is an integer, denotes an ordered array of as. n’s range is [0, 264).
a[*] denotes an implicitly-sized ordered array of as with an unspecified number of elements. The number of elements is implicitly determined from the context. This specification uses two kinds of such contexts:
-
When one or more a[*] fields are in the structure with the defined byte length, the numbers of elements in those fields are increased as needed to exactly fill the required byte length. There MUST be a single set of the numbers of elements that satisfy these conditions.
-
When an a[*] field is given with a constraint, the field extends until, but not including, the first instance of a which doesn’t satisfy the required constraint.
2.3. Structure
Structure is a logical group of one or more ordered fields of possibly differing types concatenated together. Each field has the following properties:
-
The descriptive name of the field (e.g. size, compression_params).
-
The data type of the field (e.g. leb128, u8[size]), with an optional constraint.
-
Optional criteria or conditions that dictate the presence, value, or interpretation of the field.
Field names can only coincide when the field’s type varies by conditions, in which case at most one such condition is applicable at any time. The name can be omitted when the field’s name is the same as the previous field’s.
The field type may be offset, which is a special zero-sized type interpreted as the offset to the following byte relative to the unspecified file position (origin). Offsets are only used to measure the number of bytes within a set of fields. The syntax and semantics of this specification don’t depend on the choice of origin.
The optional constraint does not affect the field’s encoding and only narrows down the possible set of values. Constraints can be one of the following:
-
a ∈ X requires that the value is in the specified set X.
-
a ∉ X requires that the value is not in the specified set X.
-
a = x requires that the value is exactly the given value x. If x is a constant expression, this effectively fixes the byte encoding of this field.
-
a ≠ x requires that the value is not the given value x.
The conditions may be one of the following:
-
"Optional" indicates that conforming implementations MAY include or exclude given field as needed.
-
"If …" indicates that the field MUST be included if and only if the specified condition is true.
-
"Depends on …" indicates that the field’s interpretation varies with the specified set of other fields.
Structures are implicitly-sized only when their byte length is not available through the context and at least one implicitly-sized field exists therein.
2.4. Constants
This specification defines and uses the following constants, all of which are denoted in UPPERCASE.
| Name | Value |
|---|---|
| HEADER_TYPE | 27816 |
| OVERLAY_HEADER_TYPE | 6916 |
| FOOTER_TYPE | 90 5BCB 957D16 |
| OVERLAY_FOOTER_TYPE | 7FC16 |
| ENCRYPTED_HEADER_TYPE | 155 04DC A57916 |
| HEADER_MAGIC | (F8 04 03 6A D6 BE)16 |
| OVERLAY_HEADER_MAGIC | (69 0D 15 AF F6 B2)16 |
| FOOTER_MAGIC | (FD AA AE DE 85 12)16 |
| OVERLAY_FOOTER_MAGIC | (FC 0F 6E 7B 4B AC)16 |
| ENCRYPTED_HEADER_MAGIC | (F9 CA F2 A6 D0 2A 01 00)16 |
| ENCRYPTED_MIRRORED_HEADER_MAGIC | (F9 CA F2 A6 D0 2A 02 00)16 |
| MIRROR_MAGIC | (02 AA F5 FE BA 2B AE 23)16 |
| OVERLAY_MIRROR_MAGIC | (01 05 34 FA BD 11 AF 53)16 |
| ENCRYPTED_MIRROR_MAGIC | (78 D7 3F 69 0A FF 44 83)16 |
2.5. Semantics and failures
This specification defines the meaning of an archive as a function from a byte sequence to an archive value, which also takes a base for an overlay archive. Every algorithm in this specification is declarative: it defines what the result is, not how or in which order it has to be computed, and a conforming implementation MAY compute it in any other way that produces the same result.
The result of interpreting an archive is either an archive value or a failure, which is one of the following:
-
An invalid archive failure occurs when the input violates any requirement of this specification. It is a property of the input alone, and all conforming decompressors agree on it.
-
A resource failure occurs when the evaluation exceeds a declared or implementation-defined limit. It is not a property of the input, and conforming decompressors MAY disagree on it.
-
A decryption failure occurs when an encrypted archive can’t be decrypted with the given passphrase, if any. It is a property of the input and the passphrase, and all conforming decompressors agree on it.
-
A base failure occurs when an overlay archive is interpreted without a base, or an import can’t be resolved in the given base (§ 8.2 Resolving an import). It is a property of the input and the base, and all conforming decompressors agree on it.
-
An unsupported failure occurs when the input contains a block of a critical type not defined by this specification (§ 3.1.1.1 Reserved block types), or a shorthand block whose registered hash the decompressor doesn’t know (§ 4.2.2 Registered dictionaries and modules). It is a property of the input, the version of this specification and the registry entries known, and all conforming decompressors implementing the same version and knowing the same entries agree on it.
A conforming compressor, given an archive value, MUST produce a byte sequence whose interpretation is that archive value. When it produces an overlay archive, it is also given a base, and the interpretation with that base MUST be that archive value. This specification doesn’t constrain how the compressor chooses blocks, block sizes, dictionaries or formulas.
3. Container
3.1. Block
Blocks are a typed byte sequence. The entirety of an unencrypted archive and the beginning of an encrypted archive consist of either blocks or block-like structures.
Blocks have the following generic structure:
| Name | Type | Conditions |
|---|---|---|
| block_start | offset | |
| type | leb128 ∉ {FOOTER_TYPE, OVERLAY_FOOTER_TYPE} | |
| size | u8 | If type % 4 = 0 |
| u16le | If type % 4 = 1 | |
| u32le | If type % 4 = 2 | |
| u64le | If type % 4 = 3 | |
| data | u8[size] | Depends on type |
| block_end | offset | |
| padding | u8 | If (block_end - block_start) % 2 = 1 |
Some type values, including FOOTER_TYPE and OVERLAY_FOOTER_TYPE, are also used to denote block-like structures. For example the archive footer has to have a distinct block type in order to be placed after blocks. This specification doesn’t assign any meaning to such block-like structures unless explicitly stated.
type MUST use the shortest leb128 encoding.
The presence of padding ensures that there are even number of bytes in any block. Conforming compressors MUST set padding to zero, but its value doesn’t affect the result of interpreting an archive.
Later sections describe the contents of data field according to each defined type. Since type also determines the byte length of size, there are generally multiple type values assigned for each logical block type.
3.1.1. Block types
The following table lists every range of block types.
A range of block types for a single logical block type is also described in binary,
e.g. 0001 0mss,
where letters denote bits that determine the parameters of the block.
ss always denotes the two least significant bits, which determine the width of size as above:
0 for u8, 1 for u16le, 2 for u32le and 3 for u64le.
Whether a reserved block type is critical is determined by its leading digit (§ 3.1.1.1 Reserved block types).
Block types of 2 hexadecimal digits are as follows:
| Block Types | Critical | Definition |
|---|---|---|
| 0016 — 0316 | Yes | § 3.3 Spacer block |
| 0416 | Yes | § 3.4 Size hint block |
| 0516 — 0F16 | Yes | Reserved |
| 1016 — 1716 | Yes | § 4.3 Metadata block |
| 1816 — 1F16 | Yes | Reserved |
| 2016 — 3716 | Yes | § 5.1 Data block |
| 3816 — 3F16 | Yes | Reserved |
| 4016 — 4B16 | Yes | § 5.1 Data block |
| 4C16 — 4F16 | Yes | Reserved |
| 5016 | Yes | § 5.2 Dictionary block, as a shorthand block (§ 4.2.2 Registered dictionaries and modules) |
| 5116 — 5716 | Yes | § 5.2 Dictionary block |
| 5816 — 5F16 | Yes | § 8.1 Import block |
| 6016 | Yes | § 7.1 Module block, as a shorthand block (§ 4.2.2 Registered dictionaries and modules) |
| 6116 — 6316 | Yes | § 7.1 Module block |
| 6416 — 6716 | Yes | § 7.2 Resource block |
| 6816 | Yes | Reserved |
| 6916 | Yes | Reserved for OVERLAY_HEADER_TYPE (§ 3.1.1.1 Reserved block types) |
| 6A16 — 6F16 | Yes | Reserved |
| 7016 — 7716 | Yes | § 6.1 Formula block |
| 7816 — 9F16 | Yes | Reserved |
| A016 — CF16 | No | Reserved |
| D016 — DF16 | No | Reserved for development environments |
| E016 — EF16 | No | Reserved for experiments |
| F016 — FF16 | No | Reserved |
Block types of 3 or more hexadecimal digits are as follows:
| Block Types | Critical | Definition |
|---|---|---|
| 10016 — 9FF16, 100016 — 9FFF16, … | Yes | Reserved, including block-like structures (§ 3.1.1.1 Reserved block types) |
| A0016 — BFF16, A00016 — BFFF16, … | No | Reserved |
| C0016 — C0F16 | No | Reserved |
| C1016 — C1716 | No | § 10.3 Mirror block for § 4.3 Metadata block |
| C1816 — C4F16 | No | Reserved |
| C5016 — C5716 | No | § 10.3 Mirror block for § 5.2 Dictionary block |
| C5816 — C5F16 | No | § 10.3 Mirror block for § 8.1 Import block |
| C6016 — C6316 | No | § 10.3 Mirror block for § 7.1 Module block |
| C6416 — C6716 | No | § 10.3 Mirror block for § 7.2 Resource block |
| C6816 — C6F16 | No | Reserved |
| C7016 — C7716 | No | § 10.3 Mirror block for § 6.1 Formula block |
| C7816 — CFF16, C00016 — CFFF16, … | No | Reserved |
| D0016 — DFF16, D00016 — DFFF16, … | No | Reserved for development environments |
| E0016 — EFF16, E00016 — EFFF16, … | No | Reserved for experiments |
| F0016 — FFF16, F00016 — FFFF16, … | No | Reserved |
3.1.1.1. Reserved block types
The leading digit of a block type is the first digit of its hexadecimal representation, padded with zeros to at least 2 digits. For example, the leading digits of 416, 4B16 and C1016 are 0, 4 and C respectively.
A block type is critical if its leading digit is 0 through 9, and non-critical if it is A through F. A block whose type is not defined by this specification:
-
Results in an unsupported failure (§ 4.4 Interpreting an archive) if its type is critical;
-
Is an ignored block otherwise. Like a mirror block, an ignored block doesn’t belong to any segment, MAY occur anywhere in blocks and doesn’t affect the result of interpreting an archive.
Blocks whose type’s leading digit is D (i.e. D016 through DF16, D0016 through DFF16, …) are reserved for development environments. This specification doesn’t assign any meanings to these blocks. These blocks MUST be written or read only with specific instructions to enable them.
Blocks whose type’s leading digit is E (i.e. E016 through EF16, E0016 through EFF16, …) are reserved for experiments. This specification doesn’t assign any meanings to those blocks.
The following block types are defined as invalid due to the use as block-like structures. A valid archive MUST NOT include such block types as a proper block.
-
Block 27816 (HEADER_TYPE) is used for the unencrypted archive header.
-
Block 6916 (OVERLAY_HEADER_TYPE) is used for the overlay archive header.
-
Block 90 5BCB 957D16 (FOOTER_TYPE) is used for the archive footer.
-
Block 7FC16 (OVERLAY_FOOTER_TYPE) is used for the overlay archive footer.
-
Block 155 04DC A57916 (ENCRYPTED_HEADER_TYPE) is used for the encrypted archive header.
3.2. Shared external structure
Many block types share the same external structure, only varying its internal structure. The structure may or may not be compressed, and uses a compression_method and a checksum_method specified by the parent structure. Each compression method defines the structure of its parameters (compression_params), whose byte length is determined by parsing it. Each checksum_method also has an associated, fixed checksum_size. Checksummed Zstandard data is the compressed external structure (compressed_data) with Zstandard as compression_method and XXH64 as checksum_method.
The compressed external structure (compressed_data) consists of the following:
| Name | Type | Conditions |
|---|---|---|
| original_size_delta | leb128 | |
| original_size_shrink | leb128 | If original_size_delta = 0 |
| compression_params | Parameters of compression_method | Depends on compression_method |
| data_start | offset | |
| data | u8[*] | |
| data_end | offset | |
| checksum | u8[checksum_size] | If checksum_method is not None, depends on checksum_method |
The compressed size (compressed_size) is inferred from the block size: compressed_size = data_end - data_start. The original size (original_size) is encoded relative to compressed_size:
-
If original_size_delta is not 0, original_size = compressed_size + original_size_delta.
-
Otherwise, original_size = compressed_size - original_size_shrink, and original_size_shrink MUST be at most compressed_size.
Each original size thus has exactly one encoding, where the original size equal to compressed_size is encoded as two zero bytes.
The uncompressed external structure (uncompressed_data) consists of the following:
| Name | Type | Conditions |
|---|---|---|
| data | u8[*] | |
| checksum | u8[checksum_size] | If checksum_method is not None |
In both structures, the checksum is computed over the decoded data (§ 5.3 Block contents), i.e. the data after decompression, rather than the stored bytes (§ 10.1 Checksums).
3.2.1. Zstandard compression method
This specification only defines a single compression method, Zstandard [RFC8878]. A Zstandard block is the "Block" structure in the Zstandard frame ([RFC8878], Section 3.1.1.2). Its parameters (compression_params) are as follows:
| Name | Type |
|---|---|
| window_descriptor | u8 |
The meaning of the window_descriptor field is identical to the Zstandard’s Window_Descriptor byte,
which determines Window_Size as in [RFC8878], Section 3.1.1.1.2.
Consequently each Zstandard block holds at most Block_Maximum_Size = min(Window_Size, 128 KiB) bytes of data.
Conforming decompressors MUST support any Window_Size up to 227 bytes (128 MiB),
and MAY return a resource failure for a larger Window_Size.
Conforming compressors SHOULD NOT use a Window_Size larger than 227 bytes
unless explicitly requested.
A dictionary is a byte sequence supplied to the compression method as if it preceded the data, typically shared by multiple small blocks to compensate for the lack of shared compression context. If a dictionary is in use, its content, entropy tables and repeat offsets are given by the dictionary block as described in § 5.2 Dictionary block.
Data compressed by this method is a sequence of Zstandard blocks, without an additional framing provided by Zstandard frames. data MUST consist of one or more such blocks without any other bytes, where only the last block is signalled as the final block by its block header.
3.2.2. XXH64 checksum method
This specification only defines a single checksum method, XXH64 [XXHASH]. Its seed MUST be zero and the result MUST be stored as u64le. The resulting checksum_size is 8.
3.3. Spacer block
Blocks 016 through 316 are spacer blocks. Their data consists of the following:
A spacer block occupies any even number of bytes from 2, which is sufficient since every block occupies an even number of bytes due to padding.
Conforming compressors MUST fill space with zero bytes in order to avoid data leak, but its content doesn’t affect the result of interpreting an archive. Conforming compressors are RECOMMENDED to utilize sparse files for performance.
3.4. Size hint block
Block 416 is a size hint block. Its data consists of the following:
| Name | Type |
|---|---|
| size_hint | leb128 ≠ 0 |
If this block is present, it MUST be the first block after the header and its size_hint field MUST be equal to the footer’s size_hint field.
3.5. Unencrypted archive
An unencrypted archive consists of a header, zero or more blocks and a footer.
| Name | Type | Conditions |
|---|---|---|
| header_magic | u8[6] ∈ {HEADER_MAGIC, OVERLAY_HEADER_MAGIC} | |
| archive_start | offset | |
| size_hint_block | size_hint_block | Optional |
| blocks | block[*] | |
| archive_end | offset | |
| footer_magic | u8[6] ∈ {FOOTER_MAGIC, OVERLAY_FOOTER_MAGIC} | |
| footer_size | u16le = footer_end - footer_start | |
| footer_start | offset | |
| size_hint | leb128 = archive_end - archive_start | |
| metadata_backward_offset | leb128 | |
| footer_end | offset | |
| footer_size_copy | u16le = footer_size |
header_magic is a fixed 6-byte sequence to identify the file as an unencrypted Aqueous archive. This header also marks the beginning of the archive, when the archive doesn’t start at the beginning of the file.
footer_magic is a fixed 6-byte sequence to mark the beginning of the footer. The footer also marks the end of the archive, when the archive doesn’t end at the end of the file.
An unencrypted archive is an overlay archive if header_magic is OVERLAY_HEADER_MAGIC, and a standalone archive otherwise. footer_magic MUST be OVERLAY_FOOTER_MAGIC in an overlay archive and FOOTER_MAGIC otherwise. An overlay archive is interpreted against a base, an existing directory tree or file given separately from the overlay archive: it starts from the base instead of an empty directory and applies its segments to it (§ 4.4 Interpreting an archive), and only an overlay archive can contain external import blocks reading the base (§ 8.1 Import block). The format of overlay archives is called the Aqueous overlay archive format, and has its own media type (§ 15.2 application/aqo Media Type).
The offset between header_magic and footer_magic is recorded as size_hint, which MAY also be replicated as size_hint_block if random access is desired.
The footer size, which is the size of all fields between footer_size and footer_size_copy (both exclusive), is recorded both in footer_size and in footer_size_copy, so that the footer can be read from the end of the archive (§ 3.6 Locating an archive).
metadata_backward_offset is the offset from footer_start back to the beginning of the last metadata block, just like previous_metadata_offset is the offset from the beginning of the current metadata block back to the previous one. It MUST be 0 if and only if there is no metadata block, so that a decompressor can tell such archives without scanning blocks.
blocks are logically divided into segments, each consisting of content blocks and a single metadata block describing them, with optional spacer, mirror and ignored blocks in between. Each segment can be independently processed and decompressed in the reading order, except that internal imports (§ 8.1 Import block) read the result of preceding segments. Segments are described in § 4 Segments and entries.
Alternatively, blocks can have no metadata block. Such an archive decompresses into a single unnamed file, whose content is the concatenation of the contents of all leaf blocks in the order of appearance. It decompresses into an empty file if there is no leaf block.
3.6. Locating an archive
A file may contain an archive with leading bytes, e.g. a self-extracting executable, or with trailing bytes, e.g. appended by a transfer. Since interpreting an archive takes the archive alone, a decompressor accepting such a file has to locate the archive first.
A header magic and a footer magic match when they are HEADER_MAGIC and FOOTER_MAGIC, or OVERLAY_HEADER_MAGIC and OVERLAY_FOOTER_MAGIC.
To locate an archive in a byte sequence file:
-
If file starts with ENCRYPTED_HEADER_MAGIC or ENCRYPTED_MIRRORED_HEADER_MAGIC, return file.
-
Let back be null. Let n be the last 2 bytes of file as u16le, and let f be the length of file − n − 10. If f ≥ 0, the bytes of file from f are FOOTER_MAGIC or OVERLAY_FOOTER_MAGIC followed by n as u16le, and the leb128 from f + 8 is s such that h = f − s − 6 ≥ 0 and the bytes of file from h are the header magic matching that footer magic, set back to h.
-
If file starts with HEADER_MAGIC or OVERLAY_HEADER_MAGIC:
-
If back is 0, return file.
-
Read blocks from the offset 6 by the generic structure (§ 3.1 Block), with data of every block taken as a byte sequence without parsing, until the bytes at the current offset e start with FOOTER_MAGIC or OVERLAY_FOOTER_MAGIC. If it fails to read a block before that, or the footer magic doesn’t match the header magic, return failure.
-
Let m be the 2 bytes of file from e + 6 as u16le. If file is shorter than e + m + 10 bytes, return failure. Otherwise, return the first e + m + 10 bytes of file.
-
-
If back is not null, return the bytes of file from back.
-
Return failure.
The located archive is then interpreted, which may still fail. A decompressor SHOULD warn the user when file has bytes outside the located archive. It MAY search for HEADER_MAGIC and OVERLAY_HEADER_MAGIC when the archive can’t be located otherwise, e.g. when file has both leading and trailing bytes.
A block can’t start with FOOTER_MAGIC or OVERLAY_FOOTER_MAGIC, since they start with the shortest leb128 encodings of FOOTER_TYPE and OVERLAY_FOOTER_TYPE (§ 2.4 Constants), which are not valid block types. The step 3.2 therefore finds the end of the first archive in file, while the step 4 finds the archive at the end of file without reading the bytes before it. Archives concatenated together are not a single archive, and only the first one is located.
4. Segments and entries
4.1. Archive model
An archive value is a struct with the following items:
-
root, either a directory node or a file node. The latter represents a single unnamed file.
-
extra, an ordered map from integers to CBOR data items [RFC8949] representing the archive-wide part of the optional metadata.
A file node is a struct with the following items:
-
content, a byte sequence.
-
link group, a link group or null.
-
extra, a CBOR data item matching
aqr-entry-extra, or null.
A directory node is a struct with the following items:
-
children, an ordered map from entry names to file nodes, directory nodes, link nodes, special nodes or unknown nodes.
-
extra, a CBOR data item matching
aqr-entry-extra, or null.
A link node is a struct with the following items, representing a symbolic link:
-
target, a link target.
-
extra, a CBOR data item matching
aqr-entry-extra, or null.
A special node is a struct with the following items, representing a FIFO or a device file:
-
kind, one of "
fifo", "char-device" or "block-device". -
device, a pair of non-negative integers (major, minor) if kind is "
char-device" or "block-device", or null otherwise. -
extra, a CBOR data item matching
aqr-entry-extra, or null.
dev_t encoding,
as tar and cpio do and as makedev takes it.
Its meaning is up to the platform extracting it:
the same pair generally denotes unrelated devices on different platforms,
e.g. /dev/null is (1, 3) on Linux but (3, 2) on macOS,
and this specification doesn’t record which platform the number came from.
An unknown node is a struct with the following items, representing a kind of node not defined by this version of this specification:
-
kind, an integer greater than 4.
-
data, a byte sequence.
-
extra, a CBOR data item matching
aqr-entry-extra, or null.
The root can’t be a link node, a special node or an unknown node.
A tombstone is a struct with no items, marking the removal of a node. It only occurs in children of the result of interpreting a segment, and is removed by merging it into the preceding result (§ 4.4.2 Merging segments), so it never occurs in an archive value.
An entry name is a struct with the following items:
-
encoding, either "
wtf-8" or "raw". -
bytes, a non-empty byte sequence which is a valid WTF-8 [WTF-8] string if encoding is "
wtf-8".
Two entry names are equal if and only if both items are equal.
In particular, a "wtf-8" name and a "raw" name are never equal even when their bytes are.
No bytes of an entry name can contain 0016 or 2F16 (/),
or be equal to (2E)16 (.) or (2E 2E)16 (..).
An entry name is encoded as a byte sequence in one of the following ways, neither of which contains a zero byte:
-
If its encoding is "
wtf-8", its bytes as is. WTF-8 [WTF-8] is an extension to UTF-8 and losslessly encodes a lone surrogate in UTF-16.
Conforming compressors are RECOMMENDED to use the "wtf-8" encoding,
and SHOULD only use the "raw" encoding as a round-trip measure.
To decode an entry name from a entry_name field bytes:
-
If bytes is not empty and bytes[0] is C116, let name be an entry name whose encoding is "
raw" and bytes are bytes without the first byte. -
Otherwise, if bytes is a valid WTF-8 string, let name be an entry name whose encoding is "
wtf-8" and bytes are bytes. -
Otherwise, return an invalid archive failure.
-
If name violates any restriction of an entry name above, return an invalid archive failure.
-
Return name.
A link target is a struct with the following items:
-
encoding, either "
wtf-8" or "raw". -
bytes, a non-empty byte sequence not containing 0016, which is a valid WTF-8 [WTF-8] string if encoding is "
wtf-8".
Unlike an entry name, a link target is a path which may contain 2F16 (/) as a separator,
and components equal to . or ...
It may be absolute, and may refer to a path that doesn’t exist in the archive value or anywhere else.
The separator is always /, even for a link target from a platform using \ as the separator, such as Windows,
where every \ is represented as /.
Otherwise a link target is not normalized, and its meaning, e.g. of a drive letter, is left to the platform.
There is no limit on its length other than implementation-defined limits.
/ can’t occur in a file name on Windows, and isn’t interpreted as a separator in a link target there,
so this replacement is lossless.
A decompressor on Windows replaces / back with \ when creating a link (§ 12 Extraction).
On Windows, the target of an absolute link or a junction is stored as an NT path starting with \??\,
which has no meaning on other platforms.
A compressor is advised to convert it to a Win32 path first,
e.g. \??\C:\foo to C:/foo, \??\UNC\server\share\x to //server/share/x
and \??\Volume{GUID}\x to //?/Volume{GUID}/x.
Platforms differ in the maximum length of a link target: 4095 bytes on Linux (less on some file systems, e.g. 1023 bytes on XFS), 1023 bytes on macOS and FreeBSD, and about 8,000 UTF-16 code units on Windows. A link that can’t be created is handled at the extraction time (§ 12 Extraction).
To decode a link target from a link_target field bytes, do the same as to decode an entry name from bytes, except that the result is a link target and restrictions of a link target apply instead.
A link target is never resolved within the archive model.
Every path in an archive value is still a plain sequence of components,
and a lookup of a path in an archive value never follows a link node, as lstat doesn’t.
In particular, no node is a descendant of a link node.
a/passwd through a preceding link a pointing to /etc
inexpressible in the archive model.
A tool MAY offer a lookup following links, but it then has to limit the number of links followed.
A link group is an opaque value that is only compared for equality, and is either a pair of a segment and a segment-local integer or an archive-wide integer (§ 11.3 link-group: Hard links). Two archive values are equal if and only if they become identical by some one-to-one renaming of the link groups of one of them.
4.2. Content blocks
Any block other than spacer, size hint, metadata, mirror and ignored blocks is a content block which has to be described by a subsequent metadata block. A metadata block and all content blocks between the previous metadata block (or the beginning of the archive if none) and that metadata block form a segment. As an exception, an archive without a metadata block (§ 3.5 Unencrypted archive) forms a single segment with no metadata block. Each segment MUST be self-contained: every reference from a block in a segment, whether by an index or by an offset, MUST point to a block in the same segment. An internal import (§ 8.1 Import block) refers to a node of preceding segments by its path rather than to a block, and is the only way for a segment to use the content of another segment.
Content blocks are classified as follows:
-
A leaf block represents the content of one or more files by itself. Leaf data blocks and formula blocks are leaf blocks.
-
A non-leaf block doesn’t represent any file by itself, and only supplies data referenced by one or more formula blocks. Non-leaf data blocks and import blocks are non-leaf blocks. Each non-leaf block supplies one or more non-leaf items to formula blocks: a non-leaf data block supplies a single item, and an import block supplies one item for each of its imports.
-
An auxiliary block supplies data used to decode other blocks. Dictionary blocks, module blocks and resource blocks are auxiliary blocks.
Dictionary, module and resource blocks are respectively indexed in the order of appearance within the segment. Two conventions are used for these indices:
-
Fields in content blocks use 0-based indices, because the presence of a reference is signalled separately (e.g. by the block type).
-
Fields in the metadata block that may indicate the absence of a reference use 1-based indices, where zero denotes the absence.
4.2.1. Block order
Blocks in a segment MUST be ordered so that a formula block is resolved by blocks preceding it and all data required to evaluate it follows it:
-
A dictionary block MUST precede any block using it.
-
A module or resource block MUST precede any formula block referencing it.
-
Every non-leaf item MUST be referenced by at least one formula block, and every non-leaf block MUST follow all formula blocks referencing its items.
-
Leaf blocks are otherwise not constrained and MAY be interleaved with non-leaf blocks.
A leaf block and all subsequent content blocks up to the next leaf block or the end of the segment form a parcel. When no leaf block is interleaved with non-leaf blocks, a parcel consists of a leaf block, the non-leaf blocks it references if it is a formula block, and possibly auxiliary blocks. In that case a decompressor can read a single file by reading its parcel and the auxiliary blocks it uses, and a streaming decompressor never has to retain a formula block beyond its parcel.
4.2.2. Registered dictionaries and modules
A registered dictionary is an entry of the Aqueous Dictionaries Registry (§ 15.5 Aqueous Dictionaries Registry), and a registered module is an entry of the Aqueous Modules Registry (§ 15.6 Aqueous Modules Registry). Each entry is identified by its registered hash, the SHA-512/256 hash [FIPS180-4] of its bytes: the decoded data of a dictionary block for a registered dictionary (§ 5.2 Dictionary block), and wasm_module for a registered module (§ 7.1 Module block).
Block 5016 (a dictionary block) and block 6016 (a module block) are shorthand blocks. The data of a shorthand block is a registered hash alone, 32 bytes long, instead of checksummed Zstandard data, and the shorthand block stands for a block of the same kind whose decoded data is the bytes of that entry:
-
size MUST be 32.
-
The registered hash of block 5016 MUST be that of a registered dictionary, which also gives has_entropy_tables, and that of block 6016 MUST be that of a registered module.
-
If the decompressor doesn’t know the entry of the registered hash in the corresponding registry, an unsupported failure occurs (§ 4.4 Interpreting an archive).
-
A shorthand block is otherwise the same as the dictionary or module block it stands for, e.g. in indexing, block order and mirror blocks.
The two registries share the hash function but not the entries, and a shorthand block is resolved only against the registry of its own kind. Knowing an entry of one registry never makes a decompressor know an entry of the other: in particular, the hash of a registered module in block 5016 doesn’t denote a dictionary of the module’s bytes, unless the same bytes are separately registered as a dictionary, in which case they are two unrelated entries.
A shorthand block has no checksum, and the registered hash is only looked up and never verified. Therefore damage to the hash can’t be distinguished from a hash that was never registered, and both result in an unsupported failure rather than an invalid archive failure.
4.3. Metadata block
Blocks 1016 through 1716 are metadata blocks,
whose type is 0001 0mss in binary (§ 3.1.1 Block types).
Their data uses the shared external structure (§ 3.2 Shared external structure),
with XXH64 as checksum_method
and compression_method given by m: None if m is 0, and Zstandard if m is 1.
The internal structure after decompression is as follows:
| Name | Type |
|---|---|
| previous_metadata_offset | leb128 |
| num_modules | leb128 |
| num_resources | leb128 |
| num_dictionaries | leb128 |
| num_directories | leb128 |
| num_files | leb128 |
| num_specials | leb128 |
| entries_size | leb128 = entries_end - entries_start |
| optional_metadata_size | leb128 |
| module_offsets | u(previous_metadata_offset)[num_modules] |
| resource_offsets | u(previous_metadata_offset)[num_resources] |
| dictionary_offsets | u(previous_metadata_offset)[num_dictionaries] |
| entry_parents | u(num_directories)[num_directories + num_files + num_specials - 1] |
| file_params | file_param[num_files] |
| entry_offsets | u(entries_size)[num_directories + num_files + num_specials - 1] |
| entries_start | offset |
| entries | entry[num_directories + num_files + num_specials] |
| entries_end | offset |
| optional_metadata | u8[optional_metadata_size] |
All relative offsets are measured backward from the beginning of the current metadata block: a relative offset o refers to the block starting o bytes before the current metadata block, implying that the offset of zero is invalid. All valid relative offsets are also even.
Each metadata block describes the content blocks in its segment, i.e. blocks appearing after the previous metadata block, if any, and until the current metadata block. The previous metadata block is linked via previous_metadata_offset, which is also the maximum possible offset that can occur in this particular metadata block. If there is no prior metadata block, previous_metadata_offset is the relative offset to the end of header_magic in the unencrypted archive structure (§ 3.5 Unencrypted archive), plus 1 so that it never points to a valid block.
num_modules, num_resources and num_dictionaries are the numbers of module, resource and dictionary blocks in the segment respectively. Relative offsets to those blocks are later recorded as module_offsets, resource_offsets and dictionary_offsets respectively, in the order of appearance.
The metadata block represents zero or more directories, zero or more files and zero or more special entries, collectively called entries and numbered together. The root directory is numbered 0, subsequent directories are numbered from 1 to num_directories, individual files are numbered from (num_directories + 1) to (num_directories + num_files), and special entries are numbered from (num_directories + num_files + 1) to (num_directories + num_files + num_specials). There MUST be at least one entry other than the root directory, i.e. num_directories + num_files + num_specials ≥ 1. entry_parents[k - 2] is the parent of the entry k for each k ≥ 2, and MUST be a directory less than k, i.e. in [0, min(k - 1, num_directories)], so that directories can’t form a cycle. The root directory (entry 0) has no parent and the parent of the entry 1 is always the root directory, so they are omitted.
Whether a segment without any entry other than the root directory should be allowed, e.g. to update only the archive-wide optional metadata or the entry extra of the root directory.
File parameters (file_param) occur for each file (but not for directories or special entries), and consist of the following:
| Name | Type | Conditions |
|---|---|---|
| file_dictionary | u(num_dictionaries) | |
| file_offset | u(previous_metadata_offset) | |
| file_output | leb128 | If file_offset is odd |
file_dictionary is the dictionary used by the block that file_offset refers to as a 1-based index, or zero if no dictionary is used. Therefore file_dictionary = k refers to the block at dictionary_offsets[k - 1], and num_dictionaries is the maximum possible value for it. A relative offset to the corresponding leaf block, which starts the file’s parcel, is recorded as file_offset, plus 1 if the file is another output of the block than the output 0. The relative offset can’t be zero. Since every relative offset is even, file_offset % 2 tells which: the file output index is 0 if file_offset is even, and file_output + 1 if it is odd. Multiple files MAY refer to the same leaf block and output, in which case they have the same content.
Entry structure (entry) then occurs for each directory, file and special entry, in this order, and consists of the following:
| Name | Type | Conditions |
|---|---|---|
| entry_name | u8[*] ≠ 0 | |
| entry_name_sentinel | u8 = 0 | |
| original_size | leb128 | If this entry is for a file |
| special_kind | leb128 | If this entry is for a special entry |
| link_target | u8[*] ≠ 0 | If this entry is for a special entry and special_kind = 1 |
| link_target_sentinel | u8 = 0 | If this entry is for a special entry and special_kind = 1 |
| device_major | leb128 | If this entry is for a special entry and special_kind is 3 or 4 |
| device_minor | leb128 | If this entry is for a special entry and special_kind is 3 or 4 |
| special_data | u8[*] | If this entry is for a special entry and special_kind > 4 |
Each entry is named according to entry_name, which encodes an entry name as described in § 4.1 Archive model and is decoded there. Entry names from ZIP are discussed in § 16.1 Entry names from ZIP.
A special entry represents a node other than a file or a directory, as determined by special_kind:
| special_kind | Node |
|---|---|
| 0 | Tombstone, which removes the node at the same path in preceding segments or in the base (§ 4.4.2 Merging segments) |
| 1 | Link node, with link_target as its target |
| 2 | Special node of the kind "fifo"
|
| 3 | Special node of the kind "char-device", with device_major and device_minor as its device number
|
| 4 | Special node of the kind "block-device", with device_major and device_minor as its device number
|
| 5 or greater | Unknown node, reserved for future versions of this specification |
link_target uses the same encodings as entry_name. special_data is not interpreted by this version of this specification; its length is determined by the beginning of the next entry in entry_offsets, or entries_end for the last entry.
Unknown kinds are not an invalid archive failure, so that a decompressor can still extract the rest of an archive using a kind defined in the future. Every special entry can be skipped without knowing its kind, thanks to entry_offsets. An unknown node replaces an earlier node at the same path like any other non-directory node (§ 4.4.2 Merging segments), so a future kind can’t change how segments are merged. This is why tombstones are defined by this version, as the first kind: a decompressor treating a tombstone as an unknown node would keep it in place of the node it was meant to remove.
The entries array is the only part that has the unspecified number of bytes, which is therefore available as entries_size beforehand. Any other field can be randomly accessed after reading the initial set of leb128 fields.
As an exception to the relative offsets above, entry_offsets are measured from entries_start. The entry 1 always starts at entries_start, so its offset is omitted, and entry_offsets[k - 2] is the offset to the beginning of the entry k for each k ≥ 2. They MUST agree with the actual positions of entries.
optional_metadata is described in Optional metadata (§ 11 Optional metadata).
4.4. Interpreting an archive
To interpret an archive given a byte sequence input, an optional passphrase passphrase and an optional base base, which is an archive value:
-
If input starts with ENCRYPTED_HEADER_MAGIC or ENCRYPTED_MIRRORED_HEADER_MAGIC:
-
Parse input as the encrypted archive structure (§ 9 Encrypted archives) in its entirety. If it fails to parse, return an invalid archive failure.
-
If header_checksum is present and doesn’t match, return an invalid archive failure.
-
If lg_work_factor exceeds an implementation-defined limit, return a resource failure.
-
If passphrase is not given, or the file key can’t be unwrapped from encrypted_file_key with passphrase as described in [AGE], return a decryption failure.
-
Using the unwrapped file key, verify age_header_mac against the reconstructed age header and decrypt the age payload as described in [AGE]. If either fails, return an invalid archive failure.
-
Set input to the decrypted payload. It MUST satisfy the additional constraints in § 9 Encrypted archives, or an invalid archive failure is returned.
-
-
If input parses as the unencrypted archive structure (§ 3.5 Unencrypted archive) in its entirety, with data of every block taken as a byte sequence without parsing, and either the type of any of blocks is critical and not defined by this specification, or any of blocks is a shorthand block of 32 bytes of data whose registered hash the decompressor doesn’t know (§ 4.2.2 Registered dictionaries and modules), return an unsupported failure.
-
Parse input as the unencrypted archive structure (§ 3.5 Unencrypted archive) in its entirety. If it fails to parse, return an invalid archive failure.
-
If input is an overlay archive and base is not given, return a base failure.
-
If input is a standalone archive, set base to an archive value whose root is a directory node with an empty map as children and null as extra, and whose extra is an empty map.
-
If blocks contain no metadata block:
-
Let content be the concatenation of the file contents of all leaf blocks in blocks, in the order of appearance, each for every output index from 0 in order (all outputs of a formula block, and the only one of a data block).
-
Return an archive value whose root is a file node with content as content, null as link group and null as extra, and whose extra is an empty map.
-
-
If the root of base is a directory node, let root be a copy of it. Otherwise let root be a directory node with an empty map as children and null as extra.
-
Let extra be a copy of the extra of base.
-
For each segment segment in blocks, in the order of appearance:
-
Let segment root be the result of interpreting segment.
-
Merge segment root into root.
-
For each key k and value v in the top-level map of the segment’s optional metadata other than
entry-extras, set extra[k] to v.
-
-
Return an archive value with root as root and extra as extra.
The archive base is base after the step 5, and the preceding result of a segment is root just before the segment is interpreted in the step 9, or the root of the archive base for an archive without a metadata block. They are what external imports and internal imports respectively refer to (§ 8.2 Resolving an import). A given base doesn’t affect the interpretation of a standalone archive.
The input to this algorithm is exactly the archive, from the first byte of header_magic to the last byte of footer_size_copy.
A file may contain an archive with leading or trailing bytes, which a decompressor removes by locating the archive (§ 3.6 Locating an archive).
Since every content block has to be described by a subsequent metadata block (§ 4.2 Content blocks), an archive with a metadata block can’t have any content block after the last metadata block. previous_metadata_offset and metadata_backward_offset MUST point to the metadata blocks found by parsing blocks as described in § 4.3 Metadata block and § 3.5 Unencrypted archive, so that a decompressor can find each segment either by scanning blocks forward or by following offsets backward.
4.4.1. Interpreting a segment
To interpret a segment segment whose metadata block is metadata block:
-
Let meta be the decoded data of metadata block, parsed as its internal structure (§ 4.3 Metadata block).
-
Let d be num_directories, f be num_directories + num_files and n be num_directories + num_files + num_specials of meta.
-
Let nodes be a list containing a single directory node with an empty map as children and the entry extra for the entry 0 as extra.
-
For each k in [1, n]:
-
Let entry be meta.entries[k - 1].
-
Let name be the result of decoding entry.entry_name.
-
Let parent be 0 if k = 1, or meta.entry_parents[k - 2] otherwise.
-
If k ≤ d, let node be a directory node with an empty map as children.
-
Otherwise, if k ≤ f:
-
Let param be meta.file_params[k - d - 1].
-
Let block be the block at the relative offset param.file_offset - param.file_offset % 2 from metadata block. It MUST be a leaf block in segment.
-
If param.file_dictionary is zero, block MUST NOT use a dictionary. Otherwise, block MUST use the dictionary block at meta.dictionary_offsets[param.file_dictionary - 1].
-
Let content be the file content of block for the file output index of param. Its length MUST be entry.original_size.
-
Let node be a file node with content as content and null as link group.
-
If the entry extra for the entry k has
link-groupwith a value g, set node.link group to a pair of segment and g if g ≥ 0, or to g otherwise.
-
-
Otherwise, if entry.special_kind is 0, let node be a tombstone. The entry extra for the entry k MUST be null.
-
Otherwise, if entry.special_kind is 1:
-
Let target be the result of decoding entry.link_target.
-
-
Otherwise, if entry.special_kind is 2, let node be a special node with "
fifo" as kind and null as device. -
Otherwise, if entry.special_kind is 3 or 4, let node be a special node with "
char-device" or "block-device" respectively as kind and (entry.device_major, entry.device_minor) as device. -
Otherwise, let node be an unknown node with entry.special_kind as kind and entry.special_data as data.
-
If node is not a tombstone, set the extra of node to the entry extra for the entry k.
-
Let children be the children of nodes[parent]. children[name] MUST NOT exist.
-
Set children[name] to node.
-
Append node to nodes.
-
-
Return nodes[0].
Since every parent is less than k and at most d by § 4.3 Metadata block, nodes[parent] is always a directory node already in nodes.
The entry extra for the entry k is the element at the 0-based index k of entry-extras
in the segment’s optional metadata, where the index 0 corresponds to the root directory,
or null if entry-extras or such element doesn’t exist.
entry-extras MUST NOT have more elements than the entries including the root directory,
i.e. num_directories + num_files + num_specials + 1.
4.4.2. Merging segments
To merge directory nodes later into earlier:
-
For each name → node of later.children:
-
Let existing be earlier.children[name], or null if it doesn’t exist.
-
If node is a tombstone, remove earlier.children[name] if it exists.
-
Otherwise, if node is a directory node:
-
If existing is not a directory node, set existing to a directory node with an empty map as children and null as extra, and set earlier.children[name] to existing.
-
Merge node into existing.
-
-
Otherwise, set earlier.children[name] to node.
-
As a result, a directory appearing in multiple segments has the union of their children,
while any other node, or a directory replaced by a node of the different kind, is replaced as a whole with its descendants.
In particular, a directory replaced by a link node loses all its descendants,
so no node is a descendant of a link node in the result.
A tombstone removes the node at its path along with its descendants, and does nothing if there is no such node.
A tombstone is never kept in the result, even under a directory that didn’t exist before.
A directory with a null extra in a later segment keeps the existing extra,
which can instead be replaced with an empty map {}.
5. Stored data
5.1. Data block
Blocks 2016 through 3716 and 4016 through 4B16 are data blocks. A data block is a compressed block if its compression_method is not None, and an uncompressed block otherwise.
The type of a data block is 001kk css in binary (§ 3.1.1 Block types) for a leaf block,
and 0100 kkss for a non-leaf block, where:
-
kk determines compression_method and whether a dictionary is used (has_dictionary): 0 for None without dictionary, 1 for Zstandard without dictionary, and 2 for Zstandard with dictionary. 3 is reserved for future compression methods.
-
c determines checksum_method: None if 0, and XXH64 if 1. A non-leaf data block always uses XXH64 (§ 10.1 Checksums).
Its data consists of the following:
| Name | Type | Conditions |
|---|---|---|
| dictionary_index | leb128 | If has_dictionary |
| compressed_data | compressed_data | If compression_method is not None |
| uncompressed_data | uncompressed_data | If compression_method is None |
If a dictionary is in use, its 0-based index is specified as dictionary_index. For a leaf data block, the metadata block also gives the dictionary as file_dictionary, which is available before reading the block; for a non-leaf data block, dictionary_index is the only reference to the dictionary. compressed_data and uncompressed_data use the shared external structure (§ 3.2 Shared external structure) with compression_method and checksum_method determined as above. uncompressed_data represents a raw, uninterpreted byte sequence.
A compressed block is a single unit of decompression: reading any part of its content requires decompressing it from the beginning. Content larger than a desired unit of random access is split into multiple blocks:
-
In general, a file is represented by a formula block concatenating multiple non-leaf blocks, which typically form a single parcel together. The formula block then acts as an index for random access within the parcel.
-
In an archive without a metadata block (§ 3.5 Unencrypted archive), the file is simply represented by multiple leaf blocks.
5.2. Dictionary block
Blocks 5016 through 5716 are dictionary blocks,
whose type is 0101 0tss in binary (§ 3.1.1 Block types).
Their data is checksummed Zstandard data (§ 3.2 Shared external structure),
except that block 5016 is a shorthand block for a registered dictionary (§ 4.2.2 Registered dictionaries and modules).
t determines whether the dictionary has entropy tables (has_entropy_tables): false if 0, and true if 1.
The internal structure after decompression is as follows:
| Name | Type | Conditions |
|---|---|---|
| entropy_tables | u8[*] | If has_entropy_tables |
| repeat_offsets | u32le[3] | If has_entropy_tables |
| content | u8[*] |
The decoded data of a dictionary block is a dictionary for blocks using it,
and corresponds to a Zstandard dictionary ([RFC8878], Section 5) without Magic_Number and Dictionary_ID:
-
If has_entropy_tables is true, entropy_tables is the
Entropy_Tablesstructure, whose length is determined by parsing it, and repeat_offsets are the threeRepeat_Offsetsin order. Each repeat offset MUST be in [1, length of content]. -
Otherwise, it is a raw content dictionary: the entropy tables start empty and the repeat offsets start at their initial values (1, 4, 8), even when content begins with the dictionary
Magic_Number.
In both cases, content is the Content of the dictionary,
which is the only part of the dictionary that sequences can reference as preceding data.
content MUST be at least 8 bytes long.
Magic_Number and Dictionary_ID are omitted because they are only meaningful for Zstandard frames
and the framing is done by this format.
An implementation using libzstd can prepend them (with an arbitrary nonzero Dictionary_ID)
to load a dictionary with entropy tables.
In that case it has to make sure that no sequence references the prepended bytes or entropy_tables,
which some decompression paths of libzstd don’t check.
ZDICT_finalizeDictionary)
and mainly benefit small blocks:
in measurements with per-file blocks of 1–20 KB files, they reduced the size by 0.4–26%,
while the benefit vanished for blocks of 64 KB or more.
Fast strategies of libzstd (e.g. levels 1–3 of 1.5.7) reuse the dictionary’s entropy tables for every block
even when the block’s own tables would be smaller.
When the repeat offsets of the dictionary are the initial values,
a compressor can avoid this by also compressing a block with the same content as a raw content dictionary
and choosing the smaller result,
since a block compressed that way never refers to the entropy tables of the dictionary.
5.3. Block contents
The decoded data of a block using the shared external structure (§ 3.2 Shared external structure), given its compression_method and checksum_method, is determined as follows:
-
If compression_method is None, it is data.
-
If compression_method is Zstandard, it is the result of decompressing data as described there, with compression_params and the dictionary given by the dictionary block in use (§ 5.2 Dictionary block), if any. The result MUST have exactly original_size bytes.
-
If checksum_method is not None, the checksum MUST be equal to the checksum of the decoded data computed by the checksum_method.
The stored content of a data block, whether a leaf block or not, is the decoded data of its compressed_data or uncompressed_data. The stored content of a non-leaf item is the stored content of the non-leaf data block supplying it, or the imported content of the import supplying it (§ 8.2 Resolving an import). A data block using a dictionary uses the dictionary block at dictionary_index within the segment.
The file content of a leaf block for an output index i is:
-
Its stored content, if it is a data block. i MUST be 0.
-
The output at the index i of evaluating it, if it is a formula block. i MUST be less than its num_outputs.
6. Formulas
6.1. Formula block
Blocks 7016 through 7716 are formula blocks,
whose type is 0111 0dss in binary (§ 3.1.1 Block types).
d determines whether a dictionary is used (has_dictionary): false if 0, and true if 1.
Its data consists of the following:
| Name | Type | Conditions |
|---|---|---|
| dictionary_index | leb128 | If has_dictionary |
| num_outputs | leb128 | |
| formula_data | compressed_data |
If a dictionary is in use, its 0-based index is specified as dictionary_index, as in data blocks.
num_outputs is the number of outputs of the formula block, which MUST be at least 1 (§ 6.3 Formula evaluation).
A formula block describes the content of a file as an expression over non-leaf items following it, evaluated with modules and resources preceding it. The internal structure of formula_data is described in Formula data (§ 6.2 Formula data).
6.2. Formula data
This section describes the internal structure of formula_data in formula blocks (§ 6.1 Formula block), after it has been decompressed.
A formula consists of the lists of blocks it references, followed by instructions of a stack machine. Each instruction pops zero or more byte sequences from the stack and pushes zero or more byte sequences, and the byte sequences remaining on the stack after the last instruction are the outputs of the formula.
| Name | Type |
|---|---|
| num_blocks | leb128 |
| block_ordinal_deltas | leb128[num_blocks] |
| num_resources | leb128 |
| resource_indices | leb128[num_resources] |
| num_modules | leb128 |
| module_indices | leb128[num_modules] |
| instructions | instruction[*] |
block_ordinal_deltas lists the non-leaf items referenced by the formula block. Despite their names, num_blocks, block_ordinal_deltas and slice_block count non-leaf items, which are blocks except for imports. Their 0-based ordinals among the non-leaf items supplied by the non-leaf blocks following the formula block within the segment, in the order of appearance, are o0 = block_ordinal_deltas[0] and oi = oi - 1 + block_ordinal_deltas[i] + 1 for i ≥ 1, so the ordinals are strictly increasing. The i-th referenced item is the non-leaf item with the ordinal oi, which MUST exist.
resource_indices and module_indices list the resource and module blocks referenced by the formula block, as 0-based indices within the segment (§ 4.2 Content blocks). Each of them MUST refer to an existing block, which precedes the formula block by § 4.2.1 Block order.
instructions fill the rest of formula_data. Each item or block listed in the above lists MUST be used by at least one instruction, and is said to be referenced by the formula block only in that case.
Each instruction consists of an opcode of type u8 followed by its operands, whose structure is determined by the opcode as described in the following sections. opcode MUST be one of the following:
| Opcode | Instruction | Pops | Pushes |
|---|---|---|---|
| 0016 | Nop | 0 | 0 |
| 0116 | Literal | 0 | 1 |
| 0F16 | Swap | 0 | 0 |
| 1016 | Slice | 0 | 1 |
| 1116 | Apply | apply_num_inputs | apply_num_outputs |
| 1216 | Concatenate | concat_count | 1 |
| 1316 | Pack | pack_count | 1 |
| 1416 | Split | 1 | split_count |
| 1516 | Patch | 2 | 1 |
| 2016 | Checksum | 0 | 0 |
Opcodes are grouped by their roles: 0016 through 0F16 push data in the formula itself or rearrange the stack, 1016 through 1F16 compute byte sequences from referenced items or the stack, and 2016 through 2F16 check them. Other opcodes are reserved.
A reference to a preceding block in the same segment, e.g. to patch a file stored earlier in the segment, is not yet defined.
A reference to another file is made by an import (§ 8.1 Import block) rather than by an instruction,
and the content of a file in preceding segments by an internal import.
The candidate is a second list of non-leaf items preceding the formula block, read by Slice instructions as referenced items are,
where the instructions of another formula block are copied when its output is needed, instead of referring to the output.
It requires a bound on the distance of such a reference declared before the referenced blocks,
so that a streaming decompressor knows which blocks to retain,
and a way to locate a preceding block without reading the segment from its beginning (docs/xform-design.md §7).
formula_data is not randomly accessible, and a decompressor is expected to read and decompress it in its entirety before evaluating any part of the formula. Every requirement on formula_data that doesn’t depend on the evaluated byte sequences, such as indices into the lists and the number of byte sequences on the stack, can be checked in a single pass over instructions without evaluating anything. The same pass determines the length of every byte sequence on the stack, so formula_data then serves as an index for random access within the output, in the same way as a seek table of other formats. The referenced items are located by skipping blocks following the formula block by their size fields, and an item of an import block by decoding that block.
To execute an instruction on a stack, which is a list of byte sequences whose last item is the top, perform the steps described for its opcode in the following sections. To pop n items, the stack MUST have at least n items, and the last n items are removed from the stack and used in their order in the stack.
6.2.1. Nop instruction
Opcode 0016 denotes a Nop instruction, which has no operands and does nothing.
6.2.2. Literal instruction
Opcode 0116 denotes a Literal instruction. Its operands consist of the following:
| Name | Type |
|---|---|
| literal_size | leb128 |
| literal_data | u8[literal_size] |
It pushes literal_data.
6.2.3. Swap instruction
Opcode 0F16 denotes a Swap instruction. Its operands consist of the following:
| Name | Type |
|---|---|
| swap_depth | leb128 |
The stack MUST have more than swap_depth items. It exchanges the top of the stack with the item swap_depth items below it.
6.2.4. Slice instruction
Opcode 1016 denotes a Slice instruction. Its operands consist of the following:
| Name | Type |
|---|---|
| slice_block | leb128 |
| slice_start | leb128 |
| slice_length | leb128 |
-
slice_block MUST be less than num_blocks.
-
Let c be the stored content of the slice_block-th referenced item.
-
slice_start + slice_length MUST be at most the length of c.
-
Push the bytes of c in [slice_start, slice_start + slice_length).
6.2.5. Apply instruction
Opcode 1116 denotes an Apply instruction. Its operands consist of the following:
| Name | Type |
|---|---|
| apply_module | leb128 |
| apply_num_resources | leb128 |
| apply_resources | leb128[apply_num_resources] |
| apply_params_size | leb128 |
| apply_params | u8[apply_params_size] |
| apply_memory_limit | leb128 |
| apply_num_inputs | leb128 |
| apply_num_outputs | leb128 |
| apply_output_sizes | leb128[apply_num_outputs] |
-
apply_module MUST be less than num_modules, and each item of apply_resources MUST be less than num_resources.
-
apply_num_outputs MUST be at least 1.
-
Pop apply_num_inputs items as inputs.
-
Let outputs be the result of applying the module block at module_indices[apply_module] with inputs, the resource blocks at resource_indices[r] for each r of apply_resources, apply_params, apply_output_sizes and apply_memory_limit (§ 7.3 Module execution).
-
Push each item of outputs in order.
6.2.6. Concatenate instruction
Opcode 1216 denotes a Concatenate instruction. Its operands consist of the following:
| Name | Type |
|---|---|
| concat_count | leb128 |
It pops concat_count items and pushes their concatenation in order.
6.2.7. Pack instruction
Opcode 1316 denotes a Pack instruction. Its operands consist of the following:
| Name | Type |
|---|---|
| pack_count | leb128 |
It pops pack_count items and pushes the packed byte sequence of them in order (§ 6.3.1 Packed byte sequences).
6.2.8. Split instruction
Opcode 1416 denotes a Split instruction. Its operands consist of the following:
| Name | Type |
|---|---|
| split_count | leb128 |
| split_sizes | leb128[split_count] |
-
split_count MUST be at least 1.
-
Pop an item x. The sum of split_sizes MUST be equal to the length of x.
-
For each n of split_sizes in order, push the next n bytes of x.
6.2.9. Patch instruction
Opcode 1516 denotes a Patch instruction. Its operands consist of the following:
| Name | Type |
|---|---|
| patch_size | leb128 |
-
Pop 2 items as source and patch.
-
patch MUST consist of one or more Zstandard blocks without any other bytes, where only the last block is signalled as the final block by its block header, and each block holds at most 128 KiB of data.
-
Let output be the result of decompressing patch as a sequence of Zstandard blocks [RFC8878] with source as the
Contentof a raw content dictionary, i.e. the entropy tables start empty and the repeat offsets start at their initial values (1, 4, 8). The offset of every match MUST be at most the length of source plus the number of bytes already decompressed. -
The length of output MUST be patch_size.
-
Push output.
zstd --patch-from does, so a decompressor reuses its Zstandard decompressor for it.
The earlier byte sequence is an operand on the stack rather than a dictionary block,
so that it can be any byte sequence computed by the formula,
e.g. an import or a stream split from it by a module.
The patch itself is typically a slice of an uncompressed non-leaf data block,
whose checksum covers the stored patch, while an output checksum covers the result.
Window_Size:
a match may refer to any byte of source or of the output so far,
and the guaranteed limit of Window_Size in § 3.2.1 Zstandard compression method doesn’t apply.
The decompressor holds both byte sequences anyway, so a long reach costs no additional memory.
With libzstd, this is decompressing a frame after ZSTD_DCtx_refPrefix with source,
whose release builds only check offsets against the start of the prefix;
a decompressor enforcing Window_Size has to lift that check for the frame.
A compressor using libzstd sets ZSTD_c_windowLog large enough to cover source and the output,
as zstd --patch-from does.
6.2.10. Checksum instruction
Opcode 2016 denotes a Checksum instruction. Its operands consist of the following:
| Name | Type |
|---|---|
| checksum_value | u64le |
The stack MUST NOT be empty. The XXH64 checksum of the top of the stack MUST be equal to checksum_value.
A Checksum instruction covers the byte sequence at the top of the stack when it is executed, and the byte sequence remains the same item when moved by Swap instructions. An output checksum is a Checksum instruction covering an output of the formula block.
6.3. Formula evaluation
To evaluate a formula block block:
-
Let data be the decoded data of formula_data of block, parsed as in § 6.2 Formula data.
-
Let stack be an empty list of byte sequences.
-
For each insn of instructions of data in order, execute insn on stack as described for its opcode in § 6.2 Formula data.
-
The number of items of stack MUST be num_outputs of block.
-
Return stack as the outputs of block, numbered from 0 in order from the bottom of the stack.
6.3.1. Packed byte sequences
The packed byte sequence of a list of byte sequences items is the concatenation of the following, where n is the number of items:
-
n as u64le;
-
For each i in [0, n), the sum of the lengths of items[0] through items[i] as u64le;
-
Each item of items in order.
A byte sequence p is a packed byte sequence of exactly one list items, if any. To unpack p:
-
If p is shorter than 8 bytes, return failure.
-
Let n be the first 8 bytes of p as u64le, and let h be 8 × (n + 1). If h exceeds the length of p, return failure.
-
Let ei be the 8 bytes of p from the offset 8 × (i + 1) as u64le for each i in [0, n), and let e-1 be 0.
-
If ei - 1 > ei for any i in [0, n), return failure.
-
If h + en - 1 is not the length of p, return failure.
-
Return the list of the bytes of p in [h + ei - 1, h + ei) for each i in [0, n).
In the other direction, a module producing a list of byte sequences outputs their plain concatenation, which a Split instruction separates by the sizes declared in the formula. The output of such a module is passed to another module taking a packed byte sequence by a Split instruction followed by a Pack instruction.
7. Modules
7.1. Module block
Blocks 6016 through 6316 are module blocks,
whose type is 0110 00ss in binary (§ 3.1.1 Block types).
Their data is checksummed Zstandard data (§ 3.2 Shared external structure),
except that block 6016 is a shorthand block for a registered module (§ 4.2.2 Registered dictionaries and modules).
The internal structure after decompression is as follows:
| Name | Type |
|---|---|
| wasm_module | u8[*] |
wasm_module is a WebAssembly module in the binary format [WASM-CORE-3],
i.e. the content of an ordinary .wasm file, starting with the magic 00 61 73 6D and the version 01 00 00 00
(hexadecimal) and spanning the whole decoded data.
It MUST decode and validate as a module of the features allowed in § 7.3 Module execution,
and MUST conform to the profile and the interface given there;
otherwise an invalid archive failure occurs.
In short, the module defines no memory of its own and imports every memory and host function from the module name aqr,
has no start function, and exports a single entry point that the host invokes once per application.
Custom sections are allowed and don’t affect the result.
The format adds nothing around the module, so that the decoded data can be inspected and run by any WebAssembly tool,
and a module block for a registered module has the same wasm_module as the entry.
The module hash of a module block is the SHA-512/256 hash [FIPS180-4] of its wasm_module, which is also the registered hash of a registered module (§ 4.2.2 Registered dictionaries and modules). Since every application of a module depends only on wasm_module and its arguments (§ 7.3 Module execution), a decompressor MAY apply a module whose module hash it knows with an implementation of its own (e.g. native code) instead of instantiating wasm_module, as long as the result is indistinguishable from the definition, including invalid archive failures. Such a decompressor needs neither the bytes of the module nor a WebAssembly engine, whether the module is given by a shorthand block or in full. A decompressor MAY refuse to apply a module as its conformance level allows (§ 13.1 Conformance levels), in which case a resource failure occurs.
-
apply an implementation of its own, which is the fastest but has to be made for every module known. Translating the module by automated means (e.g. wasm2c) is preferred to writing it anew, since the result has to be indistinguishable from the module, including its failures; or
-
instantiate the module with a WebAssembly engine without isolating it, since the hash is strong enough that no other module can have it. This needs only the hash for a module given in full, and the bytes only for a shorthand block.
For a module whose module hash it doesn’t know, it can either:
-
refuse to apply it, failing with a resource failure, unless it is at level 2; or
-
instantiate it with a WebAssembly engine in a sandbox, as untrusted code (§ 14 Security considerations).
For example, a decompressor at level 1 needs neither an engine nor a sandbox if it implements every registered module natively, and one at level 2 needs both but no native implementation.
7.2. Resource block
Blocks 6416 through 6716 are resource blocks,
whose type is 0110 01ss in binary (§ 3.1.1 Block types).
Their data is checksummed Zstandard data (§ 3.2 Shared external structure).
The internal structure after decompression is an uninterpreted byte sequence, which is supplied to modules by formula blocks referencing it (§ 6.2 Formula data, § 7.3 Module execution).
7.3. Module execution
This section is tentative and follows docs/xform-design.md §5.
A module is a WebAssembly module [WASM-CORE-3] stored as wasm_module, instantiated and invoked through the embedding interface defined in the Appendix of [WASM-CORE-3]. It MUST conform to the following profile, or an invalid archive failure occurs:
-
WebAssembly 3.0 core, executed under the deterministic profile; every NaN result is a canonical NaN.
-
None of the features added in WebAssembly 3.0 are allowed, except multiple memories and 64-bit addresses of the memories other than the working memory. That is, tail calls, extended constant expressions, typed function references, garbage collection, exception handling, relaxed SIMD, 64-bit tables and a 64-bit working memory are not allowed.
-
Imported tables are not allowed.
-
It has no start function.
-
It defines no memory, and imports each memory at most once.
-
Every
memory.growinstruction targets the working memory. -
The import type of every memory other than the working memory has 64-bit addresses, the minimum 0 and no maximum.
-
It exports exactly one of the functions
aqr_apply_wholeandaqr_apply_part, the entry point. The module is a whole module in the former case and a part module in the latter. -
If it imports
aqr_spawn, it exports the functionaqr_task, the task entry point, which has two parameters of typei32and no results.
Whether the guaranteed limit of 227 bytes (128 MiB) for the Zstandard Window_Size (§ 3.2.1 Zstandard compression method)
should also apply to the memory limit.
Every import of a module is from the module name aqr, under one of the following names.
The names of imports and exports are prefixed with aqr_ to avoid collisions in the toolchains and hosts
that link them into one namespace.
Any other import is an invalid archive failure.
aqr_memory-
A memory, the working memory.
aqr_params-
A memory of the parameters.
aqr_resourcei-
A memory of the i-th resource, where i is a decimal number without leading zeros.
aqr_inputk-
A memory of the k-th input, where k is a decimal number without leading zeros.
aqr_outputj-
A memory of the j-th output, where j is a decimal number without leading zeros.
aqr_request,aqr_release,aqr_commit-
Functions of a part module, defined below. A whole module MUST NOT import them.
aqr_heartbeat-
A function defined below.
aqr_spawn,aqr_result,aqr_join,aqr_take-
Functions of tasks, defined in § 7.3.1 Tasks.
aqr_chan,aqr_send,aqr_recv,aqr_close-
Functions of channels, defined in § 7.3.1 Tasks.
aqr_log-
The diagnostic function, defined below.
A memory is sized to fit a length L when both its minimum and maximum are ⌈L / 65536⌉ pages.
To apply a module module given a list of input byte sequences inputs, a list of resource blocks resources, a parameter byte sequence params, a list of declared output sizes sizes and a memory limit:
-
sizes MUST NOT be empty.
-
Every
aqr_resourcei imported MUST satisfy i < the number of resources, everyaqr_inputk imported MUST satisfy k < the number of inputs, and everyaqr_outputj imported MUST satisfy j < the number of sizes. -
Create the memories imported, each initially zero-filled:
-
The working memory has the minimum size of its import type.
-
The memory of the parameters is sized to fit the length of params, and holds params from offset 0.
-
The memory of the i-th resource is sized to fit its stored content c, and holds c from offset 0.
-
The memory of the k-th input is sized to fit the length of inputs[k]. If module is a whole module, it holds inputs[k] from offset 0. Otherwise let Wk be its size in bytes and the visible range of the k-th input be [0, 0).
-
The memory of the j-th output is sized to fit sizes[j]. If module is a part module, let Wout,j be its size in bytes.
-
-
Let outputs be a list of as many empty byte sequences as sizes.
-
If the total size of the memories in bytes exceeds the memory limit, return a resource failure. The memory limit applies to the total size of the memories of the application, those of its tasks included, and of the messages queued in its channels.
-
Instantiate module with the memories and the host functions below.
-
Invoke the entry point with the arguments of type
i64, in order: the length of params, the length of the stored content of each item of resources, the length of each item of inputs and each item of sizes. The entry point MUST have these parameters and no results. If it traps or any host function below signals an error, return an invalid archive failure. If it exceeds the memory limit or any implementation-defined limit, return a resource failure. If any task of the application fails, return its failure (that of any of them if several fail). If every instance of the application waits, return an invalid archive failure. Once it returns, every task it spawned MUST be taken. -
If module is a whole module, set each outputs[j] to the first sizes[j] bytes of the memory of the j-th output, if imported, and to sizes[j] zero bytes otherwise.
-
The length of each outputs[j] MUST be sizes[j].
-
Return outputs.
Windows smaller than their data for a part module, i.e. ring buffers, which would allow import types with a maximum.
A memory.grow instruction exceeds the memory limit when the total size of the memories of the application,
those of its tasks and the messages queued in its channels included, would exceed it,
and MUST succeed otherwise if the maximum of the import type of the working memory allows.
Host functions are defined as follows, where lok and hik denote the visible range of the k-th input,
k, j, ptr and len are of type i32, and every offset is of type i64.
Called by a task, aqr_request, aqr_release and aqr_commit signal an error.
- aqr_request(k, hi)
-
The k-th input MUST be imported. hi MUST be in [hik, length of inputs[k]], and hi - lok MUST be at most Wk. For each offset o in [hik, hi), write inputs[k][o] to the memory of the k-th input at o % Wk. Then set hik to hi.
- aqr_release(k, lo)
-
The k-th input MUST be imported. lo MUST be in [lok, hik]. Set lok to lo.
- aqr_commit(j, hi)
-
The j-th output MUST be imported. hi MUST be in [length of outputs[j], sizes[j]], and hi - length of outputs[j] MUST be at most Wout,j. For each offset o in [length of outputs[j], hi), append the byte at o % Wout,j of the memory of the j-th output to outputs[j].
- aqr_heartbeat()
-
Does nothing.
- aqr_log(ptr, len)
-
Does nothing. Implementations MAY present the len bytes of the working memory from the offset ptr as a diagnostic message, which is untrusted data from the archive, but doing so MUST NOT affect the application, even when the range is out of bounds. It is the only diagnostic function; a module reports a failure by
aqr_logfollowed by a trap.
A violation of any MUST in host functions is signalled as an error to the module invocation.
7.3.1. Tasks
A task is an instance of the module of its own, run on a copy of the working memory of the instance that spawns it and giving back a byte sequence, its result. The application’s instance and every task are the instances of the application; each instance has a list of handles, initially empty, each of which is a list of the tasks spawned at once. A task is either running, done (with a result), failed (with an invalid archive failure or a resource failure), or taken.
A channel links instances of an application: a queue of messages (byte sequences) of at most its capacity, whether it is closed, and its sender and receiver, instances initially none. The application has a list of channels, initially empty. When an instance ends (returns or fails), every channel of which it is the sender is closed. An instance waits while it is within aqr_join, aqr_send or aqr_recv and the condition it waits for does not hold.
To run a task given a byte sequence S, arg and i:
-
Create the memories imported: the working memory has S.length / 65536 pages and the maximum of its import type, and every other memory has the minimum and maximum 0.
-
Instantiate the module with the memories and the host functions, and then set the content of the working memory to S.
-
Invoke the task entry point with arg and i. If it traps or any host function signals an error, the task fails with an invalid archive failure. If it exceeds the memory limit or any implementation-defined limit, the task fails with a resource failure.
-
Once it returns, every task it spawned MUST be taken, or the task fails with an invalid archive failure.
-
The task is done, with the result last set by aqr_result, or an empty byte sequence if none.
The host functions of tasks are defined as follows, for the instance I calling them,
where arg, n, h, i, ptr, dst and len are of type i32, unsigned:
- aqr_spawn(arg, n)
-
Let S be the content of the working memory of I. Append to the handles of I a list of n new tasks, the i-th (from 0) of which is to run a task given S, arg and i, and return the index of that list among the handles of I.
- aqr_result(ptr, len)
-
I MUST be a task, and ptr + len MUST be at most the size of its working memory. Set the result of I to the len bytes of its working memory from the offset ptr.
- aqr_join(h, i)
-
The i-th task of the h-th handle of I MUST exist and not be taken. Wait until it is done or failed. If it failed, the instance I fails with its failure. Otherwise return the length of its result.
- aqr_take(h, i, dst, len)
-
The i-th task of the h-th handle of I MUST be done (joined), len MUST be at most the length of its result, and dst + len MUST be at most the size of the working memory of I. Copy the first len bytes of its result to the working memory of I from the offset dst, and the task is taken.
The host functions of channels are defined as follows, where c, ptr, dst, len and cap are of type i32, unsigned,
the channel C is the c-th (from 0) of the application, which MUST exist,
and taking an end of C for I means: if that end is none, set it to I; otherwise it MUST be I.
- aqr_chan(cap)
-
cap MUST be at least 1. Append to the channels of the application a new channel of capacity cap, and return its index.
- aqr_send(c, ptr, len)
-
Take the sender of C for I. C MUST NOT be closed, and ptr + len MUST be at most the size of the working memory of I. Wait until fewer messages than the capacity of C are queued, and then enqueue the len bytes of the working memory of I from the offset ptr.
- aqr_recv(c, dst, cap)
-
Take the receiver of C for I. Wait until a message is queued or C is closed. If no message is queued, return −1 (0xFFFFFFFF). Otherwise let M be the first message. If the length of M exceeds cap, return it, leaving M queued. Otherwise dst + the length of M MUST be at most the size of the working memory of I; dequeue M, copy it to the working memory of I from the offset dst, and return its length.
- aqr_close(c)
-
Take the sender of C for I, and close C.
Whether the memories other than the working memory, read-only, should be visible to tasks, which would require rejecting the instructions writing them; and the windows of a part module in tasks.
8. Overlay archives and imports
8.1. Import block
Blocks 5816 through 5F16 are import blocks,
which are non-leaf blocks supplying byte ranges of files outside the segment.
Their type is 0101 1ess in binary (§ 3.1.1 Block types),
and their data is checksummed Zstandard data (§ 3.2 Shared external structure).
e determines the source (external): preceding segments if 0, and the base if 1.
An import block is an internal import block if external is false, and an external import block otherwise. An external import block MUST NOT occur in an archive other than an overlay archive (§ 3.5 Unencrypted archive).
The internal structure after decompression is as follows:
| Name | Type |
|---|---|
| num_imports | leb128 ≠ 0 |
| imports | import[num_imports] |
Each import consists of the following:
| Name | Type |
|---|---|
| import_path | u8[*] ≠ 0 |
| import_path_sentinel | u8 = 0 |
| import_offset | leb128 |
| import_size | leb128 |
| import_checksum | u8[32] |
An import in an internal or external import block is an internal import or an external import respectively. Each import supplies a single non-leaf item, so the import block supplies num_imports items in the order of imports. The stored content of the item is the imported content of the import, given by resolving it, the bytes in [import_offset, import_offset + import_size) of the file found at import_path:
-
An internal import finds the file in the preceding result of its segment, i.e. the result of applying all preceding segments to the base, which is empty for a standalone archive.
-
An external import finds the file in the base as given.
import_path is a sequence of zero or more components separated by 2F16 (/).
Each component uses the same encodings as entry_name,
and its decoded name is subject to the same restrictions as an entry name, so no component can be empty.
An empty import_path denotes the root itself, which is a file when the base is a single file.
import_checksum is the SHA-512/256 hash [FIPS180-4] of the imported content.
/ or be . or .., splitting import_path is unambiguous,
and no path can refer to anything outside the root.
A path is relative to the root rather than to the directory of the file using it,
because a formula block referencing an import is evaluated before the metadata block names that file,
and a single leaf block may be shared by files in different directories.
The prefixes shared by paths in the same block compress well.
8.2. Resolving an import
To resolve an import import of an import block in a segment:
-
Split import.import_path at every 2F16 into components, unless it is empty, and let names be the result of decoding each component as an entry name. If any component fails to decode, return an invalid archive failure.
-
Let node be the preceding result of the segment if import is an internal import, or the root of the archive base otherwise.
-
For each name of names in order:
-
node MUST be a directory node, and node.children[name] MUST exist.
-
Set node to node.children[name].
-
-
node MUST be a file node.
-
import.import_offset + import.import_size MUST be at most the length of node.content.
-
Let content be the bytes of node.content in [import.import_offset, import.import_offset + import.import_size).
-
The SHA-512/256 hash [FIPS180-4] of content MUST be equal to import.import_checksum.
-
Return content as the imported content of import.
A violation of any MUST in this algorithm is a base failure in an overlay archive, and an invalid archive failure in a standalone archive.
8.3. Reading a base
Reading a base from a file system is outside the archive model, as is extraction. A decompressor doing so maps a directory or a single file to an archive value, and is subject to the following requirements:
-
It MUST NOT use a base unless explicitly given one, and in particular MUST NOT use the current directory or the destination directory as a base by default.
-
It MUST NOT read anything outside the given directory, including through symbolic links or other redirections already present in the base. A symbolic link in the base is a link node and is never followed, so an import whose path leads through or to a link fails to resolve (§ 8.2 Resolving an import).
-
It MUST NOT read a file other than a regular file as the content of a file node. A FIFO or a device file in the base is a special node.
-
It SHOULD map a file system name to an entry name as the reverse of the mapping in § 12 Extraction. If a name can’t be mapped, or multiple names map to the same entry name, it MUST either fail or report it to the user.
-
The base MUST appear the same throughout the interpretation, as if it were read at a single point in time. A decompressor MAY read only the parts of the base needed by imports and by the result, and the checksums of imports detect a part that changed while reading.
A decompressor MAY write the result of an overlay archive into its base directory, i.e. apply it in place. In that case:
-
The result MUST be the same as if every imported content were read before anything is written, e.g. by writing new files under temporary names and renaming them after all of them are written.
-
It MAY remove a node of the base only when the node is absent from the result, and SHOULD leave every node not given by any segment as it is.
9. Encrypted archives
An archive might have been encrypted, in which case it consists of a block-like header of type ENCRYPTED_HEADER_TYPE followed by encrypted chunks. An encrypted archive is mirrored if it starts with ENCRYPTED_MIRRORED_HEADER_MAGIC, in which case copies of its header are placed between encrypted chunks.
| Name | Type | Conditions |
|---|---|---|
| checksum_start | offset | |
| encrypted_header_magic | u8[8] ∈ {ENCRYPTED_HEADER_MAGIC, ENCRYPTED_MIRRORED_HEADER_MAGIC} | |
| lg_work_factor | u8 ∈ [0C16, 3F16) | |
| scrypt_ephemeral_secret | u8[16] | |
| encrypted_file_key | u8[32] | |
| age_header_mac | u8[32] | |
| payload_nonce | u8[16] | |
| checksum_end | offset | |
| header_checksum | u64le | If mirrored |
| chunks | u8[*] |
The 97 bytes from lg_work_factor to payload_nonce are called the header fields.
header_checksum is the XXH64 [XXHASH] hash with the seed zero of all bytes from checksum_start to checksum_end.
All fields from lg_work_factor to age_header_mac are used to reconstruct an age v1 textual file header [AGE] as follows:
age-encryption.org/v1 -> scrypt scrypt_ephemeral_secret_base64 lg_work_factor_decimal encrypted_file_key_base64 --- age_header_mac_base64
Where,
-
scrypt_ephemeral_secret_base64 is the base64 encoding of scrypt_ephemeral_secret without padding ([RFC4648], Section 4);
-
lg_work_factor_decimal is the decimal representation of lg_work_factor without leading zeros;
-
encrypted_file_key_base64 is the base64 encoding of encrypted_file_key without padding;
-
age_header_mac_base64 is the base64 encoding of age_header_mac without padding.
The age payload consists of payload_nonce followed by the encrypted chunks in chunks and is interpreted in accordance with the age v1 format. Every encrypted chunk except the last one is 65,552 bytes long, i.e. 64 KiB of plaintext followed by a 16-byte authentication tag.
If the archive is not mirrored, chunks consists of the encrypted chunks alone. Otherwise chunks also contains mirrored headers: for each encrypted chunk Cm numbered from 0, a mirrored header immediately precedes Cm if and only if m + 1 is either a power of two from 2 to 32 or a positive multiple of 64. The k-th mirrored header numbered from 1 therefore precedes Cm where m = 2k − 1 for k ≤ 6 and m = 64(k − 5) − 1 for k ≥ 6, and no mirrored header follows the last encrypted chunk. A mirrored header consists of the following:
| Name | Type |
|---|---|
| checksum_start | offset |
| encrypted_mirror_magic | u8[8] = ENCRYPTED_MIRROR_MAGIC |
| mirror_index | u32le = k |
| mirrored_fields | u8[97] = header fields |
| checksum_end | offset |
| mirror_checksum | u64le |
mirror_checksum is the XXH64 [XXHASH] hash with the seed zero of all bytes from checksum_start to checksum_end.
As with mirror blocks, the constraints on the fields of a mirrored header are requirements on conforming compressors. A mirrored header always occupies 117 bytes at its position, and its content doesn’t affect the result of interpreting an archive. A mirrored header is intact if its mirror_checksum verifies and its fields other than mirrored_fields satisfy the constraints above. A decompressor MUST NOT use the mirrored_fields of a mirrored header that is not intact.
The interval of 64 encrypted chunks follows the tentative distance of 4 MiB in § 10.3 Mirror block and changes with it (docs/recovery-design.md §5.5).
The age payload MUST decrypt into a valid unencrypted archive (§ 3.5 Unencrypted archive), either a standalone archive or an overlay archive, with the following additional constraints:
-
size_hint_block is no longer optional;
-
No spacer block (§ 3.3 Spacer block) is allowed, in order to discourage an in-place modification of the encrypted archive;
-
No trailing bytes are allowed after the archive.
10. Integrity and recovery
10.1. Checksums
The following table summarizes every checksum defined by this specification:
| Structure | Checksum | Computed over | Presence |
|---|---|---|---|
| Metadata, dictionary, import, module and resource blocks, except shorthand blocks | XXH64 | Decoded data | Always |
| Shorthand blocks | None (the registered hash identifies the entry but is not checked) | Never | |
| Non-leaf data blocks | XXH64 | Decoded data | Always |
| Leaf data blocks | XXH64 | Decoded data | By type |
| formula_data of formula blocks | XXH64 | Decoded data | Always |
| Byte sequences in a formula | XXH64 (Checksum instruction) | The byte sequence covered | Optional |
| Imports | SHA-512/256 (import_checksum) | Imported content | Always |
| Mirror blocks | XXH64 (mirror_checksum) | Stored bytes | Always |
| Header and mirrored headers of an encrypted archive | XXH64 (header_checksum, mirror_checksum) | Stored bytes | If mirrored |
The encrypted chunks of an encrypted archive are also authenticated by [AGE].
Unless explicitly requested otherwise, conforming compressors SHOULD use a checksum_method other than None for every leaf block.
Unless explicitly requested otherwise, conforming compressors SHOULD declare an output checksum for every formula block.
-
An uncompressed leaf block without a checksum holds the file content verbatim and nothing derived from it, so the file can be memory-mapped from the archive and modified in place.
-
A compressed leaf block without a checksum can be made from an existing Zstandard frame by copying its Zstandard blocks, without decompressing them to compute the checksum over the decoded data.
Neither is a concern for long-term preservation, where the archival profile requires the checksum.
10.2. Archival profile
An archive conforms to the archival profile when the content of every file is covered by a checksum:
-
Every leaf data block has a checksum_method other than None.
-
Every formula block declares an output checksum.
-
The archive is a standalone archive.
-
No block is a shorthand block (§ 4.2.2 Registered dictionaries and modules).
A conforming compressor producing an archive for long-term preservation SHOULD conform to the archival profile. A tool verifying an archive SHOULD report whether it conforms to the archival profile.
10.3. Mirror block
A mirror block holds a verbatim copy of another block, called its target block, so that the target block can be restored when it is damaged. Mirror blocks are only defined for block types whose damage affects more than a single parcel and can be detected by reading the block alone, i.e. that have a checksum:
| Block Types | Target Block Types |
|---|---|
| C1016 — C1716 | 1016 — 1716 (§ 4.3 Metadata block) |
| C5016 — C5716 | 5016 — 5716 (§ 5.2 Dictionary block) |
| C5816 — C5F16 | 5816 — 5F16 (§ 8.1 Import block) |
| C6016 — C6316 | 6016 — 6316 (§ 7.1 Module block) |
| C6416 — C6716 | 6416 — 6716 (§ 7.2 Resource block) |
| C7016 — C7716 | 7016 — 7716 (§ 6.1 Formula block) |
As with any other block, the size field of a mirror block is determined by its own type and counts the bytes of its own data. Since a mirror block is longer than its target block, its size field may have to be wider. Therefore a mirror block is either:
-
Unwidened, where the size fields of the mirror block and its target block have the same width, and its target type is its type minus C0016; or
-
Widened, where the size field of the mirror block is one step wider than that of its target block, and its target type is its type minus C0116. Its type % 4 MUST NOT be 0.
The target block MUST be of the target type, which MUST be a block type listed above for the mirror block’s type. Whether a mirror block is widened is signalled by target_offset.
Its data consists of the following:
| Name | Type |
|---|---|
| mirror_magic | u8[8] ∈ {MIRROR_MAGIC, OVERLAY_MIRROR_MAGIC} |
| archive_offset | leb128 = block_start - archive_start |
| target_offset | leb128 ≠ 0 |
| target_data | u8[*] |
| checksum_end | offset |
| mirror_checksum | u64le |
mirror_magic is a fixed byte sequence that allows mirror blocks to be found by scanning when the surrounding structure, such as the header, the footer or the metadata chain, is damaged. It MUST be OVERLAY_MIRROR_MAGIC in an overlay archive and MIRROR_MAGIC otherwise.
archive_offset is the offset from archive_start to the block_start of the mirror block.
target_offset is the offset from the block_start of the target block to the block_start of the mirror block, plus 1 if the mirror block is widened. Since the offset between any two blocks is even, target_offset % 2 tells whether the mirror block is widened. The target block always precedes the mirror block.
target_data is a copy of the data of the target block. The bytes of the target block, from its block_start to its block_end, MUST be equal to the concatenation of the following:
-
The shortest leb128 encoding of the target type;
-
The length of target_data, encoded as the size field for the target type;
The padding of the target block, if any, is not compared, and is restored as a zero byte. The type of the mirror block itself is 2 bytes long, as every block type uses the shortest leb128 encoding.
mirror_checksum is the XXH64 [XXHASH] hash with the seed zero of all bytes from the block_start of the mirror block to checksum_end, including its type and size fields.
A mirror block SHOULD be unwidened unless its data doesn’t fit in the size field for the target type.
A mirror block doesn’t belong to any segment and MAY occur anywhere in blocks, including after the last metadata block, subject only to the requirement that its target block precedes it. A single target block MAY have multiple mirror blocks. The requirements on a mirror block in this section are requirements on conforming compressors, and a mirror block violating them doesn’t make the archive an invalid archive. Mirror blocks don’t affect the result of interpreting an archive at all.
A mirror block is intact if its mirror_checksum verifies and it satisfies the requirements above other than being equal to its target block. A decompressor MUST NOT restore a target block from a mirror block that is not intact, and SHOULD report a mirror block that is not intact or differs from its intact target block when it reads one.
A mirror block and its target block are expected to be damaged together when they are close to each other. Conforming compressors SHOULD separate the end of the target block and the block_start of its mirror block by at least 4096 bytes, so that they never share an aligned 4 KiB unit of the underlying storage, and SHOULD separate them by at least 4 MiB (4,194,304 bytes) when possible.
The recommended distances are tentative, pending the measurement of burst lengths of latent sector errors
(docs/recovery-design.md §8, R1).
The interval between mirrored headers in an encrypted archive (§ 9 Encrypted archives) changes with the latter distance.
A damaged encrypted chunk fails to authenticate as a whole, and an aligned 4 KiB unit of the underlying storage may span two adjacent encrypted chunks. Therefore, in place of the distance of 4096 bytes recommended above, conforming compressors SHOULD place a mirror block and its target block in an encrypted archive so that at least one entire encrypted chunk lies between them.
10.4. Repair
The repair process is to be specified in this section (docs/recovery-design.md §5.6).
Repairing an encrypted archive with mirror blocks or mirrored headers requires decrypting
the encrypted chunks after one that fails to authenticate, which a standard age implementation doesn’t do.
This is to be specified with the repair process as a separate step producing the decrypted bytes,
where bytes of such chunks are marked as erasures and the interpretation of an archive stays unchanged
(docs/recovery-design.md §5.6).
11. Optional metadata
The optional_metadata field of the metadata block (§ 4.3 Metadata block) holds the optional metadata, encoded in CBOR [RFC8949]. This part encodes any metadata that is not strictly necessary to read the file, including associated dates and times, access control information and comments.
The optional metadata is described by the following CDDL [RFC8610] definitions:
aqr-opt-metadata= { aqr-archive-comment ? &(entry-extras: -2) => [* aqr-entry-extra / null] ; Indexed by entry numbers, starting from the root directory * $$aqr-opt-metadata-member ; For future extensions }$$aqr-opt-metadata-member= () ; No extension is defined by this specificationaqr-entry-extra= { aqr-entry-timestamps aqr-entry-comment aqr-entry-link-group aqr-entry-windows aqr-entry-unix aqr-entry-zip * $$aqr-entry-extra-member ; For future extensions }$$aqr-entry-extra-member= () ; No extension is defined by this specification ; TODO: other extras for round trip
It MUST be a single well-formed CBOR data item [RFC8949] without any following bytes,
and no map in it can have duplicate keys.
The value of every key defined by this specification MUST match its definition.
If optional_metadata_size is 0, the optional metadata is an empty map {}.
The members of aqr-entry-extra are grouped by their purposes, and each group is described in the following sections.
Keys of aqr-entry-extra from -1 to -59 are platform-neutral.
Keys from -60 to -69 are specific to Windows, -70 to -79 to Unix, -80 to -89 to macOS, and -90 to -99 to other platforms.
Keys from -100 are specific to the formats an entry was converted from.
11.1. Timestamps
The keys crtime, ctime, mtime, atime and arctime of aqr-entry-extra
record the timestamps that file systems and archive formats associate with a file, a directory or a link.
Each is an aqr-time described below, and is absent if the source doesn’t have the corresponding timestamp.
aqr-entry-timestamps = (
? &(crtime: -1) => aqr-time
? &(ctime: -2) => aqr-time
? &(mtime: -3) => aqr-time
? &(atime: -4) => aqr-time
? &(arctime: -5) => aqr-time
)
crtime-
The creation time, also known as the birth time: when the file was created at its location. It is not updated afterwards, but copying a file usually creates a new one with the time of copying. Examples include the creation time of NTFS and FAT, the create date of HFS+ and APFS,
i_crtimeof ext4 andotimeof Btrfs, which are exposed asst_birthtimeon BSD and macOS and asstx_btimeofstatxon Linux. ctime-
The status change time: when the file’s metadata (e.g. permissions, ownership or link count) or its content was last changed. It is the POSIX
st_ctime, and the change time of NTFS and the attribute modification date of HFS+ are similar. POSIX doesn’t allow it to be set to an arbitrary time, so an extracting decompressor usually can’t restore it. mtime-
The modification time: when the file’s content was last changed. For a directory, it is when an entry was last added, removed or renamed in it. This is the timestamp that virtually all file systems and archive formats have.
atime-
The access time: when the file’s content was last read. Many systems update it lazily or not at all for performance (e.g.
relatimeandnoatimeon Linux, and the access time updates of NTFS that can be disabled), and FAT only keeps the date. Reading a file for archiving can itself update it. arctime-
The backup time: when the file was last backed up, as recorded by backup software. File systems don’t update it by themselves. An example is the backup date of HFS and HFS+, exposed as
ATTR_CMN_BKUPTIMEofgetattrliston macOS. It is the time stored by the source, not the time when the archive itself was made.
st_ctime in the Microsoft C runtime, the Ctime of ZIP extra field 0x000a,
and the CTime of 7z and RAR all go to crtime, not ctime.
POSIX formats such as pax use it for the status change time, which goes to ctime.
aqr-time represents a point in time or a local date and time,
together with a quantization interval and optional time zone information.
It is described by the following CDDL definitions,
where etime is the CDDL type name for the extended time defined in [RFC9581]:
aqr-time= $aqr-base-time / ; UTC timestamp, scaled and quantized [$aqr-base-time] / ; Local time in unknown time zone, encoded like UTC (*) [$aqr-instant,offset: aqr-tz-offset] / ; UTC timestamp with time zone offset ($) etime / ; Extended time defined in [[!RFC9581]] [etime] ; Local time in unknown time zone, encoded like UTCaqr-tz-offset= -48..56 ; In 15 minutes, positive east of UTC as in ISO 9660$aqr-base-time= #6.100(int) ; 1d (FAT atime*), date as in [[!RFC8943]] $aqr-base-time /= $aqr-instant ; Can be given a time zone offset$aqr-instant= #6.121(int) ; 2s (FAT mtime*, exFAT atime$, ZIP*, CAB*) $aqr-instant /= int ; 1s (~ext3, HFS/HFS+, tar, ISO 9660$) $aqr-instant /= #6.122(int) ; 10ms (FAT ctime*, ISO 9660$, exFAT$) $aqr-instant /= #6.123(int) ; 1ms $aqr-instant /= #6.124(int) ; 1us (UDF) $aqr-instant /= #6.125(int) ; 100ns (NTFS, RAR*, 7z) $aqr-instant /= #6.126(int) ; 1ns (ext4, ZFS, Btrfs, APFS)
A base time value $aqr-base-time is an integer, either untagged or tagged as follows.
The integer multiplied by the scale is the number of seconds since 1970-01-01T00:00:00Z,
not counting leap seconds as in the CBOR tag 1 [RFC8949];
negative values denote the time before that epoch.
The tag also determines the quantization interval,
which is relative to the time value and specified in § 15.4 Quantization Interval.
#6.100 is the number of days since 1970-01-01 [RFC8943],
and is the only base time value that is not an $aqr-instant, i.e. it can’t be given a time zone offset.
| Encoding | Quantization Interval (s) | Scale (s) | Examples |
|---|---|---|---|
#6.100(int)
| [0, 86400] | 86400 | FAT atime* |
#6.121(int)
| [0, 2] | 2 | FAT mtime, exFAT atime$, ZIP, CAB* |
int
| [0, 1] | 1 | ext3 and earlier, HFS/HFS+, tar, ISO 9660$ (directory records) |
#6.122(int)
| [0, 10-2] | 10-2 | FAT ctime*, ISO 9660$ (volume descriptors), exFAT$ |
#6.123(int)
| [0, 10-3] | 10-3 | - |
#6.124(int)
| [0, 10-6] | 10-6 | UDF |
#6.125(int)
| [0, 10-7] | 10-7 | NTFS, RAR*, 7z |
#6.126(int)
| [0, 10-9] | 10-9 | ext4, ZFS, Btrfs, APFS |
In the Examples column, formats marked with * store (or can store) a local time in an unknown time zone, and formats marked with $ store a local time with its offset from UTC.
$aqr-instant,
and are not shortcuts for the extended time in general.
1001({1: 1697641200, -14: 86400, -10: "+09:00"}) for 2023-10-19 in UTC+9).
Each alternative of aqr-time has the following meaning:
$aqr-base-time-
A point in time without any time zone information.
[$aqr-base-time]-
A local date and time in an unknown time zone, encoded as if it were UTC. It does not identify a single point in time.
[$aqr-instant, aqr-tz-offset]-
A point in time, as in the bare
$aqr-base-time, with the offset of the time zone where it was recorded, in 15-minute increments as in ISO 9660. The offset is positive east of UTC: UTC+x hours is 4x and UTC−x hours is −4x. The local time is the base time value plus 900 times the offset in seconds. etime-
An extended time [RFC9581] (tag 1001), for any value which doesn’t fit the other alternatives. The time zone information, if any, is given as a Time Zone Hint (key -10 or 10), and the quantization interval is given as a key described in § 15.4 Quantization Interval.
[etime]-
A local date and time in an unknown time zone, encoded as if it were UTC. The extended time MUST NOT contain a Time Zone Hint (key -10 or 10), and its timescale (key -1, -13 or 13), if present, MUST be 0 (UTC).
etime or [etime] MUST NOT be used
if another alternative of aqr-time represents the same time value,
the same quantization interval and the same time zone information.
A Time Zone Hint is the same as an aqr-tz-offset
only when it is a numeric offset (e.g. "+09:00") equal to that offset;
an extended time without any quantization interval is never the same as other alternatives.
aqr-time using the extended time,
as an extracting decompressor is allowed to ignore any extra metadata (§ 12 Extraction).
UTCTime, 2-32 s in NTP and 1/300 s in SQL Server DATETIME,
and so are time zone offsets that are not multiples of 15 minutes as in UDF.
They are rare enough in file metadata not to deserve their own encodings.
Two aqr-time values in different encodings are different even when they denote the same point in time.
For example, 1 and 122(100) differ in their quantization intervals.
A compressor converting a timestamp from a file system or another archive format
SHOULD preserve its precision, i.e. use an encoding whose quantization interval matches the source.
-
ZIP with a DOS date and time, a local time in an unknown time zone:
[121(848878577)] -
ZIP with an extended timestamp extra field (0x5455) in UTC:
1697724754 -
git, which records a time zone offset in minutes:
[1697724754, 36] -
exFAT modification time with a UTC offset:
[122(169772475487), 36] -
NTFS:
125(16977247548700000) -
ext4:
126(1697724754870000000) -
FAT access date, a local date in an unknown time zone:
[100(19649)] -
A source rounding to the nearest second:
1001({1: 1697724755, -14: [0.5, 0.5]})
Timestamps from ZIP are discussed in § 16.2 Timestamps from ZIP.
11.2. Comments
archive-comment of aqr-opt-metadata and comment of aqr-entry-extra
are free-form comments attached to the whole archive and to each entry respectively,
e.g. an archive comment and a file comment of ZIP or RAR.
Both are an aqr-text, which is a Unicode string,
a WTF-8 [WTF-8] string with lone surrogates, or a byte sequence in an unknown encoding:
aqr-archive-comment= ( ? &(archive-comment: -1) => aqr-text )aqr-entry-comment= ( ? &(comment: -6) => aqr-text )aqr-text= tstr / ; Unicode #6.273(bstr) / ; WTF-8, i.e. UTF-16 + surrogate compat bstr ; In unknown encoding
An aqr-text MUST be a tstr if it can be represented as one,
i.e. if it is a sequence of Unicode scalar values.
The byte string in #6.273(bstr) MUST be a valid WTF-8 string that is not a valid UTF-8 string,
i.e. it MUST contain at least one encoded lone surrogate.
A bstr is not subject to these requirements even if it happens to be a valid UTF-8 string,
as its encoding is not known.
Comments from ZIP are discussed in § 16.3 Comments from ZIP.
11.3. link-group: Hard links
aqr-entry-link-group = (
? &(link-group: -7) => int ; File nodes only
)
link-group records that file nodes were originally the same file,
i.e. hard links to the same inode on Unix or to the same file record on NTFS.
It has no effect on other nodes, and a compressor SHOULD NOT give it to them.
A non-negative value is segment-local and denotes the same group only within the same segment,
while a negative value is archive-wide and denotes the same group in every segment.
The resulting link group in the archive value is determined by interpreting a segment.
A compressor MUST NOT give the same link group to files that were not the same file.
It SHOULD NOT give a link group that no other file node has.
A file node with link-group SHOULD refer to the same leaf block as other file nodes in the same group,
and SHOULD have the same entry extra as them except for link-group and comment.
st_ino) to be used as is,
as CBOR can encode any 64-bit unsigned integer as a non-negative int,
and can be assigned without reading earlier segments.
Inode numbers are still only unique within a single file system,
so a compressor reading from multiple file systems has to renumber some of them.
Archive-wide groups link files across segments,
e.g. when a compressor periodically starts a new segment while archiving a single tree,
or when it appends a segment to an existing archive.
A compressor using them is responsible for keeping them consistent with all other segments,
e.g. by reading earlier metadata blocks when appending.
Inode numbers are not suitable as archive-wide values as is,
because they can be reused after the file is removed or come from an unrelated file system,
and linking unrelated files with the same content can’t be detected by any decompressor.
A link group is consistent if all file nodes with that group in the final archive value have the same content. An inconsistent group, e.g. after a later segment has replaced only some of its file nodes, is not an invalid archive failure, but has no effect on extraction (§ 12 Extraction).
link-group
still extracts the correct files with the same contents.
Directory hard links, found in some macOS Time Machine volumes, are not represented.
11.4. Windows-specific metadata
aqr-entry-windows = (
? &(win-link-type: -60) => &(file: 0, directory: 1, junction: 2) ; Link nodes only
; TODO: file attributes (e.g. ZIP external file attributes)
)
win-link-type determines the kind of a link to be created on Windows (§ 12 Extraction).
It has no effect on nodes other than link nodes, and a compressor SHOULD NOT give it to them.
"file" and "directory" denote a symbolic link to a file and to a directory respectively,
and "junction" denotes a junction (a mount point reparse point).
11.5. Unix-specific metadata
aqr-entry-unix = (
? &(unix-mode: -70) => uint .le 4095 ; Permission bits only (0o7777), without S_IFMT
? &(unix-uid: -71) => uint
? &(unix-gid: -72) => uint
)
unix-mode only holds the permission bits, including the set-user-ID, set-group-ID and sticky bits.
The kind of an entry is solely determined by its node (§ 4.1 Archive model), so unix-mode never includes the file type bits (S_IFMT).
unix-uid and unix-gid are the numeric user and group IDs of the owner.
Unix metadata from ZIP is discussed in § 16.4 Unix metadata from ZIP.
11.6. zip-extras: ZIP extra fields
zip-extras preserves extra fields [APPNOTE] of the ZIP entry that the entry was converted from,
when they carry information about the file that has no other representation in this specification.
aqr-entry-zip= ( ? &(zip-extras: -100) => aqr-zip-extras )aqr-zip-extras= { * aqr-zip-extra-key => bstr .size (0..65531) }aqr-zip-extra-key= 0..65535 / ; Header ID n in the local header -65536..-1 ; Header ID n in the central header, as -1 - n ; TODO: multiple extra fields with the same header ID
A key n from 0 to 65535 denotes the extra field with the header ID n in the local header, and a key −1 − n from −65536 to −1 denotes the extra field with the header ID n in the central header. The value is the data of the extra field, excluding its 4-byte header of the header ID and the data size. Therefore a value with a non-negative key is always in the format for the local header, and a value with a negative key is always in the format for the central header.
A compressor converting a ZIP archive MAY put any extra field of an entry into zip-extras, subject to the following requirements:
-
It MUST NOT put the central header version of an extra field if the local header version with the same header ID is identical byte by byte. The central header version without the local header version is only put with a negative key.
-
It MUST NOT put an extra field whose information is fully represented elsewhere, i.e. in the entry name, the archive model (§ 4.1 Archive model) or any other key of
aqr-entry-extra. -
It MUST NOT put an extra field which is only meaningful to the ZIP container itself, e.g. sizes, offsets, padding, alignment, encryption and signatures over the ZIP archive.
-
It SHOULD NOT give
zip-extrasto the root directory, which has no counterpart in ZIP.
Extra fields known to be subject to these requirements are listed in § 16.5 Extra fields not to be preserved.
Any other extra field, including one with an unregistered header ID, can be put into zip-extras as is.
zip-extras is never authoritative.
If information in a preserved extra field disagrees with the archive model or any other key of aqr-entry-extra,
the latter MUST be used.
A decompressor finding an extra field that a compressor is not allowed to put SHOULD ignore that field only,
and SHOULD NOT treat the whole archive as invalid.
zip-extras is designed for a one-way conversion from ZIP;
reconstructing the original ZIP archive from the converted archive is not a goal.
Extra fields are preserved so that information without its own key yet,
e.g. platform-specific attributes, access control lists and resource forks,
can be recovered from already converted archives once a dedicated key is defined.
Unknown extra fields are preserved for the same reason,
even though some of them might be only meaningful to the ZIP container.
The local and central header versions of the same header ID often differ in their formats,
e.g. the central header version of 0x5455 only has the modification time,
and that of many Info-ZIP attribute blocks only has a summary.
Distinguishing them by keys lets a consumer interpret each value in the correct format.
The key for the central header version is −1 − n instead of −n,
so that header ID 0 doesn’t collide with the local header version,
and both versions have the same encoded length in CBOR.
12. Extraction
Writing an archive value to a file system is outside the archive model. A decompressor doing so is subject to the following requirements:
-
It MUST NOT create, modify or remove any file outside the destination directory, including through symbolic links or other redirections already present in the destination or created during the extraction.
-
It SHOULD map an entry name whose encoding is "
wtf-8" to the platform’s native Unicode name (e.g. UTF-16 with lone surrogates on Windows), and an entry name whose encoding is "raw" to the identical byte sequence where the platform allows. -
If an entry name can’t be represented on the platform or has a special meaning there (e.g. a name containing
\or:, or a reserved device name on Windows), it MUST either fail or report a substituted name to the user; it MUST NOT silently use a different name. -
If multiple entry names in a directory map to the same file system name (e.g. due to case insensitivity or Unicode normalization), it MUST either fail or report the conflict, and MUST NOT write a later entry through an earlier one.
-
It MAY ignore the extra of any node, e.g. extra and extra. It SHOULD NOT apply ownership information such as
unix-uidandunix-gidunless explicitly requested.
A and a file a/x map to the same directory on a case-insensitive file system.
The rule on conflicting names above already prevents this, and the first requirement makes it explicit.
Possible implementations include creating all links after all other nodes,
or opening each path relative to its parent directory without following links
(e.g. openat with O_NOFOLLOW, or RESOLVE_BENEATH on Linux).
A decompressor extracting a link node is subject to the following additional requirements:
-
It MUST NOT create a link whose target escapes the destination directory unless explicitly requested, and SHOULD provide a way to request it. A link target escapes the destination directory if it is absolute or relative to a drive or a root on the platform, or if resolving it from the parent directory of the link, following any link nodes in the archive value on the way, reaches a parent of the destination directory. A link not created for this reason MUST be reported to the user.
Creating such a link doesn’t write outside the destination directory by itself, but the extracted tree can lead later users or programs to do so. Resolving a target lexically is not enough: whend/e/lis a link to../../q, a link tod/e/l/../..lexically resolves todbut actually reaches the parent of the destination directory. -
On Windows, it SHOULD replace every
/in the link target with\. It SHOULD create the kind of link given bywin-link-type, if any. Otherwise, it SHOULD create a symbolic link to a directory if the target resolves to a directory node in the archive value as above, and a symbolic link to a file otherwise. -
If a link can’t be created, e.g. due to the lack of privilege, the file system or the length of its target, it MUST either fail or report it to the user; it MUST NOT silently create a different kind of node. It MAY create a copy of the target instead if it reports so.
-
It MUST NOT apply the extra of a link node to its target. Timestamps, ownership and permissions SHOULD be applied to the link itself where the platform allows (e.g.
lutimes,lchown).
A decompressor MAY create hard links among file nodes with the same consistent link group. It MUST NOT create hard links among any other file nodes, even when they have the same content.
rsync --link-dest.
But files that were independent have to remain independent.
A decompressor SHOULD NOT create a FIFO or a device file for a special node unless explicitly requested, as with ownership information. A special node not created for this reason, or because the platform can’t represent it (e.g. the lack of privilege, or a device number out of the platform’s range), MUST be reported to the user.
A decompressor MUST NOT create anything for an unknown node, and MUST report it to the user. It MAY continue extracting other nodes.
13. Conformance
As well as sections marked as non-normative, all diagrams, examples and notes in this specification are non-normative. Everything else in this specification is normative.
The key words MAY, MUST, MUST NOT, RECOMMENDED, SHOULD and SHOULD NOT in this specification are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
13.1. Conformance levels
A conforming decompressor has a conformance level, one of the following according to the modules (§ 7 Modules) it applies:
- Level 0
-
Applies no module.
- Level 1
-
Applies every registered module, i.e. every module whose module hash is the registered hash of an entry of the Aqueous Modules Registry (§ 15.6 Aqueous Modules Registry), and no other module.
- Level 2
-
Applies every module.
A decompressor at level 0 or level 1 MAY refuse to apply a module that its level doesn’t include, in which case a resource failure occurs. A decompressor at level 2 MUST NOT refuse to apply a module, though it may still fail with a resource failure when a module exceeds its limits. A level doesn’t change any other requirement on a decompressor, e.g. a decompressor at level 0 still reads formula blocks without Apply instructions.
13.2. Partial access
A decompressor MAY compute only a part of the result of interpreting an archive, such as a subset of entries, a byte range of a content, or entry names without contents. Such a partial result MUST be equal to the corresponding part of the full result, if the full result is an archive value.
A decompressor computing a partial result is not required to detect an invalid archive failure caused only by parts it doesn’t compute. It SHOULD verify every checksum covering only the bytes it has computed.
14. Security considerations
An archive is a container for arbitrary data and doesn’t by itself contain active content, except for WebAssembly modules (§ 7.3 Module execution) that a decompressor executes to reconstruct file contents. Such modules are run under a deterministic profile without access to anything but their inputs and outputs, but a decompressor still has to treat them as untrusted code and isolate them accordingly, unless it knows their module hash (§ 7.1 Module block). Formula blocks and modules can produce outputs far larger than the archive itself, and some fields (e.g. lg_work_factor) directly control the amount of computation. A decompressor is expected to enforce implementation-defined limits and fail with a resource failure when they are exceeded. A decompressor writing an archive to a file system is subject to the requirements in § 12 Extraction, in particular to prevent writing outside the destination directory. An unencrypted archive provides no confidentiality, and its XXH64 checksums (§ 10.1 Checksums) only detect accidental corruption, not intentional modification. An encrypted archive (§ 9 Encrypted archives) provides confidentiality and integrity through [AGE], which is only as strong as the passphrase used. The metadata of an archive, including entry names, timestamps and ownership, may reveal information about the system that created it.
An overlay archive is interpreted against a base, typically a directory on the file system, and can copy any part of the base into its result. A decompressor reading a base is subject to the requirements in § 8.3 Reading a base, in particular to use a base only when explicitly given one and to never read outside it. Applying an overlay archive in place modifies and removes files in the base directory.
15. IANA Considerations
15.1.
application/aqr Media Type
This document requests the registration of a new media type in the Media Types Registry ([RFC6838], section 5.6) as follows:
- Type name
- application
- Subtype name
- aqr
- Required parameters
- N/A
- Optional parameters
- N/A
- Encoding considerations
- binary
- Security considerations
- See § 14 Security considerations.
- Interoperability considerations
-
An unencrypted archive may be preceded and followed by arbitrary bytes, e.g. a self-extracting executable stub; its extent is determined by header_magic and the footer (§ 3.6 Locating an archive). An archive without any metadata block decompresses into a single unnamed file rather than a directory tree. An overlay archive has its own media type
application/aqo(§ 15.2 application/aqo Media Type); an encrypted archive whose payload is an overlay archive is also of that type. - Published specification
- This document.
- Applications that use this media type
- Archivers and compressors that implement the Aqueous archive format.
- Fragment identifier considerations
- N/A
- Additional information
-
- Deprecated alias names for this type
- N/A
- Magic number(s)
- F8 04 03 6A D6 BE (hexadecimal; HEADER_MAGIC) for an unencrypted archive, possibly preceded by other data; F9 CA F2 A6 D0 2A (hexadecimal; the common prefix of ENCRYPTED_HEADER_MAGIC and ENCRYPTED_MIRRORED_HEADER_MAGIC) at offset 0 for an encrypted archive.
- File extension(s)
- .aqr
When a single file is compressed into an archive without any metadata block, the extension may be appended to the original file name instead (e.g.example.tar.aqr). - Macintosh file type code(s)
- N/A
- Person & email address to contact for further information
- Kang Seonghoon <public+aqr@mearie.org>
- Intended usage
- COMMON
- Restrictions on usage
- N/A
- Author
- Kang Seonghoon
- Change controller
- Kang Seonghoon
15.2.
application/aqo Media Type
This document requests the registration of a new media type in the Media Types Registry ([RFC6838], section 5.6) as follows:
- Type name
- application
- Subtype name
- aqo
- Required parameters
- N/A
- Optional parameters
- N/A
- Encoding considerations
- binary
- Security considerations
- See § 14 Security considerations.
- Interoperability considerations
-
An overlay archive can’t be interpreted without its base. Its magic numbers differ from those of
application/aqr, so that a decompressor not supporting overlay archives doesn’t mistake one for a standalone archive. The interoperability considerations forapplication/aqrotherwise apply. - Published specification
- This document.
- Applications that use this media type
- Archivers, patching tools and updaters that implement the Aqueous overlay archive format.
- Fragment identifier considerations
- N/A
- Additional information
-
- Deprecated alias names for this type
- N/A
- Magic number(s)
- 69 0D 15 AF F6 B2 (hexadecimal; OVERLAY_HEADER_MAGIC) for an unencrypted overlay archive,
possibly preceded by other data.
An encrypted overlay archive has the same magic numbers as an encrypted archive of
application/aqr. - File extension(s)
- .aqo
- Macintosh file type code(s)
- N/A
- Person & email address to contact for further information
- Kang Seonghoon <public+aqr@mearie.org>
- Intended usage
- COMMON
- Restrictions on usage
- N/A
- Author
- Kang Seonghoon
- Change controller
- Kang Seonghoon
15.3.
+aqr Structured Syntax Suffix
This document requests the registration of a new structured syntax suffix in the Structured Syntax Suffix Registry ([RFC6838], section 6.2) as follows:
- Name
- Aqueous Archive Format
- +suffix
- +aqr
- References
- This document.
- Encoding considerations
- binary
- Interoperability considerations
-
A media type with the
+aqrsuffix denotes a standalone archive, either unencrypted or encrypted, whose archive value is given a more specific meaning by that media type. A generic Aqueous decompressor can interpret an archive of such media type without knowing that meaning. The interoperability considerations forapplication/aqr(§ 15.1 application/aqr Media Type) also apply. - Fragment identifier considerations
-
The syntax and semantics of fragment identifiers specified for
+aqrSHOULD be as specified forapplication/aqr. (At publication of this document, there is no fragment identification syntax defined forapplication/aqr.)The syntax and semantics for fragment identifiers for a specific
xxx/yyy+aqrSHOULD be processed as follows:- For cases defined in
+aqr, where the fragment identifier resolves per the+aqrrules, then process as specified in+aqr. - For cases defined in
+aqr, where the fragment identifier does not resolve per the+aqrrules, then process as specified inxxx/yyy+aqr. - For cases not defined in
+aqr, then process as specified inxxx/yyy+aqr.
- For cases defined in
- Security considerations
- The security considerations in § 14 Security considerations apply to any media type with the
+aqrsuffix. Each such media type may have additional security considerations specific to its interpretation of the archive. - Contact
- Kang Seonghoon <public+aqr@mearie.org>
- Author/Change controller
- Kang Seonghoon
15.4. Quantization Interval
This document requests the registration of a new key for the Time Tag Map Keys Registry ([RFC9581], section 7.3) as follows:
- Key Name
- Quantization Interval
- Key Value
- -14 (tentative)
- Reference
- The Quantization Interval section in this document.
- Semantics
-
A quantization interval specifies the interval in which the true time lies, relative to the time value given. That is, if the time value is t and the quantization interval is from a to b, the true time lies between t + a and t + b inclusive. The quantization is any means to truncate or round the true time value to a discrete representation, and notably, is not related to the accuracy of the underlying clock. For example, it is possible that the clock has a high precision but the time values are truncated to the last second, in which case the quantization interval would be [0, 1 second].
The interval is always closed. It may be slightly wider than the exact set of possible true times (e.g. [0, 1 second] for truncation, where the true time can never be exactly 1 second later), but it never excludes any possible true time, including ties in rounding.
A quantization interval can be given in terms of:
- A numeric value in seconds or a duration y, indicating the interval of [0, y];
- An array of two elements
[x, y]each of which is either a numeric value in seconds or a duration, indicating the interval of [−x, y]. That is, x is the extent before the time value and y is the extent after the time value.
Every numeric value MUST be finite and non-negative, and every duration MUST have a non-negative key 1, so that the time value given is always contained in the interval. The interval MUST NOT be a single point. Therefore a single numeric value or duration MUST NOT be zero, and an array
[x, y]MUST NOT have both x and y zero. If x is zero, the array MUST NOT be used and y MUST be given alone instead.If an extended time doesn’t contain this key, its quantization interval is unknown; in particular, the time value is not implied to be exact.
For example, the first two of the following represent a time value truncated to 10 milliseconds, and the last one represents a time value rounded to the nearest second:
1001({1: 1697724754, -3: 870, -14: 0.01}), 1001({1: 1697724754, -3: 870, -14: {1: 0, -3: 10}}), 1001({1: 1697724754, -14: [0.5, 0.5]})
The following CDDL declarations can be used to represent the Quantization Interval key:
quantization-interval = ~time / ~duration / [ 2*2 ~time/~duration ]
$$ETIME-ELECTIVE //= (-14: quantization-interval)
15.5. Aqueous Dictionaries Registry
This document requests IANA to create a new registry titled "Aqueous Dictionaries", listing the registered dictionaries that a shorthand block of type 5016 can refer to (§ 4.2.2 Registered dictionaries and modules). New entries are assigned with the Specification Required policy ([RFC8126], section 4.6). Each entry has the following fields:
- Hash
- The registered hash, i.e. the SHA-512/256 hash [FIPS180-4] of the dictionary’s bytes, written as 64 lowercase hexadecimal digits.
- Name
- A short name of the dictionary for humans, which has no effect on the format.
- Entropy Tables
- Whether the dictionary has entropy tables, i.e. the value of has_entropy_tables.
- Data
- The bytes of the dictionary, being the decoded data of a dictionary block (§ 5.2 Dictionary block), which MUST be valid as such and deposited with the registration.
- Reference
- The document describing the dictionary and its intended use.
The designated expert verifies that the hash matches the data and that no entry has the same hash. An entry, once registered, MUST NOT be modified or removed, since archives refer to it only by its hash. An entry is never shared with the Aqueous Modules Registry (§ 15.6 Aqueous Modules Registry), even when both have the same data.
The initial contents of the registry are as follows:
| Hash | Name | Entropy Tables | Reference |
|---|---|---|---|
| TBD | text | TBD | This document |
| TBD | modules | TBD | This document |
The dictionary "text" is intended for general text, and the dictionary "modules" is intended for module blocks given in full (§ 7.1 Module block).
The data and the hashes of the initial dictionaries are yet to be made.
15.6. Aqueous Modules Registry
This document requests IANA to create a new registry titled "Aqueous Modules", listing the registered modules that a shorthand block of type 6016 can refer to (§ 4.2.2 Registered dictionaries and modules). New entries are assigned with the Specification Required policy ([RFC8126], section 4.6). Each entry has the following fields:
- Hash
- The registered hash, i.e. the module hash of the module, written as 64 lowercase hexadecimal digits.
- Name
- A short name of the module for humans, which has no effect on the format,
written as the name of the module and its version joined by
@(e.g.flate-set@1). Different versions of a module have the same name before@and different hashes. - Data
- The bytes of the module, being wasm_module of a module block (§ 7.1 Module block), which MUST be valid as such and deposited with the registration.
- Reference
- The document describing the transformation that the module reverses, its inputs, resources and parameters, and preferably the source code from which the module was built.
The designated expert verifies that the hash matches the data, that no entry has the same hash, and that the module conforms to § 7.3 Module execution. An entry, once registered, MUST NOT be modified or removed, since archives refer to it only by its hash. An entry is never shared with the Aqueous Dictionaries Registry (§ 15.5 Aqueous Dictionaries Registry), even when both have the same data.
The initial contents of the registry are the modules built by the reference implementation of this document, as follows:
| Hash | Name | Reference |
|---|---|---|
d084284fbb3eaaac8e96ffb974f364d8bf9d5ea5b25bf4e236bc032369463578
| bcn-recons@1 | aqr-tex/FORMAT.md |
30ebd39ea3f768f06cf4d3e79872666d6923eb8e608ed5878bcc86c040179a21
| bundle-recons@1 | aqr-bundle/FORMAT.md |
9e6fa187a0e2001ad29b625e9ebdc8b96f8fa9720dee920e95a5338e47e25f36
| clip-recons@1 | aqr-clip/FORMAT.md |
6a9359342d4e3712baeb3d615ce0bc39a3283412da430f4c3098e2a71560e48d
| exe-recons/arm64@1 | aqr-exe/FORMAT.md |
3ca9270c8e48046ab88d1cad5f6eb1475c17cb7e329b661ec5dbf2a65f25a8fd
| exe-recons/cil@1 | aqr-exe/FORMAT.md |
2517aa3a4adf0c7bed4175c71dd6cb6928820c2a33038a8ffcf93d21d9e7c509
| exe-recons/x86@3 | aqr-exe/FORMAT.md |
f15b8d3355fc8929c25d263b6b752516a3e675cbd476ee5933f6900a8cb9718b
| fasta-recons@1 | aqr-fasta/FORMAT.md |
6206529b733fcc76f115f80f003cee937bd04c7bf4b96366ed4b64991406f265
| flac-recons@3 | aqr-flac/FORMAT.md |
efaae4c32b21f363e5d545de8a1fccbeba2859d9ec1f8e85192c37be997abb32
| flate-set@1 | aqr-flate/FORMAT.md |
17849cd2f570cd4ff266e6c7c7e7656954fff0b7b53a032682e630874b1d27e3
| git-pack-recons@1 | aqr-git/FORMAT.md |
bdbeae21d9035ece1bc14e6dfee467ac323f9406b0d78c86b017f36241a8c74e
| jpeg-recons@2 | aqr-jpeg/FORMAT.md |
a9272277b48740f1ca514cbf6a053031e73152aa2d47a29adb60317ac6d51bc7
| leveldb-recons@1 | aqr-snappy/FORMAT.md |
d1c9ec7cd6d507c6cc0974074cbeb7b6af0eeddca3bf37d7183fb85a4b898c4e
| lz4-recons@1 | aqr-lz4/FORMAT.md |
75ba5c9a68f2ba9e84a41a14bfad04c79becb1ce7c7b0789f9f2693e54611e80
| lzma-recons@1 | aqr-lzma/FORMAT.md |
b382acbe0a55d2b387ace2f8ddcae3ccc80b1001d0a4bb47e1dd6f17e175a6c4
| lzo-recons@1 | aqr-lzo/FORMAT.md |
e509302b1a4f24eef7ef7c28fcf9ff083578fe3670a030f61b572f08d210ed8b
| mp3-recons@5 | aqr-mp3/FORMAT.md |
ac352c74b976c0ea52ac3854b1c841d97cf6ebc44d37794263ebae88bf0349e1
| obj-recons@1 | aqr-obj/FORMAT.md |
7ced999c389e0aa234aea59e890ffac292cec0e1acb77c1918b4df791fc125a3
| pcm-recons@1 | aqr-raw/FORMAT.md |
59b6c21e5ce4e15f567b6af7af919ce474ecfdfe0317b1c5a24a2ebd1069a428
| pdf-recons@2 | aqr-pdf/FORMAT.md |
99cf42581dad0745303a924a9220ee4dbb98cad2d93485d5ba5ae0adcac5ac2b
| png-against@1 | aqr-png/FORMAT.md |
c989530d21e2b482e93dda8baf23827e3dccf1e35a7880830cc665a53b1b433b
| png-recons@3 | aqr-png/FORMAT.md |
cf881c57de0f4d6b182f82383c3a715874b6233a4873d03a10b62a8d7a46e20a
| png-set@2 | aqr-png/FORMAT.md |
946311bc554ece1ce8153ec662dce02d99bb6110838db94679a5faeec88f6ddd
| psd-recons@1 | aqr-psd/FORMAT.md |
99c859fce0c3faaa78c864b87e25ce33ae184fbc9a7f71688e0982c49ddaf9f5
| raster-recons@1 | aqr-raw/FORMAT.md |
6e967dac1072db4e3ab21cd10103b0773253bc24e64b7a94aa8519de42bd02db
| snappy-recons@2 | aqr-snappy/FORMAT.md |
1d5e4cdab89b3ccd790d713a9ec507f35b2f52cd58d6732c1ede1a835926e89d
| sqlite-recons@1 | aqr-sqlite/FORMAT.md |
78114341a3981b9b877f74beff267c6fcb83f23e7106d344107fbc178dab3117
| wrt-recons@1 | aqr-wrt/FORMAT.md |
426ac8c63718fcf83795ff7dd1e590c3cc9420cc82c3bfd1b23a04cb00ac2dbf
| zip-recons@3 | aqr-zip/FORMAT.md |
The initial entries are not yet fixed. Their modules and the documents describing them may still change before the first version of this document, and an entry changed until then is replaced in this table, not added as a new version.
16. Converting from ZIP
This section is non-normative.
This section describes how a compressor can convert entries of a ZIP archive [APPNOTE] into an archive value without losing information, where ZIP offers multiple sources or encodings for the same information.
16.1. Entry names from ZIP
A ZIP archive [APPNOTE] stores the path of each entry as a byte sequence in the local and central headers,
with components separated by /.
Its encoding is not always known from the header alone,
and several extra fields carry the Unicode version of the same path.
A compressor converting a ZIP archive can determine the encoding of each component from the following sources,
where earlier sources are preferred:
-
Extra field 0x7075 (Info-ZIP Unicode Path) of version 1 [INFOZIP-EXTRAFLD], whose
NameCRC32matches the CRC-32 of the path in the header. The field holds the path in UTF-8. A field with a mismatching CRC-32 is ignored as specified by [APPNOTE], as it may have been left over by a tool that changed the path without updating the field. -
The path in the header, if bit 11 (language encoding flag) of the general purpose bit flag is set. The path is in UTF-8.
-
Extra field 0x554e (Xceed Unicode), which holds the path in UTF-16LE. It is converted to WTF-8, which preserves any lone surrogate.
-
Extra field 0x5a4c (ZipArchive code page), which holds the path encoded in the code page given in the same field. If the field only has the code page, the path in the header is decoded with it instead.
-
Extra field 0xe57a (ALZip code page), which gives the code page of the path in the header.
-
Otherwise, the encoding of the path in the header is not specified. It is traditionally IBM code page 437, but often the system code page of the machine that made the archive, so the compressor has to guess the encoding. If it can’t, the raw byte sequence (prefixed with C116) is used for each component.
16.2. Timestamps from ZIP
A ZIP archive [APPNOTE] can carry the same timestamp in multiple places: the DOS date and time in the local and central headers, and extra fields 0x000a (NTFS), 0x5455 (extended timestamp), 0x5855 (Info-ZIP UNIX type 1) and 0x000d (UNIX) [INFOZIP-EXTRAFLD]. A compressor converting a ZIP archive can choose the source of each key as follows, where earlier sources are preferred. The order prefers more precise sources first, and then more recent specifications for the same precision.
| Key | Sources |
|---|---|
crtime
| 0x000a Ctime; 0x5455 CrTime (local header only)
|
ctime
| - |
mtime
| 0x000a Mtime; 0x5455 ModTime; 0x5855 or 0x000d ModTime; DOS date and time
|
atime
| 0x000a Atime; 0x5455 AcTime (local header only); 0x5855 or 0x000d AcTime
|
arctime
| - |
Each source is encoded as follows:
-
0x000a stores a Windows
FILETIMEt, the number of 100 ns intervals since 1601-01-01T00:00:00Z, which is encoded as125(t − 116444736000000000). A zero value means that the timestamp is absent. -
0x5455, 0x5855 and 0x000d store the number of seconds since 1970-01-01T00:00:00Z, which is encoded as an untagged
int. It is a signed 32-bit integer according to [INFOZIP-EXTRAFLD], though some implementations write times after 2038 as unsigned. The central header version of 0x5455 only hasModTimeeven when its flags indicate others, so the local header version is preferred when available. -
The DOS date and time is a local time in an unknown time zone with the precision of 2 seconds, which is encoded as
[121(n)]where 2n is the local time as if it were UTC.
When both the DOS date and time and a UTC source are present for mtime,
their difference reveals the time zone offset where the archive was made.
If the DOS time minus the UTC time differs from 900k seconds by less than 2 seconds,
where k is an integer in the range of aqr-tz-offset,
mtime can be encoded as [u, k] where u is the encoding of the UTC source.
Otherwise, e.g. due to a clock error or a daylight saving time transition, the DOS date and time is simply discarded.
If two sources for the same key disagree by more than their quantization intervals, a compressor can warn about it, and still uses the preferred source.
16.3. Comments from ZIP
A ZIP archive [APPNOTE] stores a comment for each entry in the central header only,
and a comment for the whole archive in the end of central directory record.
Both are byte sequences, and an empty comment is no different from the absence of one,
so the compressor can omit comment or archive-comment for an empty comment.
A compressor converting a ZIP archive can determine the encoding of each entry comment from the following sources, where earlier sources are preferred:
-
Extra field 0x6375 (Info-ZIP Unicode Comment) of version 1 in the central header [INFOZIP-EXTRAFLD], whose
ComCRC32matches the CRC-32 of the comment in the header. The field holds the comment in UTF-8. A field with a mismatching CRC-32 is ignored, as for 0x7075 in § 16.1 Entry names from ZIP. -
The comment in the header, if bit 11 (language encoding flag) of the general purpose bit flag is set. The comment is in UTF-8.
-
Extra field 0x554e (Xceed Unicode) in the central header, which holds the comment in UTF-16LE. It is converted to WTF-8, which preserves any lone surrogate.
-
Otherwise, the encoding of the comment is not specified. The compressor can decode it with the encoding guessed for entry names in the same archive. If it can’t, the raw byte sequence is used as a
bstr.
The archive comment has neither the language encoding flag nor an extra field,
so its encoding is never specified by the archive itself.
The compressor can decode it as in the last source above, and puts the result into archive-comment.
16.4. Unix metadata from ZIP
A ZIP archive [APPNOTE] can carry the Unix metadata of an entry in the external file attributes of the central header and in extra fields 0x7875 (Info-ZIP UNIX type 3), 0x7855 (Info-ZIP UNIX type 2), 0x756e (ASi UNIX), 0x5855 (Info-ZIP UNIX type 1) and 0x000d (UNIX) [INFOZIP-EXTRAFLD]. A compressor converting a ZIP archive can choose the source of each key as follows, where earlier sources are preferred and sources in the same item have the same precedence.
| Key | Sources |
|---|---|
unix-mode
| External file attributes; 0x756e Mode
|
unix-uid
| 0x7875 UID; 0x7855 UID (local header only); 0x756e, 0x5855 (local header only) or 0x000d UID
|
unix-gid
| 0x7875 GID; 0x7855 GID (local header only); 0x756e, 0x5855 (local header only) or 0x000d GID
|
Each source is encoded as follows:
-
The upper 16 bits of the external file attributes hold the Unix
st_modeif the upper byte of "version made by" is 3 (UNIX). Only its permission bits (0o7777) go tounix-mode, and its file type bits (S_IFMT) are not stored there. -
0x7875 of version 1 stores the user and group IDs as little-endian unsigned integers of variable sizes. [INFOZIP-EXTRAFLD] specifies that it takes precedence over 0x7855. Other versions are not used as a source.
-
0x7855, 0x756e, 0x5855 and 0x000d store the user and group IDs as 16-bit unsigned integers. The central header version of 0x7855 is empty and only indicates that the local header version exists, and the central header version of 0x5855 doesn’t have the user and group IDs.
If two sources for the same key disagree, a compressor can warn about it, and still uses the preferred source.
16.5. Extra fields not to be preserved
The following extra fields [APPNOTE] [INFOZIP-EXTRAFLD] are not put into zip-extras,
because their information is either absorbed into the archive model and other keys,
or discarded as only meaningful to the ZIP container.
| Header ID | Name | Treatment |
|---|---|---|
| 0x0001 | Zip64 extended information | Discarded: sizes and offsets |
| 0x0007 | AV Info | Discarded: signature |
| 0x000a | NTFS | Absorbed: timestamps (§ 16.2 Timestamps from ZIP) |
| 0x000d | UNIX (PKWARE) | Absorbed: timestamps (§ 16.2 Timestamps from ZIP), unix-uid and unix-gid (§ 16.4 Unix metadata from ZIP), the target of a link node, link-group for a hard link, and the device number of a special node
|
| 0x0014 | PKCS#7 Store for X.509 Certificates | Discarded: signature |
| 0x0015 | X.509 Certificate ID and Signature for individual file | Discarded: signature |
| 0x0016 | X.509 Certificate ID for Central Directory | Discarded: signature |
| 0x0017 | Strong Encryption Header | Discarded: encryption |
| 0x0019 | PKCS#7 Encryption Recipient Certificate List | Discarded: encryption |
| 0x0021 | Policy Decryption Key Record | Discarded: encryption |
| 0x0022 | Smartcrypt Key Provider Record | Discarded: encryption |
| 0x0023 | Smartcrypt Policy Key Data Record | Discarded: encryption |
| 0x10c5 | minizip CMS signature | Discarded: signature |
| 0x1986 | Pixar USD header ID | Discarded: padding for alignment |
| 0x5455 | Extended timestamp | Absorbed: timestamps (§ 16.2 Timestamps from ZIP) |
| 0x554e | Xceed Unicode | Absorbed: entry name (§ 16.1 Entry names from ZIP) and comment (§ 16.3 Comments from ZIP) |
| 0x5855 | Info-ZIP UNIX type 1 | Absorbed: timestamps (§ 16.2 Timestamps from ZIP), unix-uid and unix-gid (§ 16.4 Unix metadata from ZIP)
|
| 0x5a4c | ZipArchive code page | Absorbed: entry name (§ 16.1 Entry names from ZIP) |
| 0x5a4d | ZipArchive offsets array | Discarded: offsets |
| 0x6375 | Info-ZIP Unicode Comment | Absorbed: comment (§ 16.3 Comments from ZIP) |
| 0x6c78 | libarchive "xl" | Absorbed: a copy of the central header fields |
| 0x7075 | Info-ZIP Unicode Path | Absorbed: entry name (§ 16.1 Entry names from ZIP) |
| 0x7855 | Info-ZIP UNIX type 2 | Absorbed: unix-uid and unix-gid (§ 16.4 Unix metadata from ZIP)
|
| 0x7875 | Info-ZIP UNIX type 3 | Absorbed: unix-uid and unix-gid (§ 16.4 Unix metadata from ZIP), for version 1 only
|
| 0x9901 | AE-x (WinZip AES) | Discarded: encryption |
| 0x9903 | WinZip Reference | Discarded: reference to another entry, resolved by conversion |
| 0xa11e | Data Stream Alignment (Apache Commons Compress) | Discarded: padding for alignment |
| 0xa220 | Microsoft Open Packaging Growth Hint | Discarded: padding for in-place editing |
| 0xcafe | Java JAR marker | Discarded: information about the ZIP file itself |
| 0xcdcd | minizip Central Directory | Discarded: encryption and signature of the central directory |
| 0xd935 | Android ZIP Alignment | Discarded: padding for alignment |
| 0xe57a | ALZip code page | Absorbed: entry name (§ 16.1 Entry names from ZIP), if decoded with that code page |
-
0x756e (ASi UNIX) is absorbed like 0x000d: the permission bits of its mode go to
unix-mode, the user and group IDs go tounix-uidandunix-gid, and the symbolic link target goes to a link node. Its device number, however, is a rawst_rdevvalue whose bit layout differs among platforms, and can’t be split into the major and minor numbers of a special node. A device file is therefore converted to an empty file node with the original extra field preserved. -
0x4b46 (FWKCS MD5) and 0x1a51 (minizip hash) hold digests of the uncompressed content. They are only preserved when they match the actual content.