xff v0.3.7

xff - Design Document

xff - eXtended File Find: a massively improved Linux find.
Status: Draft · Started 2026-06-07 · Org: MBO Works

Purpose

One-paragraph summary of what xff is and why it exists. (To refine once the goals/features list is complete.)

Design Principles

Decision hierarchy - where to look for each answer, in order:

  1. Match find. Anything find already does, we do (defaults, syntax, semantics). Drop-in is the contract.
  2. Match the closest prior art. For functionality beyond find that we integrate (content search ← grep/ripgrep; ignore files ← ripgrep/fd; …), follow the established convention as far as is reasonable.
  3. Invent our own - only when neither find nor prior art gives a reasonable answer.

Overriding constraints (everywhere past the find-compat surface): logical above all, then self-consistent (never contradict our own established conventions). Illogical or self-inconsistent prior art is not adopted - e.g. shell set +x polarity was rejected in favour of chmod-style suffix signs -x / -x+ / -x-.

Escalation: anything that cannot satisfy the above is flagged for detailed discussion - never decided silently.

Known edge (confirmed): find is occasionally illogical itself (operator-precedence traps, -delete implies -depth, default -print, exit codes). On the drop-in surface fidelity wins - faithful compatibility is the logical requirement there - and the clean/logical version is offered as our own default/extension (modern behaviour, promoted via config).

Goals

What xff must achieve. (Captured from Marcus's list - discussion deferred.)

Features

Concrete capabilities. (Captured from Marcus's list - discussion deferred.)

Cross-cutting concerns

Accepted.

Tech / Implementation

Captured from Marcus's list - discussion deferred.

Non-Goals

Explicitly out of scope.

Open Questions

Notes / Decisions

Discussion outcomes land here after the list is complete.

Output rendering

CLI grammar & parser

Evaluation & optimization

Flag / toggle conventions

Ignore / filter family

All default to find-compatible (ignore nothing / show everything); a user's config promotes to modern behaviour.

Dotfiles vs hidden (distinct concepts)

Case sensitivity

Security & safety (review #2)

A prime goal (see Goals). Two strands:

Config system (detailed spec): the full layered model - system /etc/xff.ini policy over the user .xffrc, per-flag safe-by-default capability gating, --config / --feature / --xffrc selectors, and argv[0] dispatch - is specified in design-config.md, which is authoritative for config (e.g. the explicit-file spelling is now --xffrc=FILE, freeing --config=NAME for named-config selection). Note (2026-07-06, Option B): the auto-discovered project .xffrc layer was dropped - config is system + user + explicit --xffrc=FILE only, with no per-directory discovery or --project-config flag; see that doc's superseding banner. The notes below are the original sketch.

Config format: INI-style - flat CLI-arg lines (the same flags you'd type, parsed by our own parser) plus optional [named] blocks (saved queries / exec recipes). No separate schema; "what you can type, you can save." Parsing is inert (builds an AST, never executes) - the security work is the action-gate below, not the format.

Config trust model:

Self-documenting: registry descriptors carry a safety classification + rationale; --help, --explain, and generated docs surface every gate with its why ("ignoring ./.xff - not user-owned [safety]"; "[exec] blocks inert - pass --config to arm [safety]"). Refusals explain themselves; never silent.

Safe mode: opt-in --safe (a.k.a. --no-destructive) hard-refuses destructive (-delete, future built-in mutators) and dangerous (-exec/-execdir/-ok) operations - distinct from --dry-run (which previews). Off by default (preserves drop-in), but a cautious user sets it as their personal default in user-global config; override per-invocation with --no-safe (CLI > config). Granular --no-exec / --no-delete available. Ideal as a CI guardrail. Refusals self-documenting.

Virtual entries: archives & remote (review #3)

Archive members and remote files have no real filesystem path. The VFS tags each entry by source - real-fs / archive-member / remote - with a read-only flag; everything downstream branches on it.

Regex engines (review #4)

Output encoding - non-UTF-8 paths & content (review #5)

POSIX paths (and content) are byte strings, not guaranteed UTF-8; JSON/CSV/markdown require valid text. Decision: one uniform, user-chosen encoding per run, with a sensible default. A consumer can't auto-detect base64 vs utf8/escape, so the encoding is a single explicit mode applied to the whole output - no per-field marking (we deliberately diverge from ripgrep's {text|bytes}). The default is picked so a bad binary filename - the offender's own mess - never derails everyone else's run or burdens the clean-name common case.

Content-match cost (review #6)

Parallel exec (review #7)

macOS / cross-platform correctness (review #8)

VFS capability and host-adapter boundaries

The VFS has two deliberately different contracts. Consumers use the small capability-oriented
vfs::FileSystem interface (ReadDir, Stat, ReadLink, content reads, access checks, and
capability-scoped writes), with Status/StatusOr results and no descriptors or platform types.
Write capabilities include creating temporary files, writing/replacing bytes, truncating, and
removing entries where xff explicitly needs them. Each backend implements only the capabilities it
can provide; unsupported operations return structured errors.

The local backend keeps a separate internal host adapter for POSIX, std::filesystem, and platform
metadata primitives, including file creation and mutation. That adapter is the only place where
host handles and C APIs appear, keeping
annotations and portability code at the immediate boundary. Archive, memory, and remote backends
never need to expose those implementation details, and engine code cannot accidentally bypass the
VFS by opening a host path directly.

Exit-code model (review #9)

One consistent table; the mode decides whether "no-match" is reachable:

Binary detection & content encoding (review #10)

Content-matching path only (-contains/-grep); the find side is unaffected.

Sharded files (review #11, #84)

Off by default; opt-in --shards (find sees the separate files, so the default stays drop-in
faithful). v1 is display + stats only: --shards collapses a shard set into one logical entry
for listing and aggregate stats, while matching and actions stay per real shard (find-faithful
underneath, no ambiguous group-exec). A reassembled-content view (so -grep / -content /
-hash / -size see the concatenated whole) is a v2 built on the #83 archive vfs backend, where
it is most useful (a split archive reassembles, then dives); it is out of scope for v1.

Sharding is not rotation. A shard set is parts of one logical whole (concatenate / belong
together). Rotated logs (app.log.1, app.log-20260101) and storage segments (SSTables, WAL) are
independent files, not parts of one entity, and are deliberately out of scope - the detector
must never fold them into a set.

A shard scheme varies along six axes, and the model expresses all of them:

  1. Index position - suffix (.001), infix before the extension (.part1.rar, -s001.vmdk), or
    replaces the extension (.z01).
  2. Index form - numeric (with pad width) or alpha base-26 (aa); 0-based or 1-based.
  3. Declared total - present (-of-010, enables completeness) or absent (contiguity only).
  4. Special boundary member - a differently-named first (.rar) or last (.zip) volume.
  5. Trailing opaque suffix - a generation / attempt id excluded from shard identity (see dedup).
  6. Ordering rule - numeric ascending, alpha ascending, or "special member sorts first / last".

Capture vocabulary (one engine for built-ins and custom patterns; a scheme is defined by these
named captures):

Capture Meaning
stem the logical base name shared by every member; identifies the set
index the shard's position within the set (numeric or alpha; padding / base is part of the scheme)
total optional declared count; when present, drives completeness validation
dup optional opaque tail (e.g. a generation hash); excluded from identity, drives dedup

Logical shard identity is (stem, index, total?); the set identity is (stem, total-or-scheme).

The optional tail (dup) is a cross-cutting modifier, not its own scheme. Any scheme may carry an
optional trailing tail after the shard name so the same logical shard can be regenerated under a new
name (redundant generators race, the first to finish wins and the stragglers are killed). The tail is
described by one regex carrying exactly one capturing group - that group is the opaque dup
(excluded from identity); everything outside the group is literal context that must match as-is, so
the separator lives inside the regex rather than being a separate optional knob. The default is
`\.([0-9a-fA-F]{8,})` (a literal dot then a run of at least 8 hex digits, matching the common
Google 16-hex id) - deliberately conservative, so an ordinary extension (.tfrecord, .parquet) is
not mistaken for a tail. --shard-tail=REGEX overrides it (base64, a fixed width, a _gen- prefix,
...); an empty value disables the tail; a custom --shard-pattern instead sets it per-scheme via its
dup capture. The tail is matched against the string immediately after the shard number, and any
extension after it is preserved separately (it belongs to the set, not the tail). So
data-00000-of-00010 and data-00000-of-00010.a1b2c3d4e5f6a1b2 are the same logical shard
00000-of-00010; the tail never enters identity or completeness.

Built-in catalog (--shards[=auto|SCHEME,...], default auto tries all):

Scheme Example Notes
of data-00000-of-00010[.<tail>] TF/TFRecord; 0-based, declared total, opt tail
part part-00000, part-r-00000-<uuid>.parquet Spark/Hadoop; optional r-/m- infix, no total
webdataset shard-000042.tar, imagenet-train-001281.tar bare zero-padded numeric suffix before ext
split-alpha xaa, prefixab split(1) default; alpha base-26
split-num x00, prefix01 split --numeric-suffixes
dotnum foo.tar.001 7-Zip volumes / generic; 1-based, min-3-digit
underscore foo_001 numeric underscore suffix
zip-split arc.z01, arc.z02, …, arc.zip special last member .zip (axis 4)
rar-old arc.rar, arc.r00, arc.r01, … special first member .rar (axis 4)
rar-part arc.part1.rar / arc.part001.rar index infix before ext, 1-based
vmdk-split disk-s001.vmdk index infix -sNNN before ext

The special-boundary schemes (zip-split, rar-old, rar-part) are detected in v1 (cheap); the
reassembly that makes a split archive divable is the post-#83 v2.

Custom pattern (full-control escape hatch): --shard-pattern=REGEX, repeatable, using the named
captures above, e.g.

--shard-pattern='(?<stem>.*)-(?<index>\d+)-of-(?<total>\d+)(?:\.(?<dup>[0-9a-f]+))?'

Policy flags (representation and dedup are orthogonal to enabling / scheme selection):

Completeness is surfaced, not hidden: when the name encodes a total (-of-MMM), a set missing
members is flagged - f-???-of-003 (2/3 - INCOMPLETE) (a data-validation feature). Completeness
counts distinct indices, never files, so redundant dup copies never mask a gap. With no declared
total, contiguous runs collapse and detectable gaps are flagged. An index outside a declared total
is excluded from the logical set and surfaced as superfluous, so it cannot make the set complete or
inflate its aggregate size.

References (real-world naming conventions surveyed for the built-in catalog):