From 4301f10fffa009015dfcf4d3de6e6dd63ca9798d Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 24 Aug 2026 22:20:58 +0200 Subject: [PATCH 1/2] Truncate on the Win32 ioapi tables, and say when a rollback could not The three Win32 filefunc tables in minizip's iowin32.c set neither ztruncate64_file nor zflush_file, so a caller filling one from an uninitialized struct is left with a wild pointer. Implement both over SetFilePointerEx/SetEndOfFile and FlushFileBuffers. Where a backend has no truncate at all the rollback stays a rewind, but zipAbandonFileInZip() now returns ZIP_NOTRUNCATED rather than ZIP_OK, and the cache logs the incomplete rollback instead of continuing quietly. The cache-writefail self-test opened its injected ZIP through the 32-bit zipOpen2, where the entry is NULL, so it never truncated and could not see this regress; it now uses the table the cache itself opens with. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Xavier Roche --- src/Makefile.am | 2 + src/htscache.c | 8 +- src/htscache_selftest.c | 41 +- src/htsselftest.c | 128 ++++++- src/htszlib.c | 11 +- src/htszlib.h | 2 + src/minizip/ioapi.c | 4 +- src/minizip/ioapi.c.diff | 4 +- src/minizip/ioapi.h | 4 +- src/minizip/ioapi.h.diff | 4 +- src/minizip/iowin32.c | 47 +++ src/minizip/iowin32.c.diff | 91 +++++ src/minizip/iowin32.c.orig | 440 ++++++++++++++++++++++ src/minizip/zip.c | 9 +- src/minizip/zip.c.diff | 13 +- src/minizip/zip.h | 6 +- src/minizip/zip.h.diff | 15 +- tests/356_engine-zip-abandon-notrunc.test | 37 ++ 18 files changed, 825 insertions(+), 41 deletions(-) create mode 100644 src/minizip/iowin32.c.diff create mode 100644 src/minizip/iowin32.c.orig create mode 100755 tests/356_engine-zip-abandon-notrunc.test diff --git a/src/Makefile.am b/src/Makefile.am index 9a6e1daa2..76dd84864 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -112,6 +112,8 @@ EXTRA_DIST = httrack.h htsstats.h webhttrack.in \ coucal/murmurhash3.h.orig \ minizip/iowin32.c \ minizip/iowin32.h \ + minizip/iowin32.c.diff \ + minizip/iowin32.c.orig \ minizip/ioapi.c.diff \ minizip/ioapi.h.diff \ minizip/zip.c.diff \ diff --git a/src/htscache.c b/src/htscache.c index 4e2ab1ac2..26c80ec9b 100644 --- a/src/htscache.c +++ b/src/htscache.c @@ -184,8 +184,12 @@ static void cache_zip_write_failed(httrackp *opt, cache_back *cache, /* Roll the partial member back: closing it would commit a short body under the X-Size already written into its local header. */ - if (entry_open) - (void) zipAbandonFileInZip((zipFile) cache->zipOutput); + if (entry_open && zipAbandonFileInZip((zipFile) cache->zipOutput) != ZIP_OK) { + /* the bytes are still past the rewound position, and a tail longer than a + reader's backscan hides the directory the close writes after it */ + hts_log_print(opt, LOG_WARNING, + "cache rollback incomplete, the cache file may not reopen"); + } cache->zipWriteFailures++; if (fatal_errno || cache->zipWriteFailures >= CACHE_MAX_WRITE_FAILURES) { if (!cache->zipWriteFailed) { diff --git a/src/htscache_selftest.c b/src/htscache_selftest.c index ce813cc48..8fd467e38 100644 --- a/src/htscache_selftest.c +++ b/src/htscache_selftest.c @@ -381,6 +381,8 @@ typedef struct { int fail_errno; /**< errno set on the failing write (ENOSPC, EIO, ...) */ int writes; /**< zwrite call count, to detect re-entry into the stream */ int fail_once; /**< recover (unlimited budget) after the first failure */ + int truncates; /**< ztruncate call count, to prove the rollback truncates */ + truncate64_file_func truncate; /**< the backend's own, NULL if it has none */ } writefail_inject; /* zwrite that copies until the budget runs out, then fails with inj->fail_errno @@ -401,16 +403,30 @@ static uLong selftest_failing_zwrite(voidpf opaque, voidpf stream, return 0; /* short write -> the minizip op returns an error */ } +/* Count the truncate the rolled-back entry goes through, then run it. */ +static int ZCALLBACK selftest_counting_ztruncate(voidpf opaque, voidpf stream, + ZPOS64_T size) { + writefail_inject *inj = (writefail_inject *) opaque; + + inj->truncates++; + if (inj->truncate == NULL) + return -1; + return inj->truncate(opaque, stream, size); +} + /* Open a ZIP whose writes fail past inj->budget, so cache_add() hits an error. - */ + Through the table the cache itself opens with (#1402): a table without a + truncate rolls a member back by rewinding, leaving the bytes behind. */ static zipFile selftest_open_failing_zip(const char *path, writefail_inject *inj) { - zlib_filefunc_def ff; + zlib_filefunc64_def ff; - fill_fopen_filefunc(&ff); /* real fopen/read/seek/close; ignores opaque */ + hts_zip_filefunc64(&ff); /* real fopen/read/seek/close; ignores opaque */ + inj->truncate = ff.ztruncate64_file; + ff.ztruncate64_file = selftest_counting_ztruncate; ff.zwrite_file = selftest_failing_zwrite; ff.opaque = inj; - return zipOpen2(path, APPEND_STATUS_CREATE, NULL, &ff); + return zipOpen2_64(path, APPEND_STATUS_CREATE, NULL, &ff); } /* Store one octet-stream body into `cache` (all-in-cache, body in the ZIP). */ @@ -501,6 +517,7 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir) { inj.budget = (phase == 0) ? 4096 : 0; inj.fail_errno = (phase == 0) ? ENOSPC : EIO; inj.writes = 0; + inj.truncates = 0; inj.fail_once = 0; memset(&cache, 0, sizeof(cache)); cache.type = 1; @@ -573,6 +590,7 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir) { inj.budget = (size_t) -1; inj.fail_errno = EIO; inj.writes = 0; + inj.truncates = 0; inj.fail_once = 0; memset(&cache, 0, sizeof(cache)); cache.type = 1; @@ -614,6 +632,7 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir) { inj.budget = 4096; inj.fail_errno = EIO; inj.writes = 0; + inj.truncates = 0; inj.fail_once = 1; memset(&cache, 0, sizeof(cache)); cache.type = 1; @@ -631,6 +650,19 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir) { (int) cache.zipWriteFailed, opt->state.exit_xh); fail++; } + /* the rolled-back entry is truncated away, not merely rewound: #1402, + where a table with no truncate still reported the rollback as done */ + if (inj.truncate == NULL) { + fprintf(stderr, "cache-writefail: skip: the table the cache opens with " + "carries no truncate, so the rollback only rewound\n"); + fail++; + } else if (inj.truncates != 1) { + fprintf(stderr, + "cache-writefail: skip: abandoned entry truncated %d time(s), " + "want 1\n", + inj.truncates); + fail++; + } writefail_store(opt, &cache, "/blob2.bin", body, 16); zipClose(cache.zipOutput, NULL); cache.zipOutput = NULL; @@ -656,6 +688,7 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir) { inj.budget = (size_t) -1; /* no injected failure */ inj.fail_errno = 0; inj.writes = 0; + inj.truncates = 0; inj.fail_once = 0; memset(&cache, 0, sizeof(cache)); cache.type = 1; diff --git a/src/htsselftest.c b/src/htsselftest.c index 510162761..9e435383a 100644 --- a/src/htsselftest.c +++ b/src/htsselftest.c @@ -4013,19 +4013,15 @@ static const char *const zip_abandon_kept[] = {"before.bin", "after1.bin", "after2.bin"}; static const char zip_abandon_body[] = "zip-abandon kept member body"; -/* Build `path` with the kept members, opening a `doomed`-byte member after the +/* Fill `zf` with the kept members, opening a `doomed`-byte member after the first one and abandoning it mid-write (0: never open it, the reference). The - doomed member is stored, so its body reaches the file byte for byte. */ -static int zip_abandon_build(const char *path, size_t doomed) { - char catbuff[CATBUFF_SIZE]; + doomed member is stored, so its body reaches the file byte for byte. What the + abandon returned lands in *abandon_err. */ +static int zip_abandon_fill(zipFile zf, size_t doomed, int *abandon_err) { char chunk[4096]; zip_fileinfo fi; - zipFile zf = hts_zipOpen_utf8(fconv(catbuff, sizeof(catbuff), path), - APPEND_STATUS_CREATE); size_t i; - if (zf == NULL) - return -1; memset(&fi, 0, sizeof(fi)); memset(chunk, 'Z', sizeof(chunk)); for (i = 0; i < sizeof(zip_abandon_kept) / sizeof(zip_abandon_kept[0]); i++) { @@ -4051,16 +4047,43 @@ static int zip_abandon_build(const char *path, size_t doomed) { goto fail; left -= n; } - if (zipAbandonFileInZip(zf) != ZIP_OK) - goto fail; + *abandon_err = zipAbandonFileInZip(zf); } - return zipClose(zf, NULL) == ZIP_OK ? 0 : -1; + return 0; fail: - zipClose(zf, NULL); return -1; } +/* zip_abandon_fill() through a private filefunc table, `truncatable` telling + whether it carries a truncate entry: without one, as the Win32 tables were + before #1402, a rollback can only rewind. */ +static int zip_abandon_build_table(const char *path, size_t doomed, + hts_boolean truncatable, int *abandon_err) { + char catbuff[CATBUFF_SIZE]; + zlib_filefunc64_def ff; + zipFile zf; + int ret; + + hts_zip_filefunc64(&ff); + if (!truncatable) + ff.ztruncate64_file = NULL; + zf = zipOpen2_64(fconv(catbuff, sizeof(catbuff), path), APPEND_STATUS_CREATE, + NULL, &ff); + if (zf == NULL) + return -1; + ret = zip_abandon_fill(zf, doomed, abandon_err); + return zipClose(zf, NULL) == ZIP_OK ? ret : -1; +} + +static int zip_abandon_build(const char *path, size_t doomed) { + int abandon_err = ZIP_OK; + + if (zip_abandon_build_table(path, doomed, HTS_TRUE, &abandon_err) != 0) + return -1; + return abandon_err == ZIP_OK ? 0 : -1; +} + /* Whole file into a malloct'd buffer: binary, not terminated. */ static int zip_abandon_slurp(const char *path, char **out, size_t *len) { char catbuff[CATBUFF_SIZE]; @@ -4187,6 +4210,84 @@ static int st_zip_abandon(httrackp *opt, int argc, char **argv) { return fail; } +/* A rollback that could only rewind must say so (#1402): reporting ZIP_OK for + it hands the caller an archive whose directory no reader finds, the very + damage the truncate is there to prevent. */ +static int st_zip_abandon_notrunc(httrackp *opt, int argc, char **argv) { + static const size_t doomed = 200000; /* a tail past unzip.c's 64KB backscan */ + char refpath[HTS_URLMAXSIZE], path[HTS_URLMAXSIZE]; + char *ref = NULL, *got = NULL; + size_t reflen = 0, gotlen = 0; + int abandon_err = ZIP_OK; + int fail = 0; + unzFile uf; + + (void) opt; + if (argc < 1) { + fprintf(stderr, "zip-abandon-notrunc: needs a writable directory\n"); + return 1; + } + fconcat(refpath, sizeof(refpath), argv[0], "zip-notrunc-ref.zip"); + fconcat(path, sizeof(path), argv[0], "zip-notrunc.zip"); + + /* control: the table the cache opens with truncates, and reports ZIP_OK */ + if (zip_abandon_build_table(refpath, doomed, HTS_TRUE, &abandon_err) != 0 || + zip_abandon_slurp(refpath, &ref, &reflen) != 0 || reflen == 0) { + fprintf(stderr, "zip-abandon-notrunc: cannot build the reference\n"); + freet(ref); + return 1; + } + freet(ref); + if (abandon_err != ZIP_OK) { + fprintf(stderr, + "zip-abandon-notrunc: a truncating backend returned %d, want %d\n", + abandon_err, ZIP_OK); + fail++; + } + if (reflen >= doomed) { + fprintf(stderr, + "zip-abandon-notrunc: the reference kept %d byte(s), so the " + "abandoned member was not truncated away\n", + (int) reflen); + fail++; + } + + abandon_err = ZIP_OK; + if (zip_abandon_build_table(path, doomed, HTS_FALSE, &abandon_err) != 0 || + zip_abandon_slurp(path, &got, &gotlen) != 0) { + fprintf(stderr, "zip-abandon-notrunc: cannot build '%s'\n", path); + freet(got); + return fail + 1; + } + freet(got); + if (abandon_err == ZIP_OK) { + fprintf(stderr, "zip-abandon-notrunc: a backend with no truncate reported " + "the rollback as done\n"); + fail++; + } + /* only what the member had flushed stays, and it takes more than a reader's + 64KB backscan (unzip.c uMaxBack) to bury the directory behind it */ + if (gotlen <= reflen + 65535) { + fprintf(stderr, + "zip-abandon-notrunc: %d byte(s) left over the reference's %d, too " + "few to outgrow a 64KB backscan\n", + (int) gotlen, (int) reflen); + fail++; + } + /* what the wrong return hides: the tail buries the directory written after */ + uf = hts_unzOpen_utf8(path); + if (uf != NULL) { + fprintf(stderr, + "zip-abandon-notrunc: '%s' still opens, so the case proves " + "nothing\n", + path); + unzClose(uf); + fail++; + } + printf("zip-abandon-notrunc: %s\n", fail ? "FAIL" : "OK"); + return fail; +} + static int st_cache_legacy(httrackp *opt, int argc, char **argv) { int err; @@ -12434,6 +12535,9 @@ static const struct selftest_entry { st_zip_repair_shift}, {"zip-abandon", "", "an abandoned member leaves the archive byte-identical", st_zip_abandon}, + {"zip-abandon-notrunc", "", + "a rollback that could only rewind reports a failure", + st_zip_abandon_notrunc}, {"dns", "", "DNS resolver/cache self-test", st_dns}, {"dnstimeout", "", "a slow DNS resolve is bounded and holds no lock", st_dnstimeout}, diff --git a/src/htszlib.c b/src/htszlib.c index 5006864b1..fedd4495c 100644 --- a/src/htszlib.c +++ b/src/htszlib.c @@ -292,18 +292,21 @@ static voidpf ZCALLBACK hts_zip_fopen_utf8(voidpf opaque, const void *filename, return (voidpf) FOPEN((const char *) filename, mode_fopen); } +void hts_zip_filefunc64(zlib_filefunc64_def *ff) { + fill_fopen64_filefunc(ff); + ff->zopen64_file = hts_zip_fopen_utf8; +} + unzFile hts_unzOpen_utf8(const char *path) { zlib_filefunc64_def ff; - fill_fopen64_filefunc(&ff); - ff.zopen64_file = hts_zip_fopen_utf8; + hts_zip_filefunc64(&ff); return unzOpen2_64(path, &ff); } zipFile hts_zipOpen_utf8(const char *path, int append) { zlib_filefunc64_def ff; - fill_fopen64_filefunc(&ff); - ff.zopen64_file = hts_zip_fopen_utf8; + hts_zip_filefunc64(&ff); return zipOpen2_64(path, append, NULL, &ff); } diff --git a/src/htszlib.h b/src/htszlib.h index 703dd2365..4ddd5c21a 100644 --- a/src/htszlib.h +++ b/src/htszlib.h @@ -58,6 +58,8 @@ extern const char *hts_get_zerror(int err); (#630). `append` takes the zipOpen2_64 APPEND_STATUS_* values. */ extern unzFile hts_unzOpen_utf8(const char *path); extern zipFile hts_zipOpen_utf8(const char *path, int append); +/* The table both open with, for a caller that overrides one entry. */ +extern void hts_zip_filefunc64(zlib_filefunc64_def *ff); #endif #endif diff --git a/src/minizip/ioapi.c b/src/minizip/ioapi.c index 1c5a3660a..817b642de 100644 --- a/src/minizip/ioapi.c +++ b/src/minizip/ioapi.c @@ -72,9 +72,9 @@ ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc, voidpf filestrea int call_ztruncate64(const zlib_filefunc64_32_def *pfilefunc, voidpf filestream, ZPOS64_T size) { /* a backend with no truncate keeps its tail: better than failing a rollback - the caller has already performed */ + the caller has already performed, but the caller has to hear about it */ if (pfilefunc->zfile_func64.ztruncate64_file == NULL) - return 0; + return 1; return (*(pfilefunc->zfile_func64.ztruncate64_file))( pfilefunc->zfile_func64.opaque, filestream, size); } diff --git a/src/minizip/ioapi.c.diff b/src/minizip/ioapi.c.diff index 1e5bc01f2..2aee036f9 100644 --- a/src/minizip/ioapi.c.diff +++ b/src/minizip/ioapi.c.diff @@ -29,9 +29,9 @@ +int call_ztruncate64(const zlib_filefunc64_32_def *pfilefunc, voidpf filestream, + ZPOS64_T size) { + /* a backend with no truncate keeps its tail: better than failing a rollback -+ the caller has already performed */ ++ the caller has already performed, but the caller has to hear about it */ + if (pfilefunc->zfile_func64.ztruncate64_file == NULL) -+ return 0; ++ return 1; + return (*(pfilefunc->zfile_func64.ztruncate64_file))( + pfilefunc->zfile_func64.opaque, filestream, size); +} diff --git a/src/minizip/ioapi.h b/src/minizip/ioapi.h index 269db4c68..1967bb742 100644 --- a/src/minizip/ioapi.h +++ b/src/minizip/ioapi.h @@ -224,8 +224,8 @@ typedef struct zlib_filefunc64_32_def_s voidpf call_zopen64(const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode); long call_zseek64(const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin); ZPOS64_T call_ztell64(const zlib_filefunc64_32_def* pfilefunc,voidpf filestream); -/* Shorten filestream to `size`, 0 on success. Also 0 where the backend has no - truncate: rolling a member back is then a rewind only. */ +/* Shorten filestream to `size`: 0 on success, -1 on failure, 1 where the + backend has no truncate and rolling a member back is a rewind only. */ int call_ztruncate64(const zlib_filefunc64_32_def *pfilefunc, voidpf filestream, ZPOS64_T size); diff --git a/src/minizip/ioapi.h.diff b/src/minizip/ioapi.h.diff index 128eae1ae..448cb4579 100644 --- a/src/minizip/ioapi.h.diff +++ b/src/minizip/ioapi.h.diff @@ -82,8 +82,8 @@ voidpf call_zopen64(const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode); long call_zseek64(const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin); ZPOS64_T call_ztell64(const zlib_filefunc64_32_def* pfilefunc,voidpf filestream); -+/* Shorten filestream to `size`, 0 on success. Also 0 where the backend has no -+ truncate: rolling a member back is then a rewind only. */ ++/* Shorten filestream to `size`: 0 on success, -1 on failure, 1 where the ++ backend has no truncate and rolling a member back is a rewind only. */ +int call_ztruncate64(const zlib_filefunc64_32_def *pfilefunc, voidpf filestream, + ZPOS64_T size); diff --git a/src/minizip/iowin32.c b/src/minizip/iowin32.c index 08536e94b..9c1720e43 100644 --- a/src/minizip/iowin32.c +++ b/src/minizip/iowin32.c @@ -227,6 +227,22 @@ uLong ZCALLBACK win32_write_file_func(voidpf opaque, voidpf stream, const void* return ret; } +/* The handle carries no user-space buffer, so this is the OS write cache. */ +int ZCALLBACK win32_flush_file_func(voidpf opaque, voidpf stream) { + HANDLE hFile = NULL; + + (void) opaque; + if (stream != NULL) + hFile = ((WIN32FILE_IOWIN *) stream)->hf; + if (hFile == NULL) + return -1; + if (!FlushFileBuffers(hFile)) { + ((WIN32FILE_IOWIN *) stream)->error = (int) GetLastError(); + return -1; + } + return 0; +} + static BOOL MySetFilePointerEx(HANDLE hFile, LARGE_INTEGER pos, LARGE_INTEGER *newPos, DWORD dwMoveMethod) { #ifdef IOWIN32_USING_WINRT_API return SetFilePointerEx(hFile, pos, newPos, dwMoveMethod); @@ -367,6 +383,30 @@ long ZCALLBACK win32_seek64_file_func(voidpf opaque, voidpf stream, ZPOS64_T off return ret; } +/* Shorten the file to `size`, leaving the position where the caller had it: + SetEndOfFile cuts at the current one, so it has to be moved and put back. */ +int ZCALLBACK win32_truncate64_file_func(voidpf opaque, voidpf stream, + ZPOS64_T size) { + HANDLE hFile = NULL; + LARGE_INTEGER cur, want; + + (void) opaque; + if (stream != NULL) + hFile = ((WIN32FILE_IOWIN *) stream)->hf; + if (hFile == NULL) + return -1; + cur.QuadPart = 0; + want.QuadPart = (LONGLONG) size; + if (!MySetFilePointerEx(hFile, cur, &cur, FILE_CURRENT) || + !MySetFilePointerEx(hFile, want, NULL, FILE_BEGIN) || + !SetEndOfFile(hFile) || + !MySetFilePointerEx(hFile, cur, NULL, FILE_BEGIN)) { + ((WIN32FILE_IOWIN *) stream)->error = (int) GetLastError(); + return -1; + } + return 0; +} + int ZCALLBACK win32_close_file_func(voidpf opaque, voidpf stream) { int ret=-1; @@ -399,6 +439,7 @@ void fill_win32_filefunc(zlib_filefunc_def* pzlib_filefunc_def) { pzlib_filefunc_def->zwrite_file = win32_write_file_func; pzlib_filefunc_def->ztell_file = win32_tell_file_func; pzlib_filefunc_def->zseek_file = win32_seek_file_func; + pzlib_filefunc_def->zflush_file = win32_flush_file_func; pzlib_filefunc_def->zclose_file = win32_close_file_func; pzlib_filefunc_def->zerror_file = win32_error_file_func; pzlib_filefunc_def->opaque = NULL; @@ -410,6 +451,8 @@ void fill_win32_filefunc64(zlib_filefunc64_def* pzlib_filefunc_def) { pzlib_filefunc_def->zwrite_file = win32_write_file_func; pzlib_filefunc_def->ztell64_file = win32_tell64_file_func; pzlib_filefunc_def->zseek64_file = win32_seek64_file_func; + pzlib_filefunc_def->ztruncate64_file = win32_truncate64_file_func; + pzlib_filefunc_def->zflush_file = win32_flush_file_func; pzlib_filefunc_def->zclose_file = win32_close_file_func; pzlib_filefunc_def->zerror_file = win32_error_file_func; pzlib_filefunc_def->opaque = NULL; @@ -422,6 +465,8 @@ void fill_win32_filefunc64A(zlib_filefunc64_def* pzlib_filefunc_def) { pzlib_filefunc_def->zwrite_file = win32_write_file_func; pzlib_filefunc_def->ztell64_file = win32_tell64_file_func; pzlib_filefunc_def->zseek64_file = win32_seek64_file_func; + pzlib_filefunc_def->ztruncate64_file = win32_truncate64_file_func; + pzlib_filefunc_def->zflush_file = win32_flush_file_func; pzlib_filefunc_def->zclose_file = win32_close_file_func; pzlib_filefunc_def->zerror_file = win32_error_file_func; pzlib_filefunc_def->opaque = NULL; @@ -434,6 +479,8 @@ void fill_win32_filefunc64W(zlib_filefunc64_def* pzlib_filefunc_def) { pzlib_filefunc_def->zwrite_file = win32_write_file_func; pzlib_filefunc_def->ztell64_file = win32_tell64_file_func; pzlib_filefunc_def->zseek64_file = win32_seek64_file_func; + pzlib_filefunc_def->ztruncate64_file = win32_truncate64_file_func; + pzlib_filefunc_def->zflush_file = win32_flush_file_func; pzlib_filefunc_def->zclose_file = win32_close_file_func; pzlib_filefunc_def->zerror_file = win32_error_file_func; pzlib_filefunc_def->opaque = NULL; diff --git a/src/minizip/iowin32.c.diff b/src/minizip/iowin32.c.diff new file mode 100644 index 000000000..ad478100f --- /dev/null +++ b/src/minizip/iowin32.c.diff @@ -0,0 +1,91 @@ +--- iowin32.c.orig 2026-08-24 22:06:17.024142482 +0200 ++++ iowin32.c 2026-08-24 22:06:17.036984782 +0200 +@@ -227,6 +227,22 @@ + return ret; + } + ++/* The handle carries no user-space buffer, so this is the OS write cache. */ ++int ZCALLBACK win32_flush_file_func(voidpf opaque, voidpf stream) { ++ HANDLE hFile = NULL; ++ ++ (void) opaque; ++ if (stream != NULL) ++ hFile = ((WIN32FILE_IOWIN *) stream)->hf; ++ if (hFile == NULL) ++ return -1; ++ if (!FlushFileBuffers(hFile)) { ++ ((WIN32FILE_IOWIN *) stream)->error = (int) GetLastError(); ++ return -1; ++ } ++ return 0; ++} ++ + static BOOL MySetFilePointerEx(HANDLE hFile, LARGE_INTEGER pos, LARGE_INTEGER *newPos, DWORD dwMoveMethod) { + #ifdef IOWIN32_USING_WINRT_API + return SetFilePointerEx(hFile, pos, newPos, dwMoveMethod); +@@ -367,6 +383,30 @@ + return ret; + } + ++/* Shorten the file to `size`, leaving the position where the caller had it: ++ SetEndOfFile cuts at the current one, so it has to be moved and put back. */ ++int ZCALLBACK win32_truncate64_file_func(voidpf opaque, voidpf stream, ++ ZPOS64_T size) { ++ HANDLE hFile = NULL; ++ LARGE_INTEGER cur, want; ++ ++ (void) opaque; ++ if (stream != NULL) ++ hFile = ((WIN32FILE_IOWIN *) stream)->hf; ++ if (hFile == NULL) ++ return -1; ++ cur.QuadPart = 0; ++ want.QuadPart = (LONGLONG) size; ++ if (!MySetFilePointerEx(hFile, cur, &cur, FILE_CURRENT) || ++ !MySetFilePointerEx(hFile, want, NULL, FILE_BEGIN) || ++ !SetEndOfFile(hFile) || ++ !MySetFilePointerEx(hFile, cur, NULL, FILE_BEGIN)) { ++ ((WIN32FILE_IOWIN *) stream)->error = (int) GetLastError(); ++ return -1; ++ } ++ return 0; ++} ++ + int ZCALLBACK win32_close_file_func(voidpf opaque, voidpf stream) { + int ret=-1; + +@@ -399,6 +439,7 @@ + pzlib_filefunc_def->zwrite_file = win32_write_file_func; + pzlib_filefunc_def->ztell_file = win32_tell_file_func; + pzlib_filefunc_def->zseek_file = win32_seek_file_func; ++ pzlib_filefunc_def->zflush_file = win32_flush_file_func; + pzlib_filefunc_def->zclose_file = win32_close_file_func; + pzlib_filefunc_def->zerror_file = win32_error_file_func; + pzlib_filefunc_def->opaque = NULL; +@@ -410,6 +451,8 @@ + pzlib_filefunc_def->zwrite_file = win32_write_file_func; + pzlib_filefunc_def->ztell64_file = win32_tell64_file_func; + pzlib_filefunc_def->zseek64_file = win32_seek64_file_func; ++ pzlib_filefunc_def->ztruncate64_file = win32_truncate64_file_func; ++ pzlib_filefunc_def->zflush_file = win32_flush_file_func; + pzlib_filefunc_def->zclose_file = win32_close_file_func; + pzlib_filefunc_def->zerror_file = win32_error_file_func; + pzlib_filefunc_def->opaque = NULL; +@@ -422,6 +465,8 @@ + pzlib_filefunc_def->zwrite_file = win32_write_file_func; + pzlib_filefunc_def->ztell64_file = win32_tell64_file_func; + pzlib_filefunc_def->zseek64_file = win32_seek64_file_func; ++ pzlib_filefunc_def->ztruncate64_file = win32_truncate64_file_func; ++ pzlib_filefunc_def->zflush_file = win32_flush_file_func; + pzlib_filefunc_def->zclose_file = win32_close_file_func; + pzlib_filefunc_def->zerror_file = win32_error_file_func; + pzlib_filefunc_def->opaque = NULL; +@@ -434,6 +479,8 @@ + pzlib_filefunc_def->zwrite_file = win32_write_file_func; + pzlib_filefunc_def->ztell64_file = win32_tell64_file_func; + pzlib_filefunc_def->zseek64_file = win32_seek64_file_func; ++ pzlib_filefunc_def->ztruncate64_file = win32_truncate64_file_func; ++ pzlib_filefunc_def->zflush_file = win32_flush_file_func; + pzlib_filefunc_def->zclose_file = win32_close_file_func; + pzlib_filefunc_def->zerror_file = win32_error_file_func; + pzlib_filefunc_def->opaque = NULL; diff --git a/src/minizip/iowin32.c.orig b/src/minizip/iowin32.c.orig new file mode 100644 index 000000000..08536e94b --- /dev/null +++ b/src/minizip/iowin32.c.orig @@ -0,0 +1,440 @@ +/* iowin32.c -- IO base function header for compress/uncompress .zip + Version 1.1, February 14h, 2010 + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications for Zip64 support + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + +*/ + +#include + +#include "zlib.h" +#include "ioapi.h" +#include "iowin32.h" + +#ifndef INVALID_HANDLE_VALUE +#define INVALID_HANDLE_VALUE (0xFFFFFFFF) +#endif + +#ifndef INVALID_SET_FILE_POINTER +#define INVALID_SET_FILE_POINTER ((DWORD)-1) +#endif + + +// see Include/shared/winapifamily.h in the Windows Kit +#if defined(WINAPI_FAMILY_PARTITION) && (!(defined(IOWIN32_USING_WINRT_API))) + +#if !defined(WINAPI_FAMILY_ONE_PARTITION) +#define WINAPI_FAMILY_ONE_PARTITION(PartitionSet, Partition) ((WINAPI_FAMILY & PartitionSet) == Partition) +#endif + +#if WINAPI_FAMILY_ONE_PARTITION(WINAPI_FAMILY, WINAPI_PARTITION_APP) +#define IOWIN32_USING_WINRT_API 1 +#endif +#endif + +typedef struct +{ + HANDLE hf; + int error; +} WIN32FILE_IOWIN; + + +static void win32_translate_open_mode(int mode, + DWORD* lpdwDesiredAccess, + DWORD* lpdwCreationDisposition, + DWORD* lpdwShareMode, + DWORD* lpdwFlagsAndAttributes) { + *lpdwDesiredAccess = *lpdwShareMode = *lpdwFlagsAndAttributes = *lpdwCreationDisposition = 0; + + if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ) + { + *lpdwDesiredAccess = GENERIC_READ; + *lpdwCreationDisposition = OPEN_EXISTING; + *lpdwShareMode = FILE_SHARE_READ; + } + else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) + { + *lpdwDesiredAccess = GENERIC_WRITE | GENERIC_READ; + *lpdwCreationDisposition = OPEN_EXISTING; + } + else if (mode & ZLIB_FILEFUNC_MODE_CREATE) + { + *lpdwDesiredAccess = GENERIC_WRITE | GENERIC_READ; + *lpdwCreationDisposition = CREATE_ALWAYS; + } +} + +static voidpf win32_build_iowin(HANDLE hFile) { + voidpf ret=NULL; + + if ((hFile != NULL) && (hFile != INVALID_HANDLE_VALUE)) + { + WIN32FILE_IOWIN w32fiow; + w32fiow.hf = hFile; + w32fiow.error = 0; + ret = malloc(sizeof(WIN32FILE_IOWIN)); + + if (ret==NULL) + CloseHandle(hFile); + else + *((WIN32FILE_IOWIN*)ret) = w32fiow; + } + return ret; +} + +voidpf ZCALLBACK win32_open64_file_func(voidpf opaque, const void* filename, int mode) { + const char* mode_fopen = NULL; + DWORD dwDesiredAccess,dwCreationDisposition,dwShareMode,dwFlagsAndAttributes ; + HANDLE hFile = NULL; + + win32_translate_open_mode(mode,&dwDesiredAccess,&dwCreationDisposition,&dwShareMode,&dwFlagsAndAttributes); + +#ifdef IOWIN32_USING_WINRT_API +#ifdef UNICODE + if ((filename!=NULL) && (dwDesiredAccess != 0)) + hFile = CreateFile2((LPCTSTR)filename, dwDesiredAccess, dwShareMode, dwCreationDisposition, NULL); +#else + if ((filename!=NULL) && (dwDesiredAccess != 0)) + { + WCHAR filenameW[FILENAME_MAX + 0x200 + 1]; + MultiByteToWideChar(CP_ACP,0,(const char*)filename,-1,filenameW,FILENAME_MAX + 0x200); + hFile = CreateFile2(filenameW, dwDesiredAccess, dwShareMode, dwCreationDisposition, NULL); + } +#endif +#else + if ((filename!=NULL) && (dwDesiredAccess != 0)) + hFile = CreateFile((LPCTSTR)filename, dwDesiredAccess, dwShareMode, NULL, dwCreationDisposition, dwFlagsAndAttributes, NULL); +#endif + + return win32_build_iowin(hFile); +} + + +voidpf ZCALLBACK win32_open64_file_funcA(voidpf opaque, const void* filename, int mode) { + const char* mode_fopen = NULL; + DWORD dwDesiredAccess,dwCreationDisposition,dwShareMode,dwFlagsAndAttributes ; + HANDLE hFile = NULL; + + win32_translate_open_mode(mode,&dwDesiredAccess,&dwCreationDisposition,&dwShareMode,&dwFlagsAndAttributes); + +#ifdef IOWIN32_USING_WINRT_API + if ((filename!=NULL) && (dwDesiredAccess != 0)) + { + WCHAR filenameW[FILENAME_MAX + 0x200 + 1]; + MultiByteToWideChar(CP_ACP,0,(const char*)filename,-1,filenameW,FILENAME_MAX + 0x200); + hFile = CreateFile2(filenameW, dwDesiredAccess, dwShareMode, dwCreationDisposition, NULL); + } +#else + if ((filename!=NULL) && (dwDesiredAccess != 0)) + hFile = CreateFileA((LPCSTR)filename, dwDesiredAccess, dwShareMode, NULL, dwCreationDisposition, dwFlagsAndAttributes, NULL); +#endif + + return win32_build_iowin(hFile); +} + + +voidpf ZCALLBACK win32_open64_file_funcW(voidpf opaque, const void* filename, int mode) { + const char* mode_fopen = NULL; + DWORD dwDesiredAccess,dwCreationDisposition,dwShareMode,dwFlagsAndAttributes ; + HANDLE hFile = NULL; + + win32_translate_open_mode(mode,&dwDesiredAccess,&dwCreationDisposition,&dwShareMode,&dwFlagsAndAttributes); + +#ifdef IOWIN32_USING_WINRT_API + if ((filename!=NULL) && (dwDesiredAccess != 0)) + hFile = CreateFile2((LPCWSTR)filename, dwDesiredAccess, dwShareMode, dwCreationDisposition,NULL); +#else + if ((filename!=NULL) && (dwDesiredAccess != 0)) + hFile = CreateFileW((LPCWSTR)filename, dwDesiredAccess, dwShareMode, NULL, dwCreationDisposition, dwFlagsAndAttributes, NULL); +#endif + + return win32_build_iowin(hFile); +} + + +voidpf ZCALLBACK win32_open_file_func(voidpf opaque, const char* filename, int mode) { + const char* mode_fopen = NULL; + DWORD dwDesiredAccess,dwCreationDisposition,dwShareMode,dwFlagsAndAttributes ; + HANDLE hFile = NULL; + + win32_translate_open_mode(mode,&dwDesiredAccess,&dwCreationDisposition,&dwShareMode,&dwFlagsAndAttributes); + +#ifdef IOWIN32_USING_WINRT_API +#ifdef UNICODE + if ((filename!=NULL) && (dwDesiredAccess != 0)) + hFile = CreateFile2((LPCTSTR)filename, dwDesiredAccess, dwShareMode, dwCreationDisposition, NULL); +#else + if ((filename!=NULL) && (dwDesiredAccess != 0)) + { + WCHAR filenameW[FILENAME_MAX + 0x200 + 1]; + MultiByteToWideChar(CP_ACP,0,(const char*)filename,-1,filenameW,FILENAME_MAX + 0x200); + hFile = CreateFile2(filenameW, dwDesiredAccess, dwShareMode, dwCreationDisposition, NULL); + } +#endif +#else + if ((filename!=NULL) && (dwDesiredAccess != 0)) + hFile = CreateFile((LPCTSTR)filename, dwDesiredAccess, dwShareMode, NULL, dwCreationDisposition, dwFlagsAndAttributes, NULL); +#endif + + return win32_build_iowin(hFile); +} + + +uLong ZCALLBACK win32_read_file_func(voidpf opaque, voidpf stream, void* buf,uLong size) { + uLong ret=0; + HANDLE hFile = NULL; + if (stream!=NULL) + hFile = ((WIN32FILE_IOWIN*)stream) -> hf; + + if (hFile != NULL) + { + if (!ReadFile(hFile, buf, size, &ret, NULL)) + { + DWORD dwErr = GetLastError(); + if (dwErr == ERROR_HANDLE_EOF) + dwErr = 0; + ((WIN32FILE_IOWIN*)stream) -> error=(int)dwErr; + } + } + + return ret; +} + + +uLong ZCALLBACK win32_write_file_func(voidpf opaque, voidpf stream, const void* buf, uLong size) { + uLong ret=0; + HANDLE hFile = NULL; + if (stream!=NULL) + hFile = ((WIN32FILE_IOWIN*)stream) -> hf; + + if (hFile != NULL) + { + if (!WriteFile(hFile, buf, size, &ret, NULL)) + { + DWORD dwErr = GetLastError(); + if (dwErr == ERROR_HANDLE_EOF) + dwErr = 0; + ((WIN32FILE_IOWIN*)stream) -> error=(int)dwErr; + } + } + + return ret; +} + +static BOOL MySetFilePointerEx(HANDLE hFile, LARGE_INTEGER pos, LARGE_INTEGER *newPos, DWORD dwMoveMethod) { +#ifdef IOWIN32_USING_WINRT_API + return SetFilePointerEx(hFile, pos, newPos, dwMoveMethod); +#else + LONG lHigh = pos.HighPart; + DWORD dwNewPos = SetFilePointer(hFile, pos.LowPart, &lHigh, dwMoveMethod); + BOOL fOk = TRUE; + if (dwNewPos == 0xFFFFFFFF) + if (GetLastError() != NO_ERROR) + fOk = FALSE; + if ((newPos != NULL) && (fOk)) + { + newPos->LowPart = dwNewPos; + newPos->HighPart = lHigh; + } + return fOk; +#endif +} + +long ZCALLBACK win32_tell_file_func(voidpf opaque, voidpf stream) { + long ret=-1; + HANDLE hFile = NULL; + if (stream!=NULL) + hFile = ((WIN32FILE_IOWIN*)stream) -> hf; + if (hFile != NULL) + { + LARGE_INTEGER pos; + pos.QuadPart = 0; + + if (!MySetFilePointerEx(hFile, pos, &pos, FILE_CURRENT)) + { + DWORD dwErr = GetLastError(); + ((WIN32FILE_IOWIN*)stream) -> error=(int)dwErr; + ret = -1; + } + else + ret=(long)pos.LowPart; + } + return ret; +} + +ZPOS64_T ZCALLBACK win32_tell64_file_func(voidpf opaque, voidpf stream) { + ZPOS64_T ret= (ZPOS64_T)-1; + HANDLE hFile = NULL; + if (stream!=NULL) + hFile = ((WIN32FILE_IOWIN*)stream)->hf; + + if (hFile) + { + LARGE_INTEGER pos; + pos.QuadPart = 0; + + if (!MySetFilePointerEx(hFile, pos, &pos, FILE_CURRENT)) + { + DWORD dwErr = GetLastError(); + ((WIN32FILE_IOWIN*)stream) -> error=(int)dwErr; + ret = (ZPOS64_T)-1; + } + else + ret=pos.QuadPart; + } + return ret; +} + + +long ZCALLBACK win32_seek_file_func(voidpf opaque, voidpf stream, uLong offset, int origin) { + DWORD dwMoveMethod=0xFFFFFFFF; + HANDLE hFile = NULL; + + long ret=-1; + if (stream!=NULL) + hFile = ((WIN32FILE_IOWIN*)stream) -> hf; + switch (origin) + { + case ZLIB_FILEFUNC_SEEK_CUR : + dwMoveMethod = FILE_CURRENT; + break; + case ZLIB_FILEFUNC_SEEK_END : + dwMoveMethod = FILE_END; + break; + case ZLIB_FILEFUNC_SEEK_SET : + dwMoveMethod = FILE_BEGIN; + break; + default: return -1; + } + + if (hFile != NULL) + { + LARGE_INTEGER pos; + pos.QuadPart = offset; + if (!MySetFilePointerEx(hFile, pos, NULL, dwMoveMethod)) + { + DWORD dwErr = GetLastError(); + ((WIN32FILE_IOWIN*)stream) -> error=(int)dwErr; + ret = -1; + } + else + ret=0; + } + return ret; +} + +long ZCALLBACK win32_seek64_file_func(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin) { + DWORD dwMoveMethod=0xFFFFFFFF; + HANDLE hFile = NULL; + long ret=-1; + + if (stream!=NULL) + hFile = ((WIN32FILE_IOWIN*)stream)->hf; + + switch (origin) + { + case ZLIB_FILEFUNC_SEEK_CUR : + dwMoveMethod = FILE_CURRENT; + break; + case ZLIB_FILEFUNC_SEEK_END : + dwMoveMethod = FILE_END; + break; + case ZLIB_FILEFUNC_SEEK_SET : + dwMoveMethod = FILE_BEGIN; + break; + default: return -1; + } + + if (hFile) + { + LARGE_INTEGER pos; + pos.QuadPart = offset; + if (!MySetFilePointerEx(hFile, pos, NULL, dwMoveMethod)) + { + DWORD dwErr = GetLastError(); + ((WIN32FILE_IOWIN*)stream) -> error=(int)dwErr; + ret = -1; + } + else + ret=0; + } + return ret; +} + +int ZCALLBACK win32_close_file_func(voidpf opaque, voidpf stream) { + int ret=-1; + + if (stream!=NULL) + { + HANDLE hFile; + hFile = ((WIN32FILE_IOWIN*)stream) -> hf; + if (hFile != NULL) + { + CloseHandle(hFile); + ret=0; + } + free(stream); + } + return ret; +} + +int ZCALLBACK win32_error_file_func(voidpf opaque, voidpf stream) { + int ret=-1; + if (stream!=NULL) + { + ret = ((WIN32FILE_IOWIN*)stream) -> error; + } + return ret; +} + +void fill_win32_filefunc(zlib_filefunc_def* pzlib_filefunc_def) { + pzlib_filefunc_def->zopen_file = win32_open_file_func; + pzlib_filefunc_def->zread_file = win32_read_file_func; + pzlib_filefunc_def->zwrite_file = win32_write_file_func; + pzlib_filefunc_def->ztell_file = win32_tell_file_func; + pzlib_filefunc_def->zseek_file = win32_seek_file_func; + pzlib_filefunc_def->zclose_file = win32_close_file_func; + pzlib_filefunc_def->zerror_file = win32_error_file_func; + pzlib_filefunc_def->opaque = NULL; +} + +void fill_win32_filefunc64(zlib_filefunc64_def* pzlib_filefunc_def) { + pzlib_filefunc_def->zopen64_file = win32_open64_file_func; + pzlib_filefunc_def->zread_file = win32_read_file_func; + pzlib_filefunc_def->zwrite_file = win32_write_file_func; + pzlib_filefunc_def->ztell64_file = win32_tell64_file_func; + pzlib_filefunc_def->zseek64_file = win32_seek64_file_func; + pzlib_filefunc_def->zclose_file = win32_close_file_func; + pzlib_filefunc_def->zerror_file = win32_error_file_func; + pzlib_filefunc_def->opaque = NULL; +} + + +void fill_win32_filefunc64A(zlib_filefunc64_def* pzlib_filefunc_def) { + pzlib_filefunc_def->zopen64_file = win32_open64_file_funcA; + pzlib_filefunc_def->zread_file = win32_read_file_func; + pzlib_filefunc_def->zwrite_file = win32_write_file_func; + pzlib_filefunc_def->ztell64_file = win32_tell64_file_func; + pzlib_filefunc_def->zseek64_file = win32_seek64_file_func; + pzlib_filefunc_def->zclose_file = win32_close_file_func; + pzlib_filefunc_def->zerror_file = win32_error_file_func; + pzlib_filefunc_def->opaque = NULL; +} + + +void fill_win32_filefunc64W(zlib_filefunc64_def* pzlib_filefunc_def) { + pzlib_filefunc_def->zopen64_file = win32_open64_file_funcW; + pzlib_filefunc_def->zread_file = win32_read_file_func; + pzlib_filefunc_def->zwrite_file = win32_write_file_func; + pzlib_filefunc_def->ztell64_file = win32_tell64_file_func; + pzlib_filefunc_def->zseek64_file = win32_seek64_file_func; + pzlib_filefunc_def->zclose_file = win32_close_file_func; + pzlib_filefunc_def->zerror_file = win32_error_file_func; + pzlib_filefunc_def->opaque = NULL; +} diff --git a/src/minizip/zip.c b/src/minizip/zip.c index a3219bb7c..68cbe8d31 100644 --- a/src/minizip/zip.c +++ b/src/minizip/zip.c @@ -1712,6 +1712,7 @@ extern int ZEXPORT zipCloseFileInZip(zipFile file) { record there is no API to take it back. */ extern int ZEXPORT zipAbandonFileInZip(zipFile file) { zip64_internal *zi; + int truncate_err; if (file == NULL) return ZIP_PARAMERROR; @@ -1736,12 +1737,14 @@ extern int ZEXPORT zipAbandonFileInZip(zipFile file) { if (ZSEEK64(zi->z_filefunc, zi->filestream, zi->ci.pos_local_header, ZLIB_FILEFUNC_SEEK_SET) != 0) return ZIP_ERRNO; + truncate_err = + ZTRUNCATE64(zi->z_filefunc, zi->filestream, zi->ci.pos_local_header); + if (truncate_err < 0) + return ZIP_ERRNO; /* the rewind alone leaves what the member flushed past the end-of-central- directory written later, which no reader finds once that tail outgrows the 64KB backscan */ - if (ZTRUNCATE64(zi->z_filefunc, zi->filestream, zi->ci.pos_local_header) != 0) - return ZIP_ERRNO; - return ZIP_OK; + return truncate_err == 0 ? ZIP_OK : ZIP_NOTRUNCATED; } local int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip) { diff --git a/src/minizip/zip.c.diff b/src/minizip/zip.c.diff index 568b998ad..6fab5cef4 100644 --- a/src/minizip/zip.c.diff +++ b/src/minizip/zip.c.diff @@ -9,7 +9,7 @@ zi->ci.stream.avail_in = len; while ((err==ZIP_OK) && (zi->ci.stream.avail_in>0)) -@@ -1706,6 +1706,44 @@ +@@ -1706,6 +1706,47 @@ return zipCloseFileInZipRaw (file,0,0); } @@ -19,6 +19,7 @@ + record there is no API to take it back. */ +extern int ZEXPORT zipAbandonFileInZip(zipFile file) { + zip64_internal *zi; ++ int truncate_err; + + if (file == NULL) + return ZIP_PARAMERROR; @@ -43,18 +44,20 @@ + if (ZSEEK64(zi->z_filefunc, zi->filestream, zi->ci.pos_local_header, + ZLIB_FILEFUNC_SEEK_SET) != 0) + return ZIP_ERRNO; ++ truncate_err = ++ ZTRUNCATE64(zi->z_filefunc, zi->filestream, zi->ci.pos_local_header); ++ if (truncate_err < 0) ++ return ZIP_ERRNO; + /* the rewind alone leaves what the member flushed past the end-of-central- + directory written later, which no reader finds once that tail outgrows the + 64KB backscan */ -+ if (ZTRUNCATE64(zi->z_filefunc, zi->filestream, zi->ci.pos_local_header) != 0) -+ return ZIP_ERRNO; -+ return ZIP_OK; ++ return truncate_err == 0 ? ZIP_OK : ZIP_NOTRUNCATED; +} + local int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip) { int err = ZIP_OK; ZPOS64_T pos = zip64eocd_pos_inzip - zi->add_position_when_writing_offset; -@@ -1830,6 +1868,18 @@ +@@ -1830,6 +1871,18 @@ return err; } diff --git a/src/minizip/zip.h b/src/minizip/zip.h index bdc6cd751..b00cda26e 100644 --- a/src/minizip/zip.h +++ b/src/minizip/zip.h @@ -75,6 +75,9 @@ typedef voidp zipFile; #define ZIP_PARAMERROR (-102) #define ZIP_BADZIPFILE (-103) #define ZIP_INTERNALERROR (-104) +/* httrack addition: rolled back by a rewind only, the member's bytes still + there. Past the UNZ_* codes (-105 is UNZ_CRCERROR), which share the space. */ +#define ZIP_NOTRUNCATED (-110) #ifndef DEF_MEM_LEVEL # if MAX_MEM_LEVEL >= 8 @@ -327,7 +330,8 @@ extern int ZEXPORT zipAbandonFileInZip(zipFile file); /* Roll back the current file in the zipfile: the write position returns to its local header and no central-directory record is created, so the partial - member never appears in the archive. (httrack addition, see zip.c.diff) + member never appears in the archive. ZIP_NOTRUNCATED when the file could only + be rewound, its bytes still past the end. (httrack addition, see zip.c.diff) */ extern int ZEXPORT zipCloseFileInZipRaw(zipFile file, diff --git a/src/minizip/zip.h.diff b/src/minizip/zip.h.diff index 3a9772988..c8f9aa5a9 100644 --- a/src/minizip/zip.h.diff +++ b/src/minizip/zip.h.diff @@ -1,6 +1,16 @@ --- zip.h.orig 2024-01-27 14:07:18.636193212 +0100 +++ zip.h 2024-01-27 14:10:04.104643731 +0100 -@@ -313,11 +313,23 @@ +@@ -75,6 +75,9 @@ + #define ZIP_PARAMERROR (-102) + #define ZIP_BADZIPFILE (-103) + #define ZIP_INTERNALERROR (-104) ++/* httrack addition: rolled back by a rewind only, the member's bytes still ++ there. Past the UNZ_* codes (-105 is UNZ_CRCERROR), which share the space. */ ++#define ZIP_NOTRUNCATED (-110) + + #ifndef DEF_MEM_LEVEL + # if MAX_MEM_LEVEL >= 8 +@@ -313,11 +316,24 @@ Write data in the zipfile */ @@ -18,7 +28,8 @@ +/* + Roll back the current file in the zipfile: the write position returns to its + local header and no central-directory record is created, so the partial -+ member never appears in the archive. (httrack addition, see zip.c.diff) ++ member never appears in the archive. ZIP_NOTRUNCATED when the file could only ++ be rewound, its bytes still past the end. (httrack addition, see zip.c.diff) +*/ + extern int ZEXPORT zipCloseFileInZipRaw(zipFile file, diff --git a/tests/356_engine-zip-abandon-notrunc.test b/tests/356_engine-zip-abandon-notrunc.test new file mode 100755 index 000000000..0b844bbc5 --- /dev/null +++ b/tests/356_engine-zip-abandon-notrunc.test @@ -0,0 +1,37 @@ +#!/bin/bash +# +# A ZIP backend with no truncate entry can only rewind an abandoned member, and +# has to report that instead of ZIP_OK (#1402): the tail it leaves behind hides +# the central directory from every reader once it outgrows their backscan. + +set -eu +# shellcheck source=tests/testlib.sh +. "${0%"${0##*/}"}./testlib.sh" + +tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_zipnotrunc.XXXXXX") +cleanup_push rm -rf "$tmp" + +assert_selftest "zip-abandon-notrunc: OK" zip-abandon-notrunc "$tmp/" + +# Grade both archives with a reader that is not minizip: its own backscan is +# what the leftover tail hides the directory from. +if py=$(find_python); then + "$py" - "$tmp/zip-notrunc-ref.zip" "$tmp/zip-notrunc.zip" <<'EOF' +import os +import sys +import zipfile + +ref, kept = sys.argv[1], sys.argv[2] +with zipfile.ZipFile(ref) as z: + assert z.namelist() == ["before.bin", "after1.bin", "after2.bin"], z.namelist() + assert z.testzip() is None, ref +try: + with zipfile.ZipFile(kept) as z: + raise AssertionError("%s still opens: %s" % (kept, z.namelist())) +except zipfile.BadZipFile: + pass +assert os.path.getsize(kept) > os.path.getsize(ref), (kept, ref) +EOF +else + echo "python3 not found: the independent zip reader is skipped" +fi From 5b4c093bd4e30fb24f4ba3fa78a78fd4509a72d7 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 24 Aug 2026 23:37:05 +0200 Subject: [PATCH 2/2] Bound the reference on the backscan, and normalize a truncate errno zip-abandon-notrunc's "the reference was not truncated away" check compared against the member's nominal size, which an un-truncated archive never reaches: zip.c flushes a stored member in 64KB units, so the case it named sailed through. Bound it on the 64KB backscan, the size that matters here. _chsize_s returns an errno rather than -1, so a failed Windows truncate was labelled "this backend has no truncate" instead of a failure. Collapse any non-zero backend return to -1 in call_ztruncate64. The cache's incomplete-rollback warning ran in no test, since cache-writefail failed outright on a table with no truncate. It now also drives one such table and asserts the entry drops, the mirror lives, and the warning is printed. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Xavier Roche --- src/htscache_selftest.c | 70 ++++++++++++++++++++++++++---- src/htsselftest.c | 4 +- src/minizip/ioapi.c | 5 ++- src/minizip/ioapi.c.diff | 13 +++--- tests/01_zlib-cache-writefail.test | 3 ++ 5 files changed, 79 insertions(+), 16 deletions(-) diff --git a/src/htscache_selftest.c b/src/htscache_selftest.c index 8fd467e38..78766397c 100644 --- a/src/htscache_selftest.c +++ b/src/htscache_selftest.c @@ -415,15 +415,16 @@ static int ZCALLBACK selftest_counting_ztruncate(voidpf opaque, voidpf stream, } /* Open a ZIP whose writes fail past inj->budget, so cache_add() hits an error. - Through the table the cache itself opens with (#1402): a table without a - truncate rolls a member back by rewinding, leaving the bytes behind. */ + Through the table the cache itself opens with (#1402), or with its truncate + entry dropped, as the Win32 tables were: a rollback then only rewinds. */ static zipFile selftest_open_failing_zip(const char *path, - writefail_inject *inj) { + writefail_inject *inj, + hts_boolean truncatable) { zlib_filefunc64_def ff; hts_zip_filefunc64(&ff); /* real fopen/read/seek/close; ignores opaque */ - inj->truncate = ff.ztruncate64_file; - ff.ztruncate64_file = selftest_counting_ztruncate; + inj->truncate = truncatable ? ff.ztruncate64_file : NULL; + ff.ztruncate64_file = truncatable ? selftest_counting_ztruncate : NULL; ff.zwrite_file = selftest_failing_zwrite; ff.opaque = inj; return zipOpen2_64(path, APPEND_STATUS_CREATE, NULL, &ff); @@ -524,7 +525,7 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir) { cache.log = stderr; cache.errlog = stderr; cache.hashtable = coucal_new(0); - cache.zipOutput = selftest_open_failing_zip(path, &inj); + cache.zipOutput = selftest_open_failing_zip(path, &inj, HTS_TRUE); if (cache.zipOutput == NULL) { fprintf(stderr, "cache-writefail: could not open injected ZIP\n"); fail++; @@ -597,7 +598,7 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir) { cache.log = stderr; cache.errlog = stderr; cache.hashtable = coucal_new(0); - cache.zipOutput = selftest_open_failing_zip(path, &inj); + cache.zipOutput = selftest_open_failing_zip(path, &inj, HTS_TRUE); opt->state.exit_xh = 0; for (i = 0; i < 10; i++) { @@ -639,7 +640,7 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir) { cache.log = stderr; cache.errlog = stderr; cache.hashtable = coucal_new(0); - cache.zipOutput = selftest_open_failing_zip(path, &inj); + cache.zipOutput = selftest_open_failing_zip(path, &inj, HTS_TRUE); opt->state.exit_xh = 0; writefail_store(opt, &cache, "/blob.bin", body, body_len); @@ -677,6 +678,57 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir) { } } + /* a backend with no truncate rolls back by rewinding: the entry still drops + and the mirror still lives, and the incomplete rollback is warned about */ + { + cache_back cache; + writefail_inject inj; + char extra[8192]; + char rbody[64]; + int n; + + inj.budget = 4096; + inj.fail_errno = EIO; + inj.writes = 0; + inj.truncates = 0; + inj.fail_once = 1; + memset(&cache, 0, sizeof(cache)); + cache.type = 1; + cache.log = stderr; + cache.errlog = stderr; + cache.hashtable = coucal_new(0); + cache.zipOutput = selftest_open_failing_zip(path, &inj, HTS_FALSE); + opt->state.exit_xh = 0; + + writefail_store(opt, &cache, "/blob.bin", body, body_len); + if (cache.zipWriteFailed || opt->state.exit_xh != 0) { + fprintf(stderr, + "cache-writefail: notrunc: a rewind-only rollback aborted the " + "mirror (flagged=%d, exit_xh=%d)\n", + (int) cache.zipWriteFailed, opt->state.exit_xh); + fail++; + } + if (inj.truncates != 0) { + fprintf(stderr, + "cache-writefail: notrunc: %d truncate call(s) on a table that " + "has none\n", + inj.truncates); + fail++; + } + writefail_store(opt, &cache, "/blob2.bin", body, 16); + zipClose(cache.zipOutput, NULL); + cache.zipOutput = NULL; + n = writefail_read_entry(path, "http://example.com/blob2.bin", extra, + sizeof(extra), rbody, sizeof(rbody)); + if (n != 16 || memcmp(rbody, body, 16) != 0) { + fprintf(stderr, + "cache-writefail: notrunc: sibling entry lost after a rewind " + "(%d)\n", + n); + fail++; + } + } + /* >2GB bodies: in-memory drops the entry, on-disk degrades to headers-only */ { cache_back cache; @@ -695,7 +747,7 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir) { cache.log = stderr; cache.errlog = stderr; cache.hashtable = coucal_new(0); - cache.zipOutput = selftest_open_failing_zip(path, &inj); + cache.zipOutput = selftest_open_failing_zip(path, &inj, HTS_TRUE); opt->state.exit_xh = 0; writefail_store_oversized(opt, &cache, "/bigmem.bin", 0 /* in-memory */); diff --git a/src/htsselftest.c b/src/htsselftest.c index 9e435383a..fca2d1b44 100644 --- a/src/htsselftest.c +++ b/src/htsselftest.c @@ -4244,7 +4244,9 @@ static int st_zip_abandon_notrunc(httrackp *opt, int argc, char **argv) { abandon_err, ZIP_OK); fail++; } - if (reflen >= doomed) { + /* a flushed-but-kept member shows up as more than a backscan of leftovers, + which is the size that matters, not the member's nominal one */ + if (reflen > 65535) { fprintf(stderr, "zip-abandon-notrunc: the reference kept %d byte(s), so the " "abandoned member was not truncated away\n", diff --git a/src/minizip/ioapi.c b/src/minizip/ioapi.c index 817b642de..67c5bf060 100644 --- a/src/minizip/ioapi.c +++ b/src/minizip/ioapi.c @@ -75,8 +75,11 @@ int call_ztruncate64(const zlib_filefunc64_32_def *pfilefunc, voidpf filestream, the caller has already performed, but the caller has to hear about it */ if (pfilefunc->zfile_func64.ztruncate64_file == NULL) return 1; + /* a backend may report failure as an errno rather than -1 (_chsize_s) */ return (*(pfilefunc->zfile_func64.ztruncate64_file))( - pfilefunc->zfile_func64.opaque, filestream, size); + pfilefunc->zfile_func64.opaque, filestream, size) == 0 + ? 0 + : -1; } void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32, const zlib_filefunc_def* p_filefunc32) { diff --git a/src/minizip/ioapi.c.diff b/src/minizip/ioapi.c.diff index 2aee036f9..ba1fd7748 100644 --- a/src/minizip/ioapi.c.diff +++ b/src/minizip/ioapi.c.diff @@ -22,7 +22,7 @@ voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc, const void*filename, int mode) { if (pfilefunc->zfile_func64.zopen64_file != NULL) return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,filename,mode); -@@ -63,13 +69,26 @@ +@@ -63,13 +69,29 @@ } } @@ -32,8 +32,11 @@ + the caller has already performed, but the caller has to hear about it */ + if (pfilefunc->zfile_func64.ztruncate64_file == NULL) + return 1; ++ /* a backend may report failure as an errno rather than -1 (_chsize_s) */ + return (*(pfilefunc->zfile_func64.ztruncate64_file))( -+ pfilefunc->zfile_func64.opaque, filestream, size); ++ pfilefunc->zfile_func64.opaque, filestream, size) == 0 ++ ? 0 ++ : -1; +} + void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32, const zlib_filefunc_def* p_filefunc32) { @@ -49,7 +52,7 @@ p_filefunc64_32->zfile_func64.zclose_file = p_filefunc32->zclose_file; p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file; p_filefunc64_32->zfile_func64.opaque = p_filefunc32->opaque; -@@ -193,6 +212,27 @@ +@@ -193,6 +215,27 @@ return ret; } @@ -77,7 +80,7 @@ static int ZCALLBACK fclose_file_func(voidpf opaque, voidpf stream) { int ret; -@@ -214,6 +254,7 @@ +@@ -214,6 +257,7 @@ pzlib_filefunc_def->zwrite_file = fwrite_file_func; pzlib_filefunc_def->ztell_file = ftell_file_func; pzlib_filefunc_def->zseek_file = fseek_file_func; @@ -85,7 +88,7 @@ pzlib_filefunc_def->zclose_file = fclose_file_func; pzlib_filefunc_def->zerror_file = ferror_file_func; pzlib_filefunc_def->opaque = NULL; -@@ -225,6 +266,8 @@ +@@ -225,6 +269,8 @@ pzlib_filefunc_def->zwrite_file = fwrite_file_func; pzlib_filefunc_def->ztell64_file = ftell64_file_func; pzlib_filefunc_def->zseek64_file = fseek64_file_func; diff --git a/tests/01_zlib-cache-writefail.test b/tests/01_zlib-cache-writefail.test index 80a86b0e4..7704aea4f 100644 --- a/tests/01_zlib-cache-writefail.test +++ b/tests/01_zlib-cache-writefail.test @@ -21,3 +21,6 @@ grep -qx "cache-writefail: OK" <<<"$out" || fail "expected 'cache-writefail: OK' # A skipped entry must be warned about with its URL. grep -q "entry not cached: example.com/" <<<"$out" || fail "expected a URL-bearing skip warning" + +# A backend that cannot truncate rolls back by rewinding, and says so (#1402). +grep -q "cache rollback incomplete" <<<"$out" || fail "expected a warning about the incomplete rollback"