diff --git a/programs/util.c b/programs/util.c index 652530b1223..edd858ba723 100644 --- a/programs/util.c +++ b/programs/util.c @@ -490,6 +490,17 @@ int UTIL_isBlockDevStat(const stat_t* statbuf) return 0; } +#if defined(_WIN32) +/* Symbolic links and junctions are "name surrogate" reparse points. + * Other reparse points (OneDrive placeholders, deduplicated files, ...) + * are regular files and directories : they must not be treated as links. */ +static int UTIL_isNameSurrogate(DWORD attributes, DWORD reparseTag) +{ + return (attributes & FILE_ATTRIBUTE_REPARSE_POINT) + && IsReparseTagNameSurrogate(reparseTag); +} +#endif + int UTIL_isLink(const char* infilename) { UTIL_TRACE_CALL("UTIL_isLink(%s)", infilename); @@ -503,6 +514,24 @@ int UTIL_isLink(const char* infilename) return 1; } } +#elif defined(_WIN32) + { + DWORD const attributes = GetFileAttributesA(infilename); + if ( attributes != INVALID_FILE_ATTRIBUTES + && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) ) { + /* GetFileAttributes() does not report the reparse tag, FindFirstFile() does */ + WIN32_FIND_DATAA findData; + HANDLE const hFind = FindFirstFileA(infilename, &findData); + if (hFind != INVALID_HANDLE_VALUE) { + int const isLink = UTIL_isNameSurrogate(findData.dwFileAttributes, findData.dwReserved0); + FindClose(hFind); + if (isLink) { + UTIL_TRACE_RET(1); + return 1; + } + } + } + } #endif (void)infilename; UTIL_TRACE_RET(0); @@ -942,6 +971,11 @@ static int UTIL_prepareFileList(const char* dirName, memcpy(path+dirLength+1, cFile.cFileName, fnameLength); pathLength = dirLength+1+fnameLength; path[pathLength] = 0; + if (!followLinks && UTIL_isNameSurrogate(cFile.dwFileAttributes, cFile.dwReserved0)) { + UTIL_DISPLAYLEVEL(2, "Warning : %s is a symbolic link, ignoring\n", path); + free(path); + continue; + } if (cFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if ( strcmp (cFile.cFileName, "..") == 0 || strcmp (cFile.cFileName, ".") == 0 ) diff --git a/tests/playTests.sh b/tests/playTests.sh index fd20d59c25f..f1c26c8d9a7 100755 --- a/tests/playTests.sh +++ b/tests/playTests.sh @@ -443,6 +443,34 @@ mkdir -p tmpEmptyDir zstd -r tmpEmptyDir rm -rf tmpEmptyDir +println "test : directory links are not followed by -r" +# remove leftovers of a previous failed run +if [ "$isWindows" = true ]; then cmd //c "rmdir tmpLinkDir\tmpLink" 2> /dev/null || :; fi +rm -rf tmpLinkDir tmpOutside tmpStdErr +mkdir -p tmpLinkDir tmpOutside +echo "inner" > tmpLinkDir/tmpInner +echo "victim" > tmpOutside/tmpVictim +if [ "$isWindows" = true ]; then + # junctions don't require the privileges that symlinks do + (cd tmpLinkDir && cmd //c "mklink /J tmpLink ..\tmpOutside") +else + ln -s ../tmpOutside tmpLinkDir/tmpLink +fi +zstd -r --rm tmpLinkDir 2> tmpStdErr +# remove the link before asserting, so a failing test never leaves a live +# link behind for the later "rm -rf tmp*" cleanups to encounter +if [ "$isWindows" = true ]; then + cmd //c "rmdir tmpLinkDir\tmpLink" +else + rm -f tmpLinkDir/tmpLink +fi +$GREP "symbolic link" tmpStdErr +test -f tmpLinkDir/tmpInner.zst # file inside the tree was compressed +test ! -f tmpLinkDir/tmpInner # ... and removed (--rm) +test -f tmpOutside/tmpVictim # file outside the tree was left untouched +test ! -f tmpOutside/tmpVictim.zst # ... not compressed through the link +rm -rf tmpLinkDir tmpOutside tmpStdErr + println "\n===> file removal" zstd -f --rm tmp