SandpointsGitHook/vendor/github.com/evanw/esbuild/internal/fs/fs_zip.go

404 lines
10 KiB
Go

package fs
// The Yarn package manager (https://yarnpkg.com/) has a custom installation
// strategy called "Plug'n'Play" where they install packages as zip files
// instead of directory trees, and then modify node to treat zip files like
// directories. This reduces package installation time because Yarn now only
// has to copy a single file per package instead of a whole directory tree.
// However, it introduces overhead at run-time because the virtual file system
// is written in JavaScript.
//
// This file contains esbuild's implementation of the behavior that treats zip
// files like directories. It implements the "FS" interface and wraps an inner
// "FS" interface that treats zip files like files. That way it can run both on
// a real file system and a mock file system.
//
// This file also implements another Yarn-specific behavior where certain paths
// containing the special path segments "__virtual__" or "$$virtual" have some
// unusual behavior. See the code below for details.
import (
"archive/zip"
"io/ioutil"
"strconv"
"strings"
"sync"
"syscall"
)
type zipFS struct {
inner FS
zipFilesMutex sync.Mutex
zipFiles map[string]*zipFile
}
type zipFile struct {
reader *zip.ReadCloser
err error
dirs map[string]*compressedDir
files map[string]*compressedFile
wait sync.WaitGroup
}
type compressedDir struct {
entries map[string]EntryKind
path string
// Compatible entries are decoded lazily
mutex sync.Mutex
dirEntries DirEntries
}
type compressedFile struct {
compressed *zip.File
// The file is decompressed lazily
mutex sync.Mutex
contents string
err error
wasRead bool
}
func (fs *zipFS) checkForZip(path string, kind EntryKind) (*zipFile, string) {
var zipPath string
var pathTail string
// Do a quick check for a ".zip" in the path at all
path = strings.ReplaceAll(path, "\\", "/")
if i := strings.Index(path, ".zip/"); i != -1 {
zipPath = path[:i+len(".zip")]
pathTail = path[i+len(".zip/"):]
} else if kind == DirEntry && strings.HasSuffix(path, ".zip") {
zipPath = path
} else {
return nil, ""
}
// If there is one, then check whether it's a file on the file system or not
fs.zipFilesMutex.Lock()
archive := fs.zipFiles[zipPath]
if archive != nil {
fs.zipFilesMutex.Unlock()
archive.wait.Wait()
} else {
archive = &zipFile{}
archive.wait.Add(1)
fs.zipFiles[zipPath] = archive
fs.zipFilesMutex.Unlock()
defer archive.wait.Done()
// Try reading the zip archive if it's not in the cache
tryToReadZipArchive(zipPath, archive)
}
if archive.err != nil {
return nil, ""
}
return archive, pathTail
}
func tryToReadZipArchive(zipPath string, archive *zipFile) {
reader, err := zip.OpenReader(zipPath)
if err != nil {
archive.err = err
return
}
dirs := make(map[string]*compressedDir)
files := make(map[string]*compressedFile)
seeds := []string{}
// Build an index of all files in the archive
for _, file := range reader.File {
baseName := file.Name
if strings.HasSuffix(baseName, "/") {
baseName = baseName[:len(baseName)-1]
}
dirPath := ""
if slash := strings.LastIndexByte(baseName, '/'); slash != -1 {
dirPath = baseName[:slash]
baseName = baseName[slash+1:]
}
if file.FileInfo().IsDir() {
// Handle a directory
lowerDir := strings.ToLower(dirPath)
if _, ok := dirs[lowerDir]; !ok {
dir := &compressedDir{
path: dirPath,
entries: make(map[string]EntryKind),
}
// List the same directory both with and without the slash
dirs[lowerDir] = dir
dirs[lowerDir+"/"] = dir
seeds = append(seeds, lowerDir)
}
} else {
// Handle a file
files[strings.ToLower(file.Name)] = &compressedFile{compressed: file}
lowerDir := strings.ToLower(dirPath)
dir, ok := dirs[lowerDir]
if !ok {
dir = &compressedDir{
path: dirPath,
entries: make(map[string]EntryKind),
}
// List the same directory both with and without the slash
dirs[lowerDir] = dir
dirs[lowerDir+"/"] = dir
seeds = append(seeds, lowerDir)
}
dir.entries[baseName] = FileEntry
}
}
// Populate child directories
for _, baseName := range seeds {
for baseName != "" {
dirPath := ""
if slash := strings.LastIndexByte(baseName, '/'); slash != -1 {
dirPath = baseName[:slash]
baseName = baseName[slash+1:]
}
lowerDir := strings.ToLower(dirPath)
dir, ok := dirs[lowerDir]
if !ok {
dir = &compressedDir{
path: dirPath,
entries: make(map[string]EntryKind),
}
// List the same directory both with and without the slash
dirs[lowerDir] = dir
dirs[lowerDir+"/"] = dir
}
dir.entries[baseName] = DirEntry
baseName = dirPath
}
}
archive.dirs = dirs
archive.files = files
archive.reader = reader
}
func (fs *zipFS) ReadDirectory(path string) (entries DirEntries, canonicalError error, originalError error) {
path = mangleYarnPnPVirtualPath(path)
entries, canonicalError, originalError = fs.inner.ReadDirectory(path)
// Only continue if reading this path as a directory caused an error that's
// consistent with trying to read a zip file as a directory. Note that EINVAL
// is produced by the file system in Go's WebAssembly implementation.
if canonicalError != syscall.ENOENT && canonicalError != syscall.ENOTDIR && canonicalError != syscall.EINVAL {
return
}
// If the directory doesn't exist, try reading from an enclosing zip archive
zip, pathTail := fs.checkForZip(path, DirEntry)
if zip == nil {
return
}
// Does the zip archive have this directory?
dir, ok := zip.dirs[strings.ToLower(pathTail)]
if !ok {
return DirEntries{}, syscall.ENOENT, syscall.ENOENT
}
// Check whether it has already been converted
dir.mutex.Lock()
defer dir.mutex.Unlock()
if dir.dirEntries.data != nil {
return dir.dirEntries, nil, nil
}
// Otherwise, fill in the entries
dir.dirEntries = DirEntries{dir: path, data: make(map[string]*Entry, len(dir.entries))}
for name, kind := range dir.entries {
dir.dirEntries.data[strings.ToLower(name)] = &Entry{
dir: path,
base: name,
kind: kind,
}
}
return dir.dirEntries, nil, nil
}
func (fs *zipFS) ReadFile(path string) (contents string, canonicalError error, originalError error) {
path = mangleYarnPnPVirtualPath(path)
contents, canonicalError, originalError = fs.inner.ReadFile(path)
if canonicalError != syscall.ENOENT {
return
}
// If the file doesn't exist, try reading from an enclosing zip archive
zip, pathTail := fs.checkForZip(path, FileEntry)
if zip == nil {
return
}
// Does the zip archive have this file?
file, ok := zip.files[strings.ToLower(pathTail)]
if !ok {
return "", syscall.ENOENT, syscall.ENOENT
}
// Check whether it has already been read
file.mutex.Lock()
defer file.mutex.Unlock()
if file.wasRead {
return file.contents, file.err, file.err
}
file.wasRead = true
// If not, try to open it
reader, err := file.compressed.Open()
if err != nil {
file.err = err
return "", err, err
}
defer reader.Close()
// Then try to read it
bytes, err := ioutil.ReadAll(reader)
if err != nil {
file.err = err
return "", err, err
}
file.contents = string(bytes)
return file.contents, nil, nil
}
func (fs *zipFS) OpenFile(path string) (result OpenedFile, canonicalError error, originalError error) {
path = mangleYarnPnPVirtualPath(path)
result, canonicalError, originalError = fs.inner.OpenFile(path)
return
}
func (fs *zipFS) ModKey(path string) (modKey ModKey, err error) {
path = mangleYarnPnPVirtualPath(path)
modKey, err = fs.inner.ModKey(path)
return
}
func (fs *zipFS) IsAbs(path string) bool {
return fs.inner.IsAbs(path)
}
func (fs *zipFS) Abs(path string) (string, bool) {
return fs.inner.Abs(path)
}
func (fs *zipFS) Dir(path string) string {
if prefix, suffix, ok := ParseYarnPnPVirtualPath(path); ok && suffix == "" {
return prefix
}
return fs.inner.Dir(path)
}
func (fs *zipFS) Base(path string) string {
return fs.inner.Base(path)
}
func (fs *zipFS) Ext(path string) string {
return fs.inner.Ext(path)
}
func (fs *zipFS) Join(parts ...string) string {
return fs.inner.Join(parts...)
}
func (fs *zipFS) Cwd() string {
return fs.inner.Cwd()
}
func (fs *zipFS) Rel(base string, target string) (string, bool) {
return fs.inner.Rel(base, target)
}
func (fs *zipFS) kind(dir string, base string) (symlink string, kind EntryKind) {
return fs.inner.kind(dir, base)
}
func (fs *zipFS) WatchData() WatchData {
return fs.inner.WatchData()
}
func ParseYarnPnPVirtualPath(path string) (string, string, bool) {
i := 0
for {
start := i
slash := strings.IndexAny(path[i:], "/\\")
if slash == -1 {
break
}
i += slash + 1
// Replace the segments "__virtual__/<segment>/<n>" with N times the ".."
// operation. Note: The "__virtual__" folder name appeared with Yarn 3.0.
// Earlier releases used "$$virtual", but it was changed after discovering
// that this pattern triggered bugs in software where paths were used as
// either regexps or replacement. For example, "$$" found in the second
// parameter of "String.prototype.replace" silently turned into "$".
if segment := path[start : i-1]; segment == "__virtual__" || segment == "$$virtual" {
if slash := strings.IndexAny(path[i:], "/\\"); slash != -1 {
var count string
var suffix string
j := i + slash + 1
// Find the range of the count
if slash := strings.IndexAny(path[j:], "/\\"); slash != -1 {
count = path[j : j+slash]
suffix = path[j+slash:]
} else {
count = path[j:]
}
// Parse the count
if n, err := strconv.ParseInt(count, 10, 64); err == nil {
prefix := path[:start]
// Apply N times the ".." operator
for n > 0 && (strings.HasSuffix(prefix, "/") || strings.HasSuffix(prefix, "\\")) {
slash := strings.LastIndexAny(prefix[:len(prefix)-1], "/\\")
if slash == -1 {
break
}
prefix = prefix[:slash+1]
n--
}
// Make sure the prefix and suffix work well when joined together
if suffix == "" && strings.IndexAny(prefix, "/\\") != strings.LastIndexAny(prefix, "/\\") {
prefix = prefix[:len(prefix)-1]
} else if prefix == "" {
prefix = "."
} else if strings.HasPrefix(suffix, "/") || strings.HasPrefix(suffix, "\\") {
suffix = suffix[1:]
}
return prefix, suffix, true
}
}
}
}
return "", "", false
}
func mangleYarnPnPVirtualPath(path string) string {
if prefix, suffix, ok := ParseYarnPnPVirtualPath(path); ok {
return prefix + suffix
}
return path
}