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
libveloxcore — public API
Status: M1 in progress. util/, net/ (http_client, probe, url, content_disposition),
io/ (sparse_file, write_buffer), meta/veloxpart, segment/ (segmenter, budget),
rate/ (token_bucket), task/+engine.hpp (the download engine itself — vdm::Engine,
DownloadSpec/DownloadHandle/DownloadCallbacks), and rules/ (filename sanitization,
collision policy, rule-table matching) are landed. media/ is M4, not started — see
core/docs/engine-api-m1.md for the engine API's own
DAEMON-review history.
Layering (CLAUDE.md §3): this library knows nothing about JSON, SQL, Qt, or RPC. Input is
a spec value; output is bytes on disk plus typed callbacks. DAEMON projects engine state
onto the wire contract's TaskSummary / TaskDetail / events — see
core/docs/proto-requests-m1.md for the shapes that projection needs frozen.
Every header under core/include/vdm/ compiles standalone (-Wall -Wextra -Wpedantic -Werror, C++23). Clean under ASan/UBSan and TSan.
util/ — foundations
vdm/util/error.hpp
enum class Error — the engine-wide failure taxonomy (network / HTTP / content / local
I/O / metadata / probe / retry / internal). This is CORE's own vocabulary; it is not
a wire type. error_name(Error) gives a stable snake_case string; is_retryable(Error)
is the advisory retry hint the task policy consults.
struct ErrorInfo { Error code; std::string context; int http_status; bool retryable; Error cause; } — the payload carried by every failed Result. .to_string() renders
"<name>: <context> (HTTP <n>)".
vdm/util/result.hpp
Result<T> — return-based error channel, a thin wrapper over
std::expected<T, ErrorInfo>. Errors are returned, never thrown, on anything that
runs during a transfer.
Result<int> r = 42;/Result<int> r = Err{Error::timeout, "..."};/Result<T> r = Error::not_found;r.has_value(),explicit operator bool,r.value()/*r/r->,r.error(),r.code(),r.value_or(x)- monadic
and_then/transform/transform_error(forward tostd::expected) Result<void>specialization;vdm::ok()success sentinelVDM_TRY(expr)— return the error ifexprfailedVDM_TRY_ASSIGN(auto x, expr)— bind the value or return the error
vdm/util/bytes.hpp
Byte / ByteSpan / ConstByteSpan aliases; as_bytes(string_view) /
as_chars(span). Little-endian fixed-width codec load_le<T> / store_le<T> and a
bounds-checked sequential ByteReader (.u8/.u16/.u32/.u64, .raw(n), .lp_string(),
.overran()). Built for the .veloxpart.meta reader and the 4-byte NM framing; every
read is bounds-checked and latches on overrun (reader-first, fuzz-ready).
vdm/util/event_bus.hpp
EventBus — typed, thread-safe in-process pub/sub. subscribe<E>(fn) -> Token,
publish<E>(ev) (synchronous, calling thread, registration order), unsubscribe(Token),
and RAII subscribe_scoped<E> returning a Subscription. Handlers may (un)subscribe or
publish during dispatch. Handlers must not throw. Not a hot-path structure — progress is
coalesced to ≤4 Hz upstream.
vdm/util/thread_pool.hpp
ThreadPool — fixed-size std::jthread pool for bounded off-loop work (hashing, fsync
batches, DNS pre-resolve). submit(fn, args...) -> std::future<R>; propagates exceptions
through the future; drains already-queued tasks on destruction. Not the transfer
loop — net/ will own one curl_multi per dedicated worker.
vdm/util/log.hpp
Sink interface — core does no I/O itself. LogSink abstract base; DAEMON installs one
via set_log_sink(), default discards. CallbackSink adapter (with a min-level filter).
VDM_LOG_{TRACE,DEBUG,INFO,WARN,ERROR}(category, fmt, args...) — std::format syntax,
only formatted when a sink is installed and wants the level.
rules/ — filename sanitization, collision policy, rule-table matching
Pure functions only: no I/O, no filesystem access, no notion of the wire Rule type or
its JSON/SQL representation. DAEMON owns the rule table (storage, rules.upsert, the
generated Rule type) and decodes it into the plain structs below before calling in.
vdm/rules/filename.hpp
sanitize_filename(raw, max_bytes = 255) — turns a raw candidate (from
net::parse_content_disposition or net::url_filename, neither of which is
filesystem-safe by design — see their own headers) into one safe to create on ext4, APFS,
and NTFS alike: strips separators/control bytes, folds NTFS-illegal characters to _,
neutralizes reserved Windows device names (CON, COM1, ...), and clamps length on a
UTF-8 boundary. Total: never empty, never throws. Not the path-traversal security
boundary — that's DAEMON's fs/safepath, which runs after this and is the one that
matters adversarially.
vdm/rules/collision.hpp
resolve_collision(desired, exists, policy, max_attempts = 1000) — given an existence
predicate (DAEMON supplies a real one; tests supply an in-memory set), finds the next free
name Explorer/Finder-style ("name (1).ext", "name (2).ext", ...) under
CollisionPolicy::rename, or returns desired unchanged under ::overwrite. Never
fabricates a guaranteed-unique name past max_attempts — returns the last candidate tried
and leaves "still colliding" for the caller to treat as a real error.
vdm/rules/match.hpp
match_rules(rules, input) -> optional<RuleAction> — the evaluation half of
contracts/schema/types/Rule.schema.json: tries rules in ascending priority order
(ties keep table order), skips disabled rows, returns the first whose every present
match clause (extensions, mime_types, host_pattern, url_pattern,
min_size_bytes/max_size_bytes) is satisfied — an absent clause is not a constraint,
and a size clause never matches speculatively when MatchInput::size_bytes is still
unknown (pre-probe). std::nullopt means no rule matched; the caller's own default
category applies. glob_match(pattern, text) — the */? matcher host_pattern and
url_pattern both use, case-insensitive, bounded work even on a pathological
all-* pattern (iterative, not recursive).