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 os.DirFS("/"). func NewResolver (path ...string) Resolver { return Resolver { FS: os.DirFS("/"), 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 := resolver.FS.Open(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 && !entry.IsDir() { 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) }