Files
nfs/internal/nfsfs/fs.go
T
petrbalvin a9b8039ef7
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
feat: full NFSv4.2 server and client in pure Go
Assisted-by: GLM 5.3 Flash
2026-09-21 18:51:17 +02:00

264 lines
9.6 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// Package nfsfs defines the virtual filesystem the NFS server serves, and
// provides a backend over a local directory.
//
// The interface carries exactly what the protocol layer needs and nothing
// more: handles that the backend itself interprets, the attributes each
// GETATTR turns into an fattr4, the data operations LOOKUP, READDIR and
// READ, and the Writer half that WRITE, CREATE and the other state
// changing operations reach.
package nfsfs
import (
"errors"
"io/fs"
"time"
)
// A Handle is an opaque file handle. The backend defines its layout; the
// protocol layer treats it as bytes.
type Handle []byte
// Sentinels the dispatcher maps onto NFS4ERR statuses. Use errors.Is.
var (
ErrStale = errors.New("nfsfs: unknown file handle")
ErrNoEnt = errors.New("nfsfs: no such file or directory")
ErrNotDir = errors.New("nfsfs: not a directory")
ErrIsDir = errors.New("nfsfs: is a directory")
ErrNameTooLong = errors.New("nfsfs: name too long")
ErrBadName = errors.New("nfsfs: invalid name")
ErrPermission = errors.New("nfsfs: permission denied")
ErrIO = errors.New("nfsfs: io error")
ErrExist = errors.New("nfsfs: file exists")
ErrNoSpace = errors.New("nfsfs: no space left")
ErrNotEmpty = errors.New("nfsfs: directory not empty")
ErrInval = errors.New("nfsfs: invalid argument")
ErrNotLnk = errors.New("nfsfs: not a symlink")
)
// Access mask bits, RFC 8881 section 15.2.2. The same values the protocol
// layer speaks.
const (
AccessRead = 1 << 0
AccessLookup = 1 << 1
AccessModify = 1 << 2
AccessExtend = 1 << 3
AccessDelete = 1 << 4
AccessExec = 1 << 5
)
// MaxName is the name length limit the backends enforce.
const MaxName = 255
// An Info carries the file attributes the backends report.
type Info struct {
Size int64
Mode fs.FileMode
ModTime time.Time
Dev uint64
Ino uint64
Nlink uint64
UID uint32
GID uint32
}
// IsDir reports whether the file is a directory.
func (i Info) IsDir() bool { return i.Mode.IsDir() }
// An Entry is one READDIR row: the cookie the client resumes from, the
// name, its handle and its attributes.
type Entry struct {
Cookie uint64
Name string
Handle Handle
Info Info
}
// A DirPage is one READDIR result page: the entries the cookie asked for,
// the verifier of the directory order, and whether the listing is complete.
type DirPage struct {
Entries []Entry
Verifier [8]byte
EOF bool
}
// An FS is the virtual filesystem the server serves. Implementations must
// be safe for concurrent use.
type FS interface {
// Root returns the handle of the export root.
Root() (Handle, error)
// Lookup resolves name under the parent handle.
Lookup(parent Handle, name string) (Handle, Info, error)
// Getattr reports the attributes of a handle.
Getattr(h Handle) (Info, error)
// ReadLink reports the target of a symlink. A handle that names
// anything else is an error.
ReadLink(h Handle) (string, error)
// Parent resolves the directory that holds h and the component name
// of h under it. The root of the export has no parent name.
Parent(h Handle) (Handle, string, error)
// ReadDir lists the directory from the given cookie, returning at most
// count entries, or all of them when count is zero or less.
ReadDir(h Handle, cookie uint64, count int) (DirPage, error)
// Read reads up to count bytes at the offset. A short result means end
// of file was reached.
Read(h Handle, off int64, count int) ([]byte, error)
// Access evaluates the requested mask bits for the credential and
// returns the bits granted.
Access(h Handle, mask uint32, uid, gid uint32, groups []uint32) (uint32, error)
}
// ValidName reports whether name can appear in a LOOKUP. Names carrying a
// separator or a control byte never belong to the client, because no
// backend resolves them.
func ValidName(name string) error {
switch {
case name == "":
return ErrBadName
case name == "." || name == "..":
return ErrBadName
case len(name) > MaxName:
return ErrNameTooLong
}
for i := range len(name) {
if name[i] == '/' || name[i] == 0 {
return ErrBadName
}
}
return nil
}
// The object kinds a CREATE may carry, the same values the protocol's
// createtype4 uses. A regular file is not among them: in NFSv4 regular
// files are created by OPEN.
const (
KindDir = 2
KindLnk = 5
KindSock = 6
KindFifo = 7
KindBlk = 3
KindChr = 4
)
// An Owner names the unix owner and group an object carries after its
// creation. The server runs under its own identity, so a backend that
// serves clients of several owners applies these values when a client
// makes a new object.
type Owner struct {
UID uint32
GID uint32
}
// A CreateSpec describes one object a CREATE makes.
type CreateSpec struct {
Kind uint32
Perm fs.FileMode // permission bits, applied exactly, umask aside
LinkData string // the target of a symlink
Major uint32 // device numbers of a character or block device
Minor uint32
Owner Owner // the owner a fresh object carries
}
// A Writer is the mutating half of an FS. A backend that serves reads only
// does not implement it, and the dispatcher answers NFS4ERR_ROFS.
type Writer interface {
// Create makes the object the spec describes under the parent handle.
// An existing target is an error for every kind.
Create(parent Handle, name string, spec CreateSpec) (Handle, Info, error)
// Write writes all of data at the offset of a regular file and
// returns how many bytes landed.
Write(h Handle, off int64, data []byte) (int, error)
// Remove takes the named entry out of the directory. Removing a
// directory that is not empty is an error.
Remove(dir Handle, name string) error
// Rename moves oldName from the oldDir directory to newName in the
// newDir directory, replacing an existing plain target the way POSIX
// rename does. The handles of the moved object and of its descendants
// keep working after the move.
Rename(oldDir Handle, oldName string, newDir Handle, newName string) error
// Setattr applies the named changes to a file. Changes that the
// backend cannot apply make the whole call fail.
Setattr(h Handle, s SetAttrs) error
// Link makes newName in dir a hard link to the target file.
Link(target Handle, dir Handle, name string) (Handle, Info, error)
// Sync flushes the file's dirty data to stable storage.
Sync(h Handle) error
// Open opens the regular file name under dir for writing. When create
// is set a missing file is made with perm and carried by owner; when
// guarded is set an existing name answers ErrExist instead of
// opening, which the GUARDED and EXCLUSIVE4_1 create modes require;
// when truncate is set an existing file is cut to zero first. The
// boolean reports whether the file was created by this call.
Open(dir Handle, name string, create, guarded, truncate bool, perm fs.FileMode, owner Owner) (h Handle, info Info, created bool, err error)
}
// A TimeSet is one time attribute of a SETATTR: either the server's
// current time or the time the client names.
type TimeSet struct {
Now bool
Time time.Time
}
// SetAttrs carries the changes a SETATTR names. A nil field is a change
// the client did not ask for.
type SetAttrs struct {
Mode *uint32
Size *int64
UID *uint32
GID *uint32
Atime *TimeSet
Mtime *TimeSet
}
// XattrFS is the optional extended attribute half of a backend. A backend
// that does not implement it answers NOT_SUPP to the xattr family.
type XattrFS interface {
// GetXattr reads one named attribute of the object.
GetXattr(h Handle, name string, max int) ([]byte, error)
// SetXattr writes one named attribute; the mode follows the
// SETXATTR4mode4 enum of RFC 8276.
SetXattr(h Handle, name string, value []byte, mode uint32) error
// ListXattr names the attributes of the object.
ListXattr(h Handle, max int) ([]string, error)
// RemoveXattr deletes one named attribute.
RemoveXattr(h Handle, name string) error
}
// ErrNoXattr marks a missing attribute, NFS4ERR_NOXATTR on the wire.
var ErrNoXattr = errors.New("nfsfs: no such attribute")
// ErrXattrNotSupp marks a backend that carries no extended attributes at
// all, NFS4ERR_NOT_SUPP on the wire.
var ErrXattrNotSupp = errors.New("nfsfs: extended attributes are not supported")
// ErrBeyondEOF marks a SEEK that starts past the end of the file,
// NFS4ERR_NXIO on the wire per RFC 7862 section 15.11.
var ErrBeyondEOF = errors.New("nfsfs: seek past the end")
// ErrNoSparse marks a backend whose platform carries no space
// reservation or hole seeking calls, NFS4ERR_NOT_SUPP on the wire.
var ErrNoSparse = errors.New("nfsfs: sparse file operations are not supported")
// The SETXATTR create modes of RFC 8276, mirrored from the wire
// protocol. They are platform independent: a backend that carries no
// extended attributes answers NOT_SUPP regardless of the mode.
const (
XattrModeCreate = 1
XattrModeReplace = 2
)
// A RangeCloner is the optional half that clones or copies a byte range
// between two regular files inside the kernel: the bytes never travel
// through userspace. A backend that does not carry it, or a filesystem
// that refuses the call for one pair of files, leaves the caller its
// userspace path.
type RangeCloner interface {
// CloneRange makes dstOff carry the same bytes as srcOff through a
// reflink where the filesystem provides one.
CloneRange(src Handle, srcOff int64, dst Handle, dstOff int64, length int64) error
// CopyRange copies length bytes through the kernel's copy syscall.
CopyRange(src Handle, srcOff int64, dst Handle, dstOff int64, length int64) error
}