zrepl/internal/zfs/namecheck.go
2024-10-18 19:21:17 +02:00

195 lines
4.7 KiB
Go

package zfs
import (
"bytes"
"fmt"
"regexp"
"strings"
"unicode"
)
const MaxDatasetNameLen = 256 - 1
type EntityType string
const (
EntityTypeFilesystem EntityType = "filesystem"
EntityTypeVolume EntityType = "volume"
EntityTypeSnapshot EntityType = "snapshot"
EntityTypeBookmark EntityType = "bookmark"
)
func (e EntityType) Validate() error {
switch e {
case EntityTypeFilesystem:
return nil
case EntityTypeVolume:
return nil
case EntityTypeSnapshot:
return nil
case EntityTypeBookmark:
return nil
default:
return fmt.Errorf("invalid entity type %q", string(e))
}
}
func (e EntityType) MustValidate() {
if err := e.Validate(); err != nil {
panic(err)
}
}
func (e EntityType) String() string {
e.MustValidate()
return string(e)
}
var componentValidChar = regexp.MustCompile(`^[0-9a-zA-Z-_\.: ]+$`)
// From module/zcommon/zfs_namecheck.c
//
// Snapshot names must be made up of alphanumeric characters plus the following
// characters:
//
// [-_.: ]
func ComponentNamecheck(datasetPathComponent string) error {
if len(datasetPathComponent) == 0 {
return fmt.Errorf("must not be empty")
}
if len(datasetPathComponent) > MaxDatasetNameLen {
return fmt.Errorf("must not be longer than %d chars", MaxDatasetNameLen)
}
if !(isASCII(datasetPathComponent)) {
return fmt.Errorf("must be ASCII")
}
if !componentValidChar.MatchString(datasetPathComponent) {
return fmt.Errorf("must only contain alphanumeric chars and any in %q", "-_.: ")
}
if datasetPathComponent == "." || datasetPathComponent == ".." {
return fmt.Errorf("must not be '%s'", datasetPathComponent)
}
return nil
}
type PathValidationError struct {
path string
entityType EntityType
msg string
}
func (e *PathValidationError) Path() string { return e.path }
func (e *PathValidationError) Error() string {
return fmt.Sprintf("invalid %s %q: %s", e.entityType, e.path, e.msg)
}
// combines
//
// lib/libzfs/libzfs_dataset.c: zfs_validate_name
// module/zcommon/zfs_namecheck.c: entity_namecheck
//
// The '%' character is not allowed because it's reserved for zfs-internal use
func EntityNamecheck(path string, t EntityType) (err *PathValidationError) {
pve := func(msg string) *PathValidationError {
return &PathValidationError{path: path, entityType: t, msg: msg}
}
t.MustValidate()
// delimiter checks
if t != EntityTypeSnapshot && strings.Contains(path, "@") {
return pve("snapshot delimiter '@' is not expected here")
}
if t == EntityTypeSnapshot && !strings.Contains(path, "@") {
return pve("missing '@' delimiter in snapshot name")
}
if t != EntityTypeBookmark && strings.Contains(path, "#") {
return pve("bookmark delimiter '#' is not expected here")
}
if t == EntityTypeBookmark && !strings.Contains(path, "#") {
return pve("missing '#' delimiter in bookmark name")
}
// EntityTypeVolume and EntityTypeFilesystem are already covered above
if strings.Contains(path, "%") {
return pve("invalid character '%' in name")
}
// mimic module/zcommon/zfs_namecheck.c: entity_namecheck
if len(path) > MaxDatasetNameLen {
return pve("name too long")
}
if len(path) == 0 {
return pve("must not be empty")
}
if !isASCII(path) {
return pve("must be ASCII")
}
slashComps := bytes.Split([]byte(path), []byte("/"))
bookmarkOrSnapshotDelims := 0
for compI, comp := range slashComps {
snapCount := bytes.Count(comp, []byte("@"))
bookCount := bytes.Count(comp, []byte("#"))
if !(snapCount*bookCount == 0) {
panic("implementation error: delimiter checks before this loop must ensure this cannot happen")
}
bookmarkOrSnapshotDelims += snapCount + bookCount
if bookmarkOrSnapshotDelims > 1 {
return pve("multiple delimiters '@' or '#' are not allowed")
}
if bookmarkOrSnapshotDelims == 1 && compI != len(slashComps)-1 {
return pve("snapshot or bookmark must not contain '/'")
}
if bookmarkOrSnapshotDelims == 0 {
// hot path, all but last component
component := string(comp)
if err := ComponentNamecheck(component); err != nil {
return pve(fmt.Sprintf("component %q: %s", component, err.Error()))
}
continue
}
subComps := bytes.FieldsFunc(comp, func(r rune) bool {
return r == '#' || r == '@'
})
if len(subComps) > 2 {
panic("implementation error: delimiter checks above should ensure a single bookmark or snapshot delimiter per component")
}
if len(subComps) != 2 {
return pve("empty component, bookmark or snapshot name not allowed")
}
for _, comp := range subComps {
component := string(comp)
if err := ComponentNamecheck(component); err != nil {
return pve(fmt.Sprintf("component %q: %s", component, err.Error()))
}
}
}
return nil
}
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}