package osvfs import ( "fmt" "io/fs" "os" "path/filepath" "runtime" "strings" "sync" "time" "unicode" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/nativepath" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/internal" ) var ( // Semaphore for operations that are effectively blocking syscalls. blockingOpSema = core.NewLimitedSemaphore(128) // Semaphore for file reads. readSema = core.NewLimitedSemaphore(128) // Semaphore for file writes. writeSema = core.NewLimitedSemaphore(32) ) // FS creates a new FS from the OS file system. func FS() vfs.FS { return osVFS } var osVFS vfs.FS = &osFS{ common: internal.Common{ RootFor: os.DirFS, IsReparsePoint: isReparsePoint, }, } type osFS struct { common internal.Common } // We do this right at startup to minimize the chance that executable gets moved or deleted. var isFileSystemCaseSensitive = func() bool { // win32/win64 are case insensitive platforms if runtime.GOOS == "windows" { return false } if runtime.GOARCH == "wasm" { // !!! Who knows; this depends on the host implementation. return true } // As a proxy for case-insensitivity, we check if the current executable exists under a different case. // This is not entirely correct, since different OSs can have differing case sensitivity in different paths, // but this is largely good enough for our purposes (and what sys.ts used to do with __filename). exe, err := os.Executable() if err != nil { panic(fmt.Sprintf("vfs: failed to get executable path: %v", err)) } // If the current executable exists under a different case, we must be case-insensitive. swapped := swapCase(exe) if _, err := os.Stat(swapped); err != nil { if os.IsNotExist(err) { return true } panic(fmt.Sprintf("vfs: failed to stat %q: %v", swapped, err)) } return false }() // Convert all lowercase chars to uppercase, and vice-versa func swapCase(str string) string { return strings.Map(func(r rune) rune { upper := unicode.ToUpper(r) if upper == r { return unicode.ToLower(r) } else { return upper } }, str) } func (vfs *osFS) UseCaseSensitiveFileNames() bool { return isFileSystemCaseSensitive } func (vfs *osFS) ReadFile(path string) (contents string, ok bool) { defer readSema.Acquire()() return vfs.common.ReadFile(path) } func (vfs *osFS) DirectoryExists(path string) bool { defer blockingOpSema.Acquire()() return vfs.common.DirectoryExists(path) } func (vfs *osFS) FileExists(path string) bool { defer blockingOpSema.Acquire()() return vfs.common.FileExists(path) } func (vfs *osFS) GetAccessibleEntries(path string) vfs.Entries { defer blockingOpSema.Acquire()() return vfs.common.GetAccessibleEntries(path) } func (vfs *osFS) Stat(path string) vfs.FileInfo { defer blockingOpSema.Acquire()() return vfs.common.Stat(path) } var limitedWalkDirFuncPool = sync.Pool{ New: func() any { w := &limitedWalkDirFunc{} w.walk = w.walker return w }, } func getLimitedWalkDirFunc(walkFn vfs.WalkDirFunc) *limitedWalkDirFunc { w := limitedWalkDirFuncPool.Get().(*limitedWalkDirFunc) w.inner = walkFn return w } func putLimitedWalkDirFunc(w *limitedWalkDirFunc) { w.inner = nil limitedWalkDirFuncPool.Put(w) } type limitedWalkDirFunc struct { inner vfs.WalkDirFunc walk vfs.WalkDirFunc } func (w *limitedWalkDirFunc) walker(path string, d fs.DirEntry, err error) error { defer blockingOpSema.Acquire()() return w.inner(path, d, err) } func (vfs *osFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { walker := getLimitedWalkDirFunc(walkFn) defer putLimitedWalkDirFunc(walker) return vfs.common.WalkDir(root, walker.walk) } func (vfs *osFS) Realpath(path string) string { defer blockingOpSema.Acquire()() return osFSRealpath(path) } func osFSRealpath(path string) string { _ = internal.RootLength(path) // Assert path is rooted orig := path path = filepath.FromSlash(path) path, err := nativepath.Realpath(path) if err != nil { return orig } path, err = filepath.Abs(path) if err != nil { return orig } return tspath.NormalizeSlashes(path) } func isReparsePoint(path string) bool { return nativepath.IsSymlinkOrReparsePoint(filepath.FromSlash(path)) } func (vfs *osFS) writeFileWithFlag(path string, content string, flag int) error { defer writeSema.Acquire()() file, err := os.OpenFile(path, flag, 0o666) if err != nil { return err } defer file.Close() if _, err := file.WriteString(content); err != nil { return err } return nil } func (vfs *osFS) ensureDirectoryExists(directoryPath string) error { defer blockingOpSema.Acquire()() return os.MkdirAll(directoryPath, 0o777) } func (vfs *osFS) writeFileEnsuringDir(path string, content string, flag int) error { _ = internal.RootLength(path) // Assert path is rooted if err := vfs.writeFileWithFlag(path, content, flag); err == nil { return nil } if err := vfs.ensureDirectoryExists(tspath.GetDirectoryPath(tspath.NormalizePath(path))); err != nil { return err } return vfs.writeFileWithFlag(path, content, flag) } func (vfs *osFS) WriteFile(path string, content string) error { return vfs.writeFileEnsuringDir(path, content, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) } func (vfs *osFS) AppendFile(path string, content string) error { return vfs.writeFileEnsuringDir(path, content, os.O_WRONLY|os.O_CREATE|os.O_APPEND) } func (vfs *osFS) Remove(path string) error { defer blockingOpSema.Acquire()() // todo: #701 add retry mechanism? return os.RemoveAll(path) } func (vfs *osFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { defer blockingOpSema.Acquire()() return os.Chtimes(path, aTime, mTime) } func GetGlobalTypingsCacheLocation() string { cacheDir, err := os.UserCacheDir() if err != nil { cacheDir = os.TempDir() } var subdir string if runtime.GOOS == "windows" { subdir = "Microsoft/TypeScript" } else { subdir = "typescript" } return tspath.CombinePaths(cacheDir, subdir, core.VersionMajorMinor()) }