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