Files
vdm/core/include/vdm/rules/collision.hpp
T
samiandClaude Sonnet 5 d4ad48d494 core: stage 9 — rules/ (filename sanitization, collision policy, rule matching)
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
2026-09-11 13:25:15 +04:00

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