@specfork/archiver
Desktop file archiver for Windows, macOS and Linux. Create, extract, list, test and update 7z, zip, tar, gzip, bzip2 and xz archives, with AES-256 and safe extraction.
Archiver
A desktop program that packs files and folders into archives and unpacks them again. It creates and updates 7z, zip, tar, gzip, bzip2 and xz archives, reads many more formats through a codec contract, encrypts with AES-256 (optionally hiding file names), splits archives into volumes, tests integrity, and plugs into the system file manager. Extraction is safe by default: nothing is written outside the chosen folder. Behaviour derived from 7-Zip's open-source code (LGPL-2.1-or-later), written fresh and stack-free. Not affiliated with or endorsed by Igor Pavlov. The name is used only to credit the source.
| Area | Prefix | Covers |
|---|---|---|
| formats.md | FMT | Format registry, capabilities, methods, levels, codec contract, detection, metadata, volumes |
| create.md | CRT | Creating and updating archives: inputs, naming, paths, solid blocks, update modes, links, delete-after |
| extract.md | XTR | Extraction modes, destination, overwrite prompts, volumes, free space, metadata restore |
| safety.md | SAFE | Path traversal, absolute paths, links, special files, reserved names, permissions |
| names.md | NAM | Unicode names, legacy code pages, separators, collisions, length limits |
| encryption.md | ENC | Passwords, AES-256, header encryption, legacy zip encryption, wrong-password handling |
| browse.md | BRW | Archive browser: listing, navigation, open, add, delete, rename, properties |
| integrity.md | INT | Integrity test, error kinds, checksums of files |
| jobs.md | JOB | Progress, pause, cancel, outcomes, messages, temporary files, performance |
| integration.md | SHL | File-manager menu actions, drag and drop, file associations, command line, settings |
Single user, local only: no account, no network, no telemetry. Every rule runs on one device.
Sizes use binary units (1 KiB = 1024 B); times are local; enum values are snake_case.
Keyboard shortcuts are defaults; a build may follow its platform's conventions instead.
Archive browser
Status: Draft · Prefix: BRW
Purpose
Looking inside an archive like a folder, and changing it where the format allows.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| BRW-1 | Opening an archive shows its top level as a folder; folders implied by item paths but not stored as entries are shown too. | zip with only a/b.txt → folder a listed |
| BRW-2 | Columns: name, size, packed_size, modified, plus optional created, accessed, attributes, encrypted, crc32, method, block, host_os; a column the format lacks is blank. Folder size = sum of its contents. |
folder with 2 × 1 KiB files → 2 KiB |
| BRW-3 | Any column sorts ascending/descending on click and the choice persists. Names sort case-insensitively with digit runs compared as numbers, folders first. | f2, f10, F1 → F1, f2, f10 |
| BRW-4 | Navigation: open a folder (Enter or double-click), go up (Backspace), a path bar archive › folder › … with clickable parts, and back/forward history within the archive. |
Enter on a/, Backspace → root |
| BRW-5 | A filter box narrows the current folder by case-insensitive name substring as the user types. | type rep → only names containing rep |
| BRW-6 | Opening a file item extracts it to a private temporary folder and opens it with the OS default app (after SAFE-12 where it applies); a nested archive opens inside the browser instead. | open notes.txt → default editor shows it |
| BRW-7 | When an item opened per BRW-6 changes on disk while the archive is open, the user is asked whether to write it back (formats with update); yes runs an update job. |
edit and save → prompt; yes → item updated |
| BRW-8 | Dropping files onto the browser, or the add command, runs an update job (add_replace, CRT-8) into the current folder. Dragging items out to a file-manager folder extracts them there per XTR. |
drop x.txt in docs/ → docs/x.txt; drag a.txt to desktop → file there |
| BRW-9 | Delete (Delete key) asks for confirmation with the item count, says the change is permanent, then rewrites the archive per CRT-10. | delete 2, confirm → 2 fewer items |
| BRW-10 | Rename (F2) and new folder change the archive in place; a name already in that folder is refused. For formats without update, and split archives, add, delete, rename and new folder are disabled and the window says read-only. |
rename to existing name → refused; open .iso → delete disabled |
| BRW-11 | Properties: format, physical size, unpacked total, file and folder counts, methods, solid yes/no and block count, encrypt_names, volume count, offset when embedded in a larger file, and any open warnings. |
solid 7z → 1 block shown |
| BRW-12 | An archive of 100,000 items shows its first screen within 3 s and scrolls without visible stalls. | manual: 100,000-item zip on a mid-range laptop |
Out of scope
- A general two-pane file manager for the disk; editing archive comments.
Decisions
- The browser is the only editing surface; the file-manager menu (SHL) only creates, extracts, tests and hashes.
- Write-back of opened items is always asked, never automatic.
Create and update
Status: Draft · Prefix: CRT
Purpose
Packing files and folders into a new archive, or changing an existing one.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| CRT-1 | A create job takes inputs (one or more files or folders) and options {archive_path, format, level, method, dictionary_size, word_size, solid_block, threads, memory_limit, update_mode, path_mode, volume_sizes[], password, encrypt_names, excludes[], links, delete_after, test_after}; unset options take this spec's defaults. |
only inputs given → 7z, level 5, lzma2 |
| CRT-2 | Default archive_path: the first input's folder + a base name (one file: its name without the last extension; one folder: its name; several items: their parent folder's name) + the format extension. A typed name without the extension gets it; changing format swaps it. |
report.txt → report.7z; a.txt+b.txt in docs/ → docs/docs.7z; switch to zip → docs.zip |
| CRT-3 | Folders are added recursively with every file, hidden ones included, and with empty folders as folder entries. | folder with empty sub/ → archive lists sub/ |
| CRT-4 | path_mode: relative (default; paths start at each input's own name), full (from the volume root, without drive letter or leading separator), absolute (with root; 7z and tar only). |
/home/u/docs/a.txt: relative → a.txt; full → home/u/docs/a.txt |
| CRT-5 | excludes: wildcards (* any run, ? one character) matched against each name, or against the relative path when the pattern has a separator; a matching folder is skipped with its contents. The archive itself, its volumes and its temporary file are never added. |
exclude *.tmp → no .tmp; pack x into x/x.7z → no x.7z inside |
| CRT-6 | solid_block (7z, xz): off, 1 MiB–64 GiB, or full; default from level (≥ 64 MiB at level 5). Files in a block are ordered by extension, then name. The dialog's help says that reading or replacing one file touches its whole block. |
1,000 small text files → solid archive smaller than off |
| CRT-7 | If archive_path exists in the same format, the job updates it per update_mode; another format fails with format_mismatch; a split archive fails with volumes_read_only. Nothing changes on failure. |
add to a.7z as zip → format_mismatch; add to a.7z.001 → volumes_read_only |
| CRT-8 | update_mode: add_replace (default: add all, replace same paths), update_add (add new, replace only where the disk copy is newer), freshen (replace existing paths where newer, add nothing), sync (update_add plus remove archive items absent from the inputs). "Newer" compares modified times at the precision both sides keep; equal is not newer. |
freshen with a new file → not added; same file update_add → not rewritten |
| CRT-9 | sync that would remove ≥ 1 item shows the count and the first 20 paths and needs explicit confirmation; declining cancels the whole job. |
sync removing 3 → confirm lists 3; No → archive unchanged |
| CRT-10 | Updating writes a new temporary archive beside the old one and replaces it only on success; on error or cancel the original stays byte-identical. | cancel at 50% → original hash unchanged |
| CRT-11 | volume_sizes: sizes as number + unit B, K, M, G (binary); each volume takes the next size and the last repeats; minimum 64 KiB. Presets: 10M, 100M, 650M, 700M, 4092M, 4480M. |
10M on 25 MiB → volumes 10, 10, 5 MiB |
| CRT-12 | Unreadable or locked inputs are skipped, listed in the job messages, and the job ends completed_with_warnings (JOB-5). |
1 locked file of 5 → archive has 4, warning names it |
| CRT-13 | links: store_link (default where the format has links; the link itself is stored) or follow (store target content, loops stored once with a warning); formats without links always follow. tar stores the second and later paths of one hard-linked file as hard-link entries. |
link to a.txt in tar → link entry; 2 hard links of 1 MiB → tar ≈ 1 MiB |
| CRT-14 | delete_after (default off) removes inputs only after the archive is complete and, when test_after (default off) is on, tested ok (INT-1); skipped inputs are never removed. A failed test ends the job failed and keeps the archive. |
1 input skipped → that input still on disk |
| CRT-15 | Turning delete_after on needs a confirmation naming the item count and that removal is permanent where no trash exists; on the command line it needs an explicit switch. Inputs go to the system trash when one exists. |
enable → confirm shows "5 items" before start |
| CRT-16 | threads: 1 to logical CPU count, default all; memory_limit default 80% of RAM. While the packing estimate exceeds the limit, start is blocked showing both numbers. |
dictionary 1.5 GiB on 4 GiB RAM → start blocked |
Out of scope
- Archive comments, rename-on-add rules, scheduled backups.
Decisions
- A single file's base name drops its last extension:
photo.jpg→photo.7z. delete_afterprefers the trash; the source deletes permanently.- Minimum volume size 64 KiB: smaller slices mostly create thousands of files by mistake.
Encryption
Status: Draft · Prefix: ENC
Purpose
Password-protecting archive contents and, in 7z, the list of names.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| ENC-1 | 7z encryption is AES-256 in CBC mode; the key derives from the UTF-16 password and a random salt by iterated SHA-256 (2^19 rounds); every archive gets a fresh random IV. |
same files and password twice → different bytes, both open |
| ENC-2 | zip encryption: aes256 (default; the common AES zip extension) or legacy (traditional zip encryption), which shows a weakness warning before start. |
choose legacy → warning |
| ENC-3 | Password: 1–1024 characters, any Unicode; empty means none. Masked by default with a show_password toggle; while masked, a second entry must match before start. Formats without encrypt (FMT-1) disable the fields. |
masked entries differ → start blocked |
| ENC-4 | encrypt_names (7z only; default off) also encrypts the header: without the password nobody sees names, sizes or count, and opening asks for the password before listing. With zip, the dialog notes that names and sizes stay visible. |
header-encrypted 7z → prompt before listing; zip + password → note shown |
| ENC-5 | A wrong password gives wrong_password (when the format can tell) or data_error with a hint that the password may be wrong; nothing partial stays (XTR-8); the prompt returns, up to 3 attempts per job. |
wrong password → error, prompt again, nothing written |
| ENC-6 | Updating an encrypted 7z: new items use this job's password; kept items stay as they are; a header-encrypted archive needs its password before any update. |
add to header-encrypted archive → password asked first |
| ENC-7 | Encrypted items carry a marker in the browser. Zip archives may mix plain and encrypted items; the password is asked once per job, only when an encrypted item is read, and again only for an item it fails on. | mixed zip → plain item extracts with no prompt; 100 encrypted items → one prompt |
| ENC-8 | Passwords live only in memory for one job; they never reach settings, history, logs or temporary files, and are wiped when the job ends. | search settings and logs after job → absent |
Out of scope
- Public-key encryption, key files, hardware tokens, saved passwords, password recovery.
Decisions
- No saved passwords (ENC-8): an archive password stored on the same disk adds little and leaks easily.
aes256is the zip default;legacystays for old receivers only.
Extract
Status: Draft · Prefix: XTR
Purpose
Unpacking all or part of an archive to a folder, and what happens when files already exist.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| XTR-1 | An extract job takes an archive (or any volume of a split one), a selection (all, or browser items, kept relative to the folder shown) and options {destination, path_mode, overwrite, password, flatten_single_root, keep_broken, restore_times, origin_mark, extract_links, allow_unsafe_links}. |
in src/, extract lib/a.c → <dest>/lib/a.c |
| XTR-2 | Default destination: the archive's folder + its base name with every archive extension stripped (a.tar.gz → a, a.7z.001 → a). Missing folders, parents included, are created. |
photos.zip in Downloads/ → Downloads/photos/ |
| XTR-3 | flatten_single_root (default on): when the top level holds exactly one folder named like the destination's last component, its contents go straight into the destination. |
proj.zip with only proj/… → proj/file, not proj/proj/file |
| XTR-4 | path_mode: full (default; keep stored folders), none (all files into the destination), absolute (keep a stored root; needs a confirmation per job and stays subject to SAFE-3). |
none on a/b/c.txt → <dest>/c.txt |
| XTR-5 | overwrite: ask (default), overwrite, skip, rename_new, rename_existing. |
skip with existing file → untouched, counted skipped |
| XTR-6 | The ask prompt shows both files' name, size and modified time, and offers yes, yes_to_all, no, no_to_all, rename, cancel; *_to_all lasts for this job only. |
no_to_all → no more prompts, rest skipped |
| XTR-7 | Renaming picks the first free name (1).ext, name (2).ext, …; rename_existing renames the file on disk and writes the new one under the original name. |
existing a.txt, a (1).txt → new file a (2).txt |
| XTR-8 | Items are written under a temporary name and renamed on success. On error or cancel the partial file is removed unless keep_broken (default off); an item failing its checksum is reported crc_failed and kept only with keep_broken. |
cancel mid-file → no partial file; flipped byte → error, file absent |
| XTR-9 | Split archives: opening any volume finds siblings by name; a missing volume pauses the job with a prompt to locate it or cancel, naming the expected file. | delete .002 → prompt names a.7z.002 |
| XTR-10 | When the known unpacked total exceeds free space on the destination volume, a warning shows both numbers before writing; the user continues or cancels. | 2 GiB archive, 1 GiB free → warning |
| XTR-11 | restore_times (default on) sets modified time, plus created/accessed when stored and supported; folder times are set after their contents. POSIX modes (minus SAFE-8 bits) apply on POSIX systems, Windows attributes on Windows. |
extracted mtime = stored mtime |
| XTR-12 | origin_mark (all, documents_only, off; default all): when the archive carries the OS's downloaded-from-the-internet mark, extracted files get it too; documents_only limits it to office documents and scripts. |
marked archive → extracted .docx marked |
| XTR-13 | Several archives selected together extract each into its own default destination, one after another, as one job with a combined summary. | 3 zips → 3 folders, one summary |
Out of scope
- Restoring owners, ACLs or extended attributes other than the origin mark.
- Extracting to remote or cloud locations.
Decisions
- Rename uses
name (n).ext, the convention most file managers use; the source uses another suffix. - Write-then-rename (XTR-8) means a crash never leaves a half file under the real name.
flatten_single_rootis a default, not a menu entry, so extracting to a folder never doubles it.
Formats
Status: Draft · Prefix: FMT
Purpose
Which archive formats exist, what each can do, and the codec contract that adds more.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| FMT-1 | Every format is a registry entry {id, extensions[], signatures[], caps}; caps is a set of create, extract, update, multi_file, solid, encrypt, encrypt_names, links. Every command and dialog offers only what caps allows. |
format tar chosen → password field disabled |
| FMT-2 | Read-write formats and caps: 7z {create, extract, update, multi_file, solid, encrypt, encrypt_names, links}; zip {create, extract, update, multi_file, encrypt, links}; tar {create, extract, update, multi_file, links}; gzip, bzip2, xz {create, extract}. Output opens in other common tools for the same format. |
manual: sample 7z, zip, tar.gz open in two other archivers |
| FMT-3 | gzip, bzip2 and xz hold one stream: packing one file makes <file>.gz/.bz2/.xz; packing several items or a folder offers only tar.gz, tar.bz2, tar.xz, built in one pass. Opening .tar.gz, .tgz, .tar.bz2, .tbz2, .tar.xz, .txz goes straight to the tar listing. |
2 files + gzip → only .tar.gz offered; open a.tgz → tar entries |
| FMT-4 | Read-only formats (caps {extract, multi_file}): rar, cab, iso, udf, wim, cpio, rpm, ar/deb, dmg, xar, lzh, arj, chm, msi, nsis, squashfs, cramfs, vhd, vhdx, vmdk, vdi, qcow2, fat, ntfs, ext, hfs, apfs; single-stream read-only: lzma, z, zstd. Each is optional; the about view lists those built in. |
open .iso → list and extract work, add disabled |
| FMT-5 | Codec contract: a read-only format is a module exposing probe(first_bytes) → confidence 0..100, open(stream, volume_provider, password_provider) → archive, archive.items() → [{path, is_dir, size, packed_size?, mtime?, attributes?, crc32?, encrypted, link_target?}], archive.read(item) → byte stream, archive.properties() → map. A codec reports errors only as INT-3 kinds and never writes to disk, so every SAFE rule applies to it. |
stub codec with items a, ../x → a extracted, ../x blocked |
| FMT-6 | Detection: the signature (at the start, or at the end for formats that keep it there) wins over the extension; on disagreement the archive opens as the detected format with a warning naming it; "open as" forces a format. No match → error unknown_format, never an empty archive. |
photo.zip that is 7z → opens as 7z + warning; random bytes → unknown_format |
| FMT-7 | Methods: 7z: lzma2 (default), lzma, ppmd, bzip2, deflate, deflate64, copy; zip: deflate (default), deflate64, bzip2, lzma, ppmd, store; gzip: deflate; bzip2: bzip2; xz: lzma2; tar: no compression, layout pax (default) or gnu. |
7z defaults → method lzma2 |
| FMT-8 | Levels: 0 store, 1 fastest, 3 fast, 5 normal (default), 7 maximum, 9 ultra. Allowed: 7z 0–9; zip 0, 1, 3, 5, 7, 9; gzip 1, 5, 7, 9; bzip2 1, 3, 5, 7, 9; xz 1–9; tar 0. Another level is rejected before work starts. Level 0 stores data uncompressed. |
gzip level 3 → rejected, nothing written; 10 MiB random at level 0 → archive ≥ 10 MiB |
| FMT-9 | Advanced parameters dictionary_size, word_size, solid_block (CRT-6), threads default from the level and may be overridden; the dialog shows estimated memory for packing and for unpacking. |
dictionary 16 → 64 MiB → both estimates rise |
| FMT-10 | zip switches to 64-bit fields when any item or the archive exceeds 4 GiB − 1 B or holds more than 65,535 items; 7z and tar (pax) hold items up to 2^63 B. |
5 GiB file zipped → extracts with equal hash |
| FMT-11 | Times: modified time is stored in every format; 7z adds created and accessed time when store_times includes them (default: modified only); zip stores its 2-second local time plus an exact UTC time. |
zip file modified 10:00:01 → extracted 10:00:01 |
| FMT-12 | Attributes: POSIX mode is stored in tar, zip, 7z; Windows read_only, hidden, system, archive in zip, 7z; each is restored only where the OS has it (XTR-11). |
mode 0755 packed on Linux → 0755 on macOS |
| FMT-13 | Volumes: any created archive may be split into raw slices <archive>.001, .002, … (3 digits; 4 past 999); concatenating them gives the unsplit archive. |
25 MiB archive, 10 MiB volumes → 3 files; concatenation hash = unsplit hash |
Out of scope
- Creating any read-only format, including
rarandwim. - Self-extracting executables; format-native zip spanning (
.z01). - Executable pre-filters and other per-method tuning as user options; benchmarks.
Decisions
- RAR is extract-only and only through the codec contract (FMT-5). The source's RAR decoder ships under a separate unRAR licence that forbids using it to re-create the RAR compressor; this spec was written without reading that code. Implementers who bundle a RAR codec must honour that licence or use a decoder under a compatible one, and may simply omit
rar. - Source licence: most of 7-Zip is LGPL-2.1-or-later; two decoders are BSD-3-Clause and one hash is BSD-2-Clause.
derived_from.licenserecords the main licence. tardefaults topax(the source defaults tognu):paxstores UTF-8 names, long paths and exact times portably.- The read-only list is a menu, not a mandate: an implementation is complete with the six read-write formats plus any codecs it picks.
Desktop integration
Status: Draft · Prefix: SHL
Purpose
Reaching the archiver from the system file manager and the command line, and its settings.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| SHL-1 | File-manager menu actions: open_archive, extract_with_options, extract_here, extract_to_folder, test_archive (on archives); add_with_options, add_to_7z, add_to_zip (on any items); checksum (on files and folders). Each OS's supported extension mechanism is used. |
right-click a zip → extract actions present |
| SHL-2 | extract_here writes into the archive's folder: a single top item goes there directly, several go into a folder named per XTR-2. extract_to_folder always uses XTR-2 and shows the folder name in the entry. |
zip with 3 top files → name/ created; menu shows photos/ |
| SHL-3 | add_to_7z and add_to_zip create the archive at the default path (CRT-2) with default options and no dialog; an existing file of that name is updated with add_replace. |
2 files selected → <folder>.7z created |
| SHL-4 | Archive actions appear only on files whose extension or signature matches an extract format; with several archives selected they apply to each (XTR-13). |
right-click .txt → no extract actions |
| SHL-5 | menu_actions (which actions show; default all) and menu_cascaded (in one submenu; default on) apply at once. |
turn off checksum → gone from menu |
| SHL-6 | File associations are opt-in per extension; none are claimed without the user's choice. Dropping files on the app opens the add dialog; dropping an archive opens the browser. | fresh install → .zip still opens in previous app |
| SHL-7 | Command line: a <archive> <inputs…> create/update, x <archive> [-o <dir>] extract with paths, e extract flat, t test, l list, d <archive> <paths…> delete items, h <files…> checksums; switches cover every option of CRT-1 and XTR-1. Prompts become errors unless -y (assume yes) is given or stdin is a terminal. |
x a.zip -o out → files in out/ |
| SHL-8 | l prints one line per item (modified, attributes, size, packed size, path) and totals; l -json prints a JSON array with the fields of BRW-2. |
l -json a.zip → valid JSON array |
| SHL-9 | Exit codes: 0 succeeded, 1 completed with warnings, 2 failed or completed with errors, 7 bad command line, 8 out of memory, 255 cancelled. | test corrupt archive → 2 |
| SHL-10 | Settings, stored locally per user: default_format (7z), default_level (5), legacy_code_page, temp_folder, keep_history (on: last 20 archive paths and last options per format), keep_job_view_open (off), extract_links (on), origin_mark (all), menu_actions, menu_cascaded, language (OS language). "Clear history" empties the remembered paths and options after a confirmation. |
change default format → next add dialog preselects it; clear → no previous paths |
| SHL-11 | The app works fully offline and sends nothing over the network: no update checks, analytics or crash uploads. | manual: run every job type with network blocked, watch traffic |
Out of scope
- Email-after-compress, cloud upload, installers.
Decisions
- No
@specfork/privacyrequirement: the app keeps no personal records, only settings and path history, which SHL-10 clears. Privacy's local mode would add a ZIP export of nothing useful. - Exit codes follow the source's widely scripted convention, so existing scripts keep working.
- Silent
add_to_*actions trade options for speed; the options dialog is one entry away.
Integrity
Status: Draft · Prefix: INT
Purpose
Checking that an archive decodes cleanly, and computing checksums of plain files.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| INT-1 | Test decodes every selected item (default all) without writing to disk and checks each stored checksum (CRC-32 in zip, 7z and gzip; block CRCs in bzip2; the stream check in xz). | good archive → ok, nothing written |
| INT-2 | Result: ok, or a list of {item_path, error}, with totals of files, folders, bytes tested and errors. |
2 corrupt items → list of 2 |
| INT-3 | Error kinds shared by test, extract and codecs: crc_failed, data_error, unexpected_end, unsupported_method, wrong_password, headers_error, missing_volume, unavailable_data, data_after_end. data_after_end (bytes after a complete archive) is a warning; the result stays ok. |
truncated → unexpected_end; 100 bytes appended → ok + warning |
| INT-4 | Encrypted archives need the password to test (ENC-5); a header-encrypted 7z cannot be tested without it. | test without password → prompt |
| INT-5 | Checksums of plain files and folders: crc32, crc64, xxh64, md5, sha1, sha256 (default), sha384, sha512, blake2sp. Output lists each file's value; for several files also one value over all data and one over data and names. Copy gives one value path line per file. |
sha256 of an empty file → e3b0c442…b855 |
| INT-6 | Testing a 1 GiB 7z (lzma2, level 5) takes at most 1.2 × the time to extract it to a fast local disk. |
manual: time test vs extract |
Out of scope
- Repairing damaged archives; recovery records; digital signatures.
Decisions
- One error enum (INT-3) for test, extract and codecs keeps messages and exit codes (SHL-9) consistent.
Jobs
Status: Draft · Prefix: JOB
Purpose
Every create, update, extract, test or checksum run is a job the user can watch, pause and stop.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| JOB-1 | A job runs off the interface thread; windows stay responsive and several jobs can run at once, each with its own progress view. | start 2 extractions → both progress, UI responds |
| JOB-2 | Progress shows percent, elapsed, estimated remaining, speed (bytes/s), bytes and files processed of total, current item path, compression ratio (create), and error count; it updates at least every 500 ms. | manual: watch a 2 GiB create job |
| JOB-3 | Pause holds the job; resume continues where it stopped; background lowers its CPU priority. |
pause 10 s, resume → same result as unpaused |
| JOB-4 | Cancel stops the job within 1 s and cleans up per CRT-10 and XTR-8; files finished before cancel stay. Quitting the app with running jobs asks first, listing them, then cancels them. | cancel at 40% of 10 files → finished files remain, no partial |
| JOB-5 | Outcome: succeeded, completed_with_warnings, completed_with_errors, failed, cancelled, shown with counts. A succeeded view closes by itself unless keep_job_view_open (default off); others stay until dismissed. |
1 skipped input → completed_with_warnings, view stays |
| JOB-6 | Messages list every warning and error as {severity, item_path, kind, detail} (INT-3); the list copies as text. |
copy → one line per message |
| JOB-7 | On an item error the user picks skip_item or stop, except where this spec skips automatically. |
unreadable source → skip/stop offered |
| JOB-8 | Temporary files go to temp_folder (default: the target's volume, so the final rename is atomic) and are removed at job end whatever the outcome. |
after cancel → no temp files |
| JOB-9 | Packing 1 GiB of mixed files to 7z (level 5, all threads) keeps all logical CPUs busy for most of the run and stays within the memory estimate (FMT-9) + 10%. |
manual: monitor CPU and memory |
| JOB-10 | Extracting a 4 GiB zip of 50,000 files runs at ≥ 70% of the speed of the OS copying the same unpacked tree on the same disk. |
manual: time both |
Out of scope
- Job queues that survive restarts; scheduling.
Decisions
- Pause and cancel are checked between data blocks, which bounds their latency (JOB-4).
Names
Status: Draft · Prefix: NAM
Purpose
File names in any script survive packing and unpacking on every OS.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| NAM-1 | Names are Unicode internally. 7z stores UTF-16; tar (pax) and zip store UTF-8, and zip sets its UTF-8 flag on each item with a non-ASCII name. Stored paths use /; a \ from a Windows tool counts as a separator (SAFE-1). |
zip résumé.txt → UTF-8 flag set; zip a\b.txt → folder a, file b.txt |
| NAM-2 | Zip items without the UTF-8 flag decode with legacy_code_page (default: the OS's legacy console code page on Windows, CP437 elsewhere); the browser lets the user pick another and relists at once. |
old Cyrillic zip garbled → pick CP866 → names correct |
| NAM-3 | Bytes invalid in a name's encoding show as U+FFFD, are written to disk as _, and the rename is listed; the data still extracts. |
invalid byte in tar name → a_b.txt + rename listed |
| NAM-4 | Characters the destination OS forbids (Windows: <>:"|?* and control characters) become _, and trailing dots and spaces are removed; each change is listed. |
a:b.txt on Windows → a_b.txt |
| NAM-5 | Names reserved by the destination OS (Windows: CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9, any extension) get a _ prefix. |
NUL.txt on Windows → _NUL.txt |
| NAM-6 | Names differing only by case, or only by Unicode normalisation (NFC vs NFD), collide on a destination that treats them as equal; the second follows overwrite (XTR-5). |
Read.me + READ.ME on a case-insensitive disk → prompt on the second |
| NAM-7 | A component longer than the destination allows (typically 255 bytes), or a path longer than the OS limit after any long-path support, fails that item with name_too_long; the job continues. |
300-byte name → name_too_long, others extracted |
Out of scope
- Transliterating names.
Decisions
- Prefixing reserved names keeps them readable and easy to fix by hand.
Extraction safety
Status: Draft · Prefix: SAFE
Purpose
An archive from anywhere can never write outside the chosen folder or plant something dangerous.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| SAFE-1 | Every stored path is normalised before use: \ and / are both separators, empty and . components are dropped, and .. cancels the previous component. |
a/./b//c → a/b/c; a/x/../b → a/b |
| SAFE-2 | An item whose normalised path climbs above the destination (a .. with nothing left to cancel) is not written; it is reported unsafe_path and the job ends completed_with_errors. The check also covers codec paths (FMT-5), path_mode: none and renamed names. |
item ../../etc/x → nothing outside, unsafe_path |
| SAFE-3 | Absolute stored paths (leading separator, drive letter C:, UNC \\host\share, device prefix \\?\) lose their root and land under the destination, with one warning per job; only path_mode: absolute with its confirmation (XTR-4) keeps the root. |
item /etc/passwd → <dest>/etc/passwd |
| SAFE-4 | Symbolic links are created when extract_links is on (default on where the user may create links); where the OS forbids it, the link becomes a small text file holding its target, with a warning. |
Windows without link rights → text file + warning |
| SAFE-5 | A link whose target is absolute, or resolves outside the destination from the link's folder, is not created (unsafe_link) unless allow_unsafe_links (default off) is on for this job. |
link x → /etc → skipped, unsafe_link |
| SAFE-6 | Links are created only after every regular file of the job is written, so no file of the same archive is written through a link it created. | link d → /tmp, then file d/x → d/x is a real file in <dest> |
| SAFE-7 | If a component of a file's target path inside the destination is an existing link pointing outside it, the file is not written (unsafe_path). |
existing <dest>/d → /tmp, item d/x → refused |
| SAFE-8 | Setuid, setgid and sticky bits are never restored; file owners are never changed. | item mode 4755 → extracted 0755 |
| SAFE-9 | Hard-link items are created only when their target is an item this job already wrote inside the destination; otherwise unsafe_link. |
hard link to /etc/shadow → skipped |
| SAFE-10 | Device files, FIFOs and sockets are never created; each is listed as skipped. | tar with a character device → skipped |
| SAFE-11 | When an item decodes to more than its declared size, decoding stops at that size and the item is data_error; total output never exceeds the sum of declared sizes. |
entry declaring 1 KiB, yielding 1 GiB → stops, error |
| SAFE-12 | Extraction never runs or opens what it writes; nested archives stay files. Opening an executable, script or shortcut item from the browser (BRW-6) asks for confirmation first. | open setup.exe from archive → confirmation |
Out of scope
- Malware scanning; antivirus integration.
Decisions
- Unsafe items are skipped, not renamed into the destination: a renamed traversal payload would still surprise.
- Links last (SAFE-6) plus the existing-link check (SAFE-7) close the link-then-write escape.
- Without link rights, a text file with the target beats silently dropping the item.
From Specfork/archiver at 048b3a6, licence CC-BY-4.0.