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 }