1package fs 2 3import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "sort" 8 "strings" 9 "sync" 10 "syscall" 11) 12 13type realFS struct { 14 // Stores the file entries for directories we've listed before 15 entriesMutex sync.Mutex 16 entries map[string]entriesOrErr 17 18 // If true, do not use the "entries" cache 19 doNotCacheEntries bool 20 21 // This stores data that will end up being returned by "WatchData()" 22 watchMutex sync.Mutex 23 watchData map[string]privateWatchData 24 25 // When building with WebAssembly, the Go compiler doesn't correctly handle 26 // platform-specific path behavior. Hack around these bugs by compiling 27 // support for both Unix and Windows paths into all executables and switch 28 // between them at run-time instead. 29 fp goFilepath 30} 31 32type entriesOrErr struct { 33 entries DirEntries 34 canonicalError error 35 originalError error 36} 37 38type watchState uint8 39 40const ( 41 stateNone watchState = iota 42 stateDirHasEntries // Compare "dirEntries" 43 stateDirMissing // Compare directory presence 44 stateFileHasModKey // Compare "modKey" 45 stateFileNeedModKey // Need to transition to "stateFileHasModKey" or "stateFileUnusableModKey" before "WatchData()" returns 46 stateFileMissing // Compare file presence 47 stateFileUnusableModKey // Compare "fileContents" 48) 49 50type privateWatchData struct { 51 dirEntries []string 52 fileContents string 53 modKey ModKey 54 state watchState 55} 56 57type RealFSOptions struct { 58 WantWatchData bool 59 AbsWorkingDir string 60 DoNotCache bool 61} 62 63func RealFS(options RealFSOptions) (FS, error) { 64 var fp goFilepath 65 if CheckIfWindows() { 66 fp.isWindows = true 67 fp.pathSeparator = '\\' 68 } else { 69 fp.isWindows = false 70 fp.pathSeparator = '/' 71 } 72 73 // Come up with a default working directory if one was not specified 74 fp.cwd = options.AbsWorkingDir 75 if fp.cwd == "" { 76 if cwd, err := os.Getwd(); err == nil { 77 fp.cwd = cwd 78 } else if fp.isWindows { 79 fp.cwd = "C:\\" 80 } else { 81 fp.cwd = "/" 82 } 83 } else if !fp.isAbs(fp.cwd) { 84 return nil, fmt.Errorf("The working directory %q is not an absolute path", fp.cwd) 85 } 86 87 // Resolve symlinks in the current working directory. Symlinks are resolved 88 // when input file paths are converted to absolute paths because we need to 89 // recognize an input file as unique even if it has multiple symlinks 90 // pointing to it. The build will generate relative paths from the current 91 // working directory to the absolute input file paths for error messages, 92 // so the current working directory should be processed the same way. Not 93 // doing this causes test failures with esbuild when run from inside a 94 // symlinked directory. 95 // 96 // This deliberately ignores errors due to e.g. infinite loops. If there is 97 // an error, we will just use the original working directory and likely 98 // encounter an error later anyway. And if we don't encounter an error 99 // later, then the current working directory didn't even matter and the 100 // error is unimportant. 101 if path, err := fp.evalSymlinks(fp.cwd); err == nil { 102 fp.cwd = path 103 } 104 105 // Only allocate memory for watch data if necessary 106 var watchData map[string]privateWatchData 107 if options.WantWatchData { 108 watchData = make(map[string]privateWatchData) 109 } 110 111 return &realFS{ 112 entries: make(map[string]entriesOrErr), 113 fp: fp, 114 watchData: watchData, 115 doNotCacheEntries: options.DoNotCache, 116 }, nil 117} 118 119func (fs *realFS) ReadDirectory(dir string) (entries DirEntries, canonicalError error, originalError error) { 120 if !fs.doNotCacheEntries { 121 // First, check the cache 122 cached, ok := func() (cached entriesOrErr, ok bool) { 123 fs.entriesMutex.Lock() 124 defer fs.entriesMutex.Unlock() 125 cached, ok = fs.entries[dir] 126 return 127 }() 128 if ok { 129 // Cache hit: stop now 130 return cached.entries, cached.canonicalError, cached.originalError 131 } 132 } 133 134 // Cache miss: read the directory entries 135 names, canonicalError, originalError := fs.readdir(dir) 136 entries = DirEntries{dir, make(map[string]*Entry)} 137 138 // Unwrap to get the underlying error 139 if pathErr, ok := canonicalError.(*os.PathError); ok { 140 canonicalError = pathErr.Unwrap() 141 } 142 143 if canonicalError == nil { 144 for _, name := range names { 145 // Call "stat" lazily for performance. The "@material-ui/icons" package 146 // contains a directory with over 11,000 entries in it and running "stat" 147 // for each entry was a big performance issue for that package. 148 entries.data[strings.ToLower(name)] = &Entry{ 149 dir: dir, 150 base: name, 151 needStat: true, 152 } 153 } 154 } 155 156 // Store data for watch mode 157 if fs.watchData != nil { 158 defer fs.watchMutex.Unlock() 159 fs.watchMutex.Lock() 160 state := stateDirHasEntries 161 if canonicalError != nil { 162 state = stateDirMissing 163 } 164 sort.Strings(names) 165 fs.watchData[dir] = privateWatchData{ 166 dirEntries: names, 167 state: state, 168 } 169 } 170 171 // Update the cache unconditionally. Even if the read failed, we don't want to 172 // retry again later. The directory is inaccessible so trying again is wasted. 173 if canonicalError != nil { 174 entries.data = nil 175 } 176 if !fs.doNotCacheEntries { 177 fs.entriesMutex.Lock() 178 defer fs.entriesMutex.Unlock() 179 fs.entries[dir] = entriesOrErr{ 180 entries: entries, 181 canonicalError: canonicalError, 182 originalError: originalError, 183 } 184 } 185 return entries, canonicalError, originalError 186} 187 188func (fs *realFS) ReadFile(path string) (contents string, canonicalError error, originalError error) { 189 BeforeFileOpen() 190 defer AfterFileClose() 191 buffer, originalError := ioutil.ReadFile(path) 192 canonicalError = fs.canonicalizeError(originalError) 193 194 // Allocate the string once 195 fileContents := string(buffer) 196 197 // Store data for watch mode 198 if fs.watchData != nil { 199 defer fs.watchMutex.Unlock() 200 fs.watchMutex.Lock() 201 data, ok := fs.watchData[path] 202 if canonicalError != nil { 203 data.state = stateFileMissing 204 } else if !ok { 205 data.state = stateFileNeedModKey 206 } 207 data.fileContents = fileContents 208 fs.watchData[path] = data 209 } 210 211 return fileContents, canonicalError, originalError 212} 213 214type realOpenedFile struct { 215 handle *os.File 216 len int 217} 218 219func (f *realOpenedFile) Len() int { 220 return f.len 221} 222 223func (f *realOpenedFile) Read(start int, end int) ([]byte, error) { 224 bytes := make([]byte, end-start) 225 remaining := bytes 226 227 _, err := f.handle.Seek(int64(start), os.SEEK_SET) 228 if err != nil { 229 return nil, err 230 } 231 232 for len(remaining) > 0 { 233 n, err := f.handle.Read(remaining) 234 if err != nil && n <= 0 { 235 return nil, err 236 } 237 remaining = remaining[n:] 238 } 239 240 return bytes, nil 241} 242 243func (f *realOpenedFile) Close() error { 244 return f.handle.Close() 245} 246 247func (fs *realFS) OpenFile(path string) (OpenedFile, error, error) { 248 BeforeFileOpen() 249 defer AfterFileClose() 250 251 f, err := os.Open(path) 252 if err != nil { 253 return nil, fs.canonicalizeError(err), err 254 } 255 256 info, err := f.Stat() 257 if err != nil { 258 f.Close() 259 return nil, fs.canonicalizeError(err), err 260 } 261 262 return &realOpenedFile{f, int(info.Size())}, nil, nil 263} 264 265func (fs *realFS) ModKey(path string) (ModKey, error) { 266 BeforeFileOpen() 267 defer AfterFileClose() 268 key, err := modKey(path) 269 270 // Store data for watch mode 271 if fs.watchData != nil { 272 defer fs.watchMutex.Unlock() 273 fs.watchMutex.Lock() 274 data, ok := fs.watchData[path] 275 if !ok { 276 if err == modKeyUnusable { 277 data.state = stateFileUnusableModKey 278 } else if err != nil { 279 data.state = stateFileMissing 280 } else { 281 data.state = stateFileHasModKey 282 } 283 } else if data.state == stateFileNeedModKey { 284 data.state = stateFileHasModKey 285 } 286 data.modKey = key 287 fs.watchData[path] = data 288 } 289 290 return key, err 291} 292 293func (fs *realFS) IsAbs(p string) bool { 294 return fs.fp.isAbs(p) 295} 296 297func (fs *realFS) Abs(p string) (string, bool) { 298 abs, err := fs.fp.abs(p) 299 return abs, err == nil 300} 301 302func (fs *realFS) Dir(p string) string { 303 return fs.fp.dir(p) 304} 305 306func (fs *realFS) Base(p string) string { 307 return fs.fp.base(p) 308} 309 310func (fs *realFS) Ext(p string) string { 311 return fs.fp.ext(p) 312} 313 314func (fs *realFS) Join(parts ...string) string { 315 return fs.fp.clean(fs.fp.join(parts)) 316} 317 318func (fs *realFS) Cwd() string { 319 return fs.fp.cwd 320} 321 322func (fs *realFS) Rel(base string, target string) (string, bool) { 323 if rel, err := fs.fp.rel(base, target); err == nil { 324 return rel, true 325 } 326 return "", false 327} 328 329func (fs *realFS) readdir(dirname string) (entries []string, canonicalError error, originalError error) { 330 BeforeFileOpen() 331 defer AfterFileClose() 332 f, originalError := os.Open(dirname) 333 canonicalError = fs.canonicalizeError(originalError) 334 335 // Stop now if there was an error 336 if canonicalError != nil { 337 return nil, canonicalError, originalError 338 } 339 340 defer f.Close() 341 entries, err := f.Readdirnames(-1) 342 343 // Unwrap to get the underlying error 344 if syscallErr, ok := err.(*os.SyscallError); ok { 345 err = syscallErr.Unwrap() 346 } 347 348 // Don't convert ENOTDIR to ENOENT here. ENOTDIR is a legitimate error 349 // condition for Readdirnames() on non-Windows platforms. 350 351 return entries, canonicalError, originalError 352} 353 354func (fs *realFS) canonicalizeError(err error) error { 355 // Unwrap to get the underlying error 356 if pathErr, ok := err.(*os.PathError); ok { 357 err = pathErr.Unwrap() 358 } 359 360 // This has been copied from golang.org/x/sys/windows 361 const ERROR_INVALID_NAME syscall.Errno = 123 362 363 // Windows is much more restrictive than Unix about file names. If a file name 364 // is invalid, it will return ERROR_INVALID_NAME. Treat this as ENOENT (i.e. 365 // "the file does not exist") so that the resolver continues trying to resolve 366 // the path on this failure instead of aborting with an error. 367 if fs.fp.isWindows && err == ERROR_INVALID_NAME { 368 err = syscall.ENOENT 369 } 370 371 // Windows returns ENOTDIR here even though nothing we've done yet has asked 372 // for a directory. This really means ENOENT on Windows. Return ENOENT here 373 // so callers that check for ENOENT will successfully detect this file as 374 // missing. 375 if err == syscall.ENOTDIR { 376 err = syscall.ENOENT 377 } 378 379 return err 380} 381 382func (fs *realFS) kind(dir string, base string) (symlink string, kind EntryKind) { 383 entryPath := fs.fp.join([]string{dir, base}) 384 385 // Use "lstat" since we want information about symbolic links 386 BeforeFileOpen() 387 defer AfterFileClose() 388 stat, err := os.Lstat(entryPath) 389 if err != nil { 390 return 391 } 392 mode := stat.Mode() 393 394 // Follow symlinks now so the cache contains the translation 395 if (mode & os.ModeSymlink) != 0 { 396 symlink = entryPath 397 linksWalked := 0 398 for { 399 linksWalked++ 400 if linksWalked > 255 { 401 return // Error: too many links 402 } 403 link, err := os.Readlink(symlink) 404 if err != nil { 405 return // Skip over this entry 406 } 407 if !fs.fp.isAbs(link) { 408 link = fs.fp.join([]string{dir, link}) 409 } 410 symlink = fs.fp.clean(link) 411 412 // Re-run "lstat" on the symlink target 413 stat2, err2 := os.Lstat(symlink) 414 if err2 != nil { 415 return // Skip over this entry 416 } 417 mode = stat2.Mode() 418 if (mode & os.ModeSymlink) == 0 { 419 break 420 } 421 dir = fs.fp.dir(symlink) 422 } 423 } 424 425 // We consider the entry either a directory or a file 426 if (mode & os.ModeDir) != 0 { 427 kind = DirEntry 428 } else { 429 kind = FileEntry 430 } 431 return 432} 433 434func (fs *realFS) WatchData() WatchData { 435 paths := make(map[string]func() bool) 436 437 for path, data := range fs.watchData { 438 // Each closure below needs its own copy of these loop variables 439 path := path 440 data := data 441 442 // Each function should return true if the state has been changed 443 if data.state == stateFileNeedModKey { 444 key, err := modKey(path) 445 if err == modKeyUnusable { 446 data.state = stateFileUnusableModKey 447 } else if err != nil { 448 data.state = stateFileMissing 449 } else { 450 data.state = stateFileHasModKey 451 data.modKey = key 452 } 453 } 454 455 switch data.state { 456 case stateDirMissing: 457 paths[path] = func() bool { 458 info, err := os.Stat(path) 459 return err == nil && info.IsDir() 460 } 461 462 case stateDirHasEntries: 463 paths[path] = func() bool { 464 names, err, _ := fs.readdir(path) 465 if err != nil || len(names) != len(data.dirEntries) { 466 return true 467 } 468 sort.Strings(names) 469 for i, s := range names { 470 if s != data.dirEntries[i] { 471 return true 472 } 473 } 474 return false 475 } 476 477 case stateFileMissing: 478 paths[path] = func() bool { 479 info, err := os.Stat(path) 480 return err == nil && !info.IsDir() 481 } 482 483 case stateFileHasModKey: 484 paths[path] = func() bool { 485 key, err := modKey(path) 486 return err != nil || key != data.modKey 487 } 488 489 case stateFileUnusableModKey: 490 paths[path] = func() bool { 491 buffer, err := ioutil.ReadFile(path) 492 return err != nil || string(buffer) != data.fileContents 493 } 494 } 495 } 496 497 return WatchData{ 498 Paths: paths, 499 } 500} 501