gowebbuild/vendor/github.com/evanw/esbuild/internal/fs/fs_real.go

530 lines
13 KiB
Go

package fs
import (
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"strings"
"sync"
"syscall"
)
type realFS struct {
// Stores the file entries for directories we've listed before
entriesMutex sync.Mutex
entries map[string]entriesOrErr
// If true, do not use the "entries" cache
doNotCacheEntries bool
// This stores data that will end up being returned by "WatchData()"
watchMutex sync.Mutex
watchData map[string]privateWatchData
// When building with WebAssembly, the Go compiler doesn't correctly handle
// platform-specific path behavior. Hack around these bugs by compiling
// support for both Unix and Windows paths into all executables and switch
// between them at run-time instead.
fp goFilepath
}
type entriesOrErr struct {
entries DirEntries
canonicalError error
originalError error
}
type watchState uint8
const (
stateNone watchState = iota
stateDirHasAccessedEntries // Compare "accessedEntries"
stateDirMissing // Compare directory presence
stateFileHasModKey // Compare "modKey"
stateFileNeedModKey // Need to transition to "stateFileHasModKey" or "stateFileUnusableModKey" before "WatchData()" returns
stateFileMissing // Compare file presence
stateFileUnusableModKey // Compare "fileContents"
)
type privateWatchData struct {
accessedEntries *accessedEntries
fileContents string
modKey ModKey
state watchState
}
type RealFSOptions struct {
WantWatchData bool
AbsWorkingDir string
DoNotCache bool
}
func RealFS(options RealFSOptions) (FS, error) {
var fp goFilepath
if CheckIfWindows() {
fp.isWindows = true
fp.pathSeparator = '\\'
} else {
fp.isWindows = false
fp.pathSeparator = '/'
}
// Come up with a default working directory if one was not specified
fp.cwd = options.AbsWorkingDir
if fp.cwd == "" {
if cwd, err := os.Getwd(); err == nil {
fp.cwd = cwd
} else if fp.isWindows {
fp.cwd = "C:\\"
} else {
fp.cwd = "/"
}
} else if !fp.isAbs(fp.cwd) {
return nil, fmt.Errorf("The working directory %q is not an absolute path", fp.cwd)
}
// Resolve symlinks in the current working directory. Symlinks are resolved
// when input file paths are converted to absolute paths because we need to
// recognize an input file as unique even if it has multiple symlinks
// pointing to it. The build will generate relative paths from the current
// working directory to the absolute input file paths for error messages,
// so the current working directory should be processed the same way. Not
// doing this causes test failures with esbuild when run from inside a
// symlinked directory.
//
// This deliberately ignores errors due to e.g. infinite loops. If there is
// an error, we will just use the original working directory and likely
// encounter an error later anyway. And if we don't encounter an error
// later, then the current working directory didn't even matter and the
// error is unimportant.
if path, err := fp.evalSymlinks(fp.cwd); err == nil {
fp.cwd = path
}
// Only allocate memory for watch data if necessary
var watchData map[string]privateWatchData
if options.WantWatchData {
watchData = make(map[string]privateWatchData)
}
return &realFS{
entries: make(map[string]entriesOrErr),
fp: fp,
watchData: watchData,
doNotCacheEntries: options.DoNotCache,
}, nil
}
func (fs *realFS) ReadDirectory(dir string) (entries DirEntries, canonicalError error, originalError error) {
if !fs.doNotCacheEntries {
// First, check the cache
cached, ok := func() (cached entriesOrErr, ok bool) {
fs.entriesMutex.Lock()
defer fs.entriesMutex.Unlock()
cached, ok = fs.entries[dir]
return
}()
if ok {
// Cache hit: stop now
return cached.entries, cached.canonicalError, cached.originalError
}
}
// Cache miss: read the directory entries
names, canonicalError, originalError := fs.readdir(dir)
entries = DirEntries{dir, make(map[string]*Entry), nil}
// Unwrap to get the underlying error
if pathErr, ok := canonicalError.(*os.PathError); ok {
canonicalError = pathErr.Unwrap()
}
if canonicalError == nil {
for _, name := range names {
// Call "stat" lazily for performance. The "@material-ui/icons" package
// contains a directory with over 11,000 entries in it and running "stat"
// for each entry was a big performance issue for that package.
entries.data[strings.ToLower(name)] = &Entry{
dir: dir,
base: name,
needStat: true,
}
}
}
// Store data for watch mode
if fs.watchData != nil {
defer fs.watchMutex.Unlock()
fs.watchMutex.Lock()
state := stateDirHasAccessedEntries
if canonicalError != nil {
state = stateDirMissing
}
entries.accessedEntries = &accessedEntries{wasPresent: make(map[string]bool)}
fs.watchData[dir] = privateWatchData{
accessedEntries: entries.accessedEntries,
state: state,
}
}
// Update the cache unconditionally. Even if the read failed, we don't want to
// retry again later. The directory is inaccessible so trying again is wasted.
if canonicalError != nil {
entries.data = nil
}
if !fs.doNotCacheEntries {
fs.entriesMutex.Lock()
defer fs.entriesMutex.Unlock()
fs.entries[dir] = entriesOrErr{
entries: entries,
canonicalError: canonicalError,
originalError: originalError,
}
}
return entries, canonicalError, originalError
}
func (fs *realFS) ReadFile(path string) (contents string, canonicalError error, originalError error) {
BeforeFileOpen()
defer AfterFileClose()
buffer, originalError := ioutil.ReadFile(path)
canonicalError = fs.canonicalizeError(originalError)
// Allocate the string once
fileContents := string(buffer)
// Store data for watch mode
if fs.watchData != nil {
defer fs.watchMutex.Unlock()
fs.watchMutex.Lock()
data, ok := fs.watchData[path]
if canonicalError != nil {
data.state = stateFileMissing
} else if !ok {
data.state = stateFileNeedModKey
}
data.fileContents = fileContents
fs.watchData[path] = data
}
return fileContents, canonicalError, originalError
}
type realOpenedFile struct {
handle *os.File
len int
}
func (f *realOpenedFile) Len() int {
return f.len
}
func (f *realOpenedFile) Read(start int, end int) ([]byte, error) {
bytes := make([]byte, end-start)
remaining := bytes
_, err := f.handle.Seek(int64(start), io.SeekStart)
if err != nil {
return nil, err
}
for len(remaining) > 0 {
n, err := f.handle.Read(remaining)
if err != nil && n <= 0 {
return nil, err
}
remaining = remaining[n:]
}
return bytes, nil
}
func (f *realOpenedFile) Close() error {
return f.handle.Close()
}
func (fs *realFS) OpenFile(path string) (OpenedFile, error, error) {
BeforeFileOpen()
defer AfterFileClose()
f, err := os.Open(path)
if err != nil {
return nil, fs.canonicalizeError(err), err
}
info, err := f.Stat()
if err != nil {
f.Close()
return nil, fs.canonicalizeError(err), err
}
return &realOpenedFile{f, int(info.Size())}, nil, nil
}
func (fs *realFS) ModKey(path string) (ModKey, error) {
BeforeFileOpen()
defer AfterFileClose()
key, err := modKey(path)
// Store data for watch mode
if fs.watchData != nil {
defer fs.watchMutex.Unlock()
fs.watchMutex.Lock()
data, ok := fs.watchData[path]
if !ok {
if err == modKeyUnusable {
data.state = stateFileUnusableModKey
} else if err != nil {
data.state = stateFileMissing
} else {
data.state = stateFileHasModKey
}
} else if data.state == stateFileNeedModKey {
data.state = stateFileHasModKey
}
data.modKey = key
fs.watchData[path] = data
}
return key, err
}
func (fs *realFS) IsAbs(p string) bool {
return fs.fp.isAbs(p)
}
func (fs *realFS) Abs(p string) (string, bool) {
abs, err := fs.fp.abs(p)
return abs, err == nil
}
func (fs *realFS) Dir(p string) string {
return fs.fp.dir(p)
}
func (fs *realFS) Base(p string) string {
return fs.fp.base(p)
}
func (fs *realFS) Ext(p string) string {
return fs.fp.ext(p)
}
func (fs *realFS) Join(parts ...string) string {
return fs.fp.clean(fs.fp.join(parts))
}
func (fs *realFS) Cwd() string {
return fs.fp.cwd
}
func (fs *realFS) Rel(base string, target string) (string, bool) {
if rel, err := fs.fp.rel(base, target); err == nil {
return rel, true
}
return "", false
}
func (fs *realFS) readdir(dirname string) (entries []string, canonicalError error, originalError error) {
BeforeFileOpen()
defer AfterFileClose()
f, originalError := os.Open(dirname)
canonicalError = fs.canonicalizeError(originalError)
// Stop now if there was an error
if canonicalError != nil {
return nil, canonicalError, originalError
}
defer f.Close()
entries, err := f.Readdirnames(-1)
// Unwrap to get the underlying error
if syscallErr, ok := err.(*os.SyscallError); ok {
err = syscallErr.Unwrap()
}
// Don't convert ENOTDIR to ENOENT here. ENOTDIR is a legitimate error
// condition for Readdirnames() on non-Windows platforms.
return entries, canonicalError, originalError
}
func (fs *realFS) canonicalizeError(err error) error {
// Unwrap to get the underlying error
if pathErr, ok := err.(*os.PathError); ok {
err = pathErr.Unwrap()
}
// This has been copied from golang.org/x/sys/windows
const ERROR_INVALID_NAME syscall.Errno = 123
// Windows is much more restrictive than Unix about file names. If a file name
// is invalid, it will return ERROR_INVALID_NAME. Treat this as ENOENT (i.e.
// "the file does not exist") so that the resolver continues trying to resolve
// the path on this failure instead of aborting with an error.
if fs.fp.isWindows && err == ERROR_INVALID_NAME {
err = syscall.ENOENT
}
// Windows returns ENOTDIR here even though nothing we've done yet has asked
// for a directory. This really means ENOENT on Windows. Return ENOENT here
// so callers that check for ENOENT will successfully detect this file as
// missing.
if err == syscall.ENOTDIR {
err = syscall.ENOENT
}
return err
}
func (fs *realFS) kind(dir string, base string) (symlink string, kind EntryKind) {
entryPath := fs.fp.join([]string{dir, base})
// Use "lstat" since we want information about symbolic links
BeforeFileOpen()
defer AfterFileClose()
stat, err := os.Lstat(entryPath)
if err != nil {
return
}
mode := stat.Mode()
// Follow symlinks now so the cache contains the translation
if (mode & os.ModeSymlink) != 0 {
symlink = entryPath
linksWalked := 0
for {
linksWalked++
if linksWalked > 255 {
return // Error: too many links
}
link, err := os.Readlink(symlink)
if err != nil {
return // Skip over this entry
}
if !fs.fp.isAbs(link) {
link = fs.fp.join([]string{dir, link})
}
symlink = fs.fp.clean(link)
// Re-run "lstat" on the symlink target
stat2, err2 := os.Lstat(symlink)
if err2 != nil {
return // Skip over this entry
}
mode = stat2.Mode()
if (mode & os.ModeSymlink) == 0 {
break
}
dir = fs.fp.dir(symlink)
}
}
// We consider the entry either a directory or a file
if (mode & os.ModeDir) != 0 {
kind = DirEntry
} else {
kind = FileEntry
}
return
}
func (fs *realFS) WatchData() WatchData {
paths := make(map[string]func() string)
for path, data := range fs.watchData {
// Each closure below needs its own copy of these loop variables
path := path
data := data
// Each function should return true if the state has been changed
if data.state == stateFileNeedModKey {
key, err := modKey(path)
if err == modKeyUnusable {
data.state = stateFileUnusableModKey
} else if err != nil {
data.state = stateFileMissing
} else {
data.state = stateFileHasModKey
data.modKey = key
}
}
switch data.state {
case stateDirMissing:
paths[path] = func() string {
info, err := os.Stat(path)
if err == nil && info.IsDir() {
return path
}
return ""
}
case stateDirHasAccessedEntries:
paths[path] = func() string {
names, err, _ := fs.readdir(path)
if err != nil {
return path
}
data.accessedEntries.mutex.Lock()
defer data.accessedEntries.mutex.Unlock()
if allEntries := data.accessedEntries.allEntries; allEntries != nil {
// Check all entries
if len(names) != len(allEntries) {
return path
}
sort.Strings(names)
for i, s := range names {
if s != allEntries[i] {
return path
}
}
} else {
// Check individual entries
isPresent := make(map[string]bool, len(names))
for _, name := range names {
isPresent[strings.ToLower(name)] = true
}
for name, wasPresent := range data.accessedEntries.wasPresent {
if wasPresent != isPresent[name] {
return fs.Join(path, name)
}
}
}
return ""
}
case stateFileMissing:
paths[path] = func() string {
if info, err := os.Stat(path); err == nil && !info.IsDir() {
return path
}
return ""
}
case stateFileHasModKey:
paths[path] = func() string {
if key, err := modKey(path); err != nil || key != data.modKey {
return path
}
return ""
}
case stateFileUnusableModKey:
paths[path] = func() string {
if buffer, err := ioutil.ReadFile(path); err != nil || string(buffer) != data.fileContents {
return path
}
return ""
}
}
}
return WatchData{
Paths: paths,
}
}