| // Copyright 2018 The CUE Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package load |
| |
| import ( |
| "bytes" |
| "go/ast" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "sort" |
| "strings" |
| "time" |
| |
| "cuelang.org/go/cue/errors" |
| "cuelang.org/go/cue/token" |
| ) |
| |
| type overlayFile struct { |
| basename string |
| contents []byte |
| file *ast.File |
| modtime time.Time |
| isDir bool |
| } |
| |
| func (f *overlayFile) Name() string { return f.basename } |
| func (f *overlayFile) Size() int64 { return int64(len(f.contents)) } |
| func (f *overlayFile) Mode() os.FileMode { return 0644 } |
| func (f *overlayFile) ModTime() time.Time { return f.modtime } |
| func (f *overlayFile) IsDir() bool { return f.isDir } |
| func (f *overlayFile) Sys() interface{} { return nil } |
| |
| // A fileSystem specifies the supporting context for a build. |
| type fileSystem struct { |
| overlayDirs map[string]map[string]*overlayFile |
| cwd string |
| } |
| |
| func (fs *fileSystem) getDir(dir string, create bool) map[string]*overlayFile { |
| dir = filepath.Clean(dir) |
| m, ok := fs.overlayDirs[dir] |
| if !ok { |
| m = map[string]*overlayFile{} |
| fs.overlayDirs[dir] = m |
| } |
| return m |
| } |
| |
| func (fs *fileSystem) init(c *Config) error { |
| fs.cwd = c.Dir |
| |
| overlay := c.Overlay |
| fs.overlayDirs = map[string]map[string]*overlayFile{} |
| |
| // Organize overlay |
| for filename, src := range overlay { |
| if !strings.HasSuffix(filename, ".cue") { |
| return errors.Newf(token.NoPos, "overlay file %s not a .cue file", filename) |
| } |
| |
| // TODO: do we need to further clean the path or check that the |
| // specified files are within the root/ absolute files? |
| dir, base := filepath.Split(filename) |
| m := fs.getDir(dir, true) |
| |
| b, err := src.contents() |
| if err != nil { |
| return err |
| } |
| m[base] = &overlayFile{ |
| basename: base, |
| contents: b, |
| modtime: time.Now(), |
| } |
| |
| for { |
| prevdir := dir |
| dir, base = filepath.Split(filepath.Dir(dir)) |
| if dir == prevdir || dir == "" { |
| break |
| } |
| m := fs.getDir(dir, true) |
| if m[base] == nil { |
| m[base] = &overlayFile{ |
| basename: base, |
| modtime: time.Now(), |
| isDir: true, |
| } |
| } |
| } |
| } |
| return nil |
| } |
| |
| func (fs *fileSystem) joinPath(elem ...string) string { |
| return filepath.Join(elem...) |
| } |
| |
| func (fs *fileSystem) splitPathList(s string) []string { |
| return filepath.SplitList(s) |
| } |
| |
| func (fs *fileSystem) isAbsPath(path string) bool { |
| return filepath.IsAbs(path) |
| } |
| |
| func (fs *fileSystem) makeAbs(path string) string { |
| if fs.isAbsPath(path) { |
| return path |
| } |
| return filepath.Clean(filepath.Join(fs.cwd, path)) |
| } |
| |
| func (fs *fileSystem) isDir(path string) bool { |
| path = fs.makeAbs(path) |
| if fs.getDir(path, false) != nil { |
| return true |
| } |
| fi, err := os.Stat(path) |
| return err == nil && fi.IsDir() |
| } |
| |
| func (fs *fileSystem) hasSubdir(root, dir string) (rel string, ok bool) { |
| // Try using paths we received. |
| if rel, ok = hasSubdir(root, dir); ok { |
| return |
| } |
| |
| // Try expanding symlinks and comparing |
| // expanded against unexpanded and |
| // expanded against expanded. |
| rootSym, _ := filepath.EvalSymlinks(root) |
| dirSym, _ := filepath.EvalSymlinks(dir) |
| |
| if rel, ok = hasSubdir(rootSym, dir); ok { |
| return |
| } |
| if rel, ok = hasSubdir(root, dirSym); ok { |
| return |
| } |
| return hasSubdir(rootSym, dirSym) |
| } |
| |
| func hasSubdir(root, dir string) (rel string, ok bool) { |
| const sep = string(filepath.Separator) |
| root = filepath.Clean(root) |
| if !strings.HasSuffix(root, sep) { |
| root += sep |
| } |
| dir = filepath.Clean(dir) |
| if !strings.HasPrefix(dir, root) { |
| return "", false |
| } |
| return filepath.ToSlash(dir[len(root):]), true |
| } |
| |
| func (fs *fileSystem) readDir(path string) ([]os.FileInfo, errors.Error) { |
| path = fs.makeAbs(path) |
| m := fs.getDir(path, false) |
| items, err := ioutil.ReadDir(path) |
| if err != nil { |
| if !os.IsNotExist(err) || m == nil { |
| return nil, errors.Wrapf(err, token.NoPos, "readDir") |
| } |
| } |
| if m != nil { |
| done := map[string]bool{} |
| for i, fi := range items { |
| done[fi.Name()] = true |
| if o := m[fi.Name()]; o != nil { |
| items[i] = o |
| } |
| } |
| for _, o := range m { |
| if !done[o.Name()] { |
| items = append(items, o) |
| } |
| } |
| sort.Slice(items, func(i, j int) bool { |
| return items[i].Name() < items[j].Name() |
| }) |
| } |
| return items, nil |
| } |
| |
| func (fs *fileSystem) getOverlay(path string) *overlayFile { |
| dir, base := filepath.Split(path) |
| if m := fs.getDir(dir, false); m != nil { |
| return m[base] |
| } |
| return nil |
| } |
| |
| func (fs *fileSystem) stat(path string) (os.FileInfo, errors.Error) { |
| path = fs.makeAbs(path) |
| if fi := fs.getOverlay(path); fi != nil { |
| return fi, nil |
| } |
| fi, err := os.Stat(path) |
| if err != nil { |
| return nil, errors.Wrapf(err, token.NoPos, "stat") |
| } |
| return fi, nil |
| } |
| |
| func (fs *fileSystem) lstat(path string) (os.FileInfo, errors.Error) { |
| path = fs.makeAbs(path) |
| if fi := fs.getOverlay(path); fi != nil { |
| return fi, nil |
| } |
| fi, err := os.Lstat(path) |
| if err != nil { |
| return nil, errors.Wrapf(err, token.NoPos, "stat") |
| } |
| return fi, nil |
| } |
| |
| func (fs *fileSystem) openFile(path string) (io.ReadCloser, errors.Error) { |
| path = fs.makeAbs(path) |
| if fi := fs.getOverlay(path); fi != nil { |
| return ioutil.NopCloser(bytes.NewReader(fi.contents)), nil |
| } |
| |
| f, err := os.Open(path) |
| if err != nil { |
| return nil, errors.Wrapf(err, token.NoPos, "load") |
| } |
| return f, nil |
| } |
| |
| var skipDir = errors.Newf(token.NoPos, "skip directory") |
| |
| type walkFunc func(path string, info os.FileInfo, err errors.Error) errors.Error |
| |
| func (fs *fileSystem) walk(root string, f walkFunc) error { |
| fi, err := fs.lstat(root) |
| if err != nil { |
| err = f(root, fi, err) |
| } else if !fi.IsDir() { |
| return errors.Newf(token.NoPos, "path %q is not a directory", root) |
| } else { |
| err = fs.walkRec(root, fi, f) |
| } |
| if err == skipDir { |
| return nil |
| } |
| return err |
| |
| } |
| |
| func (fs *fileSystem) walkRec(path string, info os.FileInfo, f walkFunc) errors.Error { |
| if !info.IsDir() { |
| return f(path, info, nil) |
| } |
| |
| dir, err := fs.readDir(path) |
| err1 := f(path, info, err) |
| |
| // If err != nil, walk can't walk into this directory. |
| // err1 != nil means walkFn want walk to skip this directory or stop walking. |
| // Therefore, if one of err and err1 isn't nil, walk will return. |
| if err != nil || err1 != nil { |
| // The caller's behavior is controlled by the return value, which is decided |
| // by walkFn. walkFn may ignore err and return nil. |
| // If walkFn returns SkipDir, it will be handled by the caller. |
| // So walk should return whatever walkFn returns. |
| return err1 |
| } |
| |
| for _, info := range dir { |
| filename := fs.joinPath(path, info.Name()) |
| err = fs.walkRec(filename, info, f) |
| if err != nil { |
| if !info.IsDir() || err != skipDir { |
| return err |
| } |
| } |
| } |
| return nil |
| } |