github.com/evanw/esbuild@v0.21.4/internal/fs/fs_real.go (about)

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