minizip: place erase temp on same filesystem when TMPDIR differs

minizip_erase rewrites the archive to a temp file under TMPDIR and
then renames it onto the source. rename(2) returns EXDEV across
filesystems, so when TMPDIR (e.g. /tmp) and the source live on
different mounts (a common conda-forge pattern), the rename fails
after the original was already moved aside to .bak — the next
operation that opens the archive sees no file and reports -111.

Add mz_os_path_same_fs() and have minizip_erase fall back on POSIX to
a same-dir temp when TMPDIR is on a different filesystem. POSIX uses
stat() st_dev comparison; Win32 uses _wstati64 for the parallel check
but the caller skips it under #ifndef _WIN32 because MoveFileExW with
MOVEFILE_COPY_ALLOWED already absorbs cross-volume rename internally.

Make the same-dir tmp filename unique (mz_ms_time suffix) and refuse
to clobber an existing leftover from a prior crash.

Refs #943.
This commit is contained in:
Nathan Moin Vaziri 2026-04-25 01:01:34 -07:00
parent ece5b06dcf
commit 3f23ed7a11
4 changed files with 66 additions and 0 deletions

View file

@ -444,6 +444,28 @@ int32_t minizip_erase(const char *src_path, const char *target_path, int32_t arg
/* Construct temporary zip name with random suffix */ /* Construct temporary zip name with random suffix */
if (mz_os_get_temp_path(tmp_path, sizeof(tmp_path), "mz_") != MZ_OK) if (mz_os_get_temp_path(tmp_path, sizeof(tmp_path), "mz_") != MZ_OK)
return MZ_INTERNAL_ERROR; return MZ_INTERNAL_ERROR;
#ifndef _WIN32
/* POSIX rename(2) returns EXDEV across mounts; fall back to a same-dir tmp when
TMPDIR is on a different filesystem. MoveFileExW absorbs cross-volume natively
on Win32, so the check is skipped there. */
{
char tmp_dir[256];
strncpy(tmp_dir, tmp_path, sizeof(tmp_dir) - 1);
tmp_dir[sizeof(tmp_dir) - 1] = 0;
mz_path_remove_filename(tmp_dir);
if (mz_os_path_same_fs(tmp_dir, src_path) != MZ_OK) {
int32_t result = snprintf(tmp_path, sizeof(tmp_path), "%s.mz_tmp.%llu", src_path,
(unsigned long long)mz_os_ms_time());
if (result < 0 || result >= (int32_t)sizeof(tmp_path))
return MZ_BUF_ERROR;
if (mz_os_file_exists(tmp_path) == MZ_OK)
return MZ_EXIST_ERROR;
}
}
#endif
target_path_ptr = tmp_path; target_path_ptr = tmp_path;
} }

View file

@ -154,6 +154,9 @@ int32_t mz_os_set_file_attribs(const char *path, uint32_t attributes);
int32_t mz_os_get_temp_path(char *path, int32_t max_path, const char *prefix); int32_t mz_os_get_temp_path(char *path, int32_t max_path, const char *prefix);
/* Gets a unique temporary file path */ /* Gets a unique temporary file path */
int32_t mz_os_path_same_fs(const char *path_a, const char *path_b);
/* Checks if both paths are on the same filesystem */
int32_t mz_os_make_dir(const char *path); int32_t mz_os_make_dir(const char *path);
/* Recursively creates a directory */ /* Recursively creates a directory */

View file

@ -165,6 +165,15 @@ int32_t mz_os_rename(const char *source_path, const char *target_path) {
return MZ_OK; return MZ_OK;
} }
int32_t mz_os_path_same_fs(const char *path_a, const char *path_b) {
struct stat sa, sb;
if (!path_a || !path_b)
return MZ_PARAM_ERROR;
if (stat(path_a, &sa) != 0 || stat(path_b, &sb) != 0)
return MZ_EXIST_ERROR;
return (sa.st_dev == sb.st_dev) ? MZ_OK : MZ_EXIST_ERROR;
}
int32_t mz_os_unlink(const char *path) { int32_t mz_os_unlink(const char *path) {
if (unlink(path) == -1) if (unlink(path) == -1)
return MZ_EXIST_ERROR; return MZ_EXIST_ERROR;

View file

@ -12,6 +12,9 @@
#include "mz_os.h" #include "mz_os.h"
#include "mz_strm_os.h" #include "mz_strm_os.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <windows.h> #include <windows.h>
#include <winioctl.h> #include <winioctl.h>
@ -114,6 +117,35 @@ int32_t mz_os_rand(uint8_t *buf, int32_t size) {
return len; return len;
} }
int32_t mz_os_path_same_fs(const char *path_a, const char *path_b) {
wchar_t *path_a_wide = NULL;
wchar_t *path_b_wide = NULL;
struct _stati64 sa;
struct _stati64 sb;
int32_t err = MZ_OK;
if (!path_a || !path_b)
return MZ_PARAM_ERROR;
path_a_wide = mz_os_unicode_string_create(path_a, MZ_ENCODING_UTF8);
if (!path_a_wide)
return MZ_PARAM_ERROR;
path_b_wide = mz_os_unicode_string_create(path_b, MZ_ENCODING_UTF8);
if (!path_b_wide) {
mz_os_unicode_string_delete(&path_a_wide);
return MZ_PARAM_ERROR;
}
if (_wstati64(path_a_wide, &sa) != 0 || _wstati64(path_b_wide, &sb) != 0)
err = MZ_EXIST_ERROR;
else if (sa.st_dev != sb.st_dev)
err = MZ_EXIST_ERROR;
mz_os_unicode_string_delete(&path_a_wide);
mz_os_unicode_string_delete(&path_b_wide);
return err;
}
int32_t mz_os_rename(const char *source_path, const char *target_path) { int32_t mz_os_rename(const char *source_path, const char *target_path) {
wchar_t *source_path_wide = NULL; wchar_t *source_path_wide = NULL;
wchar_t *target_path_wide = NULL; wchar_t *target_path_wide = NULL;