cuelang.org/go@v0.10.1/cue/load/fs.go (about)

     1  // Copyright 2018 The CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package load
    16  
    17  import (
    18  	"bytes"
    19  	"cmp"
    20  	"fmt"
    21  	"io"
    22  	iofs "io/fs"
    23  	"os"
    24  	"path/filepath"
    25  	"slices"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	"cuelang.org/go/cue"
    31  	"cuelang.org/go/cue/ast"
    32  	"cuelang.org/go/cue/build"
    33  	"cuelang.org/go/cue/cuecontext"
    34  	"cuelang.org/go/cue/errors"
    35  	"cuelang.org/go/cue/token"
    36  	"cuelang.org/go/internal/encoding"
    37  	"cuelang.org/go/mod/module"
    38  )
    39  
    40  type overlayFile struct {
    41  	basename string
    42  	contents []byte
    43  	file     *ast.File
    44  	modtime  time.Time
    45  	isDir    bool
    46  }
    47  
    48  func (f *overlayFile) Name() string { return f.basename }
    49  func (f *overlayFile) Size() int64  { return int64(len(f.contents)) }
    50  func (f *overlayFile) Mode() iofs.FileMode {
    51  	if f.isDir {
    52  		return iofs.ModeDir | 0o555
    53  	}
    54  	return 0o444
    55  }
    56  func (f *overlayFile) ModTime() time.Time { return f.modtime }
    57  func (f *overlayFile) IsDir() bool        { return f.isDir }
    58  func (f *overlayFile) Sys() interface{}   { return nil }
    59  
    60  // A fileSystem specifies the supporting context for a build.
    61  type fileSystem struct {
    62  	overlayDirs map[string]map[string]*overlayFile
    63  	cwd         string
    64  	fileCache   *fileCache
    65  }
    66  
    67  func (fs *fileSystem) getDir(dir string, create bool) map[string]*overlayFile {
    68  	dir = filepath.Clean(dir)
    69  	m, ok := fs.overlayDirs[dir]
    70  	if !ok && create {
    71  		m = map[string]*overlayFile{}
    72  		fs.overlayDirs[dir] = m
    73  	}
    74  	return m
    75  }
    76  
    77  // ioFS returns an implementation of [io/fs.FS] that holds
    78  // the contents of fs under the given filepath root.
    79  //
    80  // Note: we can't return an FS implementation that covers the
    81  // entirety of fs because the overlay paths may not all share
    82  // a common root.
    83  //
    84  // Note also: the returned FS also implements
    85  // [modpkgload.OSRootFS] so that we can map
    86  // the resulting source locations back to the filesystem
    87  // paths required by most of the `cue/load` package
    88  // implementation.
    89  func (fs *fileSystem) ioFS(root string) iofs.FS {
    90  	return &ioFS{
    91  		fs:   fs,
    92  		root: root,
    93  	}
    94  }
    95  
    96  func newFileSystem(cfg *Config) (*fileSystem, error) {
    97  	fs := &fileSystem{
    98  		cwd:         cfg.Dir,
    99  		overlayDirs: map[string]map[string]*overlayFile{},
   100  	}
   101  
   102  	// Organize overlay
   103  	for filename, src := range cfg.Overlay {
   104  		if !filepath.IsAbs(filename) {
   105  			return nil, fmt.Errorf("non-absolute file path %q in overlay", filename)
   106  		}
   107  		// TODO: do we need to further clean the path or check that the
   108  		// specified files are within the root/ absolute files?
   109  		dir, base := filepath.Split(filename)
   110  		m := fs.getDir(dir, true)
   111  		b, file, err := src.contents()
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  		m[base] = &overlayFile{
   116  			basename: base,
   117  			contents: b,
   118  			file:     file,
   119  			modtime:  time.Now(),
   120  		}
   121  
   122  		for {
   123  			prevdir := dir
   124  			dir, base = filepath.Split(filepath.Dir(dir))
   125  			if dir == prevdir || dir == "" {
   126  				break
   127  			}
   128  			m := fs.getDir(dir, true)
   129  			if m[base] == nil {
   130  				m[base] = &overlayFile{
   131  					basename: base,
   132  					modtime:  time.Now(),
   133  					isDir:    true,
   134  				}
   135  			}
   136  		}
   137  	}
   138  	fs.fileCache = newFileCache(cfg)
   139  	return fs, nil
   140  }
   141  
   142  func (fs *fileSystem) makeAbs(path string) string {
   143  	if filepath.IsAbs(path) {
   144  		return path
   145  	}
   146  	return filepath.Join(fs.cwd, path)
   147  }
   148  
   149  func (fs *fileSystem) readDir(path string) ([]iofs.DirEntry, errors.Error) {
   150  	path = fs.makeAbs(path)
   151  	m := fs.getDir(path, false)
   152  	items, err := os.ReadDir(path)
   153  	if err != nil {
   154  		if !os.IsNotExist(err) || m == nil {
   155  			return nil, errors.Wrapf(err, token.NoPos, "readDir")
   156  		}
   157  	}
   158  	if m == nil {
   159  		return items, nil
   160  	}
   161  	done := map[string]bool{}
   162  	for i, fi := range items {
   163  		done[fi.Name()] = true
   164  		if o := m[fi.Name()]; o != nil {
   165  			items[i] = iofs.FileInfoToDirEntry(o)
   166  		}
   167  	}
   168  	for _, o := range m {
   169  		if !done[o.Name()] {
   170  			items = append(items, iofs.FileInfoToDirEntry(o))
   171  		}
   172  	}
   173  	slices.SortFunc(items, func(a, b iofs.DirEntry) int {
   174  		return cmp.Compare(a.Name(), b.Name())
   175  	})
   176  	return items, nil
   177  }
   178  
   179  func (fs *fileSystem) getOverlay(path string) *overlayFile {
   180  	dir, base := filepath.Split(path)
   181  	if m := fs.getDir(dir, false); m != nil {
   182  		return m[base]
   183  	}
   184  	return nil
   185  }
   186  
   187  func (fs *fileSystem) stat(path string) (iofs.FileInfo, errors.Error) {
   188  	path = fs.makeAbs(path)
   189  	if fi := fs.getOverlay(path); fi != nil {
   190  		return fi, nil
   191  	}
   192  	fi, err := os.Stat(path)
   193  	if err != nil {
   194  		return nil, errors.Wrapf(err, token.NoPos, "stat")
   195  	}
   196  	return fi, nil
   197  }
   198  
   199  func (fs *fileSystem) lstat(path string) (iofs.FileInfo, errors.Error) {
   200  	path = fs.makeAbs(path)
   201  	if fi := fs.getOverlay(path); fi != nil {
   202  		return fi, nil
   203  	}
   204  	fi, err := os.Lstat(path)
   205  	if err != nil {
   206  		return nil, errors.Wrapf(err, token.NoPos, "stat")
   207  	}
   208  	return fi, nil
   209  }
   210  
   211  func (fs *fileSystem) openFile(path string) (io.ReadCloser, errors.Error) {
   212  	path = fs.makeAbs(path)
   213  	if fi := fs.getOverlay(path); fi != nil {
   214  		return io.NopCloser(bytes.NewReader(fi.contents)), nil
   215  	}
   216  
   217  	f, err := os.Open(path)
   218  	if err != nil {
   219  		return nil, errors.Wrapf(err, token.NoPos, "load")
   220  	}
   221  	return f, nil
   222  }
   223  
   224  var skipDir = errors.Newf(token.NoPos, "skip directory")
   225  
   226  type walkFunc func(path string, entry iofs.DirEntry, err errors.Error) errors.Error
   227  
   228  func (fs *fileSystem) walk(root string, f walkFunc) error {
   229  	info, err := fs.lstat(root)
   230  	entry := iofs.FileInfoToDirEntry(info)
   231  	if err != nil {
   232  		err = f(root, entry, err)
   233  	} else if !info.IsDir() {
   234  		return errors.Newf(token.NoPos, "path %q is not a directory", root)
   235  	} else {
   236  		err = fs.walkRec(root, entry, f)
   237  	}
   238  	if err == skipDir {
   239  		return nil
   240  	}
   241  	return err
   242  
   243  }
   244  
   245  func (fs *fileSystem) walkRec(path string, entry iofs.DirEntry, f walkFunc) errors.Error {
   246  	if !entry.IsDir() {
   247  		return f(path, entry, nil)
   248  	}
   249  
   250  	dir, err := fs.readDir(path)
   251  	err1 := f(path, entry, err)
   252  
   253  	// If err != nil, walk can't walk into this directory.
   254  	// err1 != nil means walkFn want walk to skip this directory or stop walking.
   255  	// Therefore, if one of err and err1 isn't nil, walk will return.
   256  	if err != nil || err1 != nil {
   257  		// The caller's behavior is controlled by the return value, which is decided
   258  		// by walkFn. walkFn may ignore err and return nil.
   259  		// If walkFn returns SkipDir, it will be handled by the caller.
   260  		// So walk should return whatever walkFn returns.
   261  		return err1
   262  	}
   263  
   264  	for _, entry := range dir {
   265  		filename := filepath.Join(path, entry.Name())
   266  		err = fs.walkRec(filename, entry, f)
   267  		if err != nil {
   268  			if !entry.IsDir() || err != skipDir {
   269  				return err
   270  			}
   271  		}
   272  	}
   273  	return nil
   274  }
   275  
   276  var _ interface {
   277  	iofs.FS
   278  	iofs.ReadDirFS
   279  	iofs.ReadFileFS
   280  	module.OSRootFS
   281  } = (*ioFS)(nil)
   282  
   283  type ioFS struct {
   284  	fs   *fileSystem
   285  	root string
   286  }
   287  
   288  func (fs *ioFS) OSRoot() string {
   289  	return fs.root
   290  }
   291  
   292  func (fs *ioFS) Open(name string) (iofs.File, error) {
   293  	fpath, err := fs.absPathFromFSPath(name)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  	r, err := fs.fs.openFile(fpath)
   298  	if err != nil {
   299  		return nil, err // TODO convert filepath in error to fs path
   300  	}
   301  	return &ioFSFile{
   302  		fs:   fs.fs,
   303  		path: fpath,
   304  		rc:   r,
   305  	}, nil
   306  }
   307  
   308  func (fs *ioFS) absPathFromFSPath(name string) (string, error) {
   309  	if !iofs.ValidPath(name) {
   310  		return "", fmt.Errorf("invalid io/fs path %q", name)
   311  	}
   312  	// Technically we should mimic Go's internal/safefilepath.fromFS
   313  	// functionality here, but as we're using this in a relatively limited
   314  	// context, we can just prohibit some characters.
   315  	if strings.ContainsAny(name, ":\\") {
   316  		return "", fmt.Errorf("invalid io/fs path %q", name)
   317  	}
   318  	return filepath.Join(fs.root, name), nil
   319  }
   320  
   321  // ReadDir implements [io/fs.ReadDirFS].
   322  func (fs *ioFS) ReadDir(name string) ([]iofs.DirEntry, error) {
   323  	fpath, err := fs.absPathFromFSPath(name)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	return fs.fs.readDir(fpath)
   328  }
   329  
   330  // ReadFile implements [io/fs.ReadFileFS].
   331  func (fs *ioFS) ReadFile(name string) ([]byte, error) {
   332  	fpath, err := fs.absPathFromFSPath(name)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  	if fi := fs.fs.getOverlay(fpath); fi != nil {
   337  		return bytes.Clone(fi.contents), nil
   338  	}
   339  	return os.ReadFile(fpath)
   340  }
   341  
   342  var _ module.ReadCUEFS = (*ioFS)(nil)
   343  
   344  // ReadCUEFile implements [module.ReadCUEFS] by
   345  // reading and updating the syntax file cache, which
   346  // is shared with the cache used by the [fileSystem.getCUESyntax]
   347  // method.
   348  func (fs *ioFS) ReadCUEFile(path string) (*ast.File, error) {
   349  	fpath, err := fs.absPathFromFSPath(path)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	cache := fs.fs.fileCache
   354  	cache.mu.Lock()
   355  	entry, ok := cache.entries[fpath]
   356  	cache.mu.Unlock()
   357  	if ok {
   358  		return entry.file, entry.err
   359  	}
   360  	var data []byte
   361  	if fi := fs.fs.getOverlay(fpath); fi != nil {
   362  		if fi.file != nil {
   363  			// No need for a cache if we've got the contents in *ast.File
   364  			// form already.
   365  			return fi.file, nil
   366  		}
   367  		data = fi.contents
   368  	} else {
   369  		data, err = os.ReadFile(fpath)
   370  		if err != nil {
   371  			cache.mu.Lock()
   372  			defer cache.mu.Unlock()
   373  			cache.entries[fpath] = fileCacheEntry{nil, err}
   374  			return nil, err
   375  		}
   376  	}
   377  	return fs.fs.getCUESyntax(&build.File{
   378  		Filename: fpath,
   379  		Encoding: build.CUE,
   380  		//		Form:     build.Schema,
   381  		Source: data,
   382  	})
   383  }
   384  
   385  // ioFSFile implements [io/fs.File] for the overlay filesystem.
   386  type ioFSFile struct {
   387  	fs      *fileSystem
   388  	path    string
   389  	rc      io.ReadCloser
   390  	entries []iofs.DirEntry
   391  }
   392  
   393  var _ interface {
   394  	iofs.File
   395  	iofs.ReadDirFile
   396  } = (*ioFSFile)(nil)
   397  
   398  func (f *ioFSFile) Stat() (iofs.FileInfo, error) {
   399  	return f.fs.stat(f.path)
   400  }
   401  
   402  func (f *ioFSFile) Read(buf []byte) (int, error) {
   403  	return f.rc.Read(buf)
   404  }
   405  
   406  func (f *ioFSFile) Close() error {
   407  	return f.rc.Close()
   408  }
   409  
   410  func (f *ioFSFile) ReadDir(n int) ([]iofs.DirEntry, error) {
   411  	if f.entries == nil {
   412  		entries, err := f.fs.readDir(f.path)
   413  		if err != nil {
   414  			return entries, err
   415  		}
   416  		if entries == nil {
   417  			entries = []iofs.DirEntry{}
   418  		}
   419  		f.entries = entries
   420  	}
   421  	if n <= 0 {
   422  		entries := f.entries
   423  		f.entries = f.entries[len(f.entries):]
   424  		return entries, nil
   425  	}
   426  	var err error
   427  	if n >= len(f.entries) {
   428  		n = len(f.entries)
   429  		err = io.EOF
   430  	}
   431  	entries := f.entries[:n]
   432  	f.entries = f.entries[n:]
   433  	return entries, err
   434  }
   435  
   436  func (fs *fileSystem) getCUESyntax(bf *build.File) (*ast.File, error) {
   437  	fs.fileCache.mu.Lock()
   438  	defer fs.fileCache.mu.Unlock()
   439  	if bf.Encoding != build.CUE {
   440  		panic("getCUESyntax called with non-CUE file encoding")
   441  	}
   442  	// When it's a regular CUE file with no funny stuff going on, we
   443  	// check and update the syntax cache.
   444  	useCache := bf.Form == "" && bf.Interpretation == ""
   445  	if useCache {
   446  		if syntax, ok := fs.fileCache.entries[bf.Filename]; ok {
   447  			return syntax.file, syntax.err
   448  		}
   449  	}
   450  	d := encoding.NewDecoder(fs.fileCache.ctx, bf, &fs.fileCache.config)
   451  	defer d.Close()
   452  	// Note: CUE files can never have multiple file parts.
   453  	f, err := d.File(), d.Err()
   454  	if useCache {
   455  		fs.fileCache.entries[bf.Filename] = fileCacheEntry{f, err}
   456  	}
   457  	return f, err
   458  }
   459  
   460  func newFileCache(c *Config) *fileCache {
   461  	return &fileCache{
   462  		config: encoding.Config{
   463  			// Note: no need to pass Stdin, as we take care
   464  			// always to pass a non-nil source when the file is "-".
   465  			ParseFile: c.ParseFile,
   466  		},
   467  		ctx:     cuecontext.New(),
   468  		entries: make(map[string]fileCacheEntry),
   469  	}
   470  }
   471  
   472  // fileCache caches data derived from the file system.
   473  type fileCache struct {
   474  	config  encoding.Config
   475  	ctx     *cue.Context
   476  	mu      sync.Mutex
   477  	entries map[string]fileCacheEntry
   478  }
   479  
   480  type fileCacheEntry struct {
   481  	// TODO cache directory information too.
   482  
   483  	// file caches the work involved when decoding a file into an *ast.File.
   484  	// This can happen multiple times for the same file, for example when it is present in
   485  	// multiple different build instances in the same directory hierarchy.
   486  	file *ast.File
   487  	err  error
   488  }