# `AgentDb.Archive`
[🔗](https://github.com/manhvu/agent_db/blob/v0.1.0/lib/agent_db/archive.ex#L1)

A bounded tar archive, optionally gzipped, in both directions.

Two unrelated features accept a tar file a user chose: an Agent Skill bundle
and a data-transfer archive. Both had the same gzip inflater, the same listing
and extraction calls, and the same two bounds written out twice — which is how
two safety-critical copies drift apart while each one's own tests still pass.
This is that code once, and writing is here for the same reason: the exporter and
the format it writes must not each hold their own idea of what an archive is.

The bound is a parameter rather than a module attribute because the two callers
accept different sizes, and the bound is the whole point of the module.

## The two bounds

An archive is listed before it is read, so the whole of it can be refused
before a byte is expanded:

  * the **declared** total, taken from the headers, refuses a bundle that
    announces itself as too large; and
  * the **actual** total, checked while inflating and again while extracting,
    which is what holds when a header lies about what its member holds.

Gzip is undone here rather than by the tar reader, and through the streaming
inflater, because a compressed archive is inflated whole before a tar reader
could bound it. The tar reader is handed plain bytes, and the inflated total
is bounded before it is ever assembled.

# `member`

```elixir
@type member() :: %{
  name: String.t(),
  type: :regular | :directory,
  size: non_neg_integer()
}
```

One member of an archive, as its header describes it.

# `build`

```elixir
@spec build([{String.t(), binary()}]) :: {:ok, binary()} | {:error, term()}
```

Packs `members` into an uncompressed tar binary.

`:erl_tar` writes to a path rather than to memory, so the archive is built in a
scratch file and read back; the file is removed whether or not the build
succeeded. The result is the same bytes `write/3` would put on disk, which is
what makes an in-memory transfer and a file transfer the same format.

`members` is a list of `{name, content}` with `name` a string.

# `declared_size`

```elixir
@spec declared_size([member()]) :: non_neg_integer()
```

The total size the headers declare across the regular members.

# `digest`

```elixir
@spec digest(binary()) :: String.t()
```

The lower-case hex SHA-256 of `binary`.

The digest a transfer manifest records and re-checks. It lives here because
the checksum is over the bytes that were packed, which is the same question the
container answers.

# `extract`

```elixir
@spec extract(binary(), pos_integer()) ::
  {:ok, %{optional(String.t()) =&gt; binary()}} | {:error, term()}
```

The contents of every regular member, as `%{name => binary}`.

Bounded again by what is actually read rather than by what was declared: a
header can claim a size its member does not have. A directory contributes no
content.

# `list`

```elixir
@spec list(binary(), pos_integer(), pos_integer()) ::
  {:ok, [member()]} | {:error, term()}
```

Every member of `binary`.

Refuses a bundle holding more than `max_entries` members or declaring more than
`max_bytes`, and refuses the whole archive for any member that is not a regular
file or a directory — a symbolic link, a hard link, or a device entry is caught
from the listing, before its contents are read.

# `plain_tar`

```elixir
@spec plain_tar(binary(), pos_integer()) :: {:ok, binary()} | {:error, term()}
```

The bytes an archive holds once expanded, bounded by `max_bytes`.

Gzip is detected by its magic bytes rather than by a file name, so a `.tar.gz`
that is not gzip and a tar that happens to be called one are each reported for
what they are.

# `write`

```elixir
@spec write(Path.t(), [{String.t(), binary()}], boolean()) :: :ok | {:error, term()}
```

Writes `members` to `path` as a tar, gzip compressed when asked.

`compressed` is passed rather than inferred from the file name: whether an
archive is compressed is a property of the caller's destination, and the
reading side detects gzip by its content rather than by its extension, so a
name never decides what the bytes are.

---

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