Pure functions only, per AGENT-CORE.md's build order: no I/O, no JSON, no SQL,
no notion of the wire Rule type — DAEMON decodes its own stored/wire
representation into these plain structs and calls in.
- rules/filename.hpp: sanitize_filename() turns a raw candidate (from
net::parse_content_disposition or net::url_filename — neither is
filesystem-safe by design; both headers say so and point here) into one
safe to create on ext4/APFS/NTFS: strips separators and control bytes,
folds NTFS-illegal characters, neutralizes reserved Windows device names,
clamps length on a UTF-8 boundary. Total on hostile input; never empty.
Not the path-traversal security boundary — that's daemon/fs/safepath,
downstream of this and the one that actually matters adversarially.
- rules/collision.hpp: resolve_collision() finds the next free name
Explorer/Finder-style ("name (1).ext", ...) given an existence predicate,
or returns the desired name unchanged under an overwrite policy. Never
fabricates a guaranteed-unique name past its attempt bound — hands back
the last candidate tried rather than hiding a persistent collision.
- rules/match.hpp: match_rules() is the evaluation half of
contracts/schema/types/Rule.schema.json — priority order, first rule
whose present match clauses (extensions/mimeTypes/host & url glob/size
bounds) all hold, wins; a size clause never matches speculatively before
the probe fills in size_bytes. glob_match() is the iterative (not
recursive — bounded work on an all-'*' pattern) matcher both host_pattern
and url_pattern use.
Every header compiles standalone; tests (39 cases) pass under ASan+UBSan and
TSan. core/include/vdm/README.md documents the new public surface.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q3QrF7rCt21bkAjt9BCDFQ
92 lines
4.3 KiB
C++
92 lines
4.3 KiB
C++
// vdm/rules/match.hpp — the pure evaluation half of the rules engine
|
|
// (contracts/schema/types/Rule.schema.json): given a rule table and what's known about one
|
|
// candidate download, decide which rule fires and what it says to do.
|
|
//
|
|
// DAEMON owns the rule table itself — storage, `rules.upsert`/`rules.list`, the wire
|
|
// `Rule` type generated from the contract. It decodes its own JSON/SQL representation into
|
|
// the plain structs below and calls in; core never sees JSON, SQL, or the generated
|
|
// protocol types (CLAUDE.md §3) — these structs mirror the contract's shape in CORE's own
|
|
// vocabulary, the same relationship `util/error.hpp`'s `Error` has to the wire error codes.
|
|
//
|
|
// Pure and total: no I/O, no throw, no crash on any input (an empty table, an empty
|
|
// pattern, a rule with every match clause absent).
|
|
//
|
|
// This header compiles standalone.
|
|
|
|
#ifndef VDM_RULES_MATCH_HPP
|
|
#define VDM_RULES_MATCH_HPP
|
|
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
namespace vdm::rules {
|
|
|
|
enum class StartMode { now, later, queue }; // mirrors contracts' StartMode
|
|
enum class CaptureVerdict { take, ignore }; // mirrors RuleAction.capture
|
|
|
|
// All present clauses must match; an absent clause is not a constraint
|
|
// (contracts/schema/types/Rule.schema.json's own wording for RuleMatch).
|
|
struct RuleMatch {
|
|
std::optional<std::vector<std::string>> extensions; // no leading '.'; matched
|
|
// case-insensitively
|
|
std::optional<std::vector<std::string>> mime_types; // matched case-insensitively
|
|
std::optional<std::string> host_pattern; // glob_match against the host
|
|
std::optional<std::string> url_pattern; // glob_match against the whole URL
|
|
std::optional<std::uint64_t> min_size_bytes; // inclusive
|
|
std::optional<std::uint64_t> max_size_bytes; // inclusive
|
|
};
|
|
|
|
// What to do with a matching download.
|
|
struct RuleAction {
|
|
std::optional<std::string> category_id;
|
|
std::optional<std::string> save_dir;
|
|
std::optional<std::string> queue_id;
|
|
std::optional<std::uint32_t> segments;
|
|
std::optional<StartMode> start_mode;
|
|
std::optional<CaptureVerdict> capture;
|
|
};
|
|
|
|
// One row of the rules engine.
|
|
struct Rule {
|
|
std::string rule_id;
|
|
bool enabled = true;
|
|
std::int64_t priority = 0; // lower runs first
|
|
RuleMatch match;
|
|
RuleAction action;
|
|
};
|
|
|
|
// What's known about one candidate download, to match rules against. A field being empty
|
|
// (not `std::nullopt` — these are plain strings, not optionals) means "unknown, matches no
|
|
// non-empty clause that needs it" — e.g. `size_bytes` is absent pre-probe, so any rule with
|
|
// a size clause simply doesn't match yet; the caller is expected to re-run matching once
|
|
// the probe fills it in, same as DownloadSpec itself gets refined post-probe.
|
|
struct MatchInput {
|
|
std::string extension; // lowercased, no leading '.'; empty if none
|
|
std::string mime_type; // lowercased; empty if unknown
|
|
std::string host; // lowercased effective-URL host
|
|
std::string url; // the whole effective URL
|
|
std::optional<std::uint64_t> size_bytes;
|
|
};
|
|
|
|
// Rules are tried in ascending `priority` order (ties broken by table order), skipping
|
|
// disabled rows; the first whose every present match clause is satisfied wins.
|
|
// `std::nullopt` means no rule matched — the caller falls back to its own default category
|
|
// (this function has no notion of a default; that policy lives with the caller).
|
|
[[nodiscard]] std::optional<RuleAction> match_rules(const std::vector<Rule> &rules,
|
|
const MatchInput &input);
|
|
|
|
// Case-insensitive glob: '*' matches any run of characters including none, '?' matches
|
|
// exactly one character. No character classes, no escaping — rule patterns are meant to
|
|
// stay simple (contracts/schema/types/Rule.schema.json's own hostPattern/urlPattern
|
|
// description gives only "*.example.com" as the example). Exposed on its own because
|
|
// hostPattern and urlPattern are both just this against different text, and because it has
|
|
// its own test table worth keeping separate from match_rules's.
|
|
[[nodiscard]] bool glob_match(std::string_view pattern, std::string_view text) noexcept;
|
|
|
|
} // namespace vdm::rules
|
|
|
|
#endif // VDM_RULES_MATCH_HPP
|