// Package safeio reads installation files without following symlinked path components. package safeio import ( "errors" "io" "io/fs" "os" "path/filepath" "strings" "unicode/utf8" ) var ErrUnsafeFile = errors.New("unsafe file") // ErrIndeterminateFile means publication cleanup could not establish whether a // private candidate is still named. Callers must reconcile the destination and // private stages before retrying; it is never a blind-retry-safe failure. var ErrIndeterminateFile = errors.New("indeterminate file state") // ValidateCanonicalPath rejects relative or lexically non-canonical paths before they are opened. func ValidateCanonicalPath(path string) error { if !filepath.IsAbs(path) || filepath.Clean(path) != path || strings.Contains(path, string(filepath.Separator)+".."+string(filepath.Separator)) { return ErrUnsafeFile } if err := validatePlatformPathSyntax(path); err != nil { return err } return nil } func readBoundedRegularFile(file *os.File, maximum int64, before func()) ([]byte, error) { if maximum < 0 || maximum == int64(^uint64(0)>>1) { return nil, ErrUnsafeFile } info, err := file.Stat() if err != nil || !info.Mode().IsRegular() { return nil, ErrUnsafeFile } if before != nil { before() } contents, err := io.ReadAll(io.LimitReader(file, maximum+1)) if err != nil || int64(len(contents)) > maximum { return nil, ErrUnsafeFile } return contents, nil } // ReadCanonicalUTF8 reads a canonical regular file with a strict byte bound and UTF-8 validation. func ReadCanonicalUTF8(path string, maximum int64) ([]byte, error) { contents, err := ReadCanonicalRegular(path, maximum) if err != nil || !utf8.Valid(contents) { return nil, ErrUnsafeFile } return contents, nil } // WriteCanonicalExclusive creates a canonical regular file without following links or replacing // an existing leaf. The file is private to the caller and is never opened in truncate mode. func WriteCanonicalExclusive(path string, contents []byte, mode fs.FileMode) error { return writeCanonicalExclusive(path, contents, mode) } // ValidateCanonicalOutputPath verifies every parent directory without creating the leaf. func ValidateCanonicalOutputPath(path string) error { return validateCanonicalOutputPath(path) }