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
110 changes: 83 additions & 27 deletions xff/engine/evaluate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<char, std::uint64_t>;
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<SizeSpec> 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<std::uint64_t>(digit - '0');
spec.want = (spec.want * 10) + static_cast<std::uint64_t>(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<SizeSpec> 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 +/-
Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions xff/engine/evaluate.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <vector>

#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"
Expand Down Expand Up @@ -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_
39 changes: 39 additions & 0 deletions xff/engine/evaluate_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions xff/engine/run.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions xff/engine/run_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading