billy vs afero semantics¶
billy and afero are mostly one-to-one, which is what makes the adapter small. The value is in the handful of places they disagree — this page documents every one and why it was resolved the way it was. If something behaves unexpectedly, the answer is almost certainly here.
The mismatches¶
| afero expects | What the adapter does |
|---|---|
Mkdir(name, perm) |
Maps to billy's MkdirAll — creates parents and is idempotent, rather than afero's strict "fail on a missing parent or an existing target" |
RemoveAll(path) |
Uses go-billy/v5/util.RemoveAll — billy has no RemoveAll of its own |
Chmod / Chown / Chtimes |
No-ops. billy's memfs ignores modes entirely; returning an error would break otherwise-correct callers that set permissions |
Open(dir) |
billy cannot open a directory, so a synthesised read-only directory handle is returned: Readdir and Stat work; byte-level operations return an *os.PathError reading "is a directory" |
File.Readdir(count) |
Snapshots billy's ReadDir once, then paginates to os.File semantics: count <= 0 returns everything; count > 0 returns up to count and then io.EOF; on a regular file it returns "not a directory" |
File.ReadAt |
Passed straight through — billy.File guarantees io.ReaderAt |
File.WriteAt |
Uses the native io.WriterAt when the file provides one (memfs and osfs do); otherwise a seek-based emulation that restores the original offset afterwards |
| Symlinks | Lstater, Symlinker, Linker and LinkReader delegate to billy's Lstat/Symlink/Readlink |
Name() |
The backing filesystem's root (bfs.Root()), resolved once at construction; falls back to "aferobilly" when the backend reports an empty root |
Why Mkdir creates parents¶
This is the mismatch most likely to surprise, and it was a deliberate reversal of an earlier decision to emulate afero strictly.
billy has no plain Mkdir — only MkdirAll. Faithfully reproducing afero's semantics
would mean the adapter checking for a missing parent and an existing target itself, then
synthesising the errors afero would have returned: more code, more ways to be subtly
wrong, and a race between the check and the create.
Mapping straight onto MkdirAll is simpler, matches what billy actually does, and suits
the workloads this adapter exists for (building and editing trees). The cost is that code
relying on Mkdir failing — as an existence test, or to catch a missing parent — will
not get that failure. In practice, code doing that should be using Stat.
Why Chmod and friends are silent no-ops¶
The alternative is returning an error, and that is worse. Plenty of correct code sets
permissions as a matter of course after writing a file. Against a billy backend that
ignores modes anyway (memfs), failing those calls would break callers for no benefit —
the permission was never going to be honoured either way. Silently accepting them keeps
such code working, and code that genuinely depends on permissions was never going to be
satisfied by an in-memory filesystem.
Why directories get a synthesised handle¶
billy simply cannot Open a directory, but afero callers — and anything built on
afero.Walk or afero.ReadDir — expect a directory to open and answer Readdir. So the
adapter fabricates a read-only handle backed by a one-shot ReadDir snapshot.
Two consequences worth knowing. Byte-level operations on it fail with a proper
*os.PathError ("is a directory"), matching os. And because the listing is snapshotted
when the handle is opened, entries created afterwards will not appear in that handle's
Readdir — reopen to see them.
The symlink argument-order trap¶
The two interfaces name symlink arguments in opposite orders:
- billy:
Symlink(target, link) - afero:
SymlinkIfPossible(oldname, newname)
They mean the same thing, in the opposite order. The adapter maps between them, and it is called out here because it is exactly the kind of mismatch that produces a symlink pointing the wrong way in a way that is easy to miss in review.
What is not adapted¶
The adapter presents a filesystem and nothing more. It does not add caching, path
sandboxing, or a virtual root — use billy's chroot for the last one and wrap the result.
And it exposes no way to reach the billy object underneath, so the
locking guarantee cannot be bypassed.