264 lines
9.6 KiB
Go
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
|
||
|
|
}
|