defmodule Debkit do @moduledoc """ A tight codec library for the formats nested inside a Debian `.deb`. A `.deb` is four formats deep: an [**ar**](https://en.wikipedia.org/wiki/Ar_(Unix)) container holding `debian-binary`, `control.tar.*` and `data.tar.*` members; each `.tar` is a **tar** archive; and each tar is compressed with **gzip**, **xz**, or **zstd**. Debkit gives you a small, faithful codec for each layer and nothing else — assembling them into a valid `.deb` (and parsing the control fields) is left to the caller. ┌─ ar ──────────────────────────────────┐ │ debian-binary │ │ control.tar.gz ─► tar ─► gzip/xz/zstd │ │ data.tar.xz ─► tar ─► gzip/xz/zstd │ └────────────────────────────────────────┘ Everything is **in-memory** and **deterministic**: writers zero out mtime / uid / gid and omit the ar symbol table, so the same input bytes always produce the same output bytes (the basis for reproducible packages). The whole thing is a thin [Rustler](https://hexdocs.pm/rustler) NIF over the mature `ar`, `tar`, `flate2`, `xz2`, and `zstd` crates — no `xz`/`zstd`/`ar`/`tar` binaries at runtime, no shelling out. ## Modules * `Debkit` — `compress/2` and `decompress/2` for `:gzip | :xz | :zstd`. * `Debkit.Ar` — read and write `ar` containers. * `Debkit.Tar` — read and write `tar` archives. ## Errors The non-bang functions return `{:ok, result}` or `{:error, t:error/0}`; they never raise across the NIF boundary. The `!` variants raise `Debkit.Error` with the same reason. The documented reasons are: * `:corrupt` — the input is malformed for the codec (bad magic, truncated stream, bad headers, decompression failure). * `:unsupported` — the input is well-formed but uses a feature this library does not implement (e.g. a tar entry that is not a regular file). * `:name_too_long` — (writers only) a member name does not fit the target archive format. ## Example: round-trip a gzip stream iex> {:ok, gz} = Debkit.compress(:gzip, "hello, deb") iex> Debkit.decompress(:gzip, gz) {:ok, "hello, deb"} """ alias Debkit.Native @typedoc "A supported compression format." @type format :: :gzip | :xz | :zstd @typedoc "A failure reason. See the \"Errors\" section above." @type error :: :corrupt | :unsupported | :name_too_long @formats [:gzip, :xz, :zstd] @doc """ Compresses `data` with the given `format`. `format` is one of `:gzip`, `:xz`, or `:zstd`. Output is deterministic: the gzip header carries no mtime or filename, and none of the codecs embed a timestamp, so equal input yields equal output. ## Examples iex> {:ok, xz} = Debkit.compress(:xz, "control file body\\n") iex> Debkit.decompress(:xz, xz) {:ok, "control file body\\n"} """ @spec compress(format(), binary()) :: {:ok, binary()} | {:error, error()} def compress(format, data) when format in @formats and is_binary(data) do Native.compress(format, data) end @doc """ Like `compress/2` but returns the bytes directly, raising `Debkit.Error` on failure. """ @spec compress!(format(), binary()) :: binary() def compress!(format, data), do: unwrap(compress(format, data), :compress) @doc """ Decompresses `data`, which must be a single `format` stream. `format` is given explicitly rather than sniffed from a filename or magic bytes — in a `.deb` the format is known from the member name (`control.tar.xz` → `:xz`). Returns `{:error, :corrupt}` if `data` is not a valid stream for `format`. ## Examples iex> {:ok, z} = Debkit.compress(:zstd, "data") iex> Debkit.decompress(:zstd, z) {:ok, "data"} iex> Debkit.decompress(:gzip, "not actually gzip") {:error, :corrupt} """ @spec decompress(format(), binary()) :: {:ok, binary()} | {:error, error()} def decompress(format, data) when format in @formats and is_binary(data) do Native.decompress(format, data) end @doc """ Like `decompress/2` but returns the bytes directly, raising `Debkit.Error` on failure. """ @spec decompress!(format(), binary()) :: binary() def decompress!(format, data), do: unwrap(decompress(format, data), :decompress) @doc false # Shared by every `!` wrapper in the library. def unwrap({:ok, value}, _operation), do: value def unwrap({:error, reason}, operation), do: raise(Debkit.Error, reason: reason, operation: operation) end