Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

# 0.11.0

- Preserve filesystem-native name matching for dangling and cross-volume symlinks when case
sensitivity is resolved lazily.

- Avoid unnecessary per-entry stat requests for lexical listing templates while preserving required
metadata, fuzzy scores and conservative column output.

Expand Down
9 changes: 7 additions & 2 deletions docs/design-expression-program.md
Original file line number Diff line number Diff line change
Expand Up @@ -790,8 +790,13 @@ entry's metadata and consults the existing device case cache. Negated character
matches under folding, so an exact match alone is not sufficient to skip resolution. Required
metadata failures remain unknown results and cannot become matches through negation. Explicit
case folding bypasses the resolver; fuzzy matching resolves native case when reached. Deferred
entries retain their existing complete-metadata replay path. This does not infer a directory's
case rule for its children or assume a root's case rule applies across mount boundaries.
entries retain their existing complete-metadata replay path. For a non-followed symlink, a native
path query can follow the target or fail when it is dangling. On a device-cache miss, the resolver
therefore queries the containing directory only after VFS metadata verifies that it is a directory
on the link's own device. A different-device or unavailable parent keeps the original entry query;
an unavailable case answer keeps the conservative exact fallback. This optional parent check does
not introduce a reported metadata error. It does not assume that a root's case rule applies across
mount boundaries, and regular entries retain their existing lazy path.

Explicit `-print` and `-print0` actions borrow one coordinator-owned raw record buffer across
entries and deferred replay. The action chooses its own terminator independently of the implicit
Expand Down
16 changes: 15 additions & 1 deletion xff/engine/run.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4332,6 +4332,20 @@ absl::StatusOr<std::string> TreeEntryPatch(

using MatchedEntryFn = absl::FunctionRef<void(const Visit&)>;

absl::StatusOr<bool> ProbeEntryCaseSensitivity(const vfs::FileSystem& fs, const Visit& visit) {
if (visit.metadata.type == vfs::FileType::kSymlink) {
// A native path query can follow the link into another volume or fail on a dangling target.
// Use its directory only after verifying that the directory belongs to the link's device.
const auto parent = std::filesystem::path(visit.path).parent_path();
const std::string parent_path = parent.empty() ? "." : parent.string();
const auto metadata = fs.Stat(parent_path, true);
if (metadata.ok() && metadata->type == vfs::FileType::kDirectory && metadata->dev == visit.metadata.dev) {
return fs.IsCaseSensitive(parent_path);
}
}
return fs.IsCaseSensitive(visit.path);
}

RunResult RunFindCore(
const parser::Command& command,
const std::vector<HistogramSpec>& histograms,
Expand Down Expand Up @@ -6393,7 +6407,7 @@ RunResult RunFindCore(
}
auto [it, inserted] = case_sensitive_by_dev.try_emplace(visit.metadata.dev, true);
if (inserted) {
it->second = walk_fs.IsCaseSensitive(visit.path).value_or(true);
it->second = ProbeEntryCaseSensitivity(walk_fs, visit).value_or(true);
}
native_fold = !it->second;
return *native_fold;
Expand Down
136 changes: 130 additions & 6 deletions xff/engine/uniform_case_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,18 @@ using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::IsEmpty;

enum class ParentObservation { kDirectory, kDenied, kNotDirectory };

struct CasePolicy {
std::optional<bool> uniform;
bool fail_leaf_metadata = false;
bool fail_case_query = false;
bool mixed_devices = false;
bool symlink_leaf = false;
std::optional<bool> symlink_target_case;
std::uint64_t leaf_device = 1;
bool bare_link_root = false;
ParentObservation parent_observation = ParentObservation::kDirectory;
};

struct DeviceEntry {
Expand Down Expand Up @@ -73,11 +80,26 @@ class CaseFs final : public vfs::FileSystem {
}
return entries;
}
return std::vector<vfs::Entry>{{.path = "/fixture/f.txt", .name = "f.txt", .type = vfs::FileType::kRegular}};
return std::vector<vfs::Entry>{
{
.path = "/fixture/f.txt",
.name = "f.txt",
.type = policy_.symlink_leaf ? vfs::FileType::kSymlink : vfs::FileType::kRegular,
},
};
}

absl::StatusOr<vfs::Metadata> Stat(std::string_view path, bool) const override {
if (path == "/fixture") {
if (path == "/fixture" || path == ".") {
// Initial root metadata is valid; only the later optional parent observation can fail.
if (leaf_stats_.load(std::memory_order_relaxed) != 0) {
if (policy_.parent_observation == ParentObservation::kDenied) {
return absl::PermissionDeniedError("parent metadata denied after observing link");
}
if (policy_.parent_observation == ParentObservation::kNotDirectory) {
return vfs::Metadata{.type = vfs::FileType::kRegular, .ino = 1, .dev = 1};
}
}
return vfs::Metadata{.type = vfs::FileType::kDirectory, .ino = 1, .dev = 1};
}
if (policy_.mixed_devices) {
Expand All @@ -95,14 +117,19 @@ class CaseFs final : public vfs::FileSystem {
.dev = kDeviceEntries.at(index).device,
};
}
if (path != "/fixture/f.txt") {
if (path != (policy_.bare_link_root ? "f.txt" : "/fixture/f.txt")) {
return absl::NotFoundError("unknown fixture entry");
}
leaf_stats_.fetch_add(1, std::memory_order_relaxed);
if (policy_.fail_leaf_metadata) {
return absl::PermissionDeniedError("leaf metadata denied");
}
return vfs::Metadata{.type = vfs::FileType::kRegular, .mode = 0644, .ino = 2, .dev = 1};
return vfs::Metadata{
.type = policy_.symlink_leaf ? vfs::FileType::kSymlink : vfs::FileType::kRegular,
.mode = 0644,
.ino = 2,
.dev = policy_.leaf_device,
};
}

std::optional<bool> UniformCaseSensitivity() const override {
Expand All @@ -113,6 +140,13 @@ class CaseFs final : public vfs::FileSystem {
absl::StatusOr<bool> IsCaseSensitive(std::string_view path) const override {
case_queries_.fetch_add(1, std::memory_order_relaxed);
query_paths_.emplace_back(path);
if (policy_.symlink_leaf && path != "/fixture" && path != ".") {
// Darwin pathconf follows its final symlink, so a dangling target cannot answer.
if (policy_.symlink_target_case) {
return *policy_.symlink_target_case;
}
return absl::NotFoundError("dangling case-query target");
}
if (policy_.mixed_devices) {
if (path == "/fixture") {
return false;
Expand Down Expand Up @@ -177,11 +211,19 @@ struct UniformCaseTest : ::testing::Test {
CasePolicy policy,
const std::vector<std::string>& extra = {},
std::string_view pattern = "F.TXT",
registry::Style style = registry::Style::kXff) {
registry::Style style = registry::Style::kXff,
bool regular_files_only = true) {
const CaseFs filesystem(policy);
std::vector<std::string> arguments{
"--color=never", "--no-ignore", "--jobs=1", "/fixture", "-type", "f", "-name", std::string(pattern),
"--color=never",
"--no-ignore",
"--jobs=1",
policy.bare_link_root ? "f.txt" : "/fixture",
};
if (regular_files_only) {
arguments.append_range(std::vector<std::string>{"-type", "f"});
}
arguments.append_range(std::vector<std::string>{"-name", std::string(pattern)});
arguments.append_range(extra);
MBO_ASSIGN_OR_RETURN(auto command, parser::Parse(arguments));
parser::BindMatchers(
Expand Down Expand Up @@ -305,5 +347,87 @@ TEST_F(UniformCaseTest, UnknownCaseQueryFailureKeepsTheConservativeExactFallback
EXPECT_THAT(actual.case_queries, Eq(1));
}

TEST_F(UniformCaseTest, SymlinkNamesKeepTheVerifiedContainingDeviceCasePolicy) {
struct Case {
std::string_view name;
std::optional<bool> target_case;
};

constexpr auto kCases = std::to_array<Case>({
{.name = "dangling", .target_case = std::nullopt},
{.name = "folding-target", .target_case = false},
{.name = "sensitive-target-volume", .target_case = true},
});
for (const auto& scenario : kCases) {
SCOPED_TRACE(scenario.name);
for (const bool deferred : {false, true}) {
SCOPED_TRACE(deferred);
std::vector<std::string> extra{"--archive=none"};
if (deferred) {
extra.append_range(std::vector<std::string>{"-fuzzy", "f", "-top", "1", "-print"});
}
MBO_ASSERT_OK_AND_ASSIGN(
const auto actual, Observe(
{.symlink_leaf = true, .symlink_target_case = scenario.target_case}, extra, "F.TXT",
registry::Style::kXff, false));
EXPECT_THAT(actual.output, EqualsText("/fixture/f.txt\n"));
EXPECT_THAT(actual.result.errors, Eq(0));
EXPECT_THAT(actual.diagnostics, IsEmpty());
EXPECT_THAT(actual.case_paths, ElementsAre("/fixture"));
}
}
}

TEST_F(UniformCaseTest, SymlinkCaseNeverAssumesItsParentHasTheSameDevice) {
MBO_ASSERT_OK_AND_ASSIGN(
const auto actual, Observe(
{.symlink_leaf = true, .symlink_target_case = true, .leaf_device = 2}, {"--archive=none"},
"F.TXT", registry::Style::kXff, false));
EXPECT_THAT(actual.output, IsEmpty());
EXPECT_THAT(actual.result.errors, Eq(0));
EXPECT_THAT(actual.diagnostics, IsEmpty());
EXPECT_THAT(actual.case_paths, ElementsAre("/fixture/f.txt"));
}

TEST_F(UniformCaseTest, UnavailableVerifiedSymlinkPolicyKeepsTheExactFallback) {
MBO_ASSERT_OK_AND_ASSIGN(
const auto actual,
Observe(
{.fail_case_query = true, .symlink_leaf = true}, {"--archive=none"}, "F.TXT", registry::Style::kXff, false));
EXPECT_THAT(actual.output, IsEmpty());
EXPECT_THAT(actual.result.errors, Eq(0));
EXPECT_THAT(actual.diagnostics, IsEmpty());
EXPECT_THAT(actual.case_paths, ElementsAre("/fixture"));
}

TEST_F(UniformCaseTest, BareRelativeSymlinkRootUsesTheVerifiedCurrentDirectoryPolicy) {
MBO_ASSERT_OK_AND_ASSIGN(
const auto actual,
Observe(
{.symlink_leaf = true, .bare_link_root = true}, {"--archive=none"}, "F.TXT", registry::Style::kXff, false));
EXPECT_THAT(actual.output, EqualsText("f.txt\n"));
EXPECT_THAT(actual.result.errors, Eq(0));
EXPECT_THAT(actual.diagnostics, IsEmpty());
EXPECT_THAT(actual.case_paths, ElementsAre("."));
}

TEST_F(UniformCaseTest, UnavailableOrNondirectoryParentKeepsEntryPolicyWithoutAnError) {
constexpr auto kCases = std::to_array<ParentObservation>({
ParentObservation::kDenied,
ParentObservation::kNotDirectory,
});
for (const auto observation : kCases) {
SCOPED_TRACE(static_cast<int>(observation));
MBO_ASSERT_OK_AND_ASSIGN(
const auto actual, Observe(
{.symlink_leaf = true, .symlink_target_case = false, .parent_observation = observation},
{"--archive=none"}, "F.TXT", registry::Style::kXff, false));
EXPECT_THAT(actual.output, EqualsText("/fixture/f.txt\n"));
EXPECT_THAT(actual.result.errors, Eq(0));
EXPECT_THAT(actual.diagnostics, IsEmpty());
EXPECT_THAT(actual.case_paths, ElementsAre("/fixture/f.txt"));
}
}

} // namespace
} // namespace xff::engine
Loading