# `Ankusa.ClaimCheck.Pack`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/claim_check/pack.ex#L1)

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.

# `claim`

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

# `placement`

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

# `build`

```elixir
@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`

```elixir
@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`

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

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

# `digests`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
