diff --git a/xff/engine/run.cc b/xff/engine/run.cc index 9616acc..47497d0 100644 --- a/xff/engine/run.cc +++ b/xff/engine/run.cc @@ -376,6 +376,8 @@ class DryRunFileSystem : public vfs::FileSystem { absl::StatusOr FsType(std::string_view path) const override { return fs_.FsType(path); } + absl::StatusOr IsCaseSensitive(std::string_view path) const override { return fs_.IsCaseSensitive(path); } + absl::StatusOr ReadContent(std::string_view path) const override { return fs_.ReadContent(path); } absl::Status Remove(std::string_view path) const override { diff --git a/xff/engine/run_test.cc b/xff/engine/run_test.cc index 59ca280..fc6e59c 100644 --- a/xff/engine/run_test.cc +++ b/xff/engine/run_test.cc @@ -926,6 +926,8 @@ class NoBtimeFs : public vfs::FileSystem { absl::StatusOr FsType(std::string_view) const override { return std::string("fakefs"); } + absl::StatusOr IsCaseSensitive(std::string_view) const override { return true; } + absl::StatusOr ReadContent(std::string_view) const override { return std::string(); } private: diff --git a/xff/engine/walk_test.cc b/xff/engine/walk_test.cc index 5a037b5..7158ff3 100644 --- a/xff/engine/walk_test.cc +++ b/xff/engine/walk_test.cc @@ -98,6 +98,8 @@ class FakeFs : public vfs::FileSystem { return std::string("fakefs"); } // unused by walk tests + absl::StatusOr IsCaseSensitive(std::string_view) const override { return true; } // unused by walk tests + absl::StatusOr ReadContent(std::string_view) const override { return std::string(); } // unused by walk tests diff --git a/xff/vfs/filesystem.h b/xff/vfs/filesystem.h index 5fab4cb..6aa3bfe 100644 --- a/xff/vfs/filesystem.h +++ b/xff/vfs/filesystem.h @@ -71,6 +71,17 @@ class FileSystem { // "apfs", "tmpfs", "nfs". An error if `path` cannot be queried (`statfs`). virtual absl::StatusOr FsType(std::string_view path) const = 0; + // Whether name lookups on the volume holding `path` distinguish case: true = + // case-sensitive (ext4 / xfs and most Linux filesystems, where `foo` and `FOO` + // are distinct files), false = case-folding (APFS / HFS+ in their default + // configuration, NTFS, exFAT, where they are the same file). Backs xff's + // FS-native name matching (`--exact` opts out): the default xff style matches + // `-name` the way the filesystem itself would, so a lookup that the OS would + // satisfy case-insensitively also matches here. Returns an error when the + // volume cannot be probed; the caller falls back to the conservative + // case-sensitive (byte-exact) behaviour, which is always find-faithful. + virtual absl::StatusOr IsCaseSensitive(std::string_view path) const = 0; + // Reads the entire byte content of the regular file at `path` (xff's content // predicates -content / -icontent / -rxc / -irxc). Returns an error when the // path cannot be opened or read; content search treats that as a non-match diff --git a/xff/vfs/local_fs.cc b/xff/vfs/local_fs.cc index 795b9c3..1f82327 100644 --- a/xff/vfs/local_fs.cc +++ b/xff/vfs/local_fs.cc @@ -262,6 +262,33 @@ absl::StatusOr LocalFs::FsType(std::string_view path) const { #endif } +absl::StatusOr LocalFs::IsCaseSensitive(std::string_view path) const { +#if defined(_PC_CASE_SENSITIVE) + // macOS / BSD expose the volume's case rule directly: pathconf returns 1 for a + // case-sensitive volume and 0 for a case-folding one. A -1 return is either an + // error (errno set) or an indeterminate answer (errno left at 0); both fall back + // to the conservative case-sensitive default (error propagated; indeterminate + // reported as case-sensitive), which is always find-faithful. + const std::string path_str(path); + errno = 0; + const long rc = ::pathconf(path_str.c_str(), _PC_CASE_SENSITIVE); // NOLINT(google-runtime-int) + if (rc < 0) { + if (errno != 0) { + return absl::ErrnoToStatus(errno, absl::StrCat("pathconf(_PC_CASE_SENSITIVE, '", path, "')")); + } + return true; // indeterminate -> conservative byte-exact + } + return rc != 0; +#else + // Linux / glibc has no _PC_CASE_SENSITIVE. ext4 / xfs / btrfs are case-sensitive + // by default; per-directory case folding (ext4 +F / statx STATX_ATTR_CASEFOLD) is + // a future refinement. Report case-sensitive, which is also the safe default and + // matches find's byte-exact matching. + (void)path; + return true; +#endif +} + absl::StatusOr LocalFs::ReadContent(std::string_view path) const { const std::string path_str(path); const int fd = ::open(path_str.c_str(), O_RDONLY); diff --git a/xff/vfs/local_fs.h b/xff/vfs/local_fs.h index 215f40c..0bbded1 100644 --- a/xff/vfs/local_fs.h +++ b/xff/vfs/local_fs.h @@ -41,6 +41,7 @@ class LocalFs final : public FileSystem { bool Access(std::string_view path, AccessMode mode) const override; absl::StatusOr ReadLink(std::string_view path) const override; absl::StatusOr FsType(std::string_view path) const override; + absl::StatusOr IsCaseSensitive(std::string_view path) const override; absl::StatusOr ReadContent(std::string_view path) const override; }; diff --git a/xff/vfs/local_fs_test.cc b/xff/vfs/local_fs_test.cc index 9815c84..566f459 100644 --- a/xff/vfs/local_fs_test.cc +++ b/xff/vfs/local_fs_test.cc @@ -146,5 +146,21 @@ TEST_F(LocalFsTest, ReadContentMissingPathErrors) { EXPECT_THAT(local_fs_.ReadContent(Path("nope")), StatusIs(absl::StatusCode::kNotFound)); } +TEST_F(LocalFsTest, IsCaseSensitiveProbesTheVolume) { + // The probe must succeed on a normal directory. The value depends on the volume + // (case-sensitive ext4 vs case-folding APFS), so it is cross-checked against the + // volume's actual behaviour in the next test rather than hard-coded here. + EXPECT_THAT(local_fs_.IsCaseSensitive(root_.string()), IsOk()); +} + +TEST_F(LocalFsTest, IsCaseSensitiveAgreesWithTheVolumeBehaviour) { + // file.txt exists in lower case; whether the upper-case name resolves to it is + // exactly the volume's own case rule, so the probe must report the matching + // value: a case-folding volume resolves FILE.TXT (not sensitive), a + // case-sensitive one does not (sensitive). + const bool upper_resolves = fs::exists(root_ / "FILE.TXT"); + EXPECT_THAT(local_fs_.IsCaseSensitive(root_.string()), IsOkAndHolds(Eq(!upper_resolves))); +} + } // namespace } // namespace xff::vfs