vfs

package
v0.26.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 24, 2026 License: MIT Imports: 2 Imported by: 0

Documentation

Overview

Package vfs — range 0.2.25.* (ADR 0056 core/data/vfs block), and the concrete filesystem's 0.3.55.* (ADR 0056 service/data/vfs block, declared here since ADR 0160).

Package vfs — declares the sentinel *errs.Error port outcomes, and the two a concrete filesystem in internal/service/data/vfs can produce and an abstract one cannot (ADR 0160: every code is declared in the core, at the service's path). Each var's name equals its errs.Define Reason in SCREAMING_SNAKE form.

No Public string here names a path. A Public is read by third parties, and a path is the one piece of caller data a filesystem error is guaranteed to hold; it travels as a log-only field instead, reachable through errs.FieldsOf.

Package vfs — the path grammar every implementation shares.

Package vfs — the file-mode rule every implementation shares.

Package vfs declares the SDK's filesystem port. Reading is io/fs — this package does not redeclare it, does not wrap it and does not reimplement its walkers. What it adds is the half the standard library deliberately left out: writing, directory creation and removal, and publication that is ATOMIC. A core sibling admitted by ADR 0056.

What comes for free, and must never be written again

WritableFS EMBEDS fs.FS. That one line is the whole interoperability story: every implementation of this port IS an fs.FS, so fs.WalkDir, fs.Glob, fs.ReadFile, fs.Sub, fs.Stat and every third-party consumer of fs.FS work on it unchanged and with no adapter. There is deliberately no vfs.Walk and no vfs.Glob in this SDK. Adding one would not be a feature: the stdlib versions are correct, maintained by the Go project, and already reachable through the embedded interface.

The read verbs are therefore absent from this file on purpose. If a method you want already exists on fs.FS or as an io/fs package function, this port does not restate it.

Why the domain exists at all

Because "generate beside, swap by rename, flush the parent directory" is an invariant the SDK repeats by hand in every publisher, and a discipline repeated by hand is a discipline that will be forgotten exactly once. Here it is a PRIMITIVE, with its failure path tested rather than assumed: AtomicWriter promises that a caller which observes an error observes the previous bytes, byte for byte, and finds no temporary file left behind.

The path grammar, and what it does and does not guarantee

Every name handed to this port is a slash-separated, unrooted, "."- and ".."-free path — exactly fs.ValidPath, which is already what fs.FS requires of readers. ValidatePath is that check; ValidateWritePath adds the one extra refusal a writer needs, namely that the root itself is not a target.

That is a LEXICAL guarantee and it is not the whole answer: a path made only of legal elements can still leave the tree by traversing a symbolic link that points outside it. Refusing that is the implementation's job, because only the implementation knows what a link is — and it reports PathEscaped when it happens. An implementation that cannot enforce it MUST refuse to be constructed rather than accept a name it cannot confine.

Permissions are never inferred

A zero mode is refused, never read as "use a default" (ADR 0031). Any file mode the SDK could pick would be arbitrary: 0644 and 0600 differ by who may read the bytes, and only the caller knows what the bytes are. Bits outside fs.ModePerm — setuid, setgid, sticky, the type bits — are refused BY NAME for the same reason a typo must not be able to mint a setuid file.

The concrete filesystems live in internal/service/data/vfs; this package owns the contract, the two guards, and the typed sentinels.

Package vfs — the write contract and its ADR 0039 capability sibling.

They live apart from vfs.go because that file carries the package's doc and the one thing that is NOT an interface declaration: the FS alias that says reading is io/fs unchanged.

Index

Constants

View Source
const CodeDirectoryNotEmpty errs.Code = 0x00_02_19_08 // 0.2.25.8

CodeDirectoryNotEmpty identifies a Remove aimed at a directory that still has entries. RemoveAll is the call that was wanted.

View Source
const CodeDirectorySyncFailed errs.Code = 0x00_03_37_02 // 0.3.55.2

CodeDirectorySyncFailed identifies the ONE failure that arrives after a successful rename: the directory entry could not be flushed to the device.

It is a separate code from PublishFailed because it means the opposite thing. PublishFailed says nothing changed; this says the new content IS visible and may not survive a power loss.

View Source
const CodeInvalidPath errs.Code = 0x00_02_19_01 // 0.2.25.1

CodeInvalidPath identifies a name that is not a valid filesystem path for this domain: rooted, empty, backslash-separated, or carrying a "." or ".." element. It is the LEXICAL half of the confinement guarantee.

View Source
const CodeInvalidPermission errs.Code = 0x00_02_19_02 // 0.2.25.2

CodeInvalidPermission identifies a file mode that cannot be honoured as written: zero — which the SDK refuses to read as "pick a default" — or bits outside fs.ModePerm, which would make a typo able to mint a setuid file.

View Source
const CodeNotRegularFile errs.Code = 0x00_02_19_07 // 0.2.25.7

CodeNotRegularFile identifies a name that exists and is not a regular file, where writing it would replace a directory, a device or a socket.

View Source
const CodePathEscaped errs.Code = 0x00_02_19_03 // 0.2.25.3

CodePathEscaped identifies a lexically valid name that resolved OUTSIDE the filesystem's root — a symbolic link pointing away from the tree, or a traversal the operating system refused. It is the half fs.ValidPath cannot see.

View Source
const CodePublishFailed errs.Code = 0x00_02_19_06 // 0.2.25.6

CodePublishFailed identifies an atomic publication that did not complete. Its contract is the interesting part: the previous content is intact and no temporary file survives.

View Source
const CodeReadFailed errs.Code = 0x00_02_19_04 // 0.2.25.4

CodeReadFailed identifies an open, stat or directory listing the filesystem refused. The cause is wrapped rather than replaced, so errors.Is against fs.ErrNotExist and fs.ErrPermission keeps answering.

View Source
const CodeRootUnavailable errs.Code = 0x00_03_37_01 // 0.3.55.1

CodeRootUnavailable identifies a root directory that could not be opened, or that is not a directory at all. It is reported by the constructor, so the refusal arrives where the program is wired rather than at the first write.

View Source
const CodeWriteFailed errs.Code = 0x00_02_19_05 // 0.2.25.5

CodeWriteFailed identifies a create, write, mkdir or remove the filesystem refused. As with CodeReadFailed the cause stays in the chain.

Variables

View Source
var (
	// InvalidPath is returned for a name outside the fs.ValidPath grammar,
	// and by [ValidateWritePath] for the root itself.
	//
	// It is a REFUSAL rather than a cleanup. Normalising "../../etc/passwd"
	// into something safe is the single most reliable way to ship a
	// traversal bug: every normaliser is a small parser, every small parser
	// has a case its author did not think of, and the caller never learns
	// that the path it asked for is not the path it got.
	InvalidPath = errs.Static(CodeInvalidPath, "INVALID_PATH",
		"That path is not usable on this filesystem",
		"core/data/vfs: name violates fs.ValidPath (rooted, empty, backslash-separated, or carrying a . or .. element) or targets the root; refused, never normalised",
		0,
		65)

	// InvalidPermission is returned for a zero file mode, or one carrying
	// bits outside fs.ModePerm.
	//
	// ADR 0031's refusing half: any mode the SDK invented here would be
	// arbitrary. 0644 and 0600 differ by who may read the bytes, and the
	// caller is the only party that knows what the bytes are — so a zero is
	// read as "the field was never filled in", which it almost always is.
	InvalidPermission = errs.Static(CodeInvalidPermission, "INVALID_PERMISSION",
		"A file mode is required and must be a plain permission mode",
		"core/data/vfs: perm is zero (refused, never defaulted — ADR 0031) or carries bits outside fs.ModePerm such as setuid, setgid or sticky",
		0,
		78)

	// PathEscaped is returned when a lexically valid name resolved outside
	// the filesystem's root.
	//
	// fs.ValidPath cannot see this: every element of "link/secret" is legal,
	// and whether it leaves the tree depends on what "link" points at, which
	// is a runtime property of the filesystem and not of the string.
	PathEscaped = errs.Static(CodePathEscaped, "PATH_ESCAPED",
		"That path leaves the filesystem it was resolved against",
		"core/data/vfs: the name resolved outside the root — a symbolic link pointing away from the tree, or a traversal the kernel refused",
		0,
		77)

	// ReadFailed is returned when an open, stat or directory listing was
	// refused. The cause stays in the error chain, so a caller may still ask
	// errors.Is(err, fs.ErrNotExist) and get the answer io/fs would give.
	ReadFailed = errs.Static(CodeReadFailed, "READ_FAILED",
		"The filesystem could not read that path",
		"core/data/vfs: open, stat or readdir failed; the *fs.PathError cause is wrapped, not replaced, so errors.Is against fs.ErrNotExist still answers",
		0,
		74)

	// WriteFailed is returned when a create, write, mkdir or remove was
	// refused. Like [ReadFailed] it keeps the cause reachable.
	WriteFailed = errs.Static(CodeWriteFailed, "WRITE_FAILED",
		"The filesystem could not write that path",
		"core/data/vfs: create, write, mkdir or remove failed; the *fs.PathError cause is wrapped, not replaced",
		0,
		74)

	// PublishFailed is returned when an atomic publication did not complete.
	//
	// Its guarantee is the whole point of the domain and is asserted by a
	// test, not by this comment: on this error the bytes previously at the
	// name are unchanged and no temporary file survives. It is therefore
	// safe to retry, and safe to give up.
	PublishFailed = errs.Static(CodePublishFailed, "PUBLISH_FAILED",
		"The filesystem could not publish that file",
		"core/data/vfs: atomic publication aborted; the previous content is intact and the temporary was removed — the operation is safe to retry",
		0,
		74)

	// NotRegularFile is returned when a name exists and is not a regular
	// file. Writing it would replace a directory, a device or a socket, and
	// no caller has ever meant that by WriteFile.
	NotRegularFile = errs.Static(CodeNotRegularFile, "NOT_REGULAR_FILE",
		"That path already holds something other than a file",
		"core/data/vfs: the name resolves to a directory, device, socket or symlink; a write there would destroy an object of a different kind",
		0,
		65)

	// DirectoryNotEmpty is returned by Remove for a directory that still has
	// entries — POSIX ENOTEMPTY, made typed and made identical in both
	// implementations so a memory filesystem is a faithful double.
	DirectoryNotEmpty = errs.Static(CodeDirectoryNotEmpty, "DIRECTORY_NOT_EMPTY",
		"That directory still has entries in it",
		"core/data/vfs: Remove targets one file or one EMPTY directory; RemoveAll is the recursive call",
		0,
		65)

	// RootUnavailable is returned by NewOS when the root cannot be opened.
	//
	// It is a CONSTRUCTION-time refusal on purpose: a filesystem whose root
	// does not exist will fail every call it is ever given, and the useful
	// place to learn that is where the program is assembled, not on the first
	// request that happens to write something.
	RootUnavailable = errs.Static(CodeRootUnavailable, "ROOT_UNAVAILABLE",
		"The filesystem root could not be opened",
		"service/data/vfs: os.OpenRoot failed — the path is absent, is not a directory, or is not searchable by this process",
		0,
		78)

	// DirectorySyncFailed is returned when the rename succeeded and the
	// parent directory could not be flushed afterwards.
	//
	// It deliberately does NOT roll back. The rename has already happened, so
	// the new content is what every reader sees; undoing it would mean a
	// second non-atomic write to repair a durability problem, which is how a
	// good file gets replaced by a worse one. The caller is told exactly what
	// is true — published, not proven durable — and decides whether that is
	// acceptable for its data.
	DirectorySyncFailed = errs.Static(CodeDirectorySyncFailed, "DIRECTORY_SYNC_FAILED",
		"The file was published but the directory entry was not flushed",
		"service/data/vfs: fsync on the parent directory failed AFTER a successful rename; the content is visible and may not survive a power loss — deliberately not rolled back",
		0,
		74)
)

Functions

func ValidatePath

func ValidatePath(name string) error

ValidatePath reports whether name is a path this domain will resolve, and returns InvalidPath when it is not.

The rule is fs.ValidPath and nothing else, on purpose. io/fs already imposes it on every reader, so a second, subtly different grammar for writers would mean a name that can be written and not read back — and the SDK would have invented that discrepancy itself. Concretely it rejects the empty string, a leading or trailing slash, a doubled slash, a Windows-style backslash path, and any "." or ".." element.

This is a LEXICAL guarantee. It closes "../../etc/passwd" and it does not close a symbolic link that leaves the tree — see PathEscaped, which is the implementation's half of the same promise.

One result is worth stating because it surprises people: the separator here is "/" and nothing else, so `..\..\etc\passwd` is ACCEPTED — it is one legal, if peculiar, POSIX filename, and a file written under it lands inside the root rather than above it. Rejecting backslashes would make a legal filename unwritable to buy a confinement the grammar already provides.

func ValidatePerm

func ValidatePerm(perm fs.FileMode) error

ValidatePerm reports whether perm is a mode this domain will apply, and returns InvalidPermission when it is not.

Two refusals, and ADR 0031 is the reason for both.

A ZERO mode is refused rather than defaulted. ADR 0031 admits clamping where a working default needs no explanation and demands a refusal where any SDK-chosen value would be arbitrary — and a file mode is the second case, unambiguously: 0644 and 0600 differ by who may read the bytes, the SDK does not know what the bytes are, and a mode of 0 is what an unfilled struct field looks like. Silently choosing would make the SDK the author of a security decision it has no information about.

Bits OUTSIDE fs.ModePerm are refused by name. setuid, setgid, the sticky bit and the type bits are each a deliberate act; none of them should be reachable by mistyping a permission literal, and a mode argument that also carries them makes "0o4755 instead of 0o755" a one-character privilege escalation. A caller who genuinely wants one of those wants a call that says so, which this domain does not offer.

func ValidateWritePath

func ValidateWritePath(name string) error

ValidateWritePath is ValidatePath plus the one refusal a writer needs: the root itself is not a target.

fs.ValidPath accepts "." because a reader legitimately opens the tree to list it. A writer that accepts it accepts `RemoveAll(".")`, which is a call that empties the entire filesystem and reads, in a diff, exactly like a no-op — and accepts `WriteFile(".")`, which cannot mean anything at all.

Types

type AtomicWriter

type AtomicWriter interface {
	// WriteAtomic publishes data at name so that a concurrent reader sees
	// either the previous content or the new content, and never a partial
	// file, a truncated one, or a name that resolves to nothing.
	//
	// The contract on FAILURE is why this method exists, and it is what
	// [PublishFailed] asserts: when WriteAtomic returns an error, the bytes
	// previously at name are unchanged, and no temporary file survives
	// anywhere the caller can see. An implementation that cannot promise
	// both must not implement this interface.
	//
	// Durability beyond the process is the implementation's to describe: the
	// operating-system filesystem flushes the file AND the directory entry,
	// while an in-memory one has no device to flush and says so.
	WriteAtomic(name string, data []byte, perm fs.FileMode) error
}

AtomicWriter publishes a whole file in one indivisible step. It is the ADR 0039 sibling of WritableFS: reached by type assertion, never by widening the port, because it is a CAPABILITY and not every filesystem has one — an object store with no rename, a read-through overlay, a tar archive.

if publisher, ok := filesystem.(vfs.AtomicWriter); ok {
	err = publisher.WriteAtomic("index.html", page, 0o644)
}

Both filesystems in internal/service/data/vfs implement it.

type FS

type FS = fs.FS

FS is the read half of a filesystem, and it is io/fs.FS unchanged.

It is an ALIAS rather than a fresh interface, so a vfs.FS and an fs.FS are the same type to the compiler and no conversion, adapter or assertion sits between an SDK filesystem and the standard library's walkers. The name exists to say, in one place, that the SDK's answer for reading is the stdlib's answer for reading.

type FullFS

type FullFS interface {
	WritableFS
	AtomicWriter
}

FullFS is the union of WritableFS and AtomicWriter, and it is what the SDK's own constructors return.

It is the shape core/observe/metrics already uses for FullMeter, and it exists for the same reason: the port stays frozen and the capability stays assertable for a THIRD-PARTY filesystem that cannot publish atomically, while a caller wiring one of the SDK's own does not have to type-assert for the headline feature. A parameter should still ask for the narrowest thing it uses — WritableFS when it only writes, AtomicWriter when it only publishes.

type WritableFS

type WritableFS interface {
	FS
	// WriteFile writes data to name, creating the file if it is absent and
	// truncating it if it is not, with perm applied on creation. Missing
	// parent directories are NOT created: a typo in a path is a far more
	// common event than a genuinely absent directory, and inventing the
	// tree silently is how a file ends up somewhere nobody looks. It is not
	// atomic — [AtomicWriter] is the call that is.
	WriteFile(name string, data []byte, perm fs.FileMode) error
	// MkdirAll creates name and every missing parent, applying perm to each
	// directory it creates. An existing directory is success; an existing
	// non-directory is [NotRegularFile].
	MkdirAll(name string, perm fs.FileMode) error
	// Remove removes one file or one EMPTY directory. A non-empty directory
	// is [DirectoryNotEmpty] rather than a recursive delete, because the two
	// are different decisions and only one of them is reversible by accident.
	Remove(name string) error
	// RemoveAll removes name and everything beneath it. A name that does not
	// exist is success — the caller asked for it to be gone, and it is.
	RemoveAll(name string) error
}

WritableFS is the write half, and it is FROZEN at four methods on top of the embedded FS. A new capability arrives as a sibling interface reached by type assertion — never by widening this one (ADR 0039): pkg/v1/data/vfs aliases it, Go interfaces are structural, and a fifth method would break every downstream implementation at compile time with no deprecation window.

Every method validates its name through ValidateWritePath, and every method taking a mode validates it through ValidatePerm, so the two implementations refuse identical inputs identically. That is what makes an in-memory filesystem usable as a test double for a real one.

Implementations MUST be safe for concurrent use.

IFACE-PLUGIN: the concrete filesystems stay unexported behind their constructors in internal/service/data/vfs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL