From dd068a7e2c1e525751376eed63fe693c1bc655ff Mon Sep 17 00:00:00 2001 From: reionwong Date: Sat, 17 Oct 2026 10:29:23 +0800 Subject: [PATCH] fix(files): refuse to replace an item with a link to itself --- files/localfilesystem.cpp | 10 ++++++++-- files/tests/files_test.cpp | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/files/localfilesystem.cpp b/files/localfilesystem.cpp index df06247..a26316f 100644 --- a/files/localfilesystem.cpp +++ b/files/localfilesystem.cpp @@ -321,10 +321,16 @@ static void validateDestination(const std::string& source, const std::string& de throw Failure(EINVAL, destination); } -// A folder that holds the source cannot be replaced: merging into it would write the -// source's copy into the source itself, and a move would then delete both. +// Neither the source nor a folder that holds it can be replaced: a link would take +// the place of what it points to, and merging into the folder would write the source's +// copy into the source itself, which a move would then delete. static void validateReplacement(const std::string& source, const std::string& destination) { + struct stat a, b; + if (::lstat(native(source).c_str(), &a) == 0 && ::lstat(native(destination).c_str(), &b) == 0 + && a.st_dev == b.st_dev && a.st_ino == b.st_ino) + throw Failure(EINVAL, destination); + const std::string sourceParent = canonicalPath(absolutePathOf(source)); const std::string destinationParent = canonicalPath(absolutePathOf(destination)); if (sourceParent.empty() || destinationParent.empty()) diff --git a/files/tests/files_test.cpp b/files/tests/files_test.cpp index d42c24c..fd11b58 100644 --- a/files/tests/files_test.cpp +++ b/files/tests/files_test.cpp @@ -1956,6 +1956,26 @@ TEST_F(FilesTest, LinkDrop) EXPECT_TRUE(exists(dir.filePath("file"))); } +TEST_F(FilesTest, LinkDoesNotReplaceWhatItPointsTo) +{ + TemporaryDirectory dir; + ASSERT_TRUE(makePath(dir.filePath("real"))); + writeFile(dir.filePath("real/file"), "data"); + ASSERT_EQ(::symlink("real", dir.filePath("alias").c_str()), 0); + + OperationContext ctx; + EXPECT_EQ(errorCode([&] { + LocalFileSystem::link(dir.filePath("real/file"), dir.filePath("alias/file"), ConflictPolicy::Replace, + ctx); + }), + EINVAL); + + EXPECT_FALSE(isSymbolicLink(dir.filePath("real/file"))); + EXPECT_EQ(readFile(dir.filePath("real/file")), "data"); + for (const std::string& name : entries(dir.filePath("real"))) + EXPECT_FALSE(name.starts_with(".cutefish-")) << name; +} + TEST_F(FilesTest, TypeDescriptionsAndIcons) { TemporaryDirectory dir;