From 2f7a7c05512eaa3893dad2c4c178d251f022fb78 Mon Sep 17 00:00:00 2001 From: Koi Date: Mon, 2 Nov 2026 00:37:34 -0500 Subject: [PATCH] util: compare file identity in UTIL_isSameFile on Windows The Windows branch compared names with strcmp, so an output path that named the input with a different case, a ./ prefix or an absolute path was not detected and the input was truncated when the output was opened. Compare the volume serial number and file index instead. Fixes #4852 --- programs/util.c | 54 +++++++++++++++++++++++++++++++++++++--------- tests/playTests.sh | 11 ++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/programs/util.c b/programs/util.c index 652530b1223..70823633fd2 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,52 @@ int UTIL_compareStr(const void *p1, const void *p2) { return strcmp(* (char * const *) p1, * (char * const *) p2); } +#if defined(_WIN32) +/* note : Windows has no inode numbers, but a file's identity is given by + * its volume serial number and file index, which stay the same + * however the path to the file is spelled */ +static int UTIL_isSameFileWin32(const char* fName1, const char* fName2) +{ + BY_HANDLE_FILE_INFORMATION file1Info; + BY_HANDLE_FILE_INFORMATION file2Info; + HANDLE file1Handle; + HANDLE file2Handle; + int same; + + if (!strcmp(fName1, fName2)) return 1; + + file1Handle = CreateFileA(fName1, FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (file1Handle == INVALID_HANDLE_VALUE) return 0; + + file2Handle = CreateFileA(fName2, FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (file2Handle == INVALID_HANDLE_VALUE) { + CloseHandle(file1Handle); + return 0; + } + + same = GetFileInformationByHandle(file1Handle, &file1Info) + && GetFileInformationByHandle(file2Handle, &file2Info) + && file1Info.dwVolumeSerialNumber == file2Info.dwVolumeSerialNumber + && file1Info.nFileIndexHigh == file2Info.nFileIndexHigh + && file1Info.nFileIndexLow == file2Info.nFileIndexLow; + + CloseHandle(file1Handle); + CloseHandle(file2Handle); + return same; +} +#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); + ret = UTIL_isSameFileWin32(fName1, fName2); #else { stat_t file1Stat; stat_t file2Stat; @@ -412,13 +448,11 @@ 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` */ + /* note : stat_t does not carry a usable file identity on Windows, + * so compare the files themselves */ (void)file1Stat; (void)file2Stat; - ret = !strcmp(fName1, fName2); + ret = UTIL_isSameFileWin32(fName1, fName2); #else { ret = (file1Stat->st_dev == file2Stat->st_dev) diff --git a/tests/playTests.sh b/tests/playTests.sh index 44bc82b5610..b87e864776a 100755 --- a/tests/playTests.sh +++ b/tests/playTests.sh @@ -241,6 +241,17 @@ rm -f tmpCompressed zstd tmp -o tmpOut -c > tmpCompressed test -f tmpCompressed # file must be created +println "test : refuse output overwriting input through another spelling" +cp tmp tmpRef +zstd -f tmp -o ./tmp && die "should have refused to overwrite input through ./ prefix" +cmp tmp tmpRef || die "refused ./ output modified the input file" +if [ "$isWindows" = true ] ; then + println "test : refuse output overwriting input through a different case" + zstd -f tmp -o TMP && die "should have refused to overwrite input through a different case" + cmp tmp tmpRef || die "refused differently-cased output modified the input file" +fi +rm -f tmpRef + println "test : forgotten argument" cp tmp tmp2 zstd tmp2 -fo && die "-o must be followed by filename "