...

Source file src/cuelang.org/go/cue/load/fs.go

Documentation: cuelang.org/go/cue/load

     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  	"fmt"
    20  	"io"
    21  	iofs "io/fs"
    22  	"os"
    23  	"path/filepath"
    24  	"sort"
    25  	"strings"
    26  	"time"
    27  
    28  	"cuelang.org/go/cue/ast"
    29  	"cuelang.org/go/cue/errors"
    30  	"cuelang.org/go/cue/token"
    31  	"cuelang.org/go/mod/module"
    32  )
    33  
    34  type overlayFile struct {
    35  	basename string
    36  	contents []byte
    37  	file     *ast.File
    38  	modtime  time.Time
    39  	isDir    bool
    40  }
    41  
    42  func (f *overlayFile) Name() string { return f.basename }
    43  func (f *overlayFile) Size() int64  { return int64(len(f.contents)) }
    44  func (f *overlayFile) Mode() iofs.FileMode {
    45  	if f.isDir {
    46  		return iofs.ModeDir | 0o555
    47  	}
    48  	return 0o444
    49  }
    50  func (f *overlayFile) ModTime() time.Time { return f.modtime }
    51  func (f *overlayFile) IsDir() bool        { return f.isDir }
    52  func (f *overlayFile) Sys() interface{}   { return nil }
    53  
    54  // A fileSystem specifies the supporting context for a build.
    55  type fileSystem struct {
    56  	overlayDirs map[string]map[string]*overlayFile
    57  	cwd         string
    58  }
    59  
    60  func (fs *fileSystem) getDir(dir string, create bool) map[string]*overlayFile {
    61  	dir = filepath.Clean(dir)
    62  	m, ok := fs.overlayDirs[dir]
    63  	if !ok && create {
    64  		m = map[string]*overlayFile{}
    65  		fs.overlayDirs[dir] = m
    66  	}
    67  	return m
    68  }
    69  
    70  // ioFS returns an implementation of [io/fs.FS] that holds
    71  // the contents of fs under the given filepath root.
    72  //
    73  // Note: we can't return an FS implementation that covers the
    74  // entirety of fs because the overlay paths may not all share
    75  // a common root.
    76  //
    77  // Note also: the returned FS also implements
    78  // [modpkgload.OSRootFS] so that we can map
    79  // the resulting source locations back to the filesystem
    80  // paths required by most of the `cue/load` package
    81  // implementation.
    82  func (fs *fileSystem) ioFS(root string) iofs.FS {
    83  	dir := fs.getDir(root, false)
    84  	if dir == nil {
    85  		return module.OSDirFS(root)
    86  	}
    87  	return &ioFS{
    88  		fs:   fs,
    89  		root: root,
    90  	}
    91  }
    92  
    93  func (fs *fileSystem) init(cwd string, overlay map[string]Source) error {
    94  	fs.cwd = cwd
    95  	fs.overlayDirs = map[string]map[string]*overlayFile{}
    96  
    97  	// Organize overlay
    98  	for filename, src := range overlay {
    99  		if !filepath.IsAbs(filename) {
   100  			return fmt.Errorf("non-absolute file path %q in overlay", filename)
   101  		}
   102  		// TODO: do we need to further clean the path or check that the
   103  		// specified files are within the root/ absolute files?
   104  		dir, base := filepath.Split(filename)
   105  		m := fs.getDir(dir, true)
   106  		b, file, err := src.contents()
   107  		if err != nil {
   108  			return err
   109  		}
   110  		m[base] = &overlayFile{
   111  			basename: base,
   112  			contents: b,
   113  			file:     file,
   114  			modtime:  time.Now(),
   115  		}
   116  
   117  		for {
   118  			prevdir := dir
   119  			dir, base = filepath.Split(filepath.Dir(dir))
   120  			if dir == prevdir || dir == "" {
   121  				break
   122  			}
   123  			m := fs.getDir(dir, true)
   124  			if m[base] == nil {
   125  				m[base] = &overlayFile{
   126  					basename: base,
   127  					modtime:  time.Now(),
   128  					isDir:    true,
   129  				}
   130  			}
   131  		}
   132  	}
   133  	return nil
   134  }
   135  
   136  func (fs *fileSystem) joinPath(elem ...string) string {
   137  	return filepath.Join(elem...)
   138  }
   139  
   140  func (fs *fileSystem) makeAbs(path string) string {
   141  	if filepath.IsAbs(path) {
   142  		return path
   143  	}
   144  	return filepath.Join(fs.cwd, path)
   145  }
   146  
   147  func (fs *fileSystem) isDir(path string) bool {
   148  	path = fs.makeAbs(path)
   149  	if fs.getDir(path, false) != nil {
   150  		return true
   151  	}
   152  	fi, err := os.Stat(path)
   153  	return err == nil && fi.IsDir()
   154  }
   155  
   156  func (fs *fileSystem) readDir(path string) ([]iofs.DirEntry, errors.Error) {
   157  	path = fs.makeAbs(path)
   158  	m := fs.getDir(path, false)
   159  	items, err := os.ReadDir(path)
   160  	if err != nil {
   161  		if !os.IsNotExist(err) || m == nil {
   162  			return nil, errors.Wrapf(err, token.NoPos, "readDir")
   163  		}
   164  	}
   165  	if m == nil {
   166  		return items, nil
   167  	}
   168  	done := map[string]bool{}
   169  	for i, fi := range items {
   170  		done[fi.Name()] = true
   171  		if o := m[fi.Name()]; o != nil {
   172  			items[i] = iofs.FileInfoToDirEntry(o)
   173  		}
   174  	}
   175  	for _, o := range m {
   176  		if !done[o.Name()] {
   177  			items = append(items, iofs.FileInfoToDirEntry(o))
   178  		}
   179  	}
   180  	sort.Slice(items, func(i, j int) bool {
   181  		return items[i].Name() < items[j].Name()
   182  	})
   183  	return items, nil
   184  }
   185  
   186  func (fs *fileSystem) getOverlay(path string) *overlayFile {
   187  	dir, base := filepath.Split(path)
   188  	if m := fs.getDir(dir, false); m != nil {
   189  		return m[base]
   190  	}
   191  	return nil
   192  }
   193  
   194  func (fs *fileSystem) stat(path string) (iofs.FileInfo, errors.Error) {
   195  	path = fs.makeAbs(path)
   196  	if fi := fs.getOverlay(path); fi != nil {
   197  		return fi, nil
   198  	}
   199  	fi, err := os.Stat(path)
   200  	if err != nil {
   201  		return nil, errors.Wrapf(err, token.NoPos, "stat")
   202  	}
   203  	return fi, nil
   204  }
   205  
   206  func (fs *fileSystem) lstat(path string) (iofs.FileInfo, errors.Error) {
   207  	path = fs.makeAbs(path)
   208  	if fi := fs.getOverlay(path); fi != nil {
   209  		return fi, nil
   210  	}
   211  	fi, err := os.Lstat(path)
   212  	if err != nil {
   213  		return nil, errors.Wrapf(err, token.NoPos, "stat")
   214  	}
   215  	return fi, nil
   216  }
   217  
   218  func (fs *fileSystem) openFile(path string) (io.ReadCloser, errors.Error) {
   219  	path = fs.makeAbs(path)
   220  	if fi := fs.getOverlay(path); fi != nil {
   221  		return io.NopCloser(bytes.NewReader(fi.contents)), nil
   222  	}
   223  
   224  	f, err := os.Open(path)
   225  	if err != nil {
   226  		return nil, errors.Wrapf(err, token.NoPos, "load")
   227  	}
   228  	return f, nil
   229  }
   230  
   231  var skipDir = errors.Newf(token.NoPos, "skip directory")
   232  
   233  type walkFunc func(path string, entry iofs.DirEntry, err errors.Error) errors.Error
   234  
   235  func (fs *fileSystem) walk(root string, f walkFunc) error {
   236  	info, err := fs.lstat(root)
   237  	entry := iofs.FileInfoToDirEntry(info)
   238  	if err != nil {
   239  		err = f(root, entry, err)
   240  	} else if !info.IsDir() {
   241  		return errors.Newf(token.NoPos, "path %q is not a directory", root)
   242  	} else {
   243  		err = fs.walkRec(root, entry, f)
   244  	}
   245  	if err == skipDir {
   246  		return nil
   247  	}
   248  	return err
   249  
   250  }
   251  
   252  func (fs *fileSystem) walkRec(path string, entry iofs.DirEntry, f walkFunc) errors.Error {
   253  	if !entry.IsDir() {
   254  		return f(path, entry, nil)
   255  	}
   256  
   257  	dir, err := fs.readDir(path)
   258  	err1 := f(path, entry, err)
   259  
   260  	// If err != nil, walk can't walk into this directory.
   261  	// err1 != nil means walkFn want walk to skip this directory or stop walking.
   262  	// Therefore, if one of err and err1 isn't nil, walk will return.
   263  	if err != nil || err1 != nil {
   264  		// The caller's behavior is controlled by the return value, which is decided
   265  		// by walkFn. walkFn may ignore err and return nil.
   266  		// If walkFn returns SkipDir, it will be handled by the caller.
   267  		// So walk should return whatever walkFn returns.
   268  		return err1
   269  	}
   270  
   271  	for _, entry := range dir {
   272  		filename := fs.joinPath(path, entry.Name())
   273  		err = fs.walkRec(filename, entry, f)
   274  		if err != nil {
   275  			if !entry.IsDir() || err != skipDir {
   276  				return err
   277  			}
   278  		}
   279  	}
   280  	return nil
   281  }
   282  
   283  var _ interface {
   284  	iofs.FS
   285  	iofs.ReadDirFS
   286  	iofs.ReadFileFS
   287  	module.OSRootFS
   288  } = (*ioFS)(nil)
   289  
   290  type ioFS struct {
   291  	fs   *fileSystem
   292  	root string
   293  }
   294  
   295  func (fs *ioFS) OSRoot() string {
   296  	return fs.root
   297  }
   298  
   299  func (fs *ioFS) Open(name string) (iofs.File, error) {
   300  	fpath, err := fs.absPathFromFSPath(name)
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  	r, err := fs.fs.openFile(fpath)
   305  	if err != nil {
   306  		return nil, err // TODO convert filepath in error to fs path
   307  	}
   308  	return &ioFSFile{
   309  		fs:   fs.fs,
   310  		path: fpath,
   311  		rc:   r,
   312  	}, nil
   313  }
   314  
   315  func (fs *ioFS) absPathFromFSPath(name string) (string, error) {
   316  	if !iofs.ValidPath(name) {
   317  		return "", fmt.Errorf("invalid io/fs path %q", name)
   318  	}
   319  	// Technically we should mimic Go's internal/safefilepath.fromFS
   320  	// functionality here, but as we're using this in a relatively limited
   321  	// context, we can just prohibit some characters.
   322  	if strings.ContainsAny(name, ":\\") {
   323  		return "", fmt.Errorf("invalid io/fs path %q", name)
   324  	}
   325  	return filepath.Join(fs.root, name), nil
   326  }
   327  
   328  // ReadDir implements [io/fs.ReadDirFS].
   329  func (fs *ioFS) ReadDir(name string) ([]iofs.DirEntry, error) {
   330  	fpath, err := fs.absPathFromFSPath(name)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	return fs.fs.readDir(fpath)
   335  }
   336  
   337  // ReadDir implements [io/fs.ReadFileFS].
   338  func (fs *ioFS) ReadFile(name string) ([]byte, error) {
   339  	fpath, err := fs.absPathFromFSPath(name)
   340  	if err != nil {
   341  		return nil, err
   342  	}
   343  	if fi := fs.fs.getOverlay(fpath); fi != nil {
   344  		return bytes.Clone(fi.contents), nil
   345  	}
   346  	return os.ReadFile(fpath)
   347  }
   348  
   349  // ioFSFile implements [io/fs.File] for the overlay filesystem.
   350  type ioFSFile struct {
   351  	fs      *fileSystem
   352  	path    string
   353  	rc      io.ReadCloser
   354  	entries []iofs.DirEntry
   355  }
   356  
   357  var _ interface {
   358  	iofs.File
   359  	iofs.ReadDirFile
   360  } = (*ioFSFile)(nil)
   361  
   362  func (f *ioFSFile) Stat() (iofs.FileInfo, error) {
   363  	return f.fs.stat(f.path)
   364  }
   365  
   366  func (f *ioFSFile) Read(buf []byte) (int, error) {
   367  	return f.rc.Read(buf)
   368  }
   369  
   370  func (f *ioFSFile) Close() error {
   371  	return f.rc.Close()
   372  }
   373  
   374  func (f *ioFSFile) ReadDir(n int) ([]iofs.DirEntry, error) {
   375  	if f.entries == nil {
   376  		entries, err := f.fs.readDir(f.path)
   377  		if err != nil {
   378  			return entries, err
   379  		}
   380  		if entries == nil {
   381  			entries = []iofs.DirEntry{}
   382  		}
   383  		f.entries = entries
   384  	}
   385  	if n <= 0 {
   386  		entries := f.entries
   387  		f.entries = f.entries[len(f.entries):]
   388  		return entries, nil
   389  	}
   390  	var err error
   391  	if n >= len(f.entries) {
   392  		n = len(f.entries)
   393  		err = io.EOF
   394  	}
   395  	entries := f.entries[:n]
   396  	f.entries = f.entries[n:]
   397  	return entries, err
   398  }
   399  

View as plain text