Zchunk: format specification

File extension

["zck", "zhr"]

KS implementation details

License: CC0-1.0
Minimal Kaitai Struct required: 0.10

References

This page hosts a formal specification of Zchunk using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Block diagram

Format specification in Kaitai Struct YAML

meta:
  id: zchunk
  title: Zchunk
  file-extension:
    - zck # magic '\0ZCK1' (`lead.is_detached_header` is `false`)
    - zhr # magic '\0ZHR1' (`lead.is_detached_header` is `true`)
  xref:
    justsolve: Zchunk
  license: CC0-1.0
  ks-version: '0.10'
  endian: le
doc-ref: https://github.com/zchunk/zchunk/blob/99e51afa38c723e7c25834c2c3b305d20ef55d04/zchunk_format.txt
seq:
  - id: lead
    type: header_lead
  - id: header_rest
    size: lead.len_header_rest.value
    type: header_without_lead
  - id: dict
    size: header_rest.index.len_dict.value
    doc: |
      Custom dictionary used when compressing each chunk. It's compressed itself
      without a dictionary.

      The official zchunk specification calls this section "Compressed Dict".
      It's also called a "dictionary chunk". `zck_read_header -c` presents it as
      "chunk 0" (which is always shown in the chunk table, but can have size 0
      if the dictionary is not in use).
  - id: chunks
    size: header_rest.index.chunks_metadata[_index].len_chunk.value
    repeat: expr
    repeat-expr: header_rest.index.chunks_metadata.size
    if: not lead.is_detached_header
    doc: |
      Chunks of data, each compressed with the custom dictionary `dict` (if
      applicable).

      They are not included in a detached header (`.zhr`) file. Detached headers
      contain the dictionary, but none of the data chunks.
types:
  header_lead:
    seq:
      - id: magic
        size: 5
        valid:
          any-of:
            - '[0x00, 0x5a, 0x43, 0x4b, 0x31]' # '\0ZCK1'
            - '[0x00, 0x5a, 0x48, 0x52, 0x31]' # '\0ZHR1'
        doc: |
          There are two valid magic numbers for zchunk files:

          * `'\0ZCK1'` identifies a zchunk version 1 file (`.zck`)
          * `'\0ZHR1'` identifies a zchunk version 1 detached header file (`.zhr`)
      - id: overall_checksum_type
        type: checksum_type
        doc: |
          Type of the checksum used for `header_checksum` and
          `_root.header_rest.preface.data_checksum`.
      - id: len_header_rest
        type: compressed_integer
        doc: Size of the header, not including the lead
      - id: header_checksum
        size: overall_checksum_type.len_checksum
        doc: |
          Checksum of the entire header, which consists of `_root.lead` and
          `_root.header_rest` (i.e. everything from the beginning of the file to
          the end of `_root.header_rest`), not including the `header_checksum`
          field itself (i.e. the input for the checksum algorithm is a
          concatenation of the bytes preceding the `header_checksum` field with
          the bytes following it).

          For detached headers, the checksum is calculated as if the `magic`
          field were set to `'\0ZCK1'`, so that it matches the checksum in the
          full zchunk file.
    instances:
      is_detached_header:
        value: magic[2] == 0x48
        doc: |
          Determines whether this file is a zchunk detached header (`.zhr`). If
          not, it is a complete zchunk file (`.zck`).
  header_without_lead:
    seq:
      - id: preface
        type: preface
      - id: len_index
        type: compressed_integer
      - id: index
        size: len_index.value
        type: index
      - id: num_signatures
        type: compressed_integer
        valid:
          expr: _.value == 0
        doc: |
          Must be 0. The reference implementation also rejects any file with a
          non-zero "Signature count", throwing a fatal error stating "Signatures
          aren't supported yet" - see
          [`src/lib/header.c:259-264`](https://github.com/zchunk/zchunk/blob/99e51afa38c723e7c25834c2c3b305d20ef55d04/src/lib/header.c#L259-L264).

          Although the structure of signatures is defined [in the official
          textual
          specification](https://github.com/zchunk/zchunk/blob/99e51afa38c723e7c25834c2c3b305d20ef55d04/zchunk_format.txt#L219-L252),
          no signature types are defined, and as of this writing no publicly
          known implementation generates or interprets these signatures.
          Therefore, we've decided not to implement them here either.

          For more details, see
          <https://github.com/kaitai-io/kaitai_struct_formats/pull/539#discussion_r3713109887>.
  preface:
    seq:
      - id: data_checksum
        size: _root.lead.overall_checksum_type.len_checksum
        doc: |
          Total data checksum. Checksum of everything after the header,
          including the compressed dictionary (`_root.dict`) and all compressed
          chunks (`_root.chunks`). The type of this checksum is
          `_root.lead.overall_checksum_type.value`.

          If `has_uncompressed_source` is true, this checksum must not be
          checked and should not be generated. In that case, the reference
          implementation writes it as all zeros - see the sample file
          [`mini-uncomp-cksums.zck`](https://github.com/kaitai-io/kaitai_struct_samples/blob/1d2fe11c971fb7e86f343b77a1ed341a0217e86a/archive/zchunk/README.md#mini-uncomp-cksumszck).
      - id: flags
        type: compressed_integer
        valid:
          expr: _.value <= 0b111
        doc: |
          Compressed integer containing a bitmask of the flags. All unused flags
          MUST be set to 0. If a decoder sees a flag set that it doesn't
          recognize, it MUST exit with an error.
        doc-ref: https://github.com/zchunk/zchunk/blob/99e51afa38c723e7c25834c2c3b305d20ef55d04/zchunk_format.txt#L78-L81
      - id: compression_type_int
        type: compressed_integer
        -affected-by: 88 # inlining
        valid:
          # This ensures that the compression type is known. If
          # [inlining](https://github.com/kaitai-io/kaitai_struct/issues/88) was
          # implemented, we could use `enum: compression_types` directly in this
          # field and use `valid/in-enum: true` to achieve the same effect, but
          # it's not implemented.
          expr: |
            _.value == compression_types::none.to_i or
            _.value == compression_types::zstd.to_i
        doc: |
          Raw integer, don't read this field - access `compression_type`
          instead.
      - id: num_optional_elements
        type: compressed_integer
        valid:
          expr: _.value >= 1
        if: has_optional_elements
        doc: |
          If present, it must be at least 1. This is because if there are no
          optional elements, `has_optional_elements` must be false, and then
          neither this field nor `optional_elements` is present.
        doc-ref: https://github.com/zchunk/zchunk/blob/99e51afa38c723e7c25834c2c3b305d20ef55d04/zchunk_format.txt#L99-L102
      - id: optional_elements
        type: optional_element
        repeat: expr
        repeat-expr: num_optional_elements.value
        if: has_optional_elements
    instances:
      has_data_streams:
        value: flags.value & 0b1 != 0
      has_optional_elements:
        value: flags.value & 0b10 != 0
      has_uncompressed_source:
        value: flags.value & 0b100 != 0
        doc: |
          The file may be applied against an uncompressed source. This adds an
          uncompressed checksum to every index entry, including the dictionary.
      compression_type:
        value: compression_type_int.value
        enum: compression_types
  optional_element:
    -webide-representation: 'ID {element_id.value:dec}'
    seq:
      - id: element_id
        type: compressed_integer
      - id: len_data
        type: compressed_integer
      - id: data
        size: len_data.value
  index:
    seq:
      - id: chunk_checksum_type
        type: checksum_type
        doc: |
          Type of the checksum used for `dict_checksum` and for all
          `chunks_metadata[...].chunk_checksum` and
          `chunks_metadata[...].uncompressed_chunk_checksum`.
      - id: num_chunks
        type: compressed_integer
        valid:
          expr: _.value >= 1
        doc: |
          Number of chunks, **including** the dictionary chunk.

          Must be at least 1, because the dictionary chunk is always present,
          even if it is empty. The reference implementation also fails when the
          number of chunks is 0, see
          [`src/lib/index/index_read.c:181-184`](https://github.com/zchunk/zchunk/blob/99e51afa38c723e7c25834c2c3b305d20ef55d04/src/lib/index/index_read.c#L181-L184).
      - id: dict_stream
        type: compressed_integer
        valid:
          expr: _.value == 0
        if: _parent.preface.has_data_streams
        doc: If present, it must always be 0.
        doc-ref: https://github.com/zchunk/zchunk/blob/99e51afa38c723e7c25834c2c3b305d20ef55d04/zchunk_format.txt#L159-L162
      - id: dict_checksum
        size: chunk_checksum_type.len_checksum
      - id: uncompressed_dict_checksum
        size: chunk_checksum_type.len_checksum
        if: _parent.preface.has_uncompressed_source
        doc: |
          Checksum of the uncompressed dictionary. It has no real use, as the
          uncompressed source won't have a dictionary.
      - id: len_dict
        type: compressed_integer
      - id: len_uncompressed_dict
        type: compressed_integer
      - id: chunks_metadata
        type: |
          chunk(
            chunk_checksum_type.len_checksum,
            _parent.preface.has_data_streams,
            _parent.preface.has_uncompressed_source
          )
        repeat: expr
        repeat-expr: num_data_chunks
        doc: |
          Metadata of the data chunks. The dictionary is chunk 0 and its
          metadata is stored in the `*dict*` fields above, so there is one fewer
          entry here than indicated by `num_chunks`.
    instances:
      num_data_chunks:
        value: num_chunks.value - 1
        doc: |
          Number of data chunks. `num_chunks` counts the dictionary as chunk 0,
          so it is one greater than this number.
  chunk:
    params:
      - id: len_checksum
        type: u4
      - id: has_data_streams
        type: bool
      - id: has_uncompressed_source
        type: bool
    seq:
      - id: chunk_stream
        type: compressed_integer
        if: has_data_streams
      - id: chunk_checksum
        size: len_checksum
      - id: uncompressed_chunk_checksum
        size: len_checksum
        if: has_uncompressed_source
        doc: |
          Checksum of the uncompressed chunk. Used to detect whether a chunk
          from an uncompressed source is identical to the compressed chunk.
      - id: len_chunk
        type: compressed_integer
      - id: len_uncompressed_chunk
        type: compressed_integer
  # Common types
  checksum_type:
    -webide-representation: '{value}'
    seq:
      - id: raw
        type: compressed_integer
        -affected-by: 88 # inlining
        valid:
          # This ensures that the checksum type is known. If
          # [inlining](https://github.com/kaitai-io/kaitai_struct/issues/88) was
          # implemented, we could use `enum: checksum_types` directly in this
          # field and use `valid/in-enum: true` to achieve the same effect, but
          # it's not implemented.
          expr: len_checksum != 0
        doc: |
          Raw integer, don't read this field - access `value` instead.
    instances:
      value:
        value: raw.value
        enum: checksum_types
      len_checksum:
        value: |
          value == checksum_types::sha1 ? 20 :
          value == checksum_types::sha256 ? 32 :
          value == checksum_types::sha512 ? 64 :
          value == checksum_types::sha512_128 ? 16 :
          0
  compressed_integer:
    doc: |
      Like `/common/vlq_base128_le` (LEB128), but the logic of the
      "continuation" flag in the most significant bit is inverted, so instead of
      `has_next`, it is called `is_last` (if the highest bit is set to zero, it
      means "continue", whereas in standard LEB128, the highest bit set to
      **one** means "continue"). Therefore, we cannot simply import
      `/common/vlq_base128_le` and use it, because it is incompatible.
    -webide-representation: '{value:hex} = {value:dec}'
    seq:
      - id: groups
        type: group(_index)
        repeat: until
        repeat-until: _.is_last
    types:
      group:
        meta:
          bit-endian: be
        doc: |
          One byte group, clearly divided into 7-bit "value" chunk and 1-bit "continuation" flag.
        -webide-representation: '{value}'
        params:
          - id: idx
            type: s4
        seq:
          - id: is_last
            type: b1
            valid: 'idx == 9 ? true : is_last'
            doc: |
              If `true`, then this is the last byte of the compressed integer.

              Since this implementation only supports serialized values up to 10
              bytes, this must be `true` in the 10th group (`groups[9]`).
          - id: value
            type: b7
            valid:
              # See the comment in `/common/vlq_base128_le.ksy` for why the
              # `.as<u8>` is needed (it's a workaround for a bug in KSC 0.11).
              max: '(idx == 9 ? 1 : 0b111_1111).as<u8>'
            doc: |
              The 7-bit (base128) numeric value chunk of this group

              Since this implementation only supports integer values up to 64 bits,
              the `value` in the 10th group (`groups[9]`) can only be `0` or `1`
              (otherwise the width of the represented value would be 65 bits or
              more, which is not supported).
    instances:
      len:
        value: groups.size
      value:
        value: |
          (groups[0].value
          | (len >= 2 ? (groups[1].value << 7) : 0)
          | (len >= 3 ? (groups[2].value << 14) : 0)
          | (len >= 4 ? (groups[3].value << 21) : 0)
          | (len >= 5 ? (groups[4].value << 28) : 0)
          | (len >= 6 ? (groups[5].value << 35) : 0)
          | (len >= 7 ? (groups[6].value << 42) : 0)
          | (len >= 8 ? (groups[7].value << 49) : 0)
          | (len >= 9 ? (groups[8].value << 56) : 0)
          | (len >= 10 ? (groups[9].value << 63) : 0)).as<u8>
        doc: Resulting unsigned value as normal integer
enums:
  checksum_types:
    0: sha1
    1: sha256
    2: sha512
    3: sha512_128 # first 128 bits of sha512 checksum
  compression_types:
    0: none
    2: zstd