fix(files): rename an item to a new case of its name where names ignore case

pull/18/head
reionwong 2 weeks ago
parent 35113fbd85
commit 5dc27cf19b

@ -503,9 +503,45 @@ std::string LocalFileSystem::link(const std::string& source, const std::string&
}
}
// Where names ignore case, the new case of a name finds the item itself. A hard link
// in the same folder is the same inode too, but only under another name.
static bool isNewCase(const std::string& source, const std::string& destination)
{
const std::string sourceParent = canonicalPath(absolutePathOf(source));
if (fileNameOf(source) == fileNameOf(destination) || sourceParent.empty()
|| sourceParent != canonicalPath(absolutePathOf(destination)))
return false;
struct stat a, b;
return ::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 && (S_ISDIR(a.st_mode) || a.st_nlink == 1);
}
std::string LocalFileSystem::move(const std::string& source, const std::string& destination, ConflictPolicy policy,
OperationContext& ctx)
{
if (isNewCase(source, destination)) {
ctx.checkpoint(source);
Parent input(source), output(destination);
// Renaming an item to itself does nothing, so it goes by way of another name.
const std::string temporary = temporaryName(".cutefish-rename-");
if (::syscall(SYS_renameat2, input.fd.value, input.name.c_str(), input.fd.value, temporary.c_str(),
RENAME_NOREPLACE)
!= 0)
throw Failure(errno, source);
if (::syscall(SYS_renameat2, input.fd.value, temporary.c_str(), output.fd.value, output.name.c_str(),
RENAME_NOREPLACE)
!= 0) {
const int code = errno;
::renameat(input.fd.value, temporary.c_str(), input.fd.value, input.name.c_str());
throw Failure(code, destination);
}
return destination;
}
validateDestination(source, destination);
ctx.checkpoint(source);
Parent input(source), output(destination);

@ -8,9 +8,11 @@
#include <ctime>
#include <fcntl.h>
#include <linux/capability.h>
#include <linux/fs.h>
#include <map>
#include <sched.h>
#include <sys/inotify.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
@ -618,6 +620,84 @@ TEST_F(FilesTest, RemoveStopsAtMounts)
EXPECT_EQ(WEXITSTATUS(status), 0);
}
TEST_F(FilesTest, RenameToANewCaseWhereNamesIgnoreCase)
{
TemporaryDirectory dir;
ASSERT_TRUE(makePath(dir.filePath("mount")));
const uid_t uid = ::getuid();
const gid_t gid = ::getgid();
const pid_t pid = ::fork();
ASSERT_GE(pid, 0);
if (pid == 0) {
if (::unshare(CLONE_NEWUSER | CLONE_NEWNS) != 0)
::_exit(77);
if (!writeProc("/proc/self/uid_map", "0 " + std::to_string(uid) + " 1")
|| !writeProc("/proc/self/setgroups", "deny")
|| !writeProc("/proc/self/gid_map", "0 " + std::to_string(gid) + " 1"))
::_exit(77);
if (::mount("none", dir.filePath("mount").c_str(), "tmpfs", 0, "casefold") != 0)
::_exit(77);
const std::string folder = dir.filePath("mount/folder");
if (::mkdir(folder.c_str(), 0700) != 0)
::_exit(2);
const int fd = ::open(folder.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
int flags = 0;
if (fd < 0 || ::ioctl(fd, FS_IOC_GETFLAGS, &flags) != 0)
::_exit(77);
flags |= FS_CASEFOLD_FL;
if (::ioctl(fd, FS_IOC_SETFLAGS, &flags) != 0)
::_exit(77);
::close(fd);
try {
writeFile(folder + "/photo.JPG", "photo");
if (::mkdir((folder + "/album").c_str(), 0700) != 0)
::_exit(2);
OperationContext ctx;
const bool file = LocalFileSystem::move(folder + "/photo.JPG", folder + "/photo.jpg", ConflictPolicy::Ask, ctx)
== folder + "/photo.jpg";
const bool directory =
LocalFileSystem::move(folder + "/album", folder + "/Album", ConflictPolicy::Ask, ctx) == folder + "/Album";
auto names = entries(folder);
std::sort(names.begin(), names.end());
::_exit(file && directory && names == std::vector<std::string>{"Album", "photo.jpg"}
&& readFile(folder + "/photo.jpg") == "photo"
? 0
: 1);
} catch (...) {
::_exit(3);
}
}
int status = 0;
ASSERT_EQ(::waitpid(pid, &status, 0), pid);
ASSERT_TRUE(WIFEXITED(status));
if (WEXITSTATUS(status) == 77)
GTEST_SKIP() << "No filesystem that ignores case can be mounted here";
EXPECT_EQ(WEXITSTATUS(status), 0);
}
TEST_F(FilesTest, MovingAHardLinkOntoItsTwinIsRefused)
{
TemporaryDirectory dir;
writeFile(dir.filePath("a"), "data");
ASSERT_EQ(::link(dir.filePath("a").c_str(), dir.filePath("b").c_str()), 0);
OperationContext ctx;
EXPECT_EQ(errorCode([&] {
LocalFileSystem::move(dir.filePath("a"), dir.filePath("b"), ConflictPolicy::Replace, ctx);
}),
EINVAL);
EXPECT_TRUE(exists(dir.filePath("a")));
EXPECT_TRUE(exists(dir.filePath("b")));
}
TEST_F(FilesTest, PermissionErrors)
{
if (::geteuid() == 0)

Loading…
Cancel
Save