fspl/compiler/resolver.go

114 lines
3.4 KiB
Go

package compiler
import "os"
import "fmt"
import "io/fs"
import "errors"
import "strings"
import "path/filepath"
import "git.tebibyte.media/fspl/fspl/entity"
// Resolver turns addresses into absolute filepaths.
type Resolver struct {
// FS specifies a filesystem to search.
FS fs.FS
// Path specifies a list of paths that a unit may exist directly in. The
// Resolver will search the FS for each path listed, starting at the
// first and ending at the last. Thus, paths nearer the start will have
// a higher priority.
Path []string
}
// NewResolver creates a new resolver with OsFS.
func NewResolver (path ...string) *Resolver {
return &Resolver {
FS: OsFS { },
Path: path,
}
}
// AddPath adds one or more items to the resolver's search path.
func (resolver *Resolver) AddPath (path ...string) {
resolver.Path = append(resolver.Path, path...)
}
// AddPathFront adds one or more items to the beginning of the resolver's search
// path.
func (resolver *Resolver) AddPathFront (path ...string) {
// i know how memory works in go babyyyyyy
newPath := make([]string, len(path))
copy(newPath, path)
resolver.Path = append(newPath, resolver.Path...)
}
// Resolve resolves an address into an absolute filepath starting at the
// filesystem root. It follows these rules:
// - If the address starts with '.', '..', it is joined with context
// - If the address starts with '/', it is treated as an absolute path from
// the fs root
// - Else, the address is searched for in the resolver's paths
func (resolver *Resolver) Resolve (context string, address entity.Address) (string, error) {
strAddr := string(address)
switch {
case strings.HasPrefix(strAddr, "."):
return filepath.Join(context, strAddr), nil
case strings.HasPrefix(strAddr, "/"):
return strAddr, nil
default:
if resolver.Path == nil || resolver.FS == nil {
return "", errors.New(fmt.Sprintf (
"could not find unit %v: %v",
address, "no search path specified"))
}
location, err := resolver.search(strAddr)
if err != nil {
return "", errors.New(fmt.Sprintf (
"could not find unit %v: %v",
address, err))
}
return location, nil
}
}
func (resolver *Resolver) search (needle string) (string, error) {
for _, dirPath := range resolver.Path {
// attempt to open the file as dir.
// if we can't open the dir, just skip it, because it is
// perfectly reasonable that the user might not have
// /usr/local/include/fspl etc.
file, err := openAbsolute(resolver.FS, dirPath)
if err != nil { continue }
dir, ok := file.(fs.ReadDirFile)
if !ok { continue }
entries, err := dir.ReadDir(0)
if err != nil { continue }
dir.Close()
// search through the entries
for _, entry := range entries {
if entry.Name() == needle {
return filepath.Join(dirPath, entry.Name()), nil
}
}
}
return "", fs.ErrNotExist
}
// ResolveCwd resolves the address within the context of the current working
// directory.
func (resolver *Resolver) ResolveCwd (address entity.Address) (string, error) {
wd, err := os.Getwd()
if err != nil { return "", err }
return resolver.Resolve(wd, address)
}
// openAbsolute exists because fs.FS implementations do not understand absolute
// paths, which the FSPL compiler runs on. It converts an absolute path to a
// slash path relative to "/" and opens the file.
func openAbsolute (filesystem fs.FS, path string) (fs.File, error) {
path = strings.TrimPrefix(filepath.ToSlash(path), "/")
return filesystem.Open(path)
}