fix(files): restore volume trash items only to places on that volume

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

@ -13,4 +13,10 @@ Failure::Failure(int code, const std::string& path)
{
}
Failure::Failure(int code, const std::string& path, const std::string& message)
: std::runtime_error(message)
, error{code, path, message}
{
}
} // namespace cutefish::files

@ -22,6 +22,8 @@ class Failure : public std::runtime_error
{
public:
Failure(int code, const std::string& path);
// A message that says more than the code's own.
Failure(int code, const std::string& path, const std::string& message);
Error error;
};

@ -898,6 +898,97 @@ TEST_F(FilesTest, VolumeTrashListsRestoresAndEmpties)
EXPECT_TRUE(exists(userTrash(share) + "/files/remote"));
}
// A volume's trash names places on that volume only: an item whose info file points
// elsewhere stays listed, with no original location, and is not put back.
TEST_F(FilesTest, VolumeTrashKeepsItemsOnVolume)
{
TemporaryDirectory dir;
const auto volume = dir.filePath("stick");
const auto outside = dir.filePath("outside");
ASSERT_TRUE(makePath(userTrash(volume) + "/files"));
ASSERT_TRUE(makePath(userTrash(volume) + "/info"));
ASSERT_TRUE(makePath(outside));
ASSERT_TRUE(makePath(dir.filePath("stick2")));
ASSERT_EQ(::symlink(outside.c_str(), (volume + "/link").c_str()), 0);
writeMountTable(mountTable(), {{volume, "ext4"}});
struct EmptyTable
{
std::string table;
~EmptyTable() { writeFile(table, {}); }
} emptyTable{mountTable()};
const std::vector<std::pair<std::string, std::string>> infos = {
{"away", outside + "/away"},
{"sibling", dir.filePath("stick2/sibling")},
{"climb", volume + "/../outside/climb"},
{"relativeClimb", "docs/../../outside/relativeClimb"},
{"linked", "link/linked"},
{"inside", volume + "/docs/inside"},
{"relative", "notes/relative"},
};
for (const auto& [name, path] : infos) {
writeFile(userTrash(volume) + "/files/" + name, name);
writeFile(userTrash(volume) + "/info/" + name + ".trashinfo", "[Trash Info]\nPath=" + path + '\n');
}
std::map<std::string, FileItem> items;
for (const auto& item : LocalFileSystem::enumerate(Url("trash:/")))
items.emplace(item.name(), item);
ASSERT_EQ(items.size(), infos.size());
EXPECT_TRUE(items["away"].originalPath().empty());
EXPECT_TRUE(items["sibling"].originalPath().empty());
EXPECT_TRUE(items["climb"].originalPath().empty());
EXPECT_TRUE(items["relativeClimb"].originalPath().empty());
EXPECT_EQ(items["linked"].originalPath(), volume + "/link/linked");
EXPECT_EQ(items["inside"].originalPath(), volume + "/docs/inside");
EXPECT_EQ(items["relative"].originalPath(), volume + "/notes/relative");
const auto failure = [&](const std::string& name) {
try {
OperationContext ctx;
LocalFileSystem::restore(items[name].url(), ConflictPolicy::Replace, ctx);
} catch (const Failure& failure) {
return failure.error;
}
return Error();
};
for (const std::string name : {"away", "sibling", "climb", "relativeClimb"}) {
const Error error = failure(name);
EXPECT_EQ(error.code, EINVAL) << name;
EXPECT_EQ(error.path, name);
EXPECT_EQ(error.message, "The original location is not known");
EXPECT_EQ(readFile(userTrash(volume) + "/files/" + name), name);
EXPECT_TRUE(exists(userTrash(volume) + "/info/" + name + ".trashinfo"));
}
const Error linked = failure("linked");
EXPECT_EQ(linked.code, EXDEV);
EXPECT_EQ(linked.message, "The original location is outside the volume it was deleted on");
EXPECT_TRUE(exists(userTrash(volume) + "/files/linked"));
EXPECT_TRUE(entries(outside).empty());
EXPECT_FALSE(exists(dir.filePath("stick2/sibling")));
OperationContext ctx;
EXPECT_EQ(LocalFileSystem::restore(items["inside"].url(), ConflictPolicy::Fail, ctx), local(volume + "/docs/inside"));
EXPECT_EQ(readFile(volume + "/docs/inside"), "inside");
EXPECT_EQ(LocalFileSystem::restore(items["relative"].url(), ConflictPolicy::Fail, ctx),
local(volume + "/notes/relative"));
EXPECT_EQ(readFile(volume + "/notes/relative"), "relative");
// The home trash is the user's own: any absolute path goes back.
writeFile(dir.filePath("home"), "home");
const auto trashed = LocalFileSystem::trash(local(dir.filePath("home")), ctx);
writeFile(FileSystemUrl::trashRoot() + "/info/" + trashed.fileName() + ".trashinfo",
"[Trash Info]\nPath=" + outside + "/home\n");
EXPECT_EQ(LocalFileSystem::restore(trashed, ConflictPolicy::Fail, ctx), local(outside + "/home"));
EXPECT_EQ(readFile(outside + "/home"), "home");
LocalFileSystem::emptyTrash(ctx);
}
TEST_F(FilesTest, TrashMonitorSeesVolumes)
{
TemporaryDirectory dir;

@ -319,6 +319,19 @@ Url LocalFileSystem::trash(const Url& url, OperationContext& ctx)
return result;
}
// A link on a volume can lead anywhere: the folder an item goes back to, as far as
// it exists, resolves inside the volume.
static bool staysOnVolume(const TrashDirectory& trash, std::string folder)
{
struct stat st;
while (folder != "/" && ::lstat(native(folder).c_str(), &st) != 0 && errno == ENOENT)
folder = absolutePathOf(folder);
const std::string real = canonicalPath(folder);
const std::string top = canonicalPath(trash.topdir);
return !real.empty() && !top.empty() && (top == "/" || isInside(real, top));
}
Url LocalFileSystem::restore(const Url& url, ConflictPolicy policy, OperationContext& ctx)
{
std::string name;
@ -334,10 +347,13 @@ Url LocalFileSystem::restore(const Url& url, ConflictPolicy policy, OperationCon
throw Failure(existsOrLinks(info) ? EACCES : ENOENT, info);
if (details.path.empty())
throw Failure(EINVAL, info);
throw Failure(EINVAL, name, "The original location is not known");
// A folder deleted since the item was trashed is made again.
const std::string parent = absolutePathOf(details.path);
if (!trash.topdir.empty() && !staysOnVolume(trash, parent))
throw Failure(EXDEV, name, "The original location is outside the volume it was deleted on");
errno = 0;
if (!makePath(parent))
throw Failure(errno ? errno : EIO, parent);

@ -307,7 +307,8 @@ void trashWatches(std::vector<std::string>* paths, WatchEntries* entries)
}
}
// Paths are absolute, or in a volume's trash relative to its mount point.
// Paths are absolute, or in a volume's trash relative to its mount point. A volume's
// trash is the medium's to fill, so its paths name no place outside the volume.
static std::string resolvedPath(const TrashDirectory& trash, const std::string& path)
{
if (path.empty() || path.find('\0') != std::string::npos)
@ -322,7 +323,14 @@ static std::string resolvedPath(const TrashDirectory& trash, const std::string&
}
const std::string clean = cleanPath(path);
return clean == "/" ? std::string() : clean;
if (clean == "/")
return {};
const std::string top = cleanPath(trash.topdir);
if (!top.empty() && top != "/" && !clean.starts_with(top + '/'))
return {};
return clean;
}
static std::optional<int> digits(std::string_view text, size_t& at, size_t count)

@ -51,7 +51,8 @@ void trashWatches(std::vector<std::string>* paths, WatchEntries* entries);
struct TrashInfo
{
// Absolute; empty when the info file names no usable path.
// Absolute; empty when the info file names no usable path, or in a volume's
// trash one outside the volume.
std::string path;
// Milliseconds since the epoch of the local time the file gives.
std::optional<int64_t> deletionTime;

Loading…
Cancel
Save