package localfile
import (
"crypto/rand"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sync"
)
var ErrDenied = errors .New ("localfile: access denied" )
type policyMode uint8
const (
modeDenied policyMode = iota
modeRooted
modeUnrestricted
)
type Policy struct {
mode policyMode
rootPath string
root *rootState
cacheScope [32 ]byte
}
type rootState struct {
handle *os .Root
once sync .Once
err error
}
func Rooted (root string ) (Policy , error ) {
absolute , err := filepath .Abs (root )
if err != nil {
return Policy {}, fmt .Errorf ("localfile: resolve root %q: %w" , root , err )
}
absolute = filepath .Clean (absolute )
handle , err := os .OpenRoot (absolute )
if err != nil {
return Policy {}, fmt .Errorf ("localfile: open root %q: %w" , root , err )
}
var cacheScope [32 ]byte
if _ , err := rand .Read (cacheScope [:]); err != nil {
return Policy {}, errors .Join (
fmt .Errorf ("localfile: create cache scope for root %q: %w" , root , err ),
closeRoot (handle , root ),
)
}
return Policy {mode : modeRooted , rootPath : absolute , root : &rootState {handle : handle }, cacheScope : cacheScope }, nil
}
func Unrestricted () Policy {
return Policy {mode : modeUnrestricted }
}
func (p Policy ) RootPath () (string , bool ) {
return p .rootPath , p .mode == modeRooted
}
func (p Policy ) Close () error {
if p .mode != modeRooted || p .root == nil {
return nil
}
p .root .once .Do (func () {
p .root .err = closeRoot (p .root .handle , p .rootPath )
})
return p .root .err
}
func (p Policy ) CacheKey (name string ) (string , error ) {
resolved , err := p .resolve (name )
if err != nil {
return "" , err
}
switch p .mode {
case modeRooted :
return fmt .Sprintf ("root:%x:%s" , p .cacheScope , filepath .ToSlash (resolved .relative )), nil
case modeUnrestricted :
return "unrestricted:" + filepath .ToSlash (resolved .absolute ), nil
default :
panic ("localfile: resolved path under invalid policy mode" )
}
}
func (p Policy ) Open (name string ) (fs .File , error ) {
resolved , err := p .resolve (name )
if err != nil {
return nil , err
}
if p .mode == modeUnrestricted {
file , err := openFile (resolved .absolute )
if err != nil {
return nil , err
}
return requireRegular (name , file )
}
if p .root == nil || p .root .handle == nil {
return nil , errors .New ("localfile: invalid rooted policy" )
}
file , err := openFileInRoot (p .root .handle , resolved .relative )
if err != nil {
return nil , fmt .Errorf ("localfile: open %q beneath root %q: %w" , name , p .rootPath , err )
}
return requireRegular (name , file )
}
func requireRegular(name string , file *os .File ) (fs .File , error ) {
info , err := file .Stat ()
if err != nil {
return nil , errors .Join (
fmt .Errorf ("localfile: stat opened file %q: %w" , name , err ),
closeFile (file , name ),
)
}
if !info .Mode ().IsRegular () {
return nil , errors .Join (
fmt .Errorf ("localfile: %q is not a regular file" , name ),
closeFile (file , name ),
)
}
return file , nil
}
func closeFile(file *os .File , name string ) error {
if err := file .Close (); err != nil {
return fmt .Errorf ("localfile: close %q: %w" , name , err )
}
return nil
}
func closeRoot(root *os .Root , name string ) error {
if err := root .Close (); err != nil {
return fmt .Errorf ("localfile: close root %q: %w" , name , err )
}
return nil
}
type resolvedPath struct {
absolute string
relative string
}
func (p Policy ) resolve (name string ) (resolvedPath , error ) {
switch p .mode {
case modeDenied :
return resolvedPath {}, fmt .Errorf ("%w: local-file access is disabled" , ErrDenied )
case modeUnrestricted :
absolute , err := filepath .Abs (name )
if err != nil {
return resolvedPath {}, fmt .Errorf ("localfile: resolve %q: %w" , name , err )
}
return resolvedPath {absolute : filepath .Clean (absolute )}, nil
case modeRooted :
var relative string
if filepath .IsAbs (name ) {
var err error
relative , err = filepath .Rel (p .rootPath , filepath .Clean (name ))
if err != nil {
return resolvedPath {}, fmt .Errorf ("localfile: resolve %q beneath root %q: %w" , name , p .rootPath , err )
}
} else {
relative = filepath .Clean (name )
}
if !filepath .IsLocal (relative ) {
return resolvedPath {}, fmt .Errorf ("%w: %q is outside configured root %q" , ErrDenied , name , p .rootPath )
}
return resolvedPath {
absolute : filepath .Join (p .rootPath , relative ),
relative : relative ,
}, nil
default :
return resolvedPath {}, errors .New ("localfile: invalid policy" )
}
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .