Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ ordinary archives and scales to very large ones.
zget https://example.com/archive.zip README.txt
zget -o report.pdf https://example.com/documents.zip path/to/report.pdf
zget -l https://example.com/archive.zip
zget -l https://example.com/documents.zip path/to/report.pdf
zget -1 https://example.com/archive.zip
zget https://example.com/data.zip config.json | jq .
```
Expand Down Expand Up @@ -42,8 +41,8 @@ sudo install zget-X.Y.Z-linux-x86_64/zget /usr/local/bin/zget

```text
zget [-o FILE] URL MEMBER
zget -l URL [MEMBER]
zget -1 URL [MEMBER]
zget -l URL
zget -1 URL
```

## Familiar by design
Expand All @@ -58,7 +57,7 @@ and emits only names.

Extraction requires both a URL and an exact member name. A URL without a member
is a usage error rather than an implicit request to list the archive; use `-l`
explicitly to stream all entries, or add `MEMBER` to list one exact match.
or `-1` explicitly to stream the complete Central Directory listing.

Listings use the familiar `unzip -l` columns: uncompressed length, modification
date and time in UTC, and member name. Timestamps are resolved from NTFS,
Expand All @@ -67,8 +66,7 @@ Central Directory once and retain only the current entry. Control characters
and backslashes are escaped so every member stays on one safe output line.

For a compact listing, `-1` writes only member names, one per line, with no
header or totals. It accepts the same optional exact `MEMBER` as `-l` and uses
the same safe escaping.
header or totals, using the same safe escaping as `-l`.

## Why zget?

Expand Down
23 changes: 5 additions & 18 deletions cli/zget.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ struct file_output {

struct list_output {
struct file_output stream;
const char *member;
uint64_t entries;
uint64_t total_size;
int total_overflow;
int names_only;
int found;
int header_written;
};

Expand Down Expand Up @@ -138,12 +136,6 @@ static int list_member(void *opaque, const zget_member_info *member)
char prefix[64];
int length;

if (out->member != NULL &&
(out->found || strcmp(out->member, member->name) != 0))
return 0;
if (out->member != NULL)
out->found = 1;

/*
* The short form deliberately shares the normal name encoder. Besides
* making one-name-per-line output pleasant for scripts, this prevents a
Expand Down Expand Up @@ -181,15 +173,15 @@ static int list_member(void *opaque, const zget_member_info *member)
static void usage(FILE *file)
{
fprintf(file, "usage: zget [-o FILE] URL MEMBER\n"
" zget -l URL [MEMBER]\n"
" zget -1 URL [MEMBER]\n");
" zget -l URL\n"
" zget -1 URL\n");
}

int main(int argc, char **argv)
{
const char *output_path = NULL, *url, *member = NULL;
struct file_output output = {stdout, 0};
struct list_output listing = {{stdout, 0}, NULL, 0, 0, 0, 0, 0, 0};
struct list_output listing = {{stdout, 0}, 0, 0, 0, 0, 0};
int arg = 1, close_output = 0, list_mode = 0, rc, exit_status = 1;

if (argc == 2 && !strcmp(argv[1], "--version")) {
Expand All @@ -212,13 +204,13 @@ int main(int argc, char **argv)
}
output_path = argv[arg++];
}
if ((list_mode && (argc - arg < 1 || argc - arg > 2)) ||
if ((list_mode && argc - arg != 1) ||
(!list_mode && argc - arg != 2)) {
usage(stderr);
return 2;
}
url = argv[arg];
if (!list_mode || argc - arg == 2)
if (!list_mode)
member = argv[arg + 1];
if (member != NULL && member[0] == '\0') {
fprintf(stderr, "zget: member path must not be empty\n");
Expand All @@ -243,17 +235,12 @@ int main(int argc, char **argv)
char summary[96];
int length;

listing.member = member;
rc = zget_list(url, list_member, &listing);
if (rc != ZGET_OK) {
if (listing.stream.broken_pipe)
goto done;
goto zget_failure;
}
if (member != NULL && !listing.found) {
rc = ZGET_ENOTFOUND;
goto zget_failure;
}
if (!listing.names_only) {
if (write_list_header(&listing) ||
write_stream(&listing.stream, footer, sizeof(footer) - 1))
Expand Down
26 changes: 7 additions & 19 deletions docs/zget.1.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ zget \- fetch one member from a remote ZIP archive
.B zget
.B \-l
.I URL
.RI [ MEMBER ]
.br
.B zget
.B \-1
.I URL
.RI [ MEMBER ]
.br
.B zget
.RB [ \-h | \-\-help ]
Expand Down Expand Up @@ -50,17 +48,17 @@ fails after writing begins, partial output remains. The special file name
selects standard output.
.PP
.B \-l
streams a listing directly from the Central Directory. Each output row contains
the uncompressed length, UTC modification date and time, and member name
in a layout inspired by
streams the complete archive listing directly from the Central Directory. Each
output row contains the uncompressed length, UTC modification date and time, and
member name in a layout inspired by
.BR unzip (1).
Timestamps are resolved from NTFS, Extended Timestamp, or packed DOS metadata
in that order. Control characters and backslashes are escaped so an
archive name cannot create additional terminal lines. Listing memory use is
independent of the entry count.
.PP
.B \-1
uses the same streaming lookup but writes only member names, one per line,
uses the same streaming listing but writes only member names, one per line,
without a header or totals. Names receive the same safe escaping as the table
listing. This form follows the convention established by
.BR zipinfo (1).
Expand All @@ -77,15 +75,11 @@ of
selects standard output.
.TP
.B \-l
List archive members to standard output. With
.IR MEMBER ,
emit only the first exact match; otherwise emit every member. A URL without
this explicit option does not imply listing.
List all archive members to standard output. A URL without this explicit option
does not imply listing.
.TP
.B \-1
List only archive member names, one per line. With
.IR MEMBER ,
emit only the first exact match; otherwise emit every member.
List all archive member names, one per line.
.TP
.BR \-h , " " \-\-help
Show command usage and exit.
Expand Down Expand Up @@ -144,12 +138,6 @@ Stream the archive listing:
zget -l https://example.com/archive.zip
.fi
.PP
List one exact member without extracting it:
.PP
.nf
zget -l https://example.com/documents.zip path/to/report.pdf
.fi
.PP
List only member names:
.PP
.nf
Expand Down
35 changes: 3 additions & 32 deletions tests/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def main(binary):

# Syntax failures must remain local and use the conventional usage status.
for arguments in ([], ["only-a-url"], ["-o"], ["-o", ""], ["-l"], ["-1"],
["-l", "url", "member"], ["-1", "url", "member"],
["-l", "url", "member", "extra"],
["-1", "url", "member", "extra"]):
result = subprocess.run([binary, *arguments], stdout=subprocess.PIPE,
Expand Down Expand Up @@ -286,39 +287,13 @@ def suffix_fallback(base):
# One rejected suffix, one size probe, one exact tail, and one CD request.
assert run_server(data, "suffix-unsupported", suffix_fallback) == 4

def specific_listing(base):
"""An exact listing emits one row and stops at the first name match."""
url = base + "/archive.zip"
result = subprocess.run([binary, "-l", url, "stored.txt"],
check=True, stdout=subprocess.PIPE)
lines = result.stdout.splitlines()
assert lines[2].endswith(b" stored.txt")
assert lines[2].startswith(f"{14:9d} ".encode())
assert lines[-1] == f"{14:9d} 1 file".encode()
assert len(lines) == 5
short = subprocess.run([binary, "-1", url, "stored.txt"],
check=True, stdout=subprocess.PIPE)
assert short.stdout == b"stored.txt\n"
missing = subprocess.run([binary, "-l", url, "missing"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
assert missing.returncode != 0
assert b"member not found" in missing.stderr
short_missing = subprocess.run([binary, "-1", url, "missing"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
assert short_missing.returncode != 0
assert short_missing.stdout == b""
assert b"member not found" in short_missing.stderr
assert run_server(data, "normal", specific_listing) == 8

stored_cd = central_entry(data, "stored.txt")
invalid_date = mutate(data, stored_cd + 14, b"\x00\x00")

def invalid_timestamp_listing(base):
"""Reject an entry when no valid modification-time source exists."""
result = subprocess.run(
[binary, "-l", base + "/archive.zip", "stored.txt"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = subprocess.run([binary, "-l", base + "/archive.zip"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
assert result.returncode != 0
run_server(invalid_date, "normal", invalid_timestamp_listing)

Expand Down Expand Up @@ -550,10 +525,6 @@ def duplicate(base):
result = subprocess.run([binary, base + "/archive.zip", "same"],
check=True, stdout=subprocess.PIPE)
assert result.stdout == b"first"
result = subprocess.run([binary, "-l", base + "/archive.zip", "same"],
check=True, stdout=subprocess.PIPE)
assert f"{5:9d} ".encode() in result.stdout
assert result.stdout.endswith(f"{5:9d} 1 file\n".encode())
run_server(duplicates.getvalue(), "normal", duplicate)

unicode_cd = central_entry(data, "unicod\u00e9.txt")
Expand Down