diff --git a/xff/engine/evaluate.cc b/xff/engine/evaluate.cc index e20fbf7..7d5df2a 100644 --- a/xff/engine/evaluate.cc +++ b/xff/engine/evaluate.cc @@ -292,56 +292,90 @@ std::string FormatPrintf(std::string_view format, const Visit& visit, absl::Time } // find's `-size` unit suffixes -> bytes per unit (c=char/1, w=2-byte word, b=512 -// block, k/M/G binary multiples). A constexpr map, per the style's preference for a -// uniform key -> value mapping over a switch. +// block; k/M/G/T/P/E are the binary multiples 2^10..2^60). b/c/w/k/M/G and T/P are +// find-native (BSD accepts up to P); E (exabyte) is an xff continuation of the same +// scale -- a strict superset (no find-valid input changes meaning), so it is +// available in every style. The next prefixes (Z/Y/...) name a real magnitude but +// 2^70+ overflows the 64-bit byte count, so they are rejected (see ParseSizeSpec). +// A constexpr map, per the style's preference for a uniform key -> value mapping. using SizeUnitPair = std::pair; constexpr auto kSizeUnits = mbo::container::MakeLimitedMap( - SizeUnitPair{'G', 1'024ULL * 1'024 * 1'024}, - SizeUnitPair{'M', 1'024ULL * 1'024}, + SizeUnitPair{'E', 1'024ULL * 1'024 * 1'024 * 1'024 * 1'024 * 1'024}, // 2^60 exbibyte + SizeUnitPair{'G', 1'024ULL * 1'024 * 1'024}, // 2^30 gibibyte + SizeUnitPair{'M', 1'024ULL * 1'024}, // 2^20 mebibyte + SizeUnitPair{'P', 1'024ULL * 1'024 * 1'024 * 1'024 * 1'024}, // 2^50 pebibyte + SizeUnitPair{'T', 1'024ULL * 1'024 * 1'024 * 1'024}, // 2^40 tebibyte SizeUnitPair{'b', 512}, SizeUnitPair{'c', 1}, SizeUnitPair{'k', 1'024}, SizeUnitPair{'w', 2}); -// Matches find's `-size N[bcwkMG]` with an optional +/- prefix. The file size -// is rounded UP to the chosen unit (default 512-byte blocks), as find does. -bool MatchesSize(std::string_view arg, std::uint64_t size_bytes) { - char compare = '='; +// Size prefixes one step beyond E: each names a real magnitude (zetta/yotta/ronna/ +// quetta) but 2^70+ exceeds the 64-bit byte count, so xff rejects them with a clear +// message rather than silently mis-sizing. +constexpr std::string_view kOversizedUnits = "ZYRQ"; + +struct SizeSpec { + char compare = '='; // '+' greater than, '-' less than, '=' exactly + std::uint64_t want = 0; // the count, in `unit`s + std::uint64_t unit = 512; // bytes per unit (find default: 512-byte blocks) +}; + +// Parses a `-size` argument `[+|-]N[unit]` into a SizeSpec, or returns an +// InvalidArgument status naming the problem (unknown unit, an over-64-bit unit, or +// a missing/non-numeric count). Used to reject a bad value before the walk and, +// defensively, by MatchesSize. +absl::StatusOr ParseSizeSpec(std::string_view arg) { + const std::string_view original = arg; + SizeSpec spec; if (!arg.empty() && (arg.front() == '+' || arg.front() == '-')) { - compare = arg.front(); + spec.compare = arg.front(); arg.remove_prefix(1); } - if (arg.empty()) { - return false; - } - std::uint64_t unit = 512; // find default: 512-byte blocks - const char suffix = arg.back(); - if (suffix < '0' || suffix > '9') { + if (!arg.empty() && (arg.back() < '0' || arg.back() > '9')) { + const char suffix = arg.back(); const auto it = kSizeUnits.find(suffix); if (it == kSizeUnits.end()) { - return false; // unknown unit + if (kOversizedUnits.find(suffix) != std::string_view::npos) { + return absl::InvalidArgumentError( + absl::StrCat( + "'", original, "': size unit '", std::string(1, suffix), + "' exceeds xff's 64-bit byte range; the largest size unit is E (exabyte)")); + } + return absl::InvalidArgumentError( + absl::StrCat("'", original, "': unknown size unit '", std::string(1, suffix), "'")); } - unit = it->second; + spec.unit = it->second; arg.remove_suffix(1); } if (arg.empty()) { - return false; + return absl::InvalidArgumentError(absl::StrCat("'", original, "': missing numeric size")); } - std::uint64_t want = 0; for (const char digit : arg) { if (digit < '0' || digit > '9') { - return false; + return absl::InvalidArgumentError(absl::StrCat("'", original, "': size is not a number")); } - want = want * 10 + static_cast(digit - '0'); + spec.want = (spec.want * 10) + static_cast(digit - '0'); } - const std::uint64_t size_in_units = (size_bytes + unit - 1) / unit; - if (compare == '+') { - return size_in_units > want; + return spec; +} + +// Matches find's `-size N[bcwkMGTPE]` with an optional +/- prefix. The file size is +// rounded UP to the chosen unit (default 512-byte blocks), as find does. A +// malformed arg never matches (it is rejected before the walk; see ValidateSizeArgs). +bool MatchesSize(std::string_view arg, std::uint64_t size_bytes) { + const absl::StatusOr spec = ParseSizeSpec(arg); + if (!spec.ok()) { + return false; } - if (compare == '-') { - return size_in_units < want; + const std::uint64_t size_in_units = (size_bytes + spec->unit - 1) / spec->unit; + if (spec->compare == '+') { + return size_in_units > spec->want; + } + if (spec->compare == '-') { + return size_in_units < spec->want; } - return size_in_units == want; + return size_in_units == spec->want; } // Matches a plain integer metadata field (e.g. -links) with an optional +/- @@ -1464,4 +1498,26 @@ bool ContainsAction(const parser::Expr& expr) { return false; // Unreachable: every Expr::Kind returns above. } +absl::Status ValidateSizeArgs(const parser::Expr& expr) { + if (expr.kind == parser::Expr::Kind::kPredicate) { + if (expr.descriptor != nullptr && expr.descriptor->name == "-size" && !expr.args.empty()) { + if (const absl::Status status = ParseSizeSpec(expr.args.front()).status(); !status.ok()) { + return status; + } + } + return absl::OkStatus(); + } + if (expr.lhs != nullptr) { + if (const absl::Status status = ValidateSizeArgs(*expr.lhs); !status.ok()) { + return status; + } + } + if (expr.rhs != nullptr) { + if (const absl::Status status = ValidateSizeArgs(*expr.rhs); !status.ok()) { + return status; + } + } + return absl::OkStatus(); +} + } // namespace xff::engine diff --git a/xff/engine/evaluate.h b/xff/engine/evaluate.h index 90d3ccd..122b6e2 100644 --- a/xff/engine/evaluate.h +++ b/xff/engine/evaluate.h @@ -23,6 +23,7 @@ #include #include "absl/functional/function_ref.h" +#include "absl/status/status.h" #include "absl/time/time.h" #include "xff/engine/walk.h" #include "xff/parser/ast.h" @@ -107,6 +108,14 @@ bool Evaluate(const parser::Expr& expr, EvalContext& context); // expression has no action of its own. bool ContainsAction(const parser::Expr& expr); +// Validates every `-size` argument in `expr`, returning the first malformed one as +// an InvalidArgument status (unknown unit, an over-64-bit unit like Z/Y, or a +// missing/non-numeric count) or Ok when all are well-formed. The driver calls this +// before the walk so a bad `-size` is a usage error (exit 2) rather than a silent +// per-entry no-match, matching find's parse-time rejection. Style-independent: the +// size units (incl. the T/P/E continuation) are valid in every flavor. +absl::Status ValidateSizeArgs(const parser::Expr& expr); + } // namespace xff::engine #endif // XFF_ENGINE_EVALUATE_H_ diff --git a/xff/engine/evaluate_test.cc b/xff/engine/evaluate_test.cc index d370a6e..8af5f6c 100644 --- a/xff/engine/evaluate_test.cc +++ b/xff/engine/evaluate_test.cc @@ -37,6 +37,7 @@ namespace { using ::mbo::testing::IsOk; using ::mbo::testing::IsOkAndHolds; +using ::mbo::testing::StatusIs; using ::testing::ElementsAre; using ::testing::HasSubstr; using ::testing::IsEmpty; @@ -241,6 +242,44 @@ TEST_F(EvaluateTest, SizeMatchesBytesAndUnits) { EXPECT_TRUE(Match({"-size", "1k"}, visit)) << "5 bytes rounds up to one 1k unit"; } +TEST_F(EvaluateTest, SizeMatchesLargeUnits) { + // T/P/E continue the k/M/G binary scale (2^40/2^50/2^60). Exact multiples make the + // round-up-to-unit arithmetic land on a clean count. + vfs::Metadata md; + md.type = vfs::FileType::kRegular; + md.size = 3ULL * 1'024 * 1'024 * 1'024 * 1'024; // 3 TiB + const Visit tib{.path = "f", .name = "f", .depth = 1, .metadata = md}; + EXPECT_TRUE(Match({"-size", "3T"}, tib)); + EXPECT_TRUE(Match({"-size", "+2T"}, tib)); + EXPECT_FALSE(Match({"-size", "2T"}, tib)); + md.size = 2ULL * 1'024 * 1'024 * 1'024 * 1'024 * 1'024; // 2 PiB + const Visit pib{.path = "f", .name = "f", .depth = 1, .metadata = md}; + EXPECT_TRUE(Match({"-size", "2P"}, pib)); + md.size = 1ULL * 1'024 * 1'024 * 1'024 * 1'024 * 1'024 * 1'024; // 1 EiB (2^60) + const Visit eib{.path = "f", .name = "f", .depth = 1, .metadata = md}; + EXPECT_TRUE(Match({"-size", "1E"}, eib)); +} + +TEST_F(EvaluateTest, ValidateSizeArgsRejectsBadUnits) { + // Valid units (incl. the T/P/E continuation) pass; an over-64-bit unit (Z/Y/...) + // or an unknown unit is rejected with a self-documenting message, so the driver + // fails before traversing rather than silently matching nothing. + for (const std::string_view good : {"+1T", "2P", "-3E", "5c", "1k"}) { + const auto command = parser::Parse({".", "-size", std::string(good)}); + ASSERT_THAT(command, IsOk()); + EXPECT_THAT(ValidateSizeArgs(*command->expression), IsOk()) << good; + } + const auto zetta = parser::Parse({".", "-size", "+1Z"}); + ASSERT_THAT(zetta, IsOk()); + EXPECT_THAT( + ValidateSizeArgs(*zetta->expression), StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("E (exabyte)"))); + const auto unknown = parser::Parse({".", "-size", "1q"}); + ASSERT_THAT(unknown, IsOk()); + EXPECT_THAT( + ValidateSizeArgs(*unknown->expression), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("unknown size unit"))); +} + TEST_F(EvaluateTest, PermMatchesOctalModes) { vfs::Metadata md; md.type = vfs::FileType::kRegular; diff --git a/xff/engine/run.cc b/xff/engine/run.cc index 66bda70..4c0d1a1 100644 --- a/xff/engine/run.cc +++ b/xff/engine/run.cc @@ -559,6 +559,15 @@ int RunFind( if (expression != nullptr) { ScanDepthOptions(*expression, &options); } + // A malformed -size value (unknown unit, an over-64-bit unit like Z/Y, or a + // non-numeric count) is a usage error refused before the walk -- find rejects bad + // -size at parse time too, rather than silently matching nothing. + if (expression != nullptr) { + if (const absl::Status size_status = ValidateSizeArgs(*expression); !size_status.ok()) { + on_error("-size", size_status); + return 2; // do not traverse + } + } // --timezone=ZONE overrides the local zone for interpreting time-string args // (-newerXt) and -daystart's midnight. Resolved first (both need it); an unknown // zone is a usage error, refused before traversal. diff --git a/xff/engine/run_test.cc b/xff/engine/run_test.cc index 4ddb388..3e63a0c 100644 --- a/xff/engine/run_test.cc +++ b/xff/engine/run_test.cc @@ -501,6 +501,21 @@ TEST_F(RunTest, UnknownTimezoneIsRefusedBeforeTraversal) { EXPECT_FALSE(emitted) << "an invalid --timezone must not traverse"; } +TEST_F(RunTest, OversizedSizeUnitIsRefusedBeforeTraversal) { + // -size with an over-64-bit unit (Z/Y/...) is a usage error refused before the + // walk (exit 2), naming the limit -- not a silent per-entry no-match. + const auto command = parser::Parse({root_.string(), "-size", "+1Z"}); + ASSERT_THAT(command, IsOk()); + absl::Status err_status; + bool emitted = false; + const int errors = RunFind( + *command, fs_, [&](std::string_view) { emitted = true; }, + [&](std::string_view, absl::Status status) { err_status = status; }); + EXPECT_THAT(errors, 2); + EXPECT_THAT(err_status, StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("E (exabyte)"))); + EXPECT_FALSE(emitted) << "a malformed -size must not traverse"; +} + TEST_F(RunTest, ValidTimezoneIsAcceptedAndTheRunProceeds) { // A valid --timezone resolves and the run proceeds normally (here it does not // change the result, just proving the flag is accepted end to end).