---
name: archive
description: List, extract and create tar archives, gzip- or zstd-compressed or not (and read bzip2- and xz-compressed ones), and zips, and compress one file with gzip or zstd; refuses links, absolute names, .. and overwriting by rule, checks the whole archive before writing any of it, and writes the same bytes for the same files.
---

# archive 0.8.0

List, extract and create tar archives, gzip- or zstd-compressed or not (and read bzip2- and xz-compressed ones), and zips, and compress one file with gzip or zstd; refuses links, absolute names, .. and overwriting by rule, checks the whole archive before writing any of it, and writes the same bytes for the same files.

```
archive [--root DIR] --op list|extract|create [--into DIR] [--as tar|tgz|gz|zip] [--dry-run] [--max-bytes N] [--max-entries N] ARCHIVE [PATH...]
```

## Output

NDJSON: one JSON object per line, schema `archive.v1`. The last line is a record with `"type":"end"`; **a stream without one was cut short and must be treated as truncated**. `complete` is false when the tool stopped early. Errors are records with `"type":"error"`.

An error is `{code, rule, message, hint, repair, detail}`. Match on `rule`, not on `message`. When `repair` is `{"kind":"retry","argv":[...]}` the argv can be run as it stands; it never widens what the tool may touch.

`--format text` is for a person and is lossy; do not parse it.

## Flags

- `--root <path>`: resolve every path relative to this directory (default: the working directory) and refuse paths outside it
- `--op <choice:list/extract/create>`: list shows the entries, extract writes them beneath --into, create writes an archive of the PATHs Default `list`.
- `--into <path>`: extract beneath this directory, which must exist (default: the root)
- `--as <choice:tar/tgz/gz/zip/tzst/zst>` (`-a`): what create writes: a tar, a gzip-compressed tar, one file gzipped, a zip, a zstd-compressed tar, or one file zstd-compressed Default `tgz`.
- `--dry-run`: check everything and report what would be written, exit 9, write nothing
- `--max-bytes <nat>`: the largest archive read, expanded size included, and the largest archive written (ceiling 1073741824) Default `67108864`.
- `--max-entries <nat>`: the most entries read or written (ceiling 1000000) Default `10000`.

An unknown flag is refused (`args.unknown-flag`), never ignored. POSIX spellings are not promised.

## Guarantees

- **deterministic**: the same input gives byte-identical output, through a pipe, a file or a terminal (M2)
- **dry_run**: --dry-run reports what an apply would do and makes no mutating system call (M7, under strace)
- **concurrency**: read-only; it never writes.

## Exit codes

- `0` SUCCESS: what was asked was done; zero matches is success
- `1` GENERAL_ERROR: the operating system failed mid-operation, or the tool caught a bug in itself
- `2` INVALID_ARGS: the invocation is malformed or names something of the wrong kind; nothing was read
- `3` NOT_FOUND: a named input does not exist
- `4` PERMISSION_DENIED: the operating system refused, or the tool's own confinement did
- `5` CONFLICT: state is not what the caller said it would be
- `8` PRECONDITION_FAILED: the answer is no, or a limit was reached
- `9` DRY_RUN: a --dry-run completed; planned_actions lists what an apply would do
- `132`: the tool trapped. A bug in the tool; there is no JSON.
- `141`: the reader closed the pipe.

## Rules

- `args.unknown-flag` (exit 2): a flag that is not in the tool's table
- `args.missing-value` (exit 2): a flag that takes a value was given none
- `args.bad-value` (exit 2): a flag's value is not of the flag's kind
- `args.unexpected-value` (exit 2): a flag that takes no value was given one
- `args.duplicate-flag` (exit 2): the same flag was given twice
- `args.conflict` (exit 2): two flags that exclude each other were both given
- `args.missing-operand` (exit 2): an operand the tool needs was not given
- `args.too-many-operands` (exit 2): more operands than the tool takes
- `args.required-flag` (exit 2): a flag the tool needs was not given
- `path.empty` (exit 2): an empty path
- `path.dotdot` (exit 2): a path with a `..` component
- `path.absolute` (exit 2): an absolute path where one relative to --root is wanted
- `path.outside-root` (exit 4): an absolute path that is not under --root
- `path.too-long` (exit 2): a path longer than 4096 bytes
- `path.symlink` (exit 4): a path that runs through a symbolic link, which the tool does not follow
- `io.not-found` (exit 3): a named input does not exist
- `io.not-a-directory` (exit 3): a path runs through something that is not a directory
- `io.is-a-directory` (exit 2): a directory where a file is wanted
- `io.permission-denied` (exit 4): the operating system refused access
- `io.read-failed` (exit 1): the operating system failed a read
- `io.write-failed` (exit 1): the operating system failed a write, or a write was short
- `io.rename-unsupported` (exit 8): the filesystem cannot refuse an existing name atomically, so move did not rename
- `conflict.exists` (exit 5): --create was given and the path exists
- `conflict.locked` (exit 5): another writer holds the path's lock
- `archive.not-an-archive` (exit 8): neither gzip nor a tar: no gzip magic and no tar header with a valid checksum
- `archive.bad-gzip` (exit 8): a gzip header that cannot be read, a second member, or bytes after the member
- `archive.crc-mismatch` (exit 8): the gzip trailer's CRC-32 or length is not the data's
- `archive.bad-deflate` (exit 8): the DEFLATE stream is invalid
- `archive.bad-checksum` (exit 8): a tar header's checksum is not its bytes'
- `archive.bad-header` (exit 8): a tar header field that cannot be read
- `archive.truncated` (exit 8): the archive ends inside a header or body, or before its end marker
- `archive.unsafe-name` (exit 4): an entry name that is absolute, has `..` or holds a NUL
- `archive.entry-kind` (exit 8): an entry that is not a regular file or a directory
- `archive.duplicate-entry` (exit 8): two entries name the same file
- `archive.directory-missing` (exit 8): the --into directory, or one above it, does not exist
- `archive.special-mode` (exit 8): an entry has a setuid, setgid or sticky bit
- `archive.no-match` (exit 8): a selected path matches no entry of the archive
- `archive.unsupported` (exit 8): the archive uses encryption, zip64, several disks, a zip method other than stored and deflate, a randomised bzip2 block, an xz filter other than LZMA2, or a zstd frame that needs a dictionary
- `archive.bad-bzip2` (exit 8): a bzip2 stream that is not valid, or bytes after it
- `archive.bad-xz` (exit 8): an xz stream that is not valid, or bytes after it
- `archive.bad-zstd` (exit 8): a zstd frame that is not valid, or bytes after it
- `archive.name-too-long` (exit 8): a name that does not fit a tar header
- `limit.archive-too-large` (exit 8): the archive, expanded, would pass --max-bytes
- `limit.too-many-entries` (exit 8): more entries than --max-entries

## Authority

What the compiler says this program can reach (`cancho authority`), embedded at build time:

```json
{"bounded":true,"unbounded_by":[],"effects":["args","dir_read","dir_write","err_write","file_read","file_write","fs_read","heap","io_write"],"labels":[{"name":"args","argument":null,"bounded":true},{"name":"dir_read","argument":null,"bounded":true},{"name":"dir_write","argument":null,"bounded":true},{"name":"err_write","argument":null,"bounded":true},{"name":"file_read","argument":null,"bounded":true},{"name":"file_write","argument":null,"bounded":true},{"name":"fs_read","argument":"","bounded":true},{"name":"heap","argument":null,"bounded":true},{"name":"io_write","argument":null,"bounded":true}],"never_touches":["the network","signals","other programs","foreign code"],"foreign_symbols":[],"pure":["align_base","all_same","alphabet","authority","back_len","bits_at","body","built","bytes","bytes_ceiling","c_declared","c_failure","c_failure_at","c_flag","c_flagged","c_has_pending","c_header_at","c_held","c_hn","c_kind","c_mode","c_most","c_pad","c_pending_size","c_phase","c_pos","c_remaining","c_seen","c_size","catalogue","check_size","checksum_ok","code_meaning","code_name","code_of","collect_cap","compare","compiler","count","count","count","count","count","count","count_byte","covers","crc_update","cstring","data_range","decimal","decode","decode","default_accuracy","default_norm","default_of","depth_ceiling","digit_of","distance","done","emits","ends_with","enotdir","entries","entries","entries_ceiling","entry","entry","entry_at","entry_end","entry_in","equal","err_control","err_depth","err_end","err_escape","err_full","err_number","err_syntax","err_trailing","err_utf8","escape_width","exit_code","exit_in_entry","exit_of_in","expected","extra","fail","failed","failed","failed","failure","failure_at","field","field_of","field_of","file_mode","fill","find","find$int","find_end","find_in","find_slot$int","find_state","finish","flag_table","format_name","full","get$int","get$int","go_on","guarantee_keys","guarantees_has","has","has$int","has_body","has_dotdot","hash","hash3","have","header_name","hex4","hex_digit","hex_digit","hex_value","high_bit","in_choice","in_range","index_of","index_of_short","input","input_size","into_length","io_rule","is_ascii","is_digit","is_digit","is_error","is_flag","is_gzip","is_hex","is_loop","is_match","is_named","is_rep","is_rep0_long","is_rep_g0","is_rep_g1","is_rep_g2","is_tail","is_unmatched","is_utf8","is_valid","is_ws","is_zero","item","kind","kind_directory","kind_directory","kind_file","kind_file","kind_name","kind_of","kind_of","le16","le32","le32","le32","le32","le64","le_n","lead_bits","leaf","len_match","len_rep","literal_base","longest","longest","looks_like","looks_like","looks_like","looks_like","looks_like_tar","low_mask","low_mask64","lshr","lshr64","mask32","mask32","matches","max_accuracy","max_depth","max_symbol","merge","message_of","min_of","mode","mode_executable","mode_file","name","name","name_of","name_room","nearest","needed_directory","not32","not_after","nth","octal","ok","operand_count","operand_index","order_of","output","p1","p2","p3","p4","p5","p_block","p_codes","p_done","p_head","p_stored","p_trailer","parent_of","parse_nat","pax_value","ph_collect","ph_end","ph_failed","ph_header","ph_skip","piece","plain_byte","pos_slot_base","pos_special","probs_size","quantum","rename_unsupported","repairable_in","reversed","rotl","rotr32","rotr64","round_up","rule_of","s_at","s_buf","s_count","s_crc","s_end","s_eof","s_from","s_last","s_limit","s_made","s_name_len","s_phase","s_stored","s_total","scan_failure_at","scan_for","schema","second_range","selector","shell_builtin","shown","shr33","size","size","size$int","slash","slot_count","special_bits","split_point","start","starts_with","starts_zero","status","stop","string_end","summary_in","table_size","take","take","takes_value","tape_len","tool","under","unsafe_at","unsafe_name","update","usable","valid","value_index","varint","w_blcount","w_clcode","w_clfreq","w_cllen","w_dcode","w_dfreq","w_dlen","w_hw","w_lcode","w_lens","w_lfreq","w_llen","w_next","w_rles","w_rlex","w_size","was_seen","width_of","window_size","xround","xxh64"],"folded_operators":108,"folded_calls":16,"functions":584}
```

The row names *kinds* of access, not *extent*: a path from the command line is checked at run time, so it reads `fs_read("")`. Symlinks are followed.
