From f591fff70044350ba41f3c481ef8158e390e9a1e Mon Sep 17 00:00:00 2001 From: huang <13165998033@163.com> Date: Fri, 23 Oct 2026 10:41:27 +0800 Subject: [PATCH] fix(programs): identify the input file by identity on Windows UTIL_isSameFile and UTIL_isSameFileStat fall back to strcmp on Windows, so alternate spellings of the same path (case variants, ./ prefixes, absolute paths, hard links) are treated as different files. FIO_openDstFile then opens the output with O_TRUNC and destroys the input before it is read. Compare the volume serial number and file index from GetFileInformationByHandle instead, which identifies the file regardless of how the path is spelled. Adds a CLI test that compresses a file onto itself via ./file and checks the input is left intact. --- programs/util.c | 40 ++++++++++++++----- .../file-stat/compress-file-to-itself.sh | 14 +++++++ 2 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 tests/cli-tests/file-stat/compress-file-to-itself.sh diff --git a/programs/util.c b/programs/util.c index 652530b1223..d00878c7dac 100644 --- a/programs/util.c +++ b/programs/util.c @@ -23,6 +23,7 @@ #endif /* #ifdef __FreeBSD__ */ #if defined(_WIN32) +# include /* CreateFileA, GetFileInformationByHandle */ # include /* utime */ # include /* _chmod */ # define ZSTD_USE_UTIMENSAT 0 @@ -381,17 +382,34 @@ int UTIL_compareStr(const void *p1, const void *p2) { return strcmp(* (char * const *) p1, * (char * const *) p2); } +#if defined(_MSC_VER) || defined(_WIN32) +/* Windows has no inode: compare the volume serial and file index instead. */ +static int UTIL_getFileIdentity(const char* fName, BY_HANDLE_FILE_INFORMATION* info) +{ + HANDLE const h = CreateFileA(fName, FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + BOOL ok; + if (h == INVALID_HANDLE_VALUE) return 0; + ok = GetFileInformationByHandle(h, info); + CloseHandle(h); + return ok != 0; +} +#endif + int UTIL_isSameFile(const char* fName1, const char* fName2) { int ret; assert(fName1 != NULL); assert(fName2 != NULL); UTIL_TRACE_CALL("UTIL_isSameFile(%s, %s)", fName1, fName2); #if defined(_MSC_VER) || defined(_WIN32) - /* note : Visual does not support file identification by inode. - * inode does not work on Windows, even with a posix layer, like msys2. - * The following work-around is limited to detecting exact name repetition only, - * aka `filename` is considered different from `subdir/../filename` */ - ret = !strcmp(fName1, fName2); + { BY_HANDLE_FILE_INFORMATION file1Info, file2Info; + ret = UTIL_getFileIdentity(fName1, &file1Info) + && UTIL_getFileIdentity(fName2, &file2Info) + && file1Info.dwVolumeSerialNumber == file2Info.dwVolumeSerialNumber + && file1Info.nFileIndexHigh == file2Info.nFileIndexHigh + && file1Info.nFileIndexLow == file2Info.nFileIndexLow; + } #else { stat_t file1Stat; stat_t file2Stat; @@ -412,13 +430,15 @@ int UTIL_isSameFileStat( assert(fName1 != NULL); assert(fName2 != NULL); UTIL_TRACE_CALL("UTIL_isSameFileStat(%s, %s)", fName1, fName2); #if defined(_MSC_VER) || defined(_WIN32) - /* note : Visual does not support file identification by inode. - * inode does not work on Windows, even with a posix layer, like msys2. - * The following work-around is limited to detecting exact name repetition only, - * aka `filename` is considered different from `subdir/../filename` */ (void)file1Stat; (void)file2Stat; - ret = !strcmp(fName1, fName2); + { BY_HANDLE_FILE_INFORMATION file1Info, file2Info; + ret = UTIL_getFileIdentity(fName1, &file1Info) + && UTIL_getFileIdentity(fName2, &file2Info) + && file1Info.dwVolumeSerialNumber == file2Info.dwVolumeSerialNumber + && file1Info.nFileIndexHigh == file2Info.nFileIndexHigh + && file1Info.nFileIndexLow == file2Info.nFileIndexLow; + } #else { ret = (file1Stat->st_dev == file2Stat->st_dev) diff --git a/tests/cli-tests/file-stat/compress-file-to-itself.sh b/tests/cli-tests/file-stat/compress-file-to-itself.sh new file mode 100644 index 00000000000..8699dab67c5 --- /dev/null +++ b/tests/cli-tests/file-stat/compress-file-to-itself.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e + +datagen > file +cp file file.orig + +# The same path spelled differently must still be detected as the input file. +# On Windows the old strcmp fallback treated "./file" as a different file and +# truncated the input before reading it. +println "+ zstd -q -f file -o ./file" +zstd -q -f file -o ./file && die "Should not overwrite the input file" + +cmp file file.orig