// Copyright (c) 2026 Petr BalvĂ­n (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 }