Ankusa.ClaimCheck.Pack (ankusa v0.7.0)

Copy Markdown View Source

Builds and indexes a claim-check pack: a standard ZIP archive with every entry stored uncompressed, laid out as

  1. index.bin — one 8-byte row per claim, in claim order: the offset of the claim's bytes in the pack and their length, each a big-endian unsigned 32-bit integer
  2. one entry per claim, named by its claim id
  3. manifest.json — each claim's claim id, hook id, offset, length, digest, content type, and receive time

The index sits first so a reader holding only a claim id finds its bytes without knowing the pack's size: read the index entry's header and rows up to the claim's position (index_prefix_bytes/1, locate/2), then read the claim's range. Stored entries are what make that range the claim's raw bytes. The central directory at the end is what any ZIP reader (unzip, Python zipfile, java.util.zip, Erlang :zip) uses to list the pack with no Ankusa code.

No ZIP64: a pack must stay under 65,535 entries and 4 GiB. Callers split well below that (claim_check.pack_max_bytes); build/1 raises if asked to exceed it rather than write an archive no reader could open.

Output is deterministic for the same claims: fixed timestamps (1980-01-01, the DOS epoch) and no extra fields.

Summary

Functions

Build a pack. Returns the archive as iodata (no copy of the bodies) and where each claim's bytes landed, in input order.

How many claims a pack holds, from its first bytes (the index entry's header is enough). :error when the bytes don't start with a pack's index.

The digest of the claim at index, from digests/3's bytes.

The sha256 of every claim of pack pack_id, from its manifest's JSON: the raw 32-byte digests concatenated in claim order (digest/2 takes one out). :error unless the manifest lists exactly count claims, in order, each under the claim id Ankusa.ClaimCheck.Ref.claim_id/2 gives it and with a lowercase hex sha256.

Bytes a pack adds for one claim, excluding the manifest: its index row, local header, and central-directory record. Used to size packs up front.

How many bytes, from the start of a pack, locate/2 needs to find the claim at index: the index entry's header and its rows up to that claim.

Where the claim at index sits, from a pack's first index_prefix_bytes(index) bytes (a shorter read, when the object is smaller, is fine). :error when the bytes don't start with a pack's index or the pack holds fewer claims.

Bytes of the manifest entry's local header: where its JSON starts, from the entry's offset. The entry follows the last claim, so its offset is that claim's offset plus length.

The manifest's JSON size, from the entry's local header (manifest_header_bytes/0 bytes read where the last claim ends). :error when those bytes are not the manifest's header, or claim an absurd size.

Types

claim()

@type claim() :: %{
  :claim_id => String.t(),
  :id => String.t(),
  :body => binary(),
  :sha256 => String.t(),
  optional(:content_type) => String.t() | nil,
  optional(:received_at) => integer() | nil
}

placement()

@type placement() :: %{
  claim_id: String.t(),
  offset: non_neg_integer(),
  length: non_neg_integer()
}

Functions

build(claims)

@spec build([claim()]) :: {iodata(), [placement()]}

Build a pack. Returns the archive as iodata (no copy of the bodies) and where each claim's bytes landed, in input order.

count(prefix)

@spec count(binary()) :: {:ok, pos_integer()} | :error

How many claims a pack holds, from its first bytes (the index entry's header is enough). :error when the bytes don't start with a pack's index.

digest(digests, index)

@spec digest(binary(), non_neg_integer()) :: {:ok, binary()} | :error

The digest of the claim at index, from digests/3's bytes.

digests(json, pack_id, count)

@spec digests(binary(), String.t(), pos_integer()) :: {:ok, binary()} | :error

The sha256 of every claim of pack pack_id, from its manifest's JSON: the raw 32-byte digests concatenated in claim order (digest/2 takes one out). :error unless the manifest lists exactly count claims, in order, each under the claim id Ankusa.ClaimCheck.Ref.claim_id/2 gives it and with a lowercase hex sha256.

entry_overhead()

@spec entry_overhead() :: pos_integer()

Bytes a pack adds for one claim, excluding the manifest: its index row, local header, and central-directory record. Used to size packs up front.

index_prefix_bytes(index)

@spec index_prefix_bytes(non_neg_integer()) :: pos_integer()

How many bytes, from the start of a pack, locate/2 needs to find the claim at index: the index entry's header and its rows up to that claim.

locate(prefix, index)

@spec locate(binary(), non_neg_integer()) ::
  {:ok, non_neg_integer(), non_neg_integer()} | :error

Where the claim at index sits, from a pack's first index_prefix_bytes(index) bytes (a shorter read, when the object is smaller, is fine). :error when the bytes don't start with a pack's index or the pack holds fewer claims.

manifest_header_bytes()

@spec manifest_header_bytes() :: pos_integer()

Bytes of the manifest entry's local header: where its JSON starts, from the entry's offset. The entry follows the last claim, so its offset is that claim's offset plus length.

manifest_size(header)

@spec manifest_size(binary()) :: {:ok, non_neg_integer()} | :error

The manifest's JSON size, from the entry's local header (manifest_header_bytes/0 bytes read where the last claim ends). :error when those bytes are not the manifest's header, or claim an absurd size.