unzip
The atago project wrote these specs on its own initiative and runs them in its own CI, to exercise atago against a real program. They are not unzip’s official test suite, and the unzip project is not affiliated with atago.
Summary #
2 suites · 16 scenarios
Contents #
- zip and unzip (archive round trips and extracted trees) — 13 scenarios
- zipping a tree keeps the inputs and adds exactly one archive
- an extracted tree has exactly the paths the archive holds
- the extracted tree matches its snapshot manifest
- a round trip restores every byte, including the awkward ones
- unzip -j drops the directories and keeps the files
- listing an archive names every entry without writing anything
- an integrity test passes on a good archive and fails on a truncated one
- a missing archive is exit 9 and creates nothing
- an unknown option is exit 10 and a name that matches nothing is exit 11
- zip with nothing to archive is exit 12 and writes no archive
- overwriting is refused by default, forced by -o, and skipped by -n
- an updated archive replaces one entry and leaves the rest alone
- deleting an entry removes it from the extracted tree
- unzip (path traversal in a hostile archive) — 3 scenarios
zip and unzip (archive round trips and extracted trees) #
Info-ZIP zip and unzip are the
archivers a shell script reaches for. An archiver makes two promises worth
testing: what comes back out is what went in, byte for byte, and the tree it
writes is exactly the tree the archive describes — no more paths, no fewer.
This suite pins both. Round trips are compared with equals_file, so no
newline or encoding normalization can hide a difference. The extracted tree
is asserted with dir: in every mode the family offers — recursive
membership, a count over regular files, a glob, and a snapshot manifest that
fixes the whole shape at once — and changes: states exhaustively which
paths a command created, so “it also wrote something else” fails. The
documented exit codes get one scenario each: 0, the warning code 1, 9 for a
missing or unreadable archive, 10 for a bad option, 11 when a name pattern
matches nothing, and zip’s 12 for nothing to do.
Every archive is built by the spec from files the spec writes, so nothing is committed and no scenario depends on the host’s contents.
Source: test/e2e/thirdparty/unzip/unzip.atago.yaml
Scenario: zipping a tree keeps the inputs and adds exactly one archive #
only when zip -v succeeds · skipped on Windows
Given #
- Fixture file
src/a.txtis created. - Fixture file
src/sub/b.txtis created. - The command runs with an isolated home under
${workdir}/.atago-home(HOME/XDG or APPDATA redirected).
Inputs #
Fixture src/a.txt:
hello
Fixture src/sub/b.txt:
nested
When #
zip -q -r ar.zip src
Then #
- exit code is
0 - the step changed exactly created
ar.zip, modified nothing, deleted nothing
Scenario: an extracted tree has exactly the paths the archive holds #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
src/a.txtis created. - Fixture file
src/sub/b.txtis created. - Fixture file
src/sub/deep/c.txtis created.
Inputs #
Fixture src/a.txt:
hello
Fixture src/sub/b.txt:
nested
Fixture src/sub/deep/c.txt:
deeper
When #
zip -q -r ar.zip src
unzip -q ar.zip -d out
Then #
- after
zip -q -r ar.zip src:- exit code is
0
- exit code is
- after
unzip -q ar.zip -d out:- exit code is
0 - dir
outcontainssrc/a.txt, containssrc/sub/b.txt, containssrc/sub/deep/c.txt, (recursive) - dir
outhas 3 entries, (recursive) - dir
outmatches globsrc/sub/deep/*.txt, (recursive) - dir
outcontainssrc, has 1 entry
- exit code is
Scenario: the extracted tree matches its snapshot manifest #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
src/a.txtis created. - Fixture file
src/sub/b.txtis created.
Inputs #
Fixture src/a.txt:
hello
Fixture src/sub/b.txt:
nested
When #
zip -q -r ar.zip src
unzip -q ar.zip -d out
Then #
- after
unzip -q ar.zip -d out:- exit code is
0 - dir
outtree matches snapshotextracted_tree
- exit code is
Scenario: a round trip restores every byte, including the awkward ones #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
src/empty.binis created. - Fixture file
src/mixed.txtis created. - Fixture file
src/nul.binis created.
Inputs #
Fixture src/mixed.txt:
unix
windows
no trailing newline
When #
zip -q -r ar.zip src
unzip -q ar.zip -d out
Then #
- after
zip -q -r ar.zip src:- exit code is
0
- exit code is
- after
unzip -q ar.zip -d out:- exit code is
0 - file
out/src/mixed.txtis byte-identical tosrc/mixed.txt - file
out/src/nul.binis byte-identical tosrc/nul.bin - file
out/src/empty.binis byte-identical tosrc/empty.bin
- exit code is
Scenario: unzip -j drops the directories and keeps the files #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
src/a.txtis created. - Fixture file
src/sub/b.txtis created.
Inputs #
Fixture src/a.txt:
hello
Fixture src/sub/b.txt:
nested
When #
zip -q -r ar.zip src
unzip -j -q ar.zip -d flat
Then #
- after
unzip -j -q ar.zip -d flat:- exit code is
0 - dir
flatcontainsa.txt, containsb.txt, does not containsrc, does not containsub, has 2 entries, (recursive) - file
flat/b.txtis byte-identical tosrc/sub/b.txt
- exit code is
Scenario: listing an archive names every entry without writing anything #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
src/a.txtis created. - Fixture file
src/sub/b.txtis created. - The command runs with an isolated home under
${workdir}/.atago-home(HOME/XDG or APPDATA redirected).
Inputs #
Fixture src/a.txt:
hello
Fixture src/sub/b.txt:
nested
When #
zip -q -r ar.zip src
unzip -l ar.zip
Then #
- after
unzip -l ar.zip:- exit code is
0 - stdout contains
src/a.txt,src/sub/b.txt,4 files - the step changed exactly created nothing, modified nothing, deleted nothing
- exit code is
Scenario: an integrity test passes on a good archive and fails on a truncated one #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
src/a.txtis created. - Fixture file
broken.zipis created.
Inputs #
Fixture src/a.txt:
hello
Fixture broken.zip:
PK truncated garbage
When #
zip -q -r ar.zip src
unzip -t ar.zip
unzip -t broken.zip
Then #
- after
unzip -t ar.zip:- exit code is
0 - stdout contains
No errors detected in compressed data
- exit code is
- after
unzip -t broken.zip:- exit code is
9 - stdout contains
End-of-central-directory signature not found
- exit code is
Scenario: a missing archive is exit 9 and creates nothing #
only when unzip -v succeeds · skipped on Windows
Given #
- The command runs with an isolated home under
${workdir}/.atago-home(HOME/XDG or APPDATA redirected).
When #
unzip -q absent.zip -d out
Then #
- exit code is
9 - stderr contains
cannot find or open absent.zip - the step changed exactly created nothing, modified nothing, deleted nothing
- dir
outdoes not exist
Scenario: an unknown option is exit 10 and a name that matches nothing is exit 11 #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
src/a.txtis created.
Inputs #
Fixture src/a.txt:
hello
When #
zip -q -r ar.zip src
unzip -Q ar.zip
unzip -q ar.zip "no/such/*" -d out
Then #
- after
unzip -Q ar.zip:- exit code is
10 - stdout is empty
- stderr contains
Usage: unzip
- exit code is
- after
unzip -q ar.zip "no/such/*" -d out:- exit code is
11 - stderr contains
filename not matched - dir
outexists, has 0 entries
- exit code is
Scenario: zip with nothing to archive is exit 12 and writes no archive #
only when zip -v succeeds · skipped on Windows
Given #
- The command runs with an isolated home under
${workdir}/.atago-home(HOME/XDG or APPDATA redirected).
When #
zip -q empty.zip
Then #
- exit code is
12 - the step changed exactly created nothing, modified nothing, deleted nothing
Scenario: overwriting is refused by default, forced by -o, and skipped by -n #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
src/a.txtis created. - Fixture file
out/src/a.txtis created.
Inputs #
Fixture src/a.txt:
from the archive
Fixture out/src/a.txt:
already here
When #
zip -q -r ar.zip src
unzip -q ar.zip -d out < /dev/null
unzip -qn ar.zip -d out
unzip -qo ar.zip -d out
Then #
- after
unzip -q ar.zip -d out < /dev/null:- exit code is
1 - file
out/src/a.txtequals exact bytes
- exit code is
- after
unzip -qn ar.zip -d out:- exit code is
0 - file
out/src/a.txtequals exact bytes
- exit code is
- after
unzip -qo ar.zip -d out:- exit code is
0 - file
out/src/a.txtis byte-identical tosrc/a.txt
- exit code is
Scenario: an updated archive replaces one entry and leaves the rest alone #
only when zip -v succeeds · skipped on Windows
Given #
- Fixture file
src/a.txtis created. - Fixture file
src/b.txtis created. - Fixture file
src/a.txtis created. - The command runs with an isolated home under
${workdir}/.atago-home(HOME/XDG or APPDATA redirected).
Inputs #
Fixture src/a.txt:
first
Fixture src/b.txt:
second
Fixture src/a.txt:
rewritten
When #
zip -q -r ar.zip src
zip -q ar.zip src/a.txt
unzip -q ar.zip -d out
Then #
- after
zip -q -r ar.zip src:- exit code is
0
- exit code is
- after
zip -q ar.zip src/a.txt:- exit code is
0 - the step changed exactly created nothing, modified
ar.zip, deleted nothing
- exit code is
- after
unzip -q ar.zip -d out:- exit code is
0 - file
out/src/a.txtequals exact bytes - file
out/src/b.txtequals exact bytes
- exit code is
Scenario: deleting an entry removes it from the extracted tree #
only when zip -v succeeds · skipped on Windows
Given #
- Fixture file
src/keep.txtis created. - Fixture file
src/drop.txtis created.
Inputs #
Fixture src/keep.txt:
keep
Fixture src/drop.txt:
drop
When #
zip -q -r ar.zip src
zip -q -d ar.zip "src/drop.txt"
unzip -q ar.zip -d out
Then #
- after
zip -q -d ar.zip "src/drop.txt":- exit code is
0
- exit code is
- after
unzip -q ar.zip -d out:- exit code is
0 - dir
outcontainssrc/keep.txt, does not containsrc/drop.txt, has 1 entry, (recursive)
- exit code is
unzip (path traversal in a hostile archive) #
An archive is untrusted input: the names inside it come from whoever built
it. The attack known as zip slip stores an entry called ../escaped.txt (or
an absolute /etc/passwd) so that a careless extractor writes outside the
directory the user chose.
unzip refuses to. It strips the traversal, says so, extracts the entry
under the destination anyway, and reports the warning exit code 1 rather than
a silent 0. This suite pins all four parts of that contract, and proves the
filesystem claim rather than trusting the message: the destination’s tree is
asserted exhaustively with dir:, and changes: states that the workdir
around the destination gained nothing.
The hostile archives cannot be produced by zip itself, which never stores a
traversing name, so each is written by the spec as base64 — bytes authored
here, not a sample taken from anywhere.
Source: test/e2e/thirdparty/unzip/zip_slip.atago.yaml
Scenario: a traversing entry is stripped and lands inside the destination #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
hostile.zipis created. - The command runs with an isolated home under
${workdir}/.atago-home(HOME/XDG or APPDATA redirected).
When #
unzip -l hostile.zip
unzip hostile.zip -d dest
Then #
- after
unzip -l hostile.zip:- exit code is
0 - stdout contains
../escaped.txt,/abs.txt
- exit code is
- after
unzip hostile.zip -d dest:- exit code is
1 - stdout contains
skipped "../" path component(s) in ../escaped.txt - stderr contains
stripped absolute path spec from /abs.txt - the step changed exactly created
dest/escaped.txt,dest/abs.txt,dest/good.txt, modified nothing, deleted nothing - dir
destcontainsescaped.txt, containsabs.txt, containsgood.txt, has 3 entries, (recursive) - file
dest/escaped.txtequals exact bytes
- exit code is
Scenario: a deeper traversal is stripped down to its basename #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
deep.zipis created. - Fixture file
dest/placeholder.txtis created. - The command runs with an isolated home under
${workdir}/.atago-home(HOME/XDG or APPDATA redirected).
Inputs #
Fixture dest/placeholder.txt:
kept
When #
unzip -o deep.zip -d dest
Then #
- exit code is
1 - stdout contains
skipped "../" path component(s) - the step changed exactly created
dest/tmp/pwned.txt, modified nothing, deleted nothing - dir
destcontainstmp/pwned.txt, containsplaceholder.txt, has 2 entries, (recursive) - file
dest/tmp/pwned.txtequals exact bytes
Scenario: unzip -t reports the hostile archive as intact #
only when unzip -v succeeds · skipped on Windows
Given #
- Fixture file
hostile.zipis created. - The command runs with an isolated home under
${workdir}/.atago-home(HOME/XDG or APPDATA redirected).
When #
unzip -t hostile.zip
Then #
- exit code is
0 - stdout contains
No errors detected in compressed data - the step changed exactly created nothing, modified nothing, deleted nothing