diff --git a/CMakeLists.txt b/CMakeLists.txt index 31da263..3ebbeaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,8 +29,8 @@ find_package(ZLIB REQUIRED) set(ZGET_LIBRARY_SOURCES src/zget.c src/error.c src/util.c src/source/source.c - src/source/http-parse.c src/source/http.c src/source/download.c - src/source/file.c src/format/format.c src/format/zip/zip.c + src/source/http-parse.c src/source/http.c + src/format/format.c src/format/zip/zip.c src/format/zip/zip-parse.c src/format/zip/extract.c) add_library(libzget SHARED ${ZGET_LIBRARY_SOURCES}) @@ -147,13 +147,12 @@ if(BUILD_TESTING) COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/integration.py $) set_tests_properties(integration_reference PROPERTIES TIMEOUT 90) - # Keep the no-Range behavior isolated: this small fixture makes the - # fallback contract obvious without coupling it to the large oracle. - add_test(NAME integration_no_range_fallback + # Keep the strict Range contract isolated from the large oracle. + add_test(NAME integration_strict_range COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tests/no_range_fallback.py + ${CMAKE_CURRENT_SOURCE_DIR}/tests/strict_range.py $) - set_tests_properties(integration_no_range_fallback PROPERTIES TIMEOUT 30) + set_tests_properties(integration_strict_range PROPERTIES TIMEOUT 30) endif() if(Python3_Interpreter_FOUND AND ZGET_BUILD_LARGE_TESTS) add_test(NAME integration_100k_entries diff --git a/README.md b/README.md index a68d5ce..687cc99 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # zget -Fetch one file from a remote ZIP archive while downloading only the bytes needed when the server supports HTTP Range requests. +Fetch one file from a remote ZIP archive without downloading the complete archive. `zget` is a command-line tool and C library for listing and extracting -individual files from remote ZIP and ZIP64 archives. It prefers precise HTTP -Range requests and transparently falls back to a complete temporary download -when a server ignores Range requests entirely. It streams archive metadata with -memory usage independent of entry count, so the same design works for ordinary -archives and scales to very large ones. +individual files from remote ZIP and ZIP64 archives using precise HTTP Range +requests. If a server cannot provide the required ranges, zget fails rather +than silently downloading the complete archive. It streams archive metadata +with memory usage independent of entry count, so the same design works for +ordinary archives and scales to very large ones. ```sh zget https://example.com/archive.zip README.txt @@ -79,8 +79,8 @@ Remote ZIP access is not unique to zget. Python projects such as members over HTTP without downloading the whole archive when Range requests are available. `zget` is a native C library and CLI for efficient remote ZIP/ZIP64 access. Its streaming, bounded-memory design works for ordinary archives and -remains practical as archive sizes and entry counts grow, with predictable -Range behavior and a compatibility fallback for servers that ignore Range. +remains practical as archive sizes and entry counts grow, with a strict and +predictable Range-access contract. `zget` streams the Central Directory, discards metadata for non-matching entries immediately, and can stop scanning as soon as the requested entry is found. It @@ -101,18 +101,15 @@ for archives containing hundreds of thousands, millions, or tens of millions of entries; scan time and transferred metadata still depend on the target's position in the Central Directory. -When the server supports Range requests, a successful lookup uses semantically -precise byte ranges: +A successful lookup uses semantically precise byte ranges: ```text tail -> central directory -> target local header -> target payload ``` -If the server ignores Range requests entirely and returns the complete object, -zget downloads the archive once into anonymous temporary storage and continues -through the same local source and ZIP implementation. The operation remains -transparent to callers, but it necessarily transfers the whole archive and -therefore loses the bandwidth advantage of Range-based access. +If the server ignores a required Range request or otherwise cannot provide a +valid partial response, zget fails with a Range or HTTP error. It never silently +turns selective member retrieval into a complete archive download. The first exact Central Directory name match wins. Extraction is streamed through STORE or raw-DEFLATE decoding and checked against the entry CRC32. @@ -323,10 +320,9 @@ the tail as an explicit interval. These extra requests count toward `max_http_requests`. If a server ignores the required Range request entirely and returns HTTP 200 -with the complete representation, zget falls back to one complete download in -anonymous temporary storage and continues through the local-file source. This -fallback preserves extraction and listing behavior, but it transfers the entire -archive and does not provide Range-based bandwidth savings. +with the complete representation, zget rejects the response with +`ZGET_ERANGE`. Supplying `MEMBER` requests selective retrieval; zget never +silently replaces it with a complete archive download. An inconsistent `Content-Range`, changed object size, non-identity `Content-Encoding`, or failed `If-Match` aborts the Range operation. HTTPS @@ -349,7 +345,7 @@ write to stdout, which likewise cannot be rolled back after a late error. matching only when entirely ASCII; CP437 conversion is intentionally absent. - No encryption, split archives, resume, or random seeks within a DEFLATE member. -- HTTP Range is preferred for efficient remote access. Servers that ignore - Range entirely are supported through a transparent complete-download fallback. +- HTTP Range support is required. Servers that ignore required Range requests + fail with a Range error instead of triggering a complete download. This project is MIT licensed. diff --git a/docs/zget.1.in b/docs/zget.1.in index 45ae35e..a49bb66 100644 --- a/docs/zget.1.in +++ b/docs/zget.1.in @@ -25,11 +25,9 @@ zget \- fetch one member from a remote ZIP archive .SH DESCRIPTION .B zget fetches one exact, case-sensitive member from a remote ZIP or ZIP64 archive. -It prefers HTTP Range requests so only the required archive bytes are -transferred. If a server ignores Range requests entirely and returns the -complete representation, zget transparently downloads the archive into -anonymous temporary storage and continues locally. That compatibility fallback -necessarily transfers the entire archive. +It uses HTTP Range requests so only the required archive bytes are transferred. +If a server cannot provide the required ranges, zget fails rather than silently +downloading the complete archive. .PP The Central Directory is parsed as a stream. Metadata for each non-matching entry is discarded immediately, so memory use is independent of the archive @@ -103,10 +101,11 @@ probe and retries the tail as an explicit interval. These extra requests count toward the configured HTTP request limit. .PP If the server ignores a required Range request entirely and returns HTTP 200 -with the complete representation, zget performs one complete download into an -anonymous temporary file and continues through its local-file source. This -preserves normal extraction and listing semantics but forfeits the bandwidth -savings of Range-based access. +with the complete representation, zget rejects the response with a Range error. +Supplying +.I MEMBER +requests selective retrieval; zget never silently replaces it with a complete +archive download. .PP HTTPS redirects may not downgrade to HTTP. A strong ETag, when supplied by the server, is used with subsequent Range requests to ensure that all ranges refer @@ -161,8 +160,8 @@ 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 that ignore HTTP Range are supported by a complete -fallback download, so such operations may transfer the entire archive. +not supported. 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/src/source/download.c b/src/source/download.c deleted file mode 100644 index 71ca3f2..0000000 --- a/src/source/download.c +++ /dev/null @@ -1,136 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include "source/download.h" - -#include "zget.h" -#include "zget_version.h" - -#include -#include -#include -#include -#include -#include -#include - -struct download_state { - int fd; - int saved_errno; -}; - -static size_t download_write_cb(char *data, size_t size, size_t nmemb, - void *opaque) -{ - struct download_state *state = opaque; - size_t total, written = 0; - - if (nmemb != 0 && size > SIZE_MAX / nmemb) - return 0; - total = size * nmemb; - while (written < total) { - ssize_t n; - do { - n = write(state->fd, data + written, total - written); - } while (n < 0 && errno == EINTR); - if (n <= 0) { - state->saved_errno = n < 0 ? errno : EIO; - return 0; - } - written += (size_t)n; - } - return total; -} - -int zget_http_download_to_temp(const char *url, - const struct zget_http_options *options, - struct zget_error_state *error, int *out_fd) -{ - char path[] = "/tmp/zget-XXXXXX"; - struct download_state state; - struct curl_slist *headers = NULL; - CURL *curl = NULL; - CURLcode cc; - long status = 0; - int fd = -1, rc = ZGET_EHTTP; - - if (out_fd != NULL) - *out_fd = -1; - if (url == NULL || options == NULL || error == NULL || out_fd == NULL) - return ZGET_EINVAL; - - fd = mkstemp(path); - if (fd < 0) { - zget_error_set(error, ZGET_EIO, "create temporary file: %s", - strerror(errno)); - return ZGET_EIO; - } - /* The fallback is an implementation detail; never expose a named file. */ - if (unlink(path) != 0) { - int saved_errno = errno; - close(fd); - zget_error_set(error, ZGET_EIO, "unlink temporary file: %s", - strerror(saved_errno)); - return ZGET_EIO; - } - curl = curl_easy_init(); - if (curl == NULL) { - zget_error_set(error, ZGET_ENOMEM, "could not create HTTP handle"); - rc = ZGET_ENOMEM; - goto done; - } - headers = curl_slist_append(NULL, "Accept-Encoding: identity"); - if (headers == NULL) { - zget_error_set(error, ZGET_ENOMEM, "could not allocate HTTP headers"); - rc = ZGET_ENOMEM; - goto done; - } - state.fd = fd; - state.saved_errno = 0; -#define SETOPT(opt, value) do { cc = curl_easy_setopt(curl, opt, value); \ - if (cc != CURLE_OK) goto curl_failure; } while (0) - SETOPT(CURLOPT_URL, url); - SETOPT(CURLOPT_HTTPHEADER, headers); - SETOPT(CURLOPT_FOLLOWLOCATION, 1L); - SETOPT(CURLOPT_MAXREDIRS, (long)options->max_redirects); - SETOPT(CURLOPT_PROTOCOLS_STR, "http,https"); - SETOPT(CURLOPT_REDIR_PROTOCOLS_STR, "http,https"); - SETOPT(CURLOPT_USERAGENT, "zget/" ZGET_VERSION_STRING); - SETOPT(CURLOPT_WRITEFUNCTION, download_write_cb); - SETOPT(CURLOPT_WRITEDATA, &state); - cc = curl_easy_perform(curl); - if (state.saved_errno != 0) { - zget_error_set(error, ZGET_EIO, "write temporary file: %s", - strerror(state.saved_errno)); - rc = ZGET_EIO; - goto done; - } - if (cc != CURLE_OK) { - zget_error_set(error, ZGET_EHTTP, "HTTP transfer failed: %s", - curl_easy_strerror(cc)); - goto done; - } - cc = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); - if (cc != CURLE_OK) - goto curl_failure; - if (status < 200 || status >= 300) { - zget_error_set(error, ZGET_EHTTP, - "server returned HTTP %ld for complete download", status); - goto done; - } - *out_fd = fd; - fd = -1; - rc = ZGET_OK; - goto done; - -curl_failure: - zget_error_set(error, ZGET_EHTTP, "could not configure HTTP transfer: %s", - curl_easy_strerror(cc)); -done: - curl_slist_free_all(headers); - if (curl != NULL) - curl_easy_cleanup(curl); - if (fd >= 0) - close(fd); - return rc; -#undef SETOPT -} diff --git a/src/source/download.h b/src/source/download.h deleted file mode 100644 index e2475b3..0000000 --- a/src/source/download.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef ZGET_SOURCE_DOWNLOAD_H -#define ZGET_SOURCE_DOWNLOAD_H - -#include "error.h" -#include "source/http.h" - -/* - * Download one complete identity representation into an anonymous temporary - * file. On success the caller owns *out_fd and must close or transfer it. - */ -int zget_http_download_to_temp(const char *url, - const struct zget_http_options *options, - struct zget_error_state *error, int *out_fd); - -#endif diff --git a/src/source/file.c b/src/source/file.c deleted file mode 100644 index 20574b9..0000000 --- a/src/source/file.c +++ /dev/null @@ -1,161 +0,0 @@ -#include "source/file.h" - -#include "source/source-private.h" -#include "zget.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -struct zget_file_source { - struct zget_source source; - int fd; -}; - -static int file_error(struct zget_file_source *file, const char *operation) -{ - int saved_errno = errno; - - zget_error_set(file->source.error, ZGET_EIO, "%s: %s", operation, - strerror(saved_errno)); - return ZGET_EIO; -} - -static int file_read_range(struct zget_source *source, uint64_t offset, - uint64_t length, zget_source_write_cb write_cb, - void *userdata) -{ - struct zget_file_source *file = (struct zget_file_source *)source; - unsigned char buffer[64 * 1024]; - uint64_t position = offset; - uint64_t remaining = length; - - /* - * Positional reads keep the source independent of mutable descriptor state, - * so one range cannot accidentally change where a later range starts. - */ - while (remaining != 0) { - size_t want = remaining < sizeof(buffer) ? - (size_t)remaining : sizeof(buffer); - ssize_t got; - enum zget_source_action action; - - do { - got = pread(file->fd, buffer, want, (off_t)position); - } while (got < 0 && errno == EINTR); - if (got < 0) - return file_error(file, "pread"); - if (got == 0) { - zget_error_set(source->error, ZGET_EIO, - "unexpected end of local file"); - return ZGET_EIO; - } - - action = write_cb(userdata, buffer, (size_t)got); - if (action == ZGET_SOURCE_STOP) - return ZGET_OK; - if (action == ZGET_SOURCE_ERROR) - return source->error != NULL && source->error->code != ZGET_OK ? - source->error->code : ZGET_EIO; - - position += (uint64_t)got; - remaining -= (uint64_t)got; - } - return ZGET_OK; -} - -static int file_read_suffix(struct zget_source *source, uint64_t length, - zget_source_write_cb write_cb, void *userdata) -{ - uint64_t take = length < source->size ? length : source->size; - - if (take == 0) - return ZGET_EINVAL; - return file_read_range(source, source->size - take, take, write_cb, - userdata); -} - -static void file_close(struct zget_source *source) -{ - struct zget_file_source *file = (struct zget_file_source *)source; - - if (file->fd >= 0) - (void)close(file->fd); - free(file); -} - -static const struct zget_source_ops file_ops = { - file_read_range, - file_read_suffix, - file_close -}; - -int zget_file_source_open_fd(int fd, struct zget_error_state *error, - struct zget_source **out_source) -{ - struct zget_file_source *file; - struct stat st; - - if (out_source != NULL) - *out_source = NULL; - if (fd < 0 || error == NULL || out_source == NULL) - return ZGET_EINVAL; - - /* - * Ownership does not transfer until every fallible setup step succeeds. - * Callers can therefore use one simple rule: close fd after failure, and - * let zget_source_close() release it after success. - */ - if (fstat(fd, &st) != 0) { - int saved_errno = errno; - zget_error_set(error, ZGET_EIO, "stat local file: %s", - strerror(saved_errno)); - return ZGET_EIO; - } - if (st.st_size <= 0) { - zget_error_set(error, ZGET_EINVAL, "local file is empty"); - return ZGET_EINVAL; - } - - file = calloc(1, sizeof(*file)); - if (file == NULL) { - zget_error_set(error, ZGET_ENOMEM, "allocating local file source"); - return ZGET_ENOMEM; - } - - zget_source_init(&file->source, &file_ops, error); - file->fd = fd; - file->source.size = (uint64_t)st.st_size; - file->source.size_known = true; - *out_source = &file->source; - return ZGET_OK; -} - -int zget_file_source_open(const char *path, struct zget_error_state *error, - struct zget_source **out_source) -{ - int fd, rc; - - if (out_source != NULL) - *out_source = NULL; - if (path == NULL || error == NULL || out_source == NULL) - return ZGET_EINVAL; - - fd = open(path, O_RDONLY); - if (fd < 0) { - int saved_errno = errno; - zget_error_set(error, ZGET_EIO, "open local file: %s", - strerror(saved_errno)); - return ZGET_EIO; - } - - rc = zget_file_source_open_fd(fd, error, out_source); - if (rc != ZGET_OK) - (void)close(fd); - return rc; -} diff --git a/src/source/file.h b/src/source/file.h deleted file mode 100644 index 63b5fa8..0000000 --- a/src/source/file.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef ZGET_SOURCE_FILE_H -#define ZGET_SOURCE_FILE_H - -#include "error.h" -#include "source/source.h" - -/* - * Open a seekable local file as a source. The returned source owns its file - * descriptor and releases it from zget_source_close(). - */ -int zget_file_source_open(const char *path, struct zget_error_state *error, - struct zget_source **out_source); - -/* - * Adopt an already-open seekable descriptor. Ownership transfers only after a - * successful return; this makes temporary-file cleanup explicit at the caller. - */ -int zget_file_source_open_fd(int fd, struct zget_error_state *error, - struct zget_source **out_source); - -#endif diff --git a/src/zget.c b/src/zget.c index 091e0fb..8eea6df 100644 --- a/src/zget.c +++ b/src/zget.c @@ -1,14 +1,11 @@ #include "internal.h" #include "format/format.h" -#include "source/download.h" -#include "source/file.h" #include "source/http.h" #include "source/source.h" #include #include #include -#include /* * Global initialization is owned by the embedding application rather than by @@ -84,30 +81,6 @@ static int copy_options(struct zget_ctx *ctx, const zget_options *options) return ZGET_OK; } -/* - * Replace a Range source with a complete local snapshot. The failed Range - * attempt is deliberately discarded before starting the ordinary GET: the two - * transfer modes remain independent, and the format layer only ever sees one - * coherent source implementation at a time. - */ -static int open_downloaded_source(const char *archive_url, - const struct zget_http_options *http_options, - struct zget_error_state *error, - struct zget_source **out_source) -{ - int fd = -1; - int rc; - - rc = zget_http_download_to_temp(archive_url, http_options, error, &fd); - if (rc != ZGET_OK) - return rc; - - rc = zget_file_source_open_fd(fd, error, out_source); - if (rc != ZGET_OK) - (void)close(fd); - return rc; -} - int zget_open_url_ex(const char *archive_url, const zget_options *options, zget_ctx **out_ctx) { @@ -142,27 +115,6 @@ int zget_open_url_ex(const char *archive_url, const zget_options *options, format_options.max_output_size = ctx->options.max_output_size; rc = zget_format_open(ctx->source, &format_options, &ctx->error, &ctx->format); - if (rc == ZGET_ERANGE) { - /* - * Range is an optimization, not a requirement for correctness. A 200 - * response to the probe proves only that random access is unavailable; - * download the same URL through the same redirect policy and continue - * against an anonymous local snapshot. Other HTTP/protocol failures are - * intentionally not retried here because they do not prove this case. - */ - zget_format_close(ctx->format); - ctx->format = NULL; - zget_source_close(ctx->source); - ctx->source = NULL; - ctx->error.code = ZGET_OK; - ctx->error.message[0] = '\0'; - - rc = open_downloaded_source(archive_url, &http_options, &ctx->error, - &ctx->source); - if (rc == ZGET_OK) - rc = zget_format_open(ctx->source, &format_options, &ctx->error, - &ctx->format); - } if (rc != ZGET_OK) goto fail; diff --git a/tests/no_range_fallback.py b/tests/no_range_fallback.py deleted file mode 100644 index 1c6bfe1..0000000 --- a/tests/no_range_fallback.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -"""End-to-end coverage for servers that ignore HTTP Range requests. - -This test deliberately returns 200 and the complete archive for every GET, -even when zget sends a Range header. A successful extraction proves the full -fallback path: rejected Range response -> ordinary full download -> anonymous -temporary file -> local source -> normal ZIP extraction. -""" - -import http.server -import io -import subprocess -import sys -import threading -import zipfile - -from range_server import NoReverseDNSHTTPServer - - -def make_archive(): - """Build the smallest archive needed to exercise fallback extraction.""" - output = io.BytesIO() - with zipfile.ZipFile(output, "w") as archive: - archive.writestr("stored.txt", b"stored payload", - compress_type=zipfile.ZIP_STORED) - return output.getvalue() - - -class IgnoreRangeHandler(http.server.BaseHTTPRequestHandler): - """Serve the complete representation and intentionally ignore Range.""" - - archive = b"" - - def do_GET(self): - if self.path != "/archive.zip": - self.send_error(404) - return - self.send_response(200) - self.send_header("Content-Length", str(len(self.archive))) - self.end_headers() - self.wfile.write(self.archive) - - def log_message(self, _format, *args): - """Keep successful test output quiet.""" - del args - - -def run(binary): - """Prove transparent fallback with one focused extraction scenario.""" - IgnoreRangeHandler.archive = make_archive() - server = NoReverseDNSHTTPServer(("127.0.0.1", 0), IgnoreRangeHandler) - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() - try: - url = "http://127.0.0.1:{}/archive.zip".format(server.server_port) - result = subprocess.run([binary, url, "stored.txt"], check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - timeout=10) - assert result.stdout == b"stored payload" - assert result.stderr == b"" - finally: - server.shutdown() - server.server_close() - thread.join() - - -if __name__ == "__main__": - if len(sys.argv) != 2: - raise SystemExit("usage: no_range_fallback.py ZGET") - run(sys.argv[1]) diff --git a/tests/strict_range.py b/tests/strict_range.py new file mode 100644 index 0000000..7695b52 --- /dev/null +++ b/tests/strict_range.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""End-to-end coverage for the strict selective HTTP Range contract.""" + +import http.server +import io +import subprocess +import sys +import threading +import zipfile + +from range_server import NoReverseDNSHTTPServer + + +def make_archive(): + """Build the smallest archive needed to reach the initial tail request.""" + output = io.BytesIO() + with zipfile.ZipFile(output, "w") as archive: + archive.writestr("stored.txt", b"stored payload", + compress_type=zipfile.ZIP_STORED) + return output.getvalue() + + +class IgnoreRangeHandler(http.server.BaseHTTPRequestHandler): + """Ignore Range while recording whether zget ever sends an ordinary GET.""" + + archive = b"" + requests = 0 + range_requests = 0 + + def do_GET(self): + type(self).requests += 1 + if self.headers.get("Range") is not None: + type(self).range_requests += 1 + if self.path != "/archive.zip": + self.send_error(404) + return + self.send_response(200) + self.send_header("Content-Length", str(len(self.archive))) + self.end_headers() + self.wfile.write(self.archive) + + def log_message(self, _format, *args): + """Keep expected rejection output quiet.""" + del args + + +def assert_range_failure(binary, arguments): + """Require a clear failure with no member or listing bytes emitted.""" + result = subprocess.run([binary, *arguments], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, timeout=10) + assert result.returncode == 1, (arguments, result.stderr) + assert result.stdout == b"", (arguments, result.stdout) + assert b"HTTP Range unsupported" in result.stderr, result.stderr + assert (b"server ignored the required byte range" in result.stderr), \ + result.stderr + + +def run(binary): + """Prove extraction and listing remain selective when Range is ignored.""" + IgnoreRangeHandler.archive = make_archive() + IgnoreRangeHandler.requests = 0 + IgnoreRangeHandler.range_requests = 0 + server = NoReverseDNSHTTPServer(("127.0.0.1", 0), IgnoreRangeHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + url = "http://127.0.0.1:{}/archive.zip".format(server.server_port) + assert_range_failure(binary, [url, "stored.txt"]) + assert_range_failure(binary, ["-l", url]) + assert IgnoreRangeHandler.requests > 0 + assert (IgnoreRangeHandler.range_requests == + IgnoreRangeHandler.requests), "zget sent an ordinary GET" + finally: + server.shutdown() + server.server_close() + thread.join() + + +if __name__ == "__main__": + if len(sys.argv) != 2: + raise SystemExit("usage: strict_range.py ZGET") + run(sys.argv[1]) diff --git a/tests/test_file_source.c b/tests/test_file_source.c deleted file mode 100644 index dd86d34..0000000 --- a/tests/test_file_source.c +++ /dev/null @@ -1,76 +0,0 @@ -#include "source/file.h" -#include "source/source.h" -#include "zget.h" - -#include -#include -#include - -struct buffer { - unsigned char data[32]; - size_t length; -}; - -static enum zget_source_action collect(void *userdata, const void *data, - size_t size) -{ - struct buffer *buffer = userdata; - - if (size > sizeof(buffer->data) - buffer->length) - return ZGET_SOURCE_ERROR; - memcpy(buffer->data + buffer->length, data, size); - buffer->length += size; - return ZGET_SOURCE_CONTINUE; -} - -int main(void) -{ - static const char payload[] = "0123456789abcdef"; - char path[] = "/tmp/zget-file-source-XXXXXX"; - struct zget_error_state error = {0}; - struct zget_source *source = NULL; - struct buffer output = {0}; - uint64_t size = 0; - int fd = mkstemp(path); - int rc; - - if (fd < 0) - return 1; - if (write(fd, payload, sizeof(payload) - 1) != (ssize_t)(sizeof(payload) - 1)) { - close(fd); - unlink(path); - return 1; - } - close(fd); - - rc = zget_file_source_open(path, &error, &source); - if (rc != ZGET_OK || source == NULL || - !zget_source_get_size(source, &size) || size != sizeof(payload) - 1) - goto fail; - - rc = zget_source_read_range(source, 4, 6, collect, &output); - if (rc != ZGET_OK || output.length != 6 || - memcmp(output.data, "456789", 6) != 0) - goto fail; - - memset(&output, 0, sizeof(output)); - rc = zget_source_read_suffix(source, 4, collect, &output); - if (rc != ZGET_OK || output.length != 4 || - memcmp(output.data, "cdef", 4) != 0) - goto fail; - - rc = zget_source_read_range(source, size - 1, 2, collect, &output); - if (rc != ZGET_EINVAL) - goto fail; - - zget_source_close(source); - unlink(path); - return 0; - -fail: - fprintf(stderr, "file source test failed: rc=%d, error=%s\n", - rc, error.message); - zget_source_close(source); - unlink(path); - return 1; -}