Files
vdm/core/src/rules/collision.cpp
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

41 lines
1.4 KiB
C++

#include "vdm/rules/collision.hpp"
#include <string>
#include <string_view>
namespace vdm::rules {
namespace {
// Split on the last '.', unless it's a leading dot (a dotfile like ".gitignore" has no
// extension by this convention — matches sanitize_filename's own treatment). Returns
// {stem, ext} where `ext` includes the leading '.' when present.
std::pair<std::string_view, std::string_view> split_stem_ext(std::string_view name) {
auto dot = name.rfind('.');
if (dot == std::string_view::npos || dot == 0)
return {name, {}};
return {name.substr(0, dot), name.substr(dot)};
}
} // namespace
std::string resolve_collision(std::string_view desired,
const std::function<bool(std::string_view)> &exists,
CollisionPolicy policy, int max_attempts) {
if (policy == CollisionPolicy::overwrite)
return std::string(desired);
if (!exists || !exists(desired))
return std::string(desired);
auto [stem, ext] = split_stem_ext(desired);
std::string candidate;
for (int n = 1; n <= max_attempts; ++n) {
candidate = std::string(stem) + " (" + std::to_string(n) + ")" + std::string(ext);
if (!exists(candidate))
return candidate;
}
return candidate; // still colliding; see the header comment on why this isn't hidden
}
} // namespace vdm::rules