Files
vdm/core/include/vdm
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
..

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 to std::expected)
  • Result<void> specialization; vdm::ok() success sentinel
  • VDM_TRY(expr) — return the error if expr failed
  • VDM_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).