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
48 lines
2.2 KiB
C++
48 lines
2.2 KiB
C++
// vdm/rules/collision.hpp — when a chosen filename is already taken in the destination
|
|
// directory, decide what to try next.
|
|
//
|
|
// Pure: takes an existence predicate rather than touching a filesystem itself, so it never
|
|
// races what it's deciding about and stays testable without one. DAEMON (which owns the
|
|
// actual directory listing / stat calls, downstream of its own fs/safepath gate) supplies
|
|
// that predicate; a test supplies an in-memory set.
|
|
//
|
|
// This header compiles standalone.
|
|
|
|
#ifndef VDM_RULES_COLLISION_HPP
|
|
#define VDM_RULES_COLLISION_HPP
|
|
|
|
#include <functional>
|
|
#include <string>
|
|
#include <string_view>
|
|
|
|
namespace vdm::rules {
|
|
|
|
enum class CollisionPolicy {
|
|
rename, // try "name (1).ext", "name (2).ext", ... until one is free
|
|
overwrite, // return `desired` unchanged — caller intends to replace what's there
|
|
};
|
|
|
|
// Under `CollisionPolicy::rename`: calls `exists(candidate)` first with `desired` itself,
|
|
// then with "<stem> (1)<ext>", "<stem> (2)<ext>", ... (Explorer/Finder-style, splitting
|
|
// `desired` on its last '.' the same way `sanitize_filename`'s truncation does), returning
|
|
// the first candidate for which it returns false. `exists` is never called with anything
|
|
// but a single leaf name, never a path.
|
|
//
|
|
// `max_attempts` bounds a pathological `exists` that always returns true (this function
|
|
// always returns — it is not fallible): once reached, the last candidate tried is returned
|
|
// as-is, still possibly colliding. That is deliberately not papered over with a
|
|
// fabricated-unique name (a timestamp suffix, say) — silently handing back a name nobody
|
|
// asked for is exactly the kind of thing that turns into a mystery file days later; a
|
|
// caller that hits the bound should treat it as a real error, not swallow it here.
|
|
//
|
|
// Under `CollisionPolicy::overwrite`, `exists` and `max_attempts` are unused — `desired`
|
|
// is returned unchanged.
|
|
[[nodiscard]] std::string resolve_collision(std::string_view desired,
|
|
const std::function<bool(std::string_view)> &exists,
|
|
CollisionPolicy policy = CollisionPolicy::rename,
|
|
int max_attempts = 1000);
|
|
|
|
} // namespace vdm::rules
|
|
|
|
#endif // VDM_RULES_COLLISION_HPP
|