Skip to content

feat(#83): archive VFS backend - read-only FileSystem over one container - #455

Merged
helly25 merged 2 commits into
mainfrom
feat/83-archive-vfs-backend
Aug 11, 2026
Merged

feat(#83): archive VFS backend - read-only FileSystem over one container#455
helly25 merged 2 commits into
mainfrom
feat/83-archive-vfs-backend

Conversation

@helly25

@helly25 helly25 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Implements the @xff_extras_api VFS seam inside @xff_archive (never the core), spelling member paths through member_path_cc, so the whole expression grammar matches an archive's members at virtual paths.

Each decision is pinned by a test over a real tar the test writes itself:

  • Implicit parent directories are SYNTHESIZED. The fixture deliberately stores dir/sub/deep.txt with no entry for dir/ or dir/sub/, exactly as real tar streams do - without synthesis a walk finds nothing to descend into.
  • read_only + Source::kArchiveMember on every member - the pair that makes -delete and the exec family REFUSE members instead of silently skipping them.
  • Remove returns PermissionDenied, never silent success (which would make -delete look like it worked). Access is never writable and otherwise follows the stored mode bits. ReadLink resolves a symlink member and refuses a regular one. FsType is "archive"; IsCaseSensitive is true, since stored names are bytes.
  • A path outside the container, a missing member, and "not a directory" are three distinct errors, not an empty listing.
  • The configured separator is used for rendering AND parsing - tested with #, so a path the backend printed is one it can stat back.
  • ReadContent returns Unimplemented on purpose: the reader lists members but does not extract data, and an empty string would make -grep / -content silently match nothing. Member extraction is the next slice.

Two things found while writing it, both recorded rather than smoothed over:

  • Inside namespace xff::archive, unqualified archive resolves to the namespace, not libarchive's struct archive - the test needs ::archive.
  • Reader asymmetry: ListMembers("") rejects empty input as InvalidArgument, but ListMembersOfFile on an empty FILE opens fine and reports zero members (libarchive hits EOF immediately). A test pins today's behaviour; the two entry points should probably agree, which is a reader-level call.

Verified: 14 backend tests plus the reader's own pass under --//xff:xff_archive; //... is 94 green; pre-commit run --all-files clean.

Left for follow-ups: engine-side mounting when --archive selects a container (where --archive-depth and sniff-gating land), and ReadMemberOfFile.

NOTE: not armed - it queues behind #454 and #450 per your ordering. Its TODO.md note is intentionally absent here; it goes in the TODO.md repair PR, since that file needs a structural fix first (my nested bullets were being reflowed by prettier).

Implements the @xff_extras_api seam inside @xff_archive (never the core), so the
whole expression grammar matches an archive's members at virtual paths spelled by
--archive-separator / --archive-prefix.

The decisions, each pinned by a test over a real tar the test writes itself:

- Implicit parent directories are SYNTHESIZED. The fixture deliberately stores
  `dir/sub/deep.txt` with no entry for `dir/` or `dir/sub/`, exactly as real tar
  streams do; without synthesis a walk finds nothing to descend into.
- Every member carries read_only = true and Source::kArchiveMember - the pair that
  makes -delete and the exec family REFUSE members instead of skipping them.
- Remove returns PermissionDenied, never silent success (which would make -delete
  look like it worked). Access is never writable and otherwise follows the stored
  mode bits. ReadLink resolves a symlink member and refuses a regular one. FsType
  is "archive"; IsCaseSensitive is true, since stored names are bytes.
- A path outside the container, a missing member, and "not a directory" are three
  distinct errors rather than an empty listing.
- The CONFIGURED separator is used for rendering AND parsing, so a path the
  backend printed is one it can stat back (tested with `#`, not just the default).
- ReadContent returns Unimplemented on purpose: the reader lists members but does
  not extract data, and an empty string would make -grep / -content silently
  match nothing. The next slice adds member extraction.

Two things found while writing it, both recorded rather than smoothed over:

- Inside `namespace xff::archive`, unqualified `archive` resolves to the NAMESPACE,
  not libarchive's `struct archive` - the test needs `::archive`.
- Reader asymmetry: ListMembers("") rejects empty input, but ListMembersOfFile on
  an empty FILE opens fine and reports zero members. A test pins the current
  behaviour and TODO.md flags that the two entry points should probably agree.

Verified: 14 backend tests plus the reader's own pass under --//xff:xff_archive,
//... is 94 green, pre-commit clean.
@helly25
helly25 enabled auto-merge (squash) August 11, 2026 00:46
@helly25
helly25 merged commit 4223f29 into main Aug 11, 2026
11 checks passed
@helly25
helly25 deleted the feat/83-archive-vfs-backend branch August 11, 2026 00:58
helly25 added a commit that referenced this pull request Aug 11, 2026
Two problems in one section, both mine.

STRUCTURE: my sub-bullets used 4-6 space indents, which prettier reads as
continuation PROSE rather than list items, so ~23 intended bullets were reflowed
into run-on lines ("...split point. - **NEXT SLICE..."). Rewritten with at most
two bullet levels - top level at column 0, children at exactly 2 spaces, long
explanations as continuation paragraphs at 4 - which is a shape prettier keeps.

A mechanical re-split was tried first and DISCARDED: splitting on " - `" cut
sentences in half, because that sequence is also a spaced-hyphen separator in
prose. It scored perfectly on the checks I had written ("0 joined bullets", stable
trunk fmt) while the text had become nonsense - the checks were measuring the
wrong property, so this rewrite was verified by READING it.

CONTENT: pruned what later decisions had superseded, 129 lines out for 95 in:

- the long "member path spelling is TBD / here are the candidate conventions"
  debate, now that both flags have shipped - kept as one decision statement;
- the separate "correcting an earlier note" entries about the URI authority rule
  and about `/` being lossy - the erroneous claims themselves are gone, so the
  corrections have nothing to correct;
- the nine-method "NEXT SLICE, mapped onto the seam" mapping, now that the backend
  is built (#455) - replaced by what its tests actually pin;
- duplicate statements of the concatenation / first-separator rules.

Kept whole: the libarchive decision and NOTICE obligations, the ratified
--archive/-z control surface and flavor defaults, dual container identity,
--archive-depth, detection and sniff-gating, read-only member semantics, and the
two genuinely OPEN items (per-format schemes + AUTO separator, phar), each now
naming its task (#177, #176).

Also fixes one more joined bullet outside this section (the span-diagram entry),
same cause. `trunk fmt` is idempotent afterwards and zero joins remain.
helly25 added a commit that referenced this pull request Aug 11, 2026
Conflict: #455 (archive VFS backend) and this branch each appended targets to
extra_modules/archive/BUILD.bazel. Both sets are kept.

Also does what this PR promised once #457 landed: `archive_reader.cc` drops its
local copy of the member-name normalization and uses the shared
`NormalizeMemberName` from the member-path library, so the rule exists once for
both readers rather than being duplicated on main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant