fsroot

package
v0.1.0-dev.20260825003429 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package fsroot provides confined filesystem roots.

All provider I/O flows through the Dir interface, confining reads and writes to a directory tree. The confinement is the kernel's, via *os.Root: one behavior, two lifetimes — OpenConfined anchors at an existing directory for the caller's lifetime, and OpenScratch anchors at a new temporary directory whose tree is removed with the handle on Close.

Index

Constants

View Source
const (

	// PermOwner masks the owner's read, write, and execute bits.
	PermOwner os.FileMode = 0o700

	// PermGroup masks the group's read, write, and execute bits.
	PermGroup os.FileMode = 0o070

	// PermOther masks other's — the world's — read, write, and execute bits.
	PermOther os.FileMode = 0o007
)

Permission-class masks for the three classes a Unix mode names.

Go supplies no symbol for these. fs.ModePerm covers all nine bits at once, and syscall's S_IRWXU, S_IRWXG and S_IRWXO are unix-only, so they cannot appear in code that also builds for windows.

They exist because the two rules that matter are one character apart as literals — `perm&0o007` excludes only other, `perm&0o077` excludes group as well — and that is how this package's enforcement gate and its own documentation came to disagree about which one it implements. Named, the rules stop resembling each other: `perm&PermOther == 0` and `perm&(PermGroup|PermOther) == 0` cannot be misread for one another.

Migrating the existing call sites onto these, and ruling which of the two the enforcement gate should mean, is tracked separately.

Variables

This section is empty.

Functions

func RelWithin

func RelWithin(rootName, path string) (string, bool)

RelWithin reports whether the machine-absolute `path` lies within the root anchored at `rootName`, and when it does, the slash-canonical rel that names it there.

The judgment is lexical (Clean + Rel) — no symlink resolution and no disk contact; a caller that needs follow semantics resolves both sides first. A path on another volume, an escaping traversal, and the root itself all answer false.

Parameters:

  • `rootName`: the root's absolute path (matches os.Root.Name).
  • `path`: the machine-absolute path to judge.

Returns:

  • `string`: the slash-canonical rel naming `path` under the root; empty when not within.
  • `bool`: true when `path` lies strictly within the root.

Types

type Dir

type Dir interface {
	Chmod(p Path, mode os.FileMode) error
	Chown(p Path, uid, gid int) error
	Chtimes(p Path, atime, mtime time.Time) error
	Close() error
	Create(p Path) (*os.File, error)
	CreateTemp(dir Path, pattern string) (*os.File, Path, error)
	FS() fs.FS
	Lchown(p Path, uid, gid int) error
	Link(oldPath, newPath Path) error
	Lstat(p Path) (fs.FileInfo, error)
	Mkdir(p Path, perm os.FileMode) error
	MkdirAll(p Path, perm os.FileMode) error
	MkdirTemp(dir Path, pattern string) (Path, error)
	Name() string
	NewPath(name ...string) Path
	Open(p Path) (*os.File, error)
	OpenFile(p Path, flag int, perm os.FileMode) (*os.File, error)
	OpenRoot(p Path) (Dir, error)
	ReadFile(p Path) ([]byte, error)
	Readlink(p Path) (string, error)
	Remove(p Path) error
	RemoveAll(p Path) error
	Rename(oldPath, newPath Path) error
	Stat(p Path) (fs.FileInfo, error)
	Symlink(target string, link Path) error
	WriteFile(p Path, data []byte, perm os.FileMode) error
}

Dir provides scoped filesystem operations. All path arguments are Path values created through Dir.NewPath.

Two constructors produce the two lifetimes, both confined by *os.Root:

  • OpenConfined anchors at an existing directory; the root lives until the caller closes it
  • OpenScratch anchors at a new temporary directory; Close removes the tree with the handle

The method set mirrors *os.Dir in full, so code that knows the standard library's root knows this one. Every filesystem mutation in the repository is expected to flow through this interface; a direct os.* call must carry a `// Confinement:` comment stating why the root cannot serve it.

Dir.CreateTemp and Dir.MkdirTemp go beyond the mirror: *os.Dir has no equivalent, and os.CreateTemp cannot be confined to a root. Choosing between a scratch root and this one is the whole of the decision — use the session's scratch unless the bytes must end up in this tree atomically, since a rename out of scratch can cross a device boundary and degrade to a copy.

func OpenConfined

func OpenConfined(dir string) (Dir, error)

OpenConfined opens an OS-enforced confined Dir at dir.

Parameters:

  • `dir`: the directory to confine all I/O within.

Returns:

func OpenScratch

func OpenScratch(pattern string) (Dir, error)

OpenScratch opens a confined Dir at a newly created temporary directory that removes itself.

Scratch is not an escape from confinement — it is its own confined tree with a self-destroying lifetime. Process scratch (spool files, staging trees) belongs here rather than in a direct os.CreateTemp call, so that scratch I/O flows through the same seam as every other mutation and inherits its platform behavior.

IMPORTANT: Dir.Close on a scratch root does two things — it releases the handle AND removes the directory tree. Closing early destroys the contents. This overload is deliberate: it makes cleanup impossible to forget, which a separate discard method would not.

Parameters:

  • `pattern`: the os.MkdirTemp name pattern; a `*` in the pattern is replaced by a random string, otherwise the random string is appended.

Returns:

  • `Root`: a confined root anchored at the new temporary directory.
  • `error`: any error from os.MkdirTemp or OpenConfined. On an OpenConfined failure the temporary directory is removed before returning, so no tree is orphaned.

type Path

type Path struct {
	// contains filtered or unexported fields
}

Path holds both root-relative and absolute forms of a filesystem path.

Created through Dir.NewPath to guarantee both fields are populated. The root field records which root directory Rel is relative to (matching os.Root.Name). Abs is derived as filepath.Join(root, rel) and is not serialized.

noinspection GoMixedReceiverTypes

func NewPath

func NewPath(root, rel string) Path

NewPath creates a Path from a root directory and a root-relative path.

Abs is derived via filepath.Join. Rel is stored in canonical slash form regardless of the input's separators — rel is the half that serializes, and equal logical paths must produce equal document bytes on every platform (the same rule the Merkle-root digest follows). Intended for tests and deserialization.

Parameters:

  • `root`: the root directory that `rel` is relative to (matches os.Root.Name).
  • `rel`: the root-relative path; any separator form.

Returns:

  • `Path`: the constructed path, with `rel` canonicalized to slash form.

func (Path) Abs

func (p Path) Abs() string

Abs returns the absolute path used for direct os.* I/O, URIs, display, and logging.

Returns:

  • `string`: the absolute path.

func (Path) MarshalJSON

func (p Path) MarshalJSON() ([]byte, error)

MarshalJSON serializes the canonical form {root, rel}. Abs is derived on deserialization.

Returns:

  • `[]byte`: the JSON encoding of the {root, rel} form.
  • `error`: any error returned by json.Marshal.

func (Path) MarshalYAML

func (p Path) MarshalYAML() (any, error)

MarshalYAML serializes the canonical form {root, rel}. Abs is derived on deserialization.

Returns:

  • `any`: the {root, rel} form for the YAML encoder to serialize.
  • `error`: always nil; present to satisfy the yaml.Marshaler interface.

func (Path) Rel

func (p Path) Rel() string

Rel returns the root-relative path used for confined I/O.

Always canonical slash form, on every platform: rel is the half that serializes, so equal logical paths produce equal document bytes and checksums everywhere, and it feeds io/fs APIs whose contract requires slash paths. Convert with filepath.FromSlash only where an OS-native rel is genuinely needed; direct filesystem I/O flows through Path.Abs, which stays OS-native.

Returns:

  • `string`: the root-relative path, slash-separated.

func (Path) Root

func (p Path) Root() string

Root returns the root directory path that Rel is relative to. Matches os.Root.Name.

Returns:

  • `string`: the root directory path.

func (Path) String

func (p Path) String() string

String returns the absolute path.

Returns:

  • `string`: the absolute path.

func (*Path) UnmarshalJSON

func (p *Path) UnmarshalJSON(data []byte) error

UnmarshalJSON deserializes {root, rel} and derives Abs.

Pointer receiver is required by the json.Unmarshaler contract — the method must mutate the receiver to populate fields from the JSON bytes. All other Path methods use value receivers since Path is an immutable value type.

Implementation note: A pointer receiver is required by the json.Unmarshaler contract. The method mutates the receiver in place to populate `root`, `rel`, and `abs` from the encoded document, so a value receiver would fill a discarded copy.

This is the deliberate exception to Path's value-receiver convention. The getters and the Marshal methods use value receivers so the value type Path — not just *Path — satisfies json.Marshaler / yaml.Marshaler and its accessors stay callable on non-addressable values (map elements, function returns). Unmarshaling always targets an addressable variable (json.Unmarshal(data, &p)), so the pointer receiver is safe. The resulting value/pointer mix is intentional, which is why the mixed-receivers inspection is suppressed.

Parameters:

  • `data`: the JSON bytes to decode.

Returns:

  • `error`: non-nil if the JSON is malformed.

func (*Path) UnmarshalYAML

func (p *Path) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML deserializes {root, rel} and derives Abs.

Implementation note: A pointer receiver is required by the json.Unmarshaler contract. The method mutates the receiver in place to populate `root`, `rel`, and `abs` from the encoded document, so a value receiver would fill a discarded copy.

This is the deliberate exception to Path's value-receiver convention. The getters and the Marshal methods use value receivers so the value type Path — not just *Path — satisfies json.Marshaler / yaml.Marshaler and its accessors stay callable on non-addressable values (map elements, function returns). Unmarshaling always targets an addressable variable (json.Unmarshal(data, &p)), so the pointer receiver is safe. The resulting value/pointer mix is intentional, which is why the mixed-receivers inspection is suppressed.

Parameters:

  • `value`: the YAML node to decode.

Returns:

  • `error`: non-nil if the YAML is malformed.

Jump to

Keyboard shortcuts

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