diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ebbeaf..6b24823 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,20 +111,8 @@ if(BUILD_TESTING) target_link_libraries(zget_unit_tests PRIVATE ZLIB::ZLIB) add_test(NAME unit COMMAND zget_unit_tests) - # Exercise the public process lifecycle through the same static library - # embedded by the CLI; this catches accidental per-context initialization. - add_executable(zget_lifecycle_tests tests/test_lifecycle.c) - target_link_libraries(zget_lifecycle_tests PRIVATE Zget::libzget_static) - add_test(NAME lifecycle COMMAND zget_lifecycle_tests) - - # Verify that struct_size remains a real ABI boundary as options grow. - add_executable(zget_options_tests tests/test_options.c) - target_link_libraries(zget_options_tests PRIVATE Zget::libzget_static) - add_test(NAME options COMMAND zget_options_tests) - - # Keep public output parameters deterministic even when validation fails. + # Exercise the complete one-shot public API without making network calls. add_executable(zget_api_tests tests/test_api.c) - target_include_directories(zget_api_tests PRIVATE src) target_link_libraries(zget_api_tests PRIVATE Zget::libzget_static) add_test(NAME api_contracts COMMAND zget_api_tests) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d90024..c47deab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,8 +11,8 @@ The library joins two independent internal abstractions in `src/zget.c`: - `src/source/` owns range-addressable byte access, source identity, and transport policy. Backends must not interpret container metadata. - `src/format/` owns container discovery, member lookup, and extraction. - Engines borrow their source and shared error state from `zget_ctx`; they must - not close either one. + Engines borrow their source and shared error state from the active operation; + they must not close either one. ZIP-specific state and code live under `src/format/zip/`, including EOCD and ZIP64 handling, streaming Central Directory parsing, Local Headers, STORE, diff --git a/README.md b/README.md index 687cc99..380579b 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,11 @@ 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. -Listings use the familiar `unzip -l` columns: uncompressed length, ZIP-local -modification date and time, and member name. ZIP timestamps have no timezone; -invalid packed timestamps are shown as dashes rather than normalized. Listings -scan the Central Directory once and retain only the current entry. Control -characters, backslashes, and legacy non-UTF-8 name bytes are escaped so every -member stays on one safe output line. +Listings use the familiar `unzip -l` columns: uncompressed length, modification +date and time in UTC, and member name. Timestamps are resolved from NTFS, +Extended Timestamp, or packed DOS metadata in that order. Listings scan the +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 @@ -175,11 +174,11 @@ installed `libzget`; curl, zlib, TLS, and the platform C runtime remain dynamic. ## Library `libzget` is the reusable implementation behind the `zget` CLI. Its installed -header is `` and its public API deals only in URLs, opaque contexts, -format-neutral member records, callbacks, and zget error codes. libcurl and -zlib remain private implementation dependencies: applications do not include -their headers or manipulate their handles to use the high-level API. Current -library support is HTTP(S), single-volume ZIP/ZIP64, STORE, and DEFLATE. +header is ``. The public API consists of two synchronous one-shot +operations, callbacks, member records, and zget error codes; it exposes no +contexts, global lifecycle, or transport handles. libcurl and zlib remain +private implementation dependencies. Current library support is HTTP(S), +single-volume ZIP/ZIP64, STORE, and DEFLATE. The library is pre-1.0. Its API and ABI may change between minor releases while the design is refined; patch releases preserve the ABI within one `0.MINOR` @@ -193,7 +192,7 @@ cc -o example example.c $(pkg-config --cflags --libs libzget) ``` Or use the installed CMake target `Zget::libzget` after -`find_package(Zget 0.4 REQUIRED)`. +`find_package(Zget 0.5 REQUIRED)`. ### Minimal extraction example @@ -210,30 +209,15 @@ static int write_stdout(void *userdata, const void *data, size_t size) int main(int argc, char **argv) { - zget_ctx *ctx = NULL; int rc; if (argc != 3) { fprintf(stderr, "usage: %s URL MEMBER\n", argv[0]); return 2; } - rc = zget_global_init(); - if (rc != ZGET_OK) { + rc = zget_get(argv[1], argv[2], write_stdout, stdout); + if (rc != ZGET_OK) fprintf(stderr, "zget: %s\n", zget_error_string(rc)); - return 1; - } - - rc = zget_open_url_ex(argv[1], NULL, &ctx); - if (rc == ZGET_OK) - rc = zget_extract_member(ctx, argv[2], write_stdout, stdout); - if (rc != ZGET_OK) { - const char *detail = ctx != NULL ? zget_last_error_message(ctx) : ""; - fprintf(stderr, "zget: %s%s%s\n", zget_error_string(rc), - detail[0] != '\0' ? ": " : "", detail); - } - - zget_close(ctx); - zget_global_cleanup(); return rc == ZGET_OK ? 0 : 1; } ``` @@ -247,9 +231,8 @@ semantics instead. ### Listing -`zget_list()` streams one borrowed, length-delimited record at a time and does -not construct an archive-wide index. Pass NULL as the member path to list every -entry, or an exact path to emit only the first match: +`zget_list()` streams every Central Directory entry as one borrowed record and +does not construct an archive-wide index: ```c static int print_member(void *userdata, const zget_member_info *member) @@ -262,53 +245,18 @@ static int print_member(void *userdata, const zget_member_info *member) return 0; } -/* ctx is an open zget_ctx. */ -rc = zget_list(ctx, NULL, print_member, stdout); -``` - -The record and its name become invalid when the callback returns. Validated -modification components are local calendar values because ZIP stores no -timezone. When `ZGET_MEMBER_NAME_UTF8` is absent, the name contains legacy raw -bytes and should be preserved or escaped rather than decoded by guesswork. - -### Lifecycle, limits, and threads - -Call `zget_global_init()` during single-threaded application startup and match -every successful call with `zget_global_cleanup()` during shutdown after all -contexts are closed. The CLI handles this lifecycle automatically. - -`zget_open_url_ex()` retains a diagnostic context on most opening failures; -inspect it with `zget_last_error_message()` and always release it with -`zget_close()`. `zget_open_url()` is shorter when only success or failure is -needed, while `zget_get()` performs one open/extract/close sequence using -default options. - -A context must not be used concurrently or reentered from one of its callbacks. -After global initialization, different contexts may be used by different -threads. `max_output_size`, `max_metadata_bytes`, and `max_http_requests` let -embedding applications impose defensive limits. The CLI leaves output and -metadata sizes unrestricted. Always start custom options with -`zget_options_init()`; the library copies the structure while opening. - -### Migrating to 0.3 - -Version 0.3 removes the ZIP-specific `zget_entry`, `zget_find()`, and -`zget_extract()` interface. Replace the old two-step extraction: - -```c -zget_find(ctx, path, &entry); -zget_extract(ctx, &entry, write_cb, userdata); +rc = zget_list(url, print_member, stdout); ``` -with the format-neutral operation: - -```c -zget_extract_member(ctx, path, write_cb, userdata); -``` +The record and its NUL-terminated name become invalid when the callback +returns. Every name is valid UTF-8. Resolution uses the ZIP UTF-8 flag first, +then a valid Info-ZIP Unicode Path (`0x7075`) field, then CP437 conversion. +`mtime` is signed Unix time in UTC seconds. The record also supplies compressed +and uncompressed sizes, CRC32, and the numeric ZIP compression method. -Use `zget_list(ctx, path, list_cb, userdata)` when only member metadata is -needed. This keeps ZIP offsets, flags, compression details, and CRC state inside -the library so those implementation details can evolve safely. +Both public operations manage their own resources and libcurl lifecycle. They +are synchronous and callbacks run inline. Independent calls share no archive +state and may be made from different threads on supported libcurl builds. ## HTTP invariants @@ -316,8 +264,7 @@ Range-capable servers must return each accepted archive-data response as a matching `206 Partial Content` response with the correct body length. The initial tail normally uses a suffix range. If a server rejects or ignores that syntax but supports explicit ranges, zget uses a one-byte size probe and retries -the tail as an explicit interval. These extra requests count toward -`max_http_requests`. +the tail as an explicit interval. If a server ignores the required Range request entirely and returns HTTP 200 with the complete representation, zget rejects the response with @@ -341,8 +288,8 @@ write to stdout, which likewise cannot be rolled back after a late error. - Exact, case-sensitive full member paths; no normalization or globbing. - Single-volume ZIP32 and ZIP64, including data descriptors. - Compression methods STORE (0) and DEFLATE (8) only. -- UTF-8-flagged names must be valid UTF-8. Legacy names are eligible for exact - matching only when entirely ASCII; CP437 conversion is intentionally absent. +- Names are resolved to valid UTF-8 using the UTF-8 flag, a valid Info-ZIP + Unicode Path field, or CP437. Embedded NUL bytes are malformed. - No encryption, split archives, resume, or random seeks within a DEFLATE member. - HTTP Range support is required. Servers that ignore required Range requests diff --git a/cli/zget.c b/cli/zget.c index 1e33f21..b607d85 100644 --- a/cli/zget.c +++ b/cli/zget.c @@ -14,10 +14,18 @@ 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; +}; + +struct utc_time { + int64_t year; + unsigned int month, day, hour, minute; }; static int write_stream(struct file_output *out, const void *data, size_t size) @@ -43,9 +51,7 @@ static int write_escaped_name(struct list_output *out, for (i = 0; i < member->name_length; ++i) { const unsigned char byte = name[i]; - const int raw = byte >= 0x20 && byte != 0x7f && byte != '\\' && - ((member->flags & ZGET_MEMBER_NAME_UTF8) != 0 || - byte < 0x80); + const int raw = byte >= 0x20 && byte != 0x7f && byte != '\\'; const char *escape = NULL; char hex[4]; @@ -78,12 +84,66 @@ static int write_escaped_name(struct list_output *out, member->name_length - begin); } +static int write_list_header(struct list_output *out) +{ + static const char header[] = + " Length Date Time Name\n" + "--------- ---------- ----- ----\n"; + + if (out->header_written) + return 0; + out->header_written = 1; + return write_stream(&out->stream, header, sizeof(header) - 1); +} + +/* Convert days since 1970-01-01 using the proleptic Gregorian calendar. */ +static void civil_from_days(int64_t days, struct utc_time *time) +{ + int64_t z, era, year_of_era, day_of_year, month_prime; + + z = days + 719468; + era = (z >= 0 ? z : z - 146096) / 146097; + day_of_year = z - era * 146097; + year_of_era = (day_of_year - day_of_year / 1460 + + day_of_year / 36524 - day_of_year / 146096) / 365; + time->year = year_of_era + era * 400; + day_of_year -= 365 * year_of_era + year_of_era / 4 - year_of_era / 100; + month_prime = (5 * day_of_year + 2) / 153; + time->day = (unsigned int)(day_of_year - + (153 * month_prime + 2) / 5 + 1); + time->month = (unsigned int)(month_prime + + (month_prime < 10 ? 3 : -9)); + time->year += time->month <= 2; +} + +static int utc_time_from_epoch(int64_t seconds, struct utc_time *time) +{ + int64_t days = seconds / 86400; + int64_t remainder = seconds % 86400; + + if (remainder < 0) { + --days; + remainder += 86400; + } + civil_from_days(days, time); + time->hour = (unsigned int)(remainder / 3600); + time->minute = (unsigned int)((remainder % 3600) / 60); + return time->year >= 0 && time->year <= 9999; +} + static int list_member(void *opaque, const zget_member_info *member) { struct list_output *out = opaque; + struct utc_time time; 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 @@ -93,15 +153,14 @@ static int list_member(void *opaque, const zget_member_info *member) return write_escaped_name(out, member) || write_stream(&out->stream, "\n", 1); - if ((member->flags & ZGET_MEMBER_HAS_MODIFIED_TIME) != 0) + if (write_list_header(out)) + return 1; + if (utc_time_from_epoch(member->mtime, &time)) length = snprintf(prefix, sizeof(prefix), - "%9" PRIu64 " %02u-%02u-%04u %02u:%02u ", + "%9" PRIu64 " %02u-%02u-%04" PRId64 " %02u:%02u ", member->uncompressed_size, - (unsigned int)member->modified_month, - (unsigned int)member->modified_day, - (unsigned int)member->modified_year, - (unsigned int)member->modified_hour, - (unsigned int)member->modified_minute); + time.month, time.day, time.year, + time.hour, time.minute); else length = snprintf(prefix, sizeof(prefix), "%9" PRIu64 " ---------- ----- ", @@ -130,9 +189,7 @@ 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}, 0, 0, 0, 0}; - zget_ctx *ctx = NULL; - zget_options options; + struct list_output listing = {{stdout, 0}, NULL, 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")) { @@ -180,40 +237,26 @@ int main(int argc, char **argv) } #endif - /* The CLI owns one process-wide acquisition around all network work. */ - rc = zget_global_init(); - if (rc != ZGET_OK) { - fprintf(stderr, "zget: %s\n", zget_error_string(rc)); - return 1; - } - zget_options_init(&options); - /* CLI users may legitimately target archives with enormous directories. */ - options.max_metadata_bytes = UINT64_MAX; - rc = zget_open_url_ex(url, &options, &ctx); - if (rc != ZGET_OK) - goto zget_failure; - if (list_mode) { - static const char header[] = - " Length Date Time Name\n" - "--------- ---------- ----- ----\n"; static const char footer[] = "--------- -------\n"; char summary[96]; int length; - /* Names-only output has no decoration, matching zipinfo -1. */ - if (!listing.names_only && - write_stream(&listing.stream, header, sizeof(header) - 1)) - goto done; - rc = zget_list(ctx, member, list_member, &listing); + 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_stream(&listing.stream, footer, sizeof(footer) - 1)) + if (write_list_header(&listing) || + write_stream(&listing.stream, footer, sizeof(footer) - 1)) goto done; if (listing.total_overflow) length = snprintf(summary, sizeof(summary), @@ -249,8 +292,7 @@ int main(int argc, char **argv) close_output = 1; } - /* Keep ZIP-specific lookup details behind libzget's format-neutral API. */ - rc = zget_extract_member(ctx, member, write_file, &output); + rc = zget_get(url, member, write_file, &output); if (rc != ZGET_OK) { if (output.broken_pipe) goto done; @@ -275,14 +317,10 @@ int main(int argc, char **argv) goto done; zget_failure: - fprintf(stderr, "zget: %s%s%s\n", zget_error_string(rc), - ctx != NULL && zget_last_error_message(ctx)[0] != '\0' ? ": " : "", - ctx != NULL ? zget_last_error_message(ctx) : ""); + fprintf(stderr, "zget: %s\n", zget_error_string(rc)); done: /* stdout is borrowed; only a stream opened for named output is owned. */ if (close_output && output.file != NULL) (void)fclose(output.file); - zget_close(ctx); - zget_global_cleanup(); return exit_status; } diff --git a/docs/zget.1.in b/docs/zget.1.in index a49bb66..2c204dd 100644 --- a/docs/zget.1.in +++ b/docs/zget.1.in @@ -1,4 +1,4 @@ -.TH ZGET 1 "August 24, 2026" "zget @PROJECT_VERSION@" "User Commands" +.TH ZGET 1 "August 25, 2026" "zget @PROJECT_VERSION@" "User Commands" .SH NAME zget \- fetch one member from a remote ZIP archive .SH SYNOPSIS @@ -51,11 +51,11 @@ selects standard output. .PP .B \-l streams a listing directly from the Central Directory. Each output row contains -the uncompressed length, ZIP-local modification date and time, and member name +the uncompressed length, UTC modification date and time, and member name in a layout inspired by .BR unzip (1). -ZIP timestamps have no timezone; invalid packed timestamps are shown as dashes. -Control characters, backslashes, and legacy non-UTF-8 bytes are escaped so an +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 @@ -79,13 +79,13 @@ selects standard output. .B \-l List archive members to standard output. With .IR MEMBER , -emit the first exact match and stop; otherwise emit every member. A URL without +emit only the first exact match; otherwise emit every member. 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 the first exact match and stop; otherwise emit every member. +emit only the first exact match; otherwise emit every member. .TP .BR \-h , " " \-\-help Show command usage and exit. @@ -97,8 +97,7 @@ For Range-capable servers, each response accepted as archive data must be a matching 206 Partial Content response with an identity Content-Encoding and the expected Content-Range and body length. If a server rejects or ignores the initial suffix range but supports explicit ranges, zget uses a one-byte size -probe and retries the tail as an explicit interval. These extra requests count -toward the configured HTTP request limit. +probe and retries the tail as an explicit interval. .PP If the server ignores a required Range request entirely and returns HTTP 200 with the complete representation, zget rejects the response with a Range error. @@ -160,8 +159,9 @@ zget -1 https://example.com/archive.zip Only single-volume ZIP32 and ZIP64 archives are supported. Member names are exact full paths; there is no path normalization or globbing. Compression methods are limited to STORE and DEFLATE. Encryption and split archives are -not supported. Servers must provide valid HTTP Range responses; zget does not -fall back to downloading the complete archive. +not supported. Names resolve to UTF-8 using the ZIP UTF-8 flag, a valid Info-ZIP +Unicode Path field, or CP437. Servers must provide valid HTTP Range responses; +zget does not fall back to downloading the complete archive. .SH SEE ALSO .BR curl (1), .BR unzip (1), diff --git a/include/zget.h b/include/zget.h index e7f0965..a236ce9 100644 --- a/include/zget.h +++ b/include/zget.h @@ -3,6 +3,7 @@ #include #include + #include "zget_version.h" #ifdef __cplusplus @@ -12,11 +13,6 @@ extern "C" { /* * libzget is pre-1.0: public API and ABI may change between minor releases. * Patch releases preserve the ABI of their minor release line. - * - * Global initialization and cleanup must run during single-threaded process - * startup and shutdown. One context must not be used concurrently or - * reentrantly; after initialization, separate contexts are independent and may - * be used by separate threads. */ #if defined(_WIN32) && defined(ZGET_SHARED) @@ -31,216 +27,79 @@ extern "C" { # define ZGET_API #endif -/** Opaque handle for one open remote archive. Release it with zget_close(). */ -typedef struct zget_ctx zget_ctx; - -/** Result codes used by fallible public operations and diagnostics. */ +/** Result codes returned by public operations. */ typedef enum zget_error { - ZGET_OK = 0, /**< Operation completed successfully. */ - ZGET_EINVAL = 1, /**< An argument, URL, or requested range is invalid. */ - ZGET_EHTTP = 2, /**< HTTP setup, transfer, or response validation failed. */ - ZGET_ERANGE = 3, /**< The server cannot provide usable byte ranges. */ - ZGET_ECHANGED = 4, /**< The remote object changed between requests. */ - ZGET_EZIP = 5, /**< ZIP metadata or member data is malformed. */ - ZGET_EUNSUPPORTED = 6, /**< A ZIP feature such as encryption is unsupported. */ - ZGET_ENOTFOUND = 7, /**< No exact member-name match was found. */ - ZGET_ECOMPRESSION = 8, /**< The member's compression method is unsupported. */ - ZGET_ECRC = 9, /**< Uncompressed output failed CRC32 validation. */ - ZGET_EDEFLATE = 10, /**< DEFLATE initialization or decoding failed. */ - ZGET_EIO = 11, /**< An output or listing callback rejected data. */ - ZGET_ELIMIT = 12, /**< A configured resource limit was exceeded. */ - ZGET_ENOMEM = 13, /**< Memory allocation failed. */ - ZGET_ENOTINITIALIZED = 14 /**< zget_global_init() has not been acquired. */ + ZGET_OK = 0, /**< Operation completed successfully. */ + ZGET_EINVAL = 1, /**< An argument, URL, or requested range is invalid. */ + ZGET_EHTTP = 2, /**< HTTP setup, transfer, or response validation failed. */ + ZGET_ERANGE = 3, /**< The server cannot provide usable byte ranges. */ + ZGET_ECHANGED = 4, /**< The remote object changed between requests. */ + ZGET_EZIP = 5, /**< ZIP metadata or member data is malformed. */ + ZGET_EUNSUPPORTED = 6, /**< A ZIP feature such as encryption is unsupported. */ + ZGET_ENOTFOUND = 7, /**< No exact member-name match was found. */ + ZGET_ECOMPRESSION = 8, /**< The compression method or data is unsupported. */ + ZGET_ECRC = 9, /**< Uncompressed output failed CRC32 validation. */ + ZGET_ECALLBACK = 10, /**< An output or listing callback rejected data. */ + ZGET_ENOMEM = 11 /**< Memory allocation failed. */ } zget_error; /** * Receives one immutable fragment of uncompressed member data. * - * `userdata` is the value passed to the extraction function. `data` is owned by - * libzget and remains valid only until the callback returns; the callback must - * copy bytes it needs to retain. Calls are synchronous and may use different - * buffers. Return zero after consuming all `size` bytes. Returning nonzero - * aborts extraction with ZGET_EIO. + * `userdata` is the value passed to zget_get(). `data` is owned by libzget and + * remains valid only until the callback returns. Calls are synchronous and may + * use different buffers. Return zero after consuming all `size` bytes. A + * nonzero return aborts extraction with ZGET_ECALLBACK. */ typedef int (*zget_write_cb)(void *userdata, const void *data, size_t size); -/** `zget_member_info.name` is known to contain valid UTF-8. */ -#define ZGET_MEMBER_NAME_UTF8 0x00000001u -/** The modification-time fields contain validated local calendar components. */ -#define ZGET_MEMBER_HAS_MODIFIED_TIME 0x00000002u - -/** - * One format-neutral member record produced during a streaming listing. - * - * The record and `name` are immutable, owned by libzget, and valid only during - * the listing callback. `name` is length-delimited and is not necessarily - * NUL-terminated. When ZGET_MEMBER_NAME_UTF8 is absent, preserve or escape the - * raw bytes rather than guessing an encoding. Modification fields are zero - * unless ZGET_MEMBER_HAS_MODIFIED_TIME is set. Ignore unknown flag bits. - */ +/** One semantic member record produced during a streaming listing. */ typedef struct zget_member_info { - size_t struct_size; /**< Bytes supplied by the running library. */ - const char *name; /**< Borrowed, length-delimited member name. */ - size_t name_length; /**< Number of bytes in `name`. */ - uint64_t uncompressed_size; /**< Expected uncompressed member size. */ - uint32_t flags; /**< ZGET_MEMBER_* flags; unknown bits are reserved. */ - uint16_t modified_year; /**< Local year, when the time flag is set. */ - uint8_t modified_month; /**< Local month in [1, 12]. */ - uint8_t modified_day; /**< Local day in [1, 31], validated for month. */ - uint8_t modified_hour; /**< Local hour in [0, 23]. */ - uint8_t modified_minute; /**< Local minute in [0, 59]. */ - uint8_t modified_second; /**< Local second in [0, 59]. */ + const char *name; /**< Borrowed, NUL-terminated valid UTF-8 name. */ + size_t name_length; /**< UTF-8 byte length, excluding the terminator. */ + uint64_t compressed_size; /**< Compressed member size in bytes. */ + uint64_t uncompressed_size; /**< Uncompressed member size in bytes. */ + uint32_t crc32; /**< CRC32 of the uncompressed member data. */ + uint16_t compression_method; /**< Numeric ZIP compression method identifier. */ + int64_t mtime; /**< Modification time as Unix UTC seconds. */ } zget_member_info; -/** Minimum `struct_size` containing every currently defined member-info field. */ -#define ZGET_MEMBER_INFO_V1_SIZE \ - (offsetof(zget_member_info, modified_second) + sizeof(uint8_t)) - /** * Receives one immutable member record at a time. * * `userdata` is the value passed to zget_list(). The record and its name become - * invalid when the callback returns. Return zero to continue listing; returning - * nonzero stops immediately with ZGET_EIO. + * invalid when the callback returns. Return zero to continue listing. A + * nonzero return stops immediately with ZGET_ECALLBACK. */ typedef int (*zget_list_cb)(void *userdata, const zget_member_info *member); /** - * Limits and HTTP policy copied when an archive is opened. - * - * Always call zget_options_init() before overriding fields. `struct_size` lets - * a newer library preserve defaults for fields unknown to an older caller. - */ -typedef struct zget_options { - size_t struct_size; /**< Initialized by zget_options_init(); do not alter. */ - uint64_t max_output_size; /**< Per extraction; zero means unlimited. */ - uint64_t max_metadata_bytes; /**< ZIP metadata limit; zero selects 8 MiB. */ - uint32_t max_http_requests; /**< Per context, including open; zero is unlimited. */ - uint32_t max_redirects; /**< Zero selects 8; values above 50 are invalid. */ -} zget_options; - -/** Minimum `struct_size` accepted for the original options layout. */ -#define ZGET_OPTIONS_V1_SIZE \ - (offsetof(zget_options, max_redirects) + sizeof(uint32_t)) - -/** - * Acquire zget's process-wide HTTP backend during single-threaded application - * startup. Every successful call must be matched by zget_global_cleanup() - * after all zget contexts have been closed and application threads using zget - * have stopped. Multiple acquisitions are supported and reference-counted. - * - * The lifecycle is deliberately explicit: libcurl's global state belongs to - * the process, not to an individual zget_ctx, and some libcurl builds require - * initialization before any additional threads are started. - * - * Returns ZGET_OK, ZGET_EHTTP, or ZGET_ELIMIT. - */ -ZGET_API int zget_global_init(void); - -/** - * Release one successful zget_global_init() acquisition. - * - * Call this during single-threaded shutdown after closing every context. An - * unmatched call is harmless, but does not replace correct lifecycle pairing. - */ -ZGET_API void zget_global_cleanup(void); - -/** - * Initialize `options`, including `struct_size`, to documented defaults. - * Passing NULL is a no-op. The caller retains ownership of the structure. - */ -ZGET_API void zget_options_init(zget_options *options); - -/** - * Open an HTTP(S) archive URL using copied `options` or defaults when NULL. + * Find an exact, case-sensitive UTF-8 member name and stream its contents. * - * zget_global_init() must already be acquired. On success, returns a new context - * owned by the caller and eventually released with zget_close(). On failure, - * returns NULL and discards detailed diagnostics; use zget_open_url_ex() when - * diagnostics are required. The URL and options need not outlive this call. + * Names are matched against the same resolved UTF-8 representation emitted by + * zget_list(), without path, Unicode, slash, or locale normalization. The first + * matching Central Directory entry wins. Output accepted before a later size, + * decompression, or CRC failure cannot be rolled back; only ZGET_OK guarantees + * complete validated output. */ -ZGET_API zget_ctx *zget_open_url(const char *archive_url, - const zget_options *options); - -/** - * Diagnostic form of zget_open_url(). - * - * `out_ctx` is required and is set to NULL before other validation. On success, - * returns ZGET_OK and stores a ready context. On most failures after allocation, - * it stores a diagnostic context whose error can be inspected; that context is - * not usable for archive operations. In every case, the caller owns any - * non-NULL result and must pass it to zget_close(). The URL and options are - * copied as needed and may be changed or released after this call. - */ -ZGET_API int zget_open_url_ex(const char *archive_url, - const zget_options *options, - zget_ctx **out_ctx); - -/** - * Find an exact, case-sensitive member name and stream its uncompressed data. - * - * `member_path` must be nonempty UTF-8 of at most 65535 bytes. The callback is - * invoked synchronously and must not reenter the same context. Data already - * accepted by the callback cannot be rolled back if a later size, DEFLATE, or - * CRC check fails; only ZGET_OK guarantees complete validated output. Reusing a - * context avoids reopening the URL but performs a fresh metadata scan. Returns - * ZGET_OK on success or another zget_error code; use the context diagnostics - * for details. - */ -ZGET_API int zget_extract_member(zget_ctx *ctx, const char *member_path, - zget_write_cb write_cb, void *userdata); - -/** - * Stream member metadata to `list_cb`. - * - * A NULL `member_path` lists all entries. A non-NULL path uses the same exact - * validation and matching rules as extraction, emits the first match, and - * returns ZGET_ENOTFOUND if none exists. Records are borrowed only for their - * synchronous callback. The callback must not reenter the same context. Returns - * ZGET_OK on success or another zget_error code; use the context diagnostics - * for details. - */ -ZGET_API int zget_list(zget_ctx *ctx, const char *member_path, - zget_list_cb list_cb, void *userdata); - -/** - * Open, extract, and close one member using default options. - * - * Global initialization is still required. This convenience call returns a - * zget_error code but discards detailed context diagnostics. Callback ownership - * and partial-output rules are identical to zget_extract_member(). - */ -ZGET_API int zget_get(const char *archive_url, const char *member_path, +ZGET_API int zget_get(const char *archive_url, const char *member_name, zget_write_cb write_cb, void *userdata); /** - * Release a successful or diagnostic context. Passing NULL is a no-op. Any - * pointers borrowed from the context become invalid when this function returns. - */ -ZGET_API void zget_close(zget_ctx *ctx); - -/** - * Return the context's current zget_error code, or ZGET_EINVAL when `ctx` is - * NULL. A successful context operation resets the stored code to ZGET_OK. + * Stream every archive member in Central Directory order. + * + * Records and names are borrowed only for their synchronous callback. The + * operation retains metadata for only one entry at a time. */ -ZGET_API int zget_last_error(const zget_ctx *ctx); +ZGET_API int zget_list(const char *archive_url, zget_list_cb list_cb, + void *userdata); /** * Return an immutable, process-lifetime description for a result code. - * Unknown integer values return a stable generic description. Never free the - * returned string. + * Unknown integer values return a stable generic description. */ ZGET_API const char *zget_error_string(int error); -/** - * Return the context's borrowed detailed diagnostic string. - * - * The pointer remains valid until the next operation on `ctx` or zget_close(), - * whichever occurs first, and must not be modified or freed. The string may be - * empty after success. Passing NULL returns a static diagnostic string. - */ -ZGET_API const char *zget_last_error_message(const zget_ctx *ctx); - /** Return the immutable, process-lifetime runtime library version string. */ ZGET_API const char *zget_version(void); diff --git a/src/error.h b/src/error.h index 43a9789..501b0f2 100644 --- a/src/error.h +++ b/src/error.h @@ -3,8 +3,8 @@ /* * Every internal layer reports through the same compact error state without - * depending on zget_ctx. This keeps sources and format engines independently - * testable while preserving one detailed diagnostic for public callers. + * depending on public operation state. This keeps sources and format engines + * independently testable while preserving one detailed internal diagnostic. */ struct zget_error_state { int code; diff --git a/src/format/format-private.h b/src/format/format-private.h index 388fb56..70b3aad 100644 --- a/src/format/format-private.h +++ b/src/format/format-private.h @@ -5,13 +5,12 @@ /* * The ZIP implementation embeds this header as its first member. The source - * and error state are borrowed from zget_ctx and therefore outlive the format - * object. Keeping ownership at the context makes teardown order explicit. + * and error state are borrowed from the active operation and outlive the format + * object. Keeping ownership at the operation makes teardown order explicit. */ struct zget_format { struct zget_source *source; struct zget_error_state *error; - struct zget_format_options options; }; #endif diff --git a/src/format/format.c b/src/format/format.c index 5b92c37..6cf8a9f 100644 --- a/src/format/format.c +++ b/src/format/format.c @@ -2,14 +2,13 @@ #include "format/zip/zip.h" int zget_format_open(struct zget_source *source, - const struct zget_format_options *options, struct zget_error_state *error, struct zget_format **out_format) { if (out_format == NULL) return ZGET_EINVAL; *out_format = NULL; - if (source == NULL || options == NULL || error == NULL) + if (source == NULL || error == NULL) return ZGET_EINVAL; /* @@ -17,7 +16,7 @@ int zget_format_open(struct zget_source *source, * narrow façade gives public orchestration a format-neutral vocabulary * without introducing a registry, vtable, or caller-visible extension API. */ - return zget_zip_format_open(source, options, error, out_format); + return zget_zip_format_open(source, error, out_format); } int zget_format_extract_member(struct zget_format *format, const char *member, @@ -28,12 +27,12 @@ int zget_format_extract_member(struct zget_format *format, const char *member, return zget_zip_format_extract_member(format, member, write_cb, userdata); } -int zget_format_list(struct zget_format *format, const char *member, - zget_list_cb list_cb, void *userdata) +int zget_format_list(struct zget_format *format, zget_list_cb list_cb, + void *userdata) { if (format == NULL || list_cb == NULL) return ZGET_EINVAL; - return zget_zip_format_list(format, member, list_cb, userdata); + return zget_zip_format_list(format, list_cb, userdata); } void zget_format_close(struct zget_format *format) diff --git a/src/format/format.h b/src/format/format.h index e94d910..9a2bc67 100644 --- a/src/format/format.h +++ b/src/format/format.h @@ -5,27 +5,15 @@ #include "source/source.h" #include "zget.h" -#include - struct zget_format; -/* - * Format engines receive only limits that affect parsing or extraction. HTTP - * request and redirect policy stays with the source that enforces it. - */ -struct zget_format_options { - uint64_t max_metadata_bytes; - uint64_t max_output_size; -}; - int zget_format_open(struct zget_source *source, - const struct zget_format_options *options, struct zget_error_state *error, struct zget_format **out_format); int zget_format_extract_member(struct zget_format *format, const char *member, zget_write_cb write_cb, void *userdata); -int zget_format_list(struct zget_format *format, const char *member, - zget_list_cb list_cb, void *userdata); +int zget_format_list(struct zget_format *format, zget_list_cb list_cb, + void *userdata); void zget_format_close(struct zget_format *format); #endif diff --git a/src/format/zip/extract.c b/src/format/zip/extract.c index 40a4246..c4f2d9a 100644 --- a/src/format/zip/extract.c +++ b/src/format/zip/extract.c @@ -23,16 +23,13 @@ struct extractor { static int emit(struct extractor *x, const unsigned char *data, size_t length) { - /* Enforce the limit before exposing bytes; a failed call produces nothing. */ - if (length > UINT64_MAX - x->produced || - (x->zip->format.options.max_output_size != 0 && - x->produced + length > x->zip->format.options.max_output_size)) { - zget_error_set(x->zip->format.error, ZGET_ELIMIT, - "output size limit exceeded"); + if (length > UINT64_MAX - x->produced) { + zget_error_set(x->zip->format.error, ZGET_EZIP, + "uncompressed size overflows uint64_t"); return 1; } if (length != 0 && x->output(x->userdata, data, length) != 0) { - zget_error_set(x->zip->format.error, ZGET_EIO, + zget_error_set(x->zip->format.error, ZGET_ECALLBACK, "output callback failed"); return 1; } @@ -72,7 +69,7 @@ static enum zget_source_action deflate_write(void *opaque, const void *data, * than permission to ignore trailing bytes. */ if (x->stream_end) { - zget_error_set(x->zip->format.error, ZGET_EDEFLATE, + zget_error_set(x->zip->format.error, ZGET_ECOMPRESSION, "trailing bytes after DEFLATE stream"); return ZGET_SOURCE_ERROR; } @@ -87,7 +84,7 @@ static enum zget_source_action deflate_write(void *opaque, const void *data, x->stream.avail_out = sizeof(output); zr = inflate(&x->stream, Z_NO_FLUSH); if (zr != Z_OK && zr != Z_STREAM_END) { - zget_error_set(x->zip->format.error, ZGET_EDEFLATE, + zget_error_set(x->zip->format.error, ZGET_ECOMPRESSION, "invalid DEFLATE stream"); return ZGET_SOURCE_ERROR; } @@ -97,7 +94,7 @@ static enum zget_source_action deflate_write(void *opaque, const void *data, if (zr == Z_STREAM_END) { x->stream_end = true; if (x->stream.avail_in != 0 || length != part) { - zget_error_set(x->zip->format.error, ZGET_EDEFLATE, + zget_error_set(x->zip->format.error, ZGET_ECOMPRESSION, "trailing bytes after DEFLATE stream"); return ZGET_SOURCE_ERROR; } @@ -125,18 +122,12 @@ int zget_zip_extract_payload(struct zget_zip_format *zip, x.userdata = userdata; x.crc = crc32(0L, Z_NULL, 0); - if (zip->format.options.max_output_size != 0 && - entry->uncompressed_size > zip->format.options.max_output_size) { - zget_error_set(zip->format.error, ZGET_ELIMIT, - "member exceeds output size limit"); - return ZGET_ELIMIT; - } if (entry->compression_method == 8) { /* ZIP stores raw DEFLATE blocks with no zlib or gzip wrapper/trailer. */ if (inflateInit2(&x.stream, -MAX_WBITS) != Z_OK) { - zget_error_set(zip->format.error, ZGET_EDEFLATE, + zget_error_set(zip->format.error, ZGET_ECOMPRESSION, "could not initialize zlib"); - return ZGET_EDEFLATE; + return ZGET_ECOMPRESSION; } x.inflate_initialized = true; } @@ -154,9 +145,9 @@ int zget_zip_extract_payload(struct zget_zip_format *zip, } /* A fully consumed source range is not proof that DEFLATE reached its end. */ if (entry->compression_method == 8 && !x.stream_end) { - zget_error_set(zip->format.error, ZGET_EDEFLATE, + zget_error_set(zip->format.error, ZGET_ECOMPRESSION, "truncated DEFLATE stream"); - rc = ZGET_EDEFLATE; + rc = ZGET_ECOMPRESSION; goto done; } if (x.produced != entry->uncompressed_size) { diff --git a/src/format/zip/zip.c b/src/format/zip/zip.c index 2785429..5869d5c 100644 --- a/src/format/zip/zip.c +++ b/src/format/zip/zip.c @@ -3,8 +3,10 @@ #include "util.h" +#include #include #include +#include #define ZIP_TAIL_SIZE 131072u @@ -14,6 +16,25 @@ struct buffer { size_t length; }; +static const uint16_t cp437_high[128] = { + 0x00c7, 0x00fc, 0x00e9, 0x00e2, 0x00e4, 0x00e0, 0x00e5, 0x00e7, + 0x00ea, 0x00eb, 0x00e8, 0x00ef, 0x00ee, 0x00ec, 0x00c4, 0x00c5, + 0x00c9, 0x00e6, 0x00c6, 0x00f4, 0x00f6, 0x00f2, 0x00fb, 0x00f9, + 0x00ff, 0x00d6, 0x00dc, 0x00a2, 0x00a3, 0x00a5, 0x20a7, 0x0192, + 0x00e1, 0x00ed, 0x00f3, 0x00fa, 0x00f1, 0x00d1, 0x00aa, 0x00ba, + 0x00bf, 0x2310, 0x00ac, 0x00bd, 0x00bc, 0x00a1, 0x00ab, 0x00bb, + 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, + 0x2555, 0x2563, 0x2551, 0x2557, 0x255d, 0x255c, 0x255b, 0x2510, + 0x2514, 0x2534, 0x252c, 0x251c, 0x2500, 0x253c, 0x255e, 0x255f, + 0x255a, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256c, 0x2567, + 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256b, + 0x256a, 0x2518, 0x250c, 0x2588, 0x2584, 0x258c, 0x2590, 0x2580, + 0x03b1, 0x00df, 0x0393, 0x03c0, 0x03a3, 0x03c3, 0x00b5, 0x03c4, + 0x03a6, 0x0398, 0x03a9, 0x03b4, 0x221e, 0x03c6, 0x03b5, 0x2229, + 0x2261, 0x00b1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00f7, 0x2248, + 0x00b0, 0x2219, 0x00b7, 0x221a, 0x207f, 0x00b2, 0x25a0, 0x00a0 +}; + static enum zget_source_action buffer_write(void *opaque, const void *data, size_t length) { @@ -94,13 +115,6 @@ static int zip_parse_tail(struct zget_zip_format *zip, "central directory lies outside the archive"); return ZGET_EZIP; } - if ((zip->format.options.max_metadata_bytes != 0 && - eocd.cd_size > zip->format.options.max_metadata_bytes) || - eocd.cd_size > SIZE_MAX) { - zget_error_set(zip->format.error, ZGET_ELIMIT, - "central directory exceeds metadata limit"); - return ZGET_ELIMIT; - } zip->cd_offset = eocd.cd_offset; zip->cd_size = eocd.cd_size; zip->entry_count = eocd.entries; @@ -129,15 +143,16 @@ struct cd_parser { size_t have; unsigned char *name; unsigned char *extra; - size_t name_capacity, extra_capacity; + unsigned char *resolved; + size_t name_capacity, extra_capacity, resolved_capacity; + size_t resolved_length; size_t name_pos, extra_pos, comment_pos; uint16_t name_len, extra_len, comment_len, flags, method, disk16; uint16_t modified_time, modified_date; uint32_t crc, compressed32, size32, offset32; uint64_t entries; uint64_t archive_size; - uint32_t name_flags; - bool matching, retaining_extra; + bool matching; }; static int cd_fail(struct cd_parser *p, int error, const char *message) @@ -146,6 +161,225 @@ static int cd_fail(struct cd_parser *p, int error, const char *message) return 1; } +static bool extra_fields_valid(const unsigned char *extra, size_t length) +{ + size_t pos = 0; + + while (pos < length) { + uint16_t field_length; + + if (length - pos < 4) + return false; + field_length = zget_le16(extra + pos + 2); + pos += 4; + if (field_length > length - pos) + return false; + pos += field_length; + } + return true; +} + +static int resolved_reserve(struct cd_parser *p, size_t capacity) +{ + unsigned char *larger; + + if (capacity <= p->resolved_capacity) + return 0; + larger = realloc(p->resolved, capacity); + if (larger == NULL) + return cd_fail(p, ZGET_ENOMEM, "could not allocate resolved entry name"); + p->resolved = larger; + p->resolved_capacity = capacity; + return 0; +} + +static size_t utf8_encode(uint16_t codepoint, unsigned char *output) +{ + if (codepoint < 0x80) { + output[0] = (unsigned char)codepoint; + return 1; + } + if (codepoint < 0x800) { + output[0] = (unsigned char)(0xc0u | (codepoint >> 6)); + output[1] = (unsigned char)(0x80u | (codepoint & 0x3fu)); + return 2; + } + output[0] = (unsigned char)(0xe0u | (codepoint >> 12)); + output[1] = (unsigned char)(0x80u | ((codepoint >> 6) & 0x3fu)); + output[2] = (unsigned char)(0x80u | (codepoint & 0x3fu)); + return 3; +} + +static bool unicode_path_name(struct cd_parser *p, + const unsigned char **name, size_t *length) +{ + uLong raw_crc = crc32(0L, Z_NULL, 0); + size_t pos = 0; + + if (p->name_len != 0) + raw_crc = crc32(raw_crc, p->name, p->name_len); + while (pos < p->extra_len) { + uint16_t id = zget_le16(p->extra + pos); + uint16_t field_length = zget_le16(p->extra + pos + 2); + const unsigned char *data = p->extra + pos + 4; + + pos += 4u + field_length; + if (id != 0x7075u || field_length < 5 || data[0] != 1 || + zget_le32(data + 1) != (uint32_t)raw_crc) + continue; + *name = data + 5; + *length = field_length - 5u; + if (zget_valid_utf8(*name, *length) && + (*length == 0 || memchr(*name, '\0', *length) == NULL)) + return true; + } + return false; +} + +static int resolve_name(struct cd_parser *p) +{ + const unsigned char *source = p->name; + size_t source_length = p->name_len; + bool utf8 = (p->flags & (1u << 11)) != 0; + size_t i, length = 0; + + if (utf8) { + if (!zget_valid_utf8(source, source_length)) + return cd_fail(p, ZGET_EZIP, "entry name marked UTF-8 is invalid"); + } else if (!unicode_path_name(p, &source, &source_length)) { + if (resolved_reserve(p, (size_t)p->name_len * 3u + 1u) != 0) + return 1; + for (i = 0; i < p->name_len; ++i) { + uint16_t codepoint = p->name[i] < 0x80 ? + p->name[i] : cp437_high[p->name[i] - 0x80]; + length += utf8_encode(codepoint, p->resolved + length); + } + p->resolved[length] = '\0'; + p->resolved_length = length; + if (memchr(p->resolved, '\0', length) != NULL) + return cd_fail(p, ZGET_EZIP, "entry name contains an embedded NUL"); + return 0; + } + + if (source_length != 0 && memchr(source, '\0', source_length) != NULL) + return cd_fail(p, ZGET_EZIP, "entry name contains an embedded NUL"); + if (resolved_reserve(p, source_length + 1u) != 0) + return 1; + if (source_length != 0) + memcpy(p->resolved, source, source_length); + p->resolved[source_length] = '\0'; + p->resolved_length = source_length; + return 0; +} + +static bool ntfs_mtime(struct cd_parser *p, int64_t *mtime) +{ + static const uint64_t unix_epoch = UINT64_C(116444736000000000); + static const uint64_t ticks_per_second = UINT64_C(10000000); + size_t pos = 0; + + while (pos < p->extra_len) { + uint16_t id = zget_le16(p->extra + pos); + uint16_t field_length = zget_le16(p->extra + pos + 2); + const unsigned char *data = p->extra + pos + 4; + size_t nested = 4; + + pos += 4u + field_length; + if (id != 0x000au || field_length < 4) + continue; + while (nested < field_length) { + uint16_t tag, attribute_length; + uint64_t ticks, delta; + + if (field_length - nested < 4) + break; + tag = zget_le16(data + nested); + attribute_length = zget_le16(data + nested + 2); + nested += 4; + if (attribute_length > field_length - nested) + break; + if (tag == 0x0001u && attribute_length == 24) { + ticks = zget_le64(data + nested); + if (ticks >= unix_epoch) + *mtime = (int64_t)((ticks - unix_epoch) / ticks_per_second); + else { + delta = unix_epoch - ticks; + *mtime = -(int64_t)(delta / ticks_per_second); + if (delta % ticks_per_second != 0) + --*mtime; + } + return true; + } + nested += attribute_length; + } + } + return false; +} + +static bool extended_mtime(struct cd_parser *p, int64_t *mtime) +{ + size_t pos = 0; + + while (pos < p->extra_len) { + uint16_t id = zget_le16(p->extra + pos); + uint16_t field_length = zget_le16(p->extra + pos + 2); + const unsigned char *data = p->extra + pos + 4; + + pos += 4u + field_length; + if (id == 0x5455u && field_length >= 5 && (data[0] & 1u) != 0) { + uint32_t raw = zget_le32(data + 1); + + /* Decode the signed wire value without implementation-defined casts. */ + *mtime = raw <= INT32_MAX ? (int64_t)raw : + -(int64_t)(UINT32_MAX - raw) - 1; + return true; + } + } + return false; +} + +static bool dos_mtime(struct cd_parser *p, int64_t *mtime) +{ + static const uint8_t month_days[] = + {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + unsigned int year = 1980u + (p->modified_date >> 9); + unsigned int month = (p->modified_date >> 5) & 0x0fu; + unsigned int day = p->modified_date & 0x1fu; + unsigned int hour = p->modified_time >> 11; + unsigned int minute = (p->modified_time >> 5) & 0x3fu; + unsigned int second = (p->modified_time & 0x1fu) * 2u; + unsigned int maximum_day; + int64_t adjusted_year, era, year_of_era, day_of_year, day_of_era, days; + + if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) + return false; + maximum_day = month_days[month - 1]; + if (month == 2 && year % 4u == 0 && + (year % 100u != 0 || year % 400u == 0)) + ++maximum_day; + if (day < 1 || day > maximum_day) + return false; + + adjusted_year = year - (month <= 2 ? 1 : 0); + era = adjusted_year / 400; + year_of_era = adjusted_year - era * 400; + day_of_year = (153 * ((int64_t)month + (month > 2 ? -3 : 9)) + 2) / 5 + + day - 1; + day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + + day_of_year; + days = era * 146097 + day_of_era - 719468; + *mtime = days * 86400 + hour * 3600 + minute * 60 + second; + return true; +} + +static int resolve_mtime(struct cd_parser *p, int64_t *mtime) +{ + if (ntfs_mtime(p, mtime) || extended_mtime(p, mtime) || + dos_mtime(p, mtime)) + return 0; + return cd_fail(p, ZGET_EZIP, "entry has an invalid modification time"); +} + static int cd_resolve_entry(struct cd_parser *p, uint64_t *size, uint64_t *compressed, uint64_t *offset) { @@ -171,50 +405,22 @@ static int cd_emit_listing_entry(struct cd_parser *p) { zget_member_info member; uint64_t size, compressed, offset; - static const uint8_t month_days[] = - {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - unsigned int year, month, day, hour, minute, second, maximum_day; if (cd_resolve_entry(p, &size, &compressed, &offset) != 0) return 1; - (void)compressed; (void)offset; memset(&member, 0, sizeof(member)); - member.struct_size = sizeof(member); - member.name = p->name_len == 0 ? "" : (const char *)p->name; - member.name_length = p->name_len; + member.name = (const char *)p->resolved; + member.name_length = p->resolved_length; + member.compressed_size = compressed; member.uncompressed_size = size; - member.flags = p->name_flags; - /* - * DOS timestamps are local calendar values with no timezone. Preserve that - * honest representation instead of inventing an epoch or applying the - * machine's current timezone. Invalid packed fields remain unavailable. - */ - year = 1980u + (p->modified_date >> 9); - month = (p->modified_date >> 5) & 0x0fu; - day = p->modified_date & 0x1fu; - hour = p->modified_time >> 11; - minute = (p->modified_time >> 5) & 0x3fu; - second = (p->modified_time & 0x1fu) * 2u; - if (month >= 1 && month <= 12) { - maximum_day = month_days[month - 1]; - if (month == 2 && (year % 4u == 0) && - (year % 100u != 0 || year % 400u == 0)) - ++maximum_day; - if (day >= 1 && day <= maximum_day && hour <= 23 && minute <= 59 && - second <= 59) { - member.modified_year = (uint16_t)year; - member.modified_month = (uint8_t)month; - member.modified_day = (uint8_t)day; - member.modified_hour = (uint8_t)hour; - member.modified_minute = (uint8_t)minute; - member.modified_second = (uint8_t)second; - member.flags |= ZGET_MEMBER_HAS_MODIFIED_TIME; - } - } + member.crc32 = p->crc; + member.compression_method = p->method; + if (resolve_mtime(p, &member.mtime) != 0) + return 1; /* The borrowed name remains valid until this synchronous callback returns. */ if (p->list_cb(p->list_userdata, &member) != 0) - return cd_fail(p, ZGET_EIO, "listing callback rejected entry"); + return cd_fail(p, ZGET_ECALLBACK, "listing callback rejected entry"); return 0; } @@ -242,31 +448,9 @@ static int cd_finish_match(struct cd_parser *p) static int cd_name_done(struct cd_parser *p) { - bool utf8 = (p->flags & (1u << 11)) != 0; - bool match_eligible = true; - size_t i; unsigned char *larger; - /* - * UTF-8-flagged names must be well-formed. Legacy non-ASCII bytes are not - * converted, so they are deliberately ineligible for a match rather than - * compared under a locale or silently mistaken for UTF-8. Listing may still - * expose those raw bytes, clearly marked for the CLI to escape. - */ - if (utf8) { - if (!zget_valid_utf8(p->name, p->name_len)) - return cd_fail(p, ZGET_EZIP, "entry name marked UTF-8 is invalid"); - } else { - for (i = 0; i < p->name_len; ++i) - if (p->name[i] >= 0x80) { match_eligible = false; break; } - } - p->name_flags = (utf8 || match_eligible) ? ZGET_MEMBER_NAME_UTF8 : 0; - p->matching = p->target != NULL && match_eligible && - p->name_len == p->target_len && - memcmp(p->name, p->target, p->target_len) == 0; - p->retaining_extra = p->matching || - (p->list_cb != NULL && p->target == NULL); - if (p->retaining_extra && p->extra_len > p->extra_capacity) { + if (p->extra_len > p->extra_capacity) { /* Reuse the largest buffer instead of allocating once per listed entry. */ larger = realloc(p->extra, p->extra_len); if (larger == NULL) @@ -281,15 +465,18 @@ static int cd_name_done(struct cd_parser *p) static int cd_extra_done(struct cd_parser *p) { + if (!extra_fields_valid(p->extra, p->extra_len)) + return cd_fail(p, ZGET_EZIP, "malformed ZIP entry extra fields"); + if (resolve_name(p) != 0) + return 1; + p->matching = p->target != NULL && + p->resolved_length == p->target_len && + memcmp(p->resolved, p->target, p->target_len) == 0; if (p->matching && p->result != NULL) return cd_finish_match(p); - if (p->list_cb != NULL && (p->target == NULL || p->matching)) { + if (p->list_cb != NULL) { if (cd_emit_listing_entry(p) != 0) return 1; - if (p->matching) { - p->state = CD_COMPLETE; - return 0; - } } ++p->entries; p->comment_pos = 0; @@ -368,8 +555,7 @@ static enum zget_source_action cd_write(void *opaque, const void *data, } } else if (p->state == CD_EXTRA) { need = p->extra_len - p->extra_pos; take = length < need ? length : need; - if (p->retaining_extra) - memcpy(p->extra + p->extra_pos, in, take); + memcpy(p->extra + p->extra_pos, in, take); p->extra_pos += take; in += take; length -= take; if (p->extra_pos == p->extra_len) { if (cd_extra_done(p) != 0) @@ -416,6 +602,7 @@ int zget_zip_fuzz_cd_stream(const unsigned char *data, size_t length, } free(p.name); free(p.extra); + free(p.resolved); return (int)action; } @@ -448,8 +635,10 @@ static int zip_walk_cd(struct zget_zip_format *zip, struct cd_parser *p) /* Parser scratch storage is owned here on success, failure, and early stop. */ free(p->name); free(p->extra); + free(p->resolved); p->name = NULL; p->extra = NULL; + p->resolved = NULL; if (rc != ZGET_OK || p->state == CD_COMPLETE) return rc; if (p->state != CD_HEADER || p->have != 0 || @@ -488,36 +677,24 @@ static int zip_validate_member(struct zget_format *format, const char *member) { size_t length = strlen(member); - /* ZIP names are exact byte identifiers, never normalized filesystem paths. */ - if (length == 0 || length > UINT16_MAX || - !zget_valid_utf8((const unsigned char *)member, length)) { + /* Resolved ZIP names are exact UTF-8 identifiers, never normalized paths. */ + if (length == 0 || !zget_valid_utf8((const unsigned char *)member, length)) { zget_error_set(format->error, ZGET_EINVAL, - "member path must be non-empty, valid UTF-8, and at most 65535 bytes"); + "member name must be non-empty valid UTF-8"); return ZGET_EINVAL; } return ZGET_OK; } -int zget_zip_format_list(struct zget_format *format, const char *member, - zget_list_cb list_cb, void *userdata) +int zget_zip_format_list(struct zget_format *format, zget_list_cb list_cb, + void *userdata) { struct zget_zip_format *zip = (struct zget_zip_format *)format; struct cd_parser p = {0}; - int rc; p.list_cb = list_cb; p.list_userdata = userdata; - if (member != NULL) { - if ((rc = zip_validate_member(format, member)) != ZGET_OK) - return rc; - p.target = (const unsigned char *)member; - p.target_len = strlen(member); - } - rc = zip_walk_cd(zip, &p); - if (rc != ZGET_OK || member == NULL || p.state == CD_COMPLETE) - return rc; - zget_error_set(format->error, ZGET_ENOTFOUND, "member not found"); - return ZGET_ENOTFOUND; + return zip_walk_cd(zip, &p); } static int zip_read_local_header(struct zget_zip_format *zip, @@ -647,7 +824,6 @@ void zget_zip_format_close(struct zget_format *format) } int zget_zip_format_open(struct zget_source *source, - const struct zget_format_options *options, struct zget_error_state *error, struct zget_format **out_format) { @@ -660,7 +836,7 @@ int zget_zip_format_open(struct zget_source *source, if (out_format == NULL) return ZGET_EINVAL; *out_format = NULL; - if (source == NULL || options == NULL || error == NULL) + if (source == NULL || error == NULL) return ZGET_EINVAL; zip = calloc(1, sizeof(*zip)); @@ -670,21 +846,12 @@ int zget_zip_format_open(struct zget_source *source, } zip->format.source = source; zip->format.error = error; - zip->format.options = *options; /* * EOCD is at least 22 bytes and can follow a 65535-byte archive comment. * A 128 KiB suffix also normally contains the ZIP64 locator and fixed EOCD, * keeping the common open path to one bounded metadata request. */ - if (options->max_metadata_bytes < request_size) - request_size = options->max_metadata_bytes; - if (request_size < 22) { - zget_error_set(error, ZGET_ELIMIT, - "metadata limit is too small for ZIP EOCD"); - rc = ZGET_ELIMIT; - goto fail; - } tail.capacity = (size_t)request_size; tail.data = malloc(tail.capacity); if (tail.data == NULL) { diff --git a/src/format/zip/zip.h b/src/format/zip/zip.h index ca01679..1dfdfce 100644 --- a/src/format/zip/zip.h +++ b/src/format/zip/zip.h @@ -4,14 +4,13 @@ #include "format/format.h" int zget_zip_format_open(struct zget_source *source, - const struct zget_format_options *options, struct zget_error_state *error, struct zget_format **out_format); int zget_zip_format_extract_member(struct zget_format *format, const char *member, zget_write_cb write_cb, void *userdata); -int zget_zip_format_list(struct zget_format *format, const char *member, - zget_list_cb list_cb, void *userdata); +int zget_zip_format_list(struct zget_format *format, zget_list_cb list_cb, + void *userdata); void zget_zip_format_close(struct zget_format *format); #endif diff --git a/src/internal.h b/src/internal.h index c5201fd..7fe8aad 100644 --- a/src/internal.h +++ b/src/internal.h @@ -2,26 +2,14 @@ #define ZGET_INTERNAL_H #include "error.h" -#include "zget.h" - -#include - -#define ZGET_DEFAULT_METADATA (8u * 1024u * 1024u) struct zget_format; struct zget_source; -/* - * The public context coordinates two independently owned internal objects. - * Format engines borrow the source and shared error state, so close the format - * first and the source second. No transport- or format-specific state belongs - * in this public-API orchestration object. - */ -struct zget_ctx { +/* One short-lived archive operation owns one source and format instance. */ +struct zget_operation { struct zget_source *source; struct zget_format *format; - bool ready; - zget_options options; struct zget_error_state error; }; diff --git a/src/source/http.c b/src/source/http.c index ac6918f..bdd3209 100644 --- a/src/source/http.c +++ b/src/source/http.c @@ -19,8 +19,6 @@ struct zget_http_source { char *url; char *effective_url; char *strong_etag; - uint64_t requests; - uint32_t max_requests; uint32_t max_redirects; }; @@ -287,11 +285,6 @@ static int perform_range(struct zget_http_source *http, uint64_t offset, "invalid zero-length range request"); return ZGET_EINVAL; } - if (http->max_requests != 0 && http->requests >= http->max_requests) { - zget_error_set(http->source.error, ZGET_ELIMIT, - "HTTP request limit exceeded"); - return ZGET_ELIMIT; - } if (suffix) (void)snprintf(range, sizeof(range), "-%" PRIu64, length); else { @@ -354,7 +347,6 @@ static int perform_range(struct zget_http_source *http, uint64_t offset, SETOPT(CURLOPT_HTTPHEADER, headers); SETOPT(CURLOPT_WRITEFUNCTION, body_cb); SETOPT(CURLOPT_WRITEDATA, &r); - ++http->requests; cc = curl_easy_perform(http->curl); /* Zero-length/error responses may never invoke body_cb; inspect them here. */ @@ -365,7 +357,7 @@ static int perform_range(struct zget_http_source *http, uint64_t offset, goto done; if (r.callback_failed) { if (http->source.error->code == ZGET_OK) - zget_error_set(http->source.error, ZGET_EIO, + zget_error_set(http->source.error, ZGET_ECALLBACK, "range consumer rejected output"); goto done; } @@ -596,7 +588,6 @@ int zget_http_source_open(const char *url, return ZGET_ENOMEM; } zget_source_init(&http_source->source, &http_ops, error); - http_source->max_requests = options->max_requests; http_source->max_redirects = options->max_redirects; http_source->url = zget_strdup(url); if (http_source->url == NULL) { diff --git a/src/source/http.h b/src/source/http.h index 9e08ec0..ddf137f 100644 --- a/src/source/http.h +++ b/src/source/http.h @@ -7,7 +7,6 @@ #include struct zget_http_options { - uint32_t max_requests; uint32_t max_redirects; }; diff --git a/src/zget.c b/src/zget.c index 8eea6df..9c28913 100644 --- a/src/zget.c +++ b/src/zget.c @@ -1,236 +1,94 @@ #include "internal.h" + #include "format/format.h" #include "source/http.h" #include "source/source.h" +#include "util.h" +#include "zget.h" -#include -#include #include -/* - * Global initialization is owned by the embedding application rather than by - * individual contexts. Keep our count aligned with libcurl's own acquisition - * count so zget composes safely with other libraries that also use libcurl. - * - * This counter intentionally needs no lock: the public contract requires init - * and cleanup during single-threaded startup and shutdown. Between those - * phases it is read-only, so distinct contexts can be used by distinct threads. - */ -static unsigned int global_references; +#define ZGET_DEFAULT_REDIRECTS 8u -int zget_global_init(void) +static int operation_open(const char *archive_url, + struct zget_operation *operation) { + struct zget_http_options http_options = {ZGET_DEFAULT_REDIRECTS}; int rc; - if (global_references == UINT_MAX) - return ZGET_ELIMIT; - rc = zget_http_global_init(); - if (rc != ZGET_OK) - return rc; - ++global_references; - return ZGET_OK; -} - -void zget_global_cleanup(void) -{ - if (global_references == 0) - return; - --global_references; - zget_http_global_cleanup(); + memset(operation, 0, sizeof(*operation)); + rc = zget_http_source_open(archive_url, &http_options, &operation->error, + &operation->source); + if (rc == ZGET_OK) + rc = zget_format_open(operation->source, &operation->error, + &operation->format); + return rc; } -void zget_options_init(zget_options *options) +static void operation_close(struct zget_operation *operation) { - if (options == NULL) - return; - memset(options, 0, sizeof(*options)); - options->struct_size = sizeof(*options); - options->max_metadata_bytes = ZGET_DEFAULT_METADATA; - options->max_redirects = 8; + zget_format_close(operation->format); + zget_source_close(operation->source); } -static int copy_options(struct zget_ctx *ctx, const zget_options *options) +static int member_valid(const char *member) { - size_t copy_size; + size_t length; - /* Defaults survive for every field beyond the caller's struct_size. */ - zget_options_init(&ctx->options); - if (options == NULL) - return ZGET_OK; - if (options->struct_size < ZGET_OPTIONS_V1_SIZE) { - zget_error_set(&ctx->error, ZGET_EINVAL, - "zget_options struct is too small"); - return ZGET_EINVAL; - } - copy_size = options->struct_size; - if (copy_size > sizeof(ctx->options)) - copy_size = sizeof(ctx->options); - memcpy(&ctx->options, options, copy_size); - - /* Internal code always sees the library's complete, normalized structure. */ - ctx->options.struct_size = sizeof(ctx->options); - if (ctx->options.max_metadata_bytes == 0) - ctx->options.max_metadata_bytes = ZGET_DEFAULT_METADATA; - if (ctx->options.max_redirects == 0) - ctx->options.max_redirects = 8; - if (ctx->options.max_redirects > 50) { - zget_error_set(&ctx->error, ZGET_EINVAL, - "max_redirects must not exceed 50"); - return ZGET_EINVAL; - } - return ZGET_OK; + if (member == NULL || member[0] == '\0') + return 0; + length = strlen(member); + return zget_valid_utf8((const unsigned char *)member, length); } -int zget_open_url_ex(const char *archive_url, const zget_options *options, - zget_ctx **out_ctx) +int zget_get(const char *archive_url, const char *member_name, + zget_write_cb write_cb, void *userdata) { - struct zget_ctx *ctx; - struct zget_format_options format_options; - struct zget_http_options http_options; + struct zget_operation operation; int rc; - if (out_ctx == NULL) - return ZGET_EINVAL; - /* - * Establish a safe ownership state before validating anything else. A - * caller can therefore unconditionally close *out_ctx after any failure - * without accidentally reusing the pointer that it supplied. - */ - *out_ctx = NULL; - if (archive_url == NULL || archive_url[0] == '\0') - return ZGET_EINVAL; - if (global_references == 0) - return ZGET_ENOTINITIALIZED; - ctx = calloc(1, sizeof(*ctx)); - if (ctx == NULL) - return ZGET_ENOMEM; - if ((rc = copy_options(ctx, options)) != ZGET_OK) - goto fail; - http_options.max_requests = ctx->options.max_http_requests; - http_options.max_redirects = ctx->options.max_redirects; - if ((rc = zget_http_source_open(archive_url, &http_options, &ctx->error, - &ctx->source)) != ZGET_OK) - goto fail; - format_options.max_metadata_bytes = ctx->options.max_metadata_bytes; - format_options.max_output_size = ctx->options.max_output_size; - rc = zget_format_open(ctx->source, &format_options, &ctx->error, - &ctx->format); + if (archive_url == NULL || archive_url[0] == '\0' || + !member_valid(member_name) || write_cb == NULL) + return ZGET_EINVAL; + rc = zget_http_global_init(); if (rc != ZGET_OK) - goto fail; - - ctx->ready = true; - *out_ctx = ctx; - return ZGET_OK; -fail: - if (ctx->error.code == ZGET_OK) - zget_error_set(&ctx->error, rc, "%s", zget_error_string(rc)); - /* - * Unlike zget_open_url(), the explicit API transfers this failed context to - * the caller so the detailed diagnostic survives. zget_close() accepts it. - */ - *out_ctx = ctx; + return rc; + rc = operation_open(archive_url, &operation); + if (rc == ZGET_OK) + rc = zget_format_extract_member(operation.format, member_name, + write_cb, userdata); + operation_close(&operation); + zget_http_global_cleanup(); return rc; } -zget_ctx *zget_open_url(const char *archive_url, const zget_options *options) -{ - zget_ctx *ctx = NULL; - if (zget_open_url_ex(archive_url, options, &ctx) != ZGET_OK) { - zget_close(ctx); - return NULL; - } - return ctx; -} - -int zget_extract_member(zget_ctx *ctx, const char *member_path, - zget_write_cb write_cb, void *userdata) +int zget_list(const char *archive_url, zget_list_cb list_cb, void *userdata) { - if (ctx == NULL) - return ZGET_EINVAL; - if (!ctx->ready) - return ctx->error.code != ZGET_OK ? ctx->error.code : ZGET_EINVAL; - /* - * Every attempted operation on a ready context replaces its diagnostic. - * In particular, report invalid call arguments instead of leaving an older - * network or archive error visible through zget_last_error(). - */ - ctx->error.code = ZGET_OK; - ctx->error.message[0] = '\0'; - if (member_path == NULL || write_cb == NULL) { - zget_error_set(&ctx->error, ZGET_EINVAL, - "member path and write callback are required"); - return ZGET_EINVAL; - } - return zget_format_extract_member(ctx->format, member_path, - write_cb, userdata); -} - -int zget_list(zget_ctx *ctx, const char *member_path, - zget_list_cb list_cb, void *userdata) -{ - if (ctx == NULL) - return ZGET_EINVAL; - if (!ctx->ready) - return ctx->error.code != ZGET_OK ? ctx->error.code : ZGET_EINVAL; - /* See zget_extract_member(): ready-context diagnostics are per operation. */ - ctx->error.code = ZGET_OK; - ctx->error.message[0] = '\0'; - if (list_cb == NULL) { - zget_error_set(&ctx->error, ZGET_EINVAL, - "listing callback is required"); - return ZGET_EINVAL; - } - return zget_format_list(ctx->format, member_path, list_cb, userdata); -} - -int zget_get(const char *archive_url, const char *member_path, - zget_write_cb write_cb, void *userdata) -{ - zget_ctx *ctx = NULL; + struct zget_operation operation; int rc; - /* Reject invalid convenience-API arguments before they can cause I/O. */ - if (archive_url == NULL || archive_url[0] == '\0' || member_path == NULL || - member_path[0] == '\0' || write_cb == NULL) + + if (archive_url == NULL || archive_url[0] == '\0' || list_cb == NULL) return ZGET_EINVAL; - rc = zget_open_url_ex(archive_url, NULL, &ctx); + rc = zget_http_global_init(); + if (rc != ZGET_OK) + return rc; + rc = operation_open(archive_url, &operation); if (rc == ZGET_OK) - rc = zget_extract_member(ctx, member_path, write_cb, userdata); - zget_close(ctx); + rc = zget_format_list(operation.format, list_cb, userdata); + operation_close(&operation); + zget_http_global_cleanup(); return rc; } -void zget_close(zget_ctx *ctx) -{ - /* - * Only context-owned state ends here. Process-wide libcurl state is paired - * by zget_global_init()/zget_global_cleanup(), never by a context destructor. - */ - if (ctx == NULL) - return; - zget_format_close(ctx->format); - zget_source_close(ctx->source); - free(ctx); -} - -int zget_last_error(const zget_ctx *ctx) -{ - return ctx == NULL ? ZGET_EINVAL : ctx->error.code; -} - -const char *zget_last_error_message(const zget_ctx *ctx) -{ - return ctx == NULL ? "invalid zget context" : ctx->error.message; -} - const char *zget_error_string(int error) { static const char *const messages[] = { "success", "invalid argument", "HTTP error", "HTTP Range unsupported", "remote object changed", "malformed ZIP", "unsupported ZIP feature", - "member not found", "unsupported compression", "CRC32 mismatch", - "decompression error", "output I/O error", "resource limit exceeded", - "out of memory", "zget is not initialized" + "member not found", "compression error", "CRC32 mismatch", + "callback rejected data", "out of memory" }; + return error >= 0 && (size_t)error < sizeof(messages) / sizeof(messages[0]) ? messages[error] : "unknown zget error"; } diff --git a/tests/integration.py b/tests/integration.py index 031a481..2decbea 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -147,6 +147,45 @@ def zip64_entry_archive(): return local + central + eocd +def semantic_archive(include_unicode=True, unicode_crc_valid=True, + include_extended=True, include_ntfs=True, + extended_value=1_000_000_000): + """Build one stored entry with independently controlled semantic metadata.""" + raw_name = b"legacy\x82.txt" + resolved_name = "preferred.txt".encode() + payload = b"semantic payload" + crc = zlib.crc32(payload) + extra = b"" + if include_unicode: + name_crc = zlib.crc32(raw_name) + if not unicode_crc_valid: + name_crc ^= 1 + unicode_data = b"\x01" + struct.pack(" #include #include @@ -18,74 +18,41 @@ static int discard_member(void *userdata, const zget_member_info *member) return 0; } -static int expect_error_values(void) +int main(void) { + static const char invalid_utf8[] = {'b', (char)0x80, '\0'}; + int failed = 0; + if (ZGET_OK != 0 || ZGET_EINVAL != 1 || ZGET_EHTTP != 2 || ZGET_ERANGE != 3 || ZGET_ECHANGED != 4 || ZGET_EZIP != 5 || ZGET_EUNSUPPORTED != 6 || ZGET_ENOTFOUND != 7 || - ZGET_ECOMPRESSION != 8 || ZGET_ECRC != 9 || ZGET_EDEFLATE != 10 || - ZGET_EIO != 11 || ZGET_ELIMIT != 12 || ZGET_ENOMEM != 13 || - ZGET_ENOTINITIALIZED != 14) { + ZGET_ECOMPRESSION != 8 || ZGET_ECRC != 9 || + ZGET_ECALLBACK != 10 || ZGET_ENOMEM != 11) { fprintf(stderr, "public error values changed\n"); - return 1; - } - return 0; -} - -static int expect_open_clears_context(const char *url) -{ - /* A non-NULL sentinel makes failure to replace the caller's value visible. */ - zget_ctx *ctx = (zget_ctx *)1; - int rc = zget_open_url_ex(url, NULL, &ctx); - - if (rc != ZGET_EINVAL || ctx != NULL) { - fprintf(stderr, "invalid URL returned %d and context %p\n", - rc, (void *)ctx); - return 1; - } - return 0; -} - -static int expect_invalid_call_updates_error(void) -{ - struct zget_ctx ctx = {0}; - - ctx.ready = true; - ctx.error.code = ZGET_EHTTP; - strcpy(ctx.error.message, "stale error"); - if (zget_extract_member(&ctx, NULL, discard, NULL) != ZGET_EINVAL || - zget_last_error(&ctx) != ZGET_EINVAL || - strcmp(zget_last_error_message(&ctx), - "member path and write callback are required") != 0) { - fprintf(stderr, "invalid extraction left stale context error state\n"); - return 1; + failed = 1; } - if (zget_list(&ctx, NULL, NULL, NULL) != ZGET_EINVAL || - zget_last_error(&ctx) != ZGET_EINVAL || - strcmp(zget_last_error_message(&ctx), - "listing callback is required") != 0) { - fprintf(stderr, "invalid listing left stale context error state\n"); - return 1; + if (zget_get(NULL, "member", discard, NULL) != ZGET_EINVAL || + zget_get("", "member", discard, NULL) != ZGET_EINVAL || + zget_get("url", NULL, discard, NULL) != ZGET_EINVAL || + zget_get("url", "", discard, NULL) != ZGET_EINVAL || + zget_get("url", invalid_utf8, discard, NULL) != ZGET_EINVAL || + zget_get("url", "member", NULL, NULL) != ZGET_EINVAL) { + fprintf(stderr, "zget_get accepted invalid input\n"); + failed = 1; } - return 0; -} - -int main(void) -{ - int failed = 0; - - /* These fail before global initialization and must still sanitize output. */ - failed |= expect_open_clears_context(NULL); - failed |= expect_open_clears_context(""); - failed |= expect_error_values(); - failed |= expect_invalid_call_updates_error(); - if (zget_extract_member(NULL, "member", discard, NULL) != ZGET_EINVAL) { - fprintf(stderr, "extract-member accepted a NULL context\n"); + if (zget_list(NULL, discard_member, NULL) != ZGET_EINVAL || + zget_list("", discard_member, NULL) != ZGET_EINVAL || + zget_list("url", NULL, NULL) != ZGET_EINVAL) { + fprintf(stderr, "zget_list accepted invalid input\n"); failed = 1; } - if (zget_list(NULL, NULL, discard_member, NULL) != ZGET_EINVAL) { - fprintf(stderr, "list accepted a NULL context\n"); + if (strcmp(zget_error_string(ZGET_ECALLBACK), + "callback rejected data") != 0 || + strcmp(zget_error_string(-1), "unknown zget error") != 0 || + strcmp(zget_error_string(999), "unknown zget error") != 0 || + zget_version() == NULL || zget_version()[0] == '\0') { + fprintf(stderr, "error strings or version are invalid\n"); failed = 1; } - return failed ? 1 : 0; + return failed; } diff --git a/tests/test_format.c b/tests/test_format.c index 52433a7..057c04e 100644 --- a/tests/test_format.c +++ b/tests/test_format.c @@ -22,10 +22,11 @@ struct output_buffer { struct member_buffer { char name[16]; size_t name_length; + uint64_t compressed_size; uint64_t uncompressed_size; - uint32_t flags; - uint16_t modified_year; - uint8_t modified_month, modified_day, modified_hour, modified_minute; + uint32_t crc32; + uint16_t compression_method; + int64_t mtime; unsigned int count; }; @@ -44,7 +45,7 @@ static int deliver(struct memory_source *memory, uint64_t offset, /* Format callbacks set the useful diagnostic before rejecting input. */ return memory->source.error->code != ZGET_OK ? - memory->source.error->code : ZGET_EIO; + memory->source.error->code : ZGET_EZIP; } static int memory_read_range(struct zget_source *source, uint64_t offset, @@ -149,22 +150,27 @@ static int collect(void *userdata, const void *data, size_t length) return 0; } +static int reject_output(void *userdata, const void *data, size_t length) +{ + (void)userdata; + (void)data; + (void)length; + return 1; +} + static int collect_member(void *userdata, const zget_member_info *member) { struct member_buffer *output = userdata; - if (member->struct_size < ZGET_MEMBER_INFO_V1_SIZE || - member->name_length > sizeof(output->name)) + if (member->name_length > sizeof(output->name)) return 1; memcpy(output->name, member->name, member->name_length); output->name_length = member->name_length; + output->compressed_size = member->compressed_size; output->uncompressed_size = member->uncompressed_size; - output->flags = member->flags; - output->modified_year = member->modified_year; - output->modified_month = member->modified_month; - output->modified_day = member->modified_day; - output->modified_hour = member->modified_hour; - output->modified_minute = member->modified_minute; + output->crc32 = member->crc32; + output->compression_method = member->compression_method; + output->mtime = member->mtime; ++output->count; return 0; } @@ -181,7 +187,6 @@ int main(void) unsigned char bytes[113]; struct zget_error_state error = {0}; struct memory_source memory = {0}; - struct zget_format_options options = {1024 * 1024, 0}; struct zget_format *format = NULL; struct output_buffer output = {0}; struct member_buffer members = {0}; @@ -193,7 +198,7 @@ int main(void) memory.source.size = memory.length; memory.source.size_known = true; - rc = zget_format_open(&memory.source, &options, &error, &format); + rc = zget_format_open(&memory.source, &error, &format); if (rc != ZGET_OK || format == NULL || memory.reads != 1) goto fail; /* Extraction keeps the ZIP locator entirely inside the format engine. */ @@ -204,25 +209,18 @@ int main(void) goto fail; /* Listing traverses the same format engine and borrows one record at a time. */ - rc = zget_format_list(format, NULL, collect_member, &members); + rc = zget_format_list(format, collect_member, &members); if (rc != ZGET_OK || members.count != 1 || members.name_length != 5 || memcmp(members.name, "a.txt", 5) != 0 || - members.uncompressed_size != 5 || - members.flags != (ZGET_MEMBER_NAME_UTF8 | - ZGET_MEMBER_HAS_MODIFIED_TIME) || - members.modified_year != 2026 || members.modified_month != 4 || - members.modified_day != 8 || members.modified_hour != 13 || - members.modified_minute != 40 || memory.reads != 5) - goto fail; - memset(&members, 0, sizeof(members)); - rc = zget_format_list(format, "a.txt", collect_member, &members); - if (rc != ZGET_OK || members.count != 1 || memory.reads != 6) + members.compressed_size != 5 || members.uncompressed_size != 5 || + members.crc32 != 0x3610a686u || members.compression_method != 0 || + members.mtime != INT64_C(1775655600) || memory.reads != 5) goto fail; - rc = zget_format_list(format, NULL, reject_member, NULL); - if (rc != ZGET_EIO || memory.reads != 7) + rc = zget_format_list(format, reject_member, NULL); + if (rc != ZGET_ECALLBACK || memory.reads != 6) goto fail; - rc = zget_format_list(format, "missing", collect_member, &members); - if (rc != ZGET_ENOTFOUND || memory.reads != 8) + rc = zget_format_extract_member(format, "a.txt", reject_output, NULL); + if (rc != ZGET_ECALLBACK || memory.reads != 9) goto fail; zget_format_close(format); diff --git a/tests/test_lifecycle.c b/tests/test_lifecycle.c deleted file mode 100644 index 1725b74..0000000 --- a/tests/test_lifecycle.c +++ /dev/null @@ -1,53 +0,0 @@ -#include "zget.h" - -#include -#include - -static int discard_output(void *userdata, const void *data, size_t size) -{ - (void)userdata; - (void)data; - (void)size; - return 0; -} - -static int expect_uninitialized(void) -{ - zget_ctx *ctx = (zget_ctx *)1; - int rc = zget_open_url_ex("https://example.invalid/archive.zip", NULL, - &ctx); - - if (rc != ZGET_ENOTINITIALIZED || ctx != NULL) { - fprintf(stderr, "open without initialization returned %d\n", rc); - return 1; - } - if (zget_get("https://example.invalid/archive.zip", "member", discard_output, - NULL) != ZGET_ENOTINITIALIZED) { - fprintf(stderr, "convenience API bypassed global initialization\n"); - return 1; - } - if (strcmp(zget_error_string(ZGET_ENOTINITIALIZED), - "zget is not initialized") != 0) { - fprintf(stderr, "initialization error has the wrong description\n"); - return 1; - } - return 0; -} - -int main(void) -{ - /* Extra cleanup is intentionally harmless, matching libcurl's behavior. */ - zget_global_cleanup(); - if (expect_uninitialized() != 0) - return 1; - - /* Exercise nested owners: both successful acquisitions must be released. */ - if (zget_global_init() != ZGET_OK || zget_global_init() != ZGET_OK) { - fprintf(stderr, "could not initialize zget\n"); - return 1; - } - zget_global_cleanup(); - zget_global_cleanup(); - - return expect_uninitialized(); -} diff --git a/tests/test_options.c b/tests/test_options.c deleted file mode 100644 index a69d70f..0000000 --- a/tests/test_options.c +++ /dev/null @@ -1,61 +0,0 @@ -#include "zget.h" - -#include -#include - -struct future_options { - zget_options known; - uint64_t field_unknown_to_this_library; -}; - -static int expect_option_error(zget_options *options, const char *message) -{ - zget_ctx *ctx = NULL; - int rc = zget_open_url_ex("file:///not-an-http-url", options, &ctx); - const char *actual = ctx == NULL ? "" : zget_last_error_message(ctx); - int failed = rc != ZGET_EINVAL || strcmp(actual, message) != 0; - - if (failed) - fprintf(stderr, "expected '%s', got error %d: '%s'\n", - message, rc, actual); - zget_close(ctx); - return failed; -} - -int main(void) -{ - zget_options options; - struct future_options future; - int failed = 0; - - if (ZGET_OPTIONS_V1_SIZE > sizeof(options)) { - fprintf(stderr, "v1 options boundary exceeds the current structure\n"); - return 1; - } - if (zget_global_init() != ZGET_OK) { - fprintf(stderr, "could not initialize zget\n"); - return 1; - } - - zget_options_init(&options); - options.struct_size = ZGET_OPTIONS_V1_SIZE - 1; - failed |= expect_option_error(&options, "zget_options struct is too small"); - - /* The original ABI boundary remains valid after future fields are added. */ - zget_options_init(&options); - options.struct_size = ZGET_OPTIONS_V1_SIZE; - failed |= expect_option_error(&options, "URL must use HTTP or HTTPS"); - - zget_options_init(&options); - failed |= expect_option_error(&options, "URL must use HTTP or HTTPS"); - - /* A newer caller may pass a larger append-only structure; ignore its tail. */ - memset(&future, 0, sizeof(future)); - zget_options_init(&future.known); - future.known.struct_size = sizeof(future); - future.field_unknown_to_this_library = UINT64_MAX; - failed |= expect_option_error(&future.known, "URL must use HTTP or HTTPS"); - - zget_global_cleanup(); - return failed ? 1 : 0; -}