| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package fsys implements a virtual file system that the go command |
| // uses to read source file trees. The virtual file system redirects some |
| // OS file paths to other OS file paths, according to an overlay file. |
| // Editors can use this overlay support to invoke the go command on |
| // temporary files that have been edited but not yet saved into their |
| // final locations. |
| package fsys |
| |
| import ( |
| "cmd/go/internal/str" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "internal/godebug" |
| "io" |
| "io/fs" |
| "iter" |
| "log" |
| "maps" |
| "os" |
| pathpkg "path" |
| "path/filepath" |
| "runtime/debug" |
| "slices" |
| "strings" |
| "sync" |
| "time" |
| ) |
| |
| // Trace emits a trace event for the operation and file path to the trace log, |
| // but only when $GODEBUG contains gofsystrace=1. |
| // The traces are appended to the file named by the $GODEBUG setting gofsystracelog, or else standard error. |
| // For debugging, if the $GODEBUG setting gofsystracestack is non-empty, then trace events for paths |
| // matching that glob pattern (using path.Match) will be followed by a full stack trace. |
| func Trace(op, path string) { |
| if !doTrace { |
| return |
| } |
| traceMu.Lock() |
| defer traceMu.Unlock() |
| fmt.Fprintf(traceFile, "%d gofsystrace %s %s\n", os.Getpid(), op, path) |
| if pattern := gofsystracestack.Value(); pattern != "" { |
| if match, _ := pathpkg.Match(pattern, path); match { |
| traceFile.Write(debug.Stack()) |
| } |
| } |
| } |
| |
| var ( |
| doTrace bool |
| traceFile *os.File |
| traceMu sync.Mutex |
| |
| gofsystrace = godebug.New("#gofsystrace") |
| gofsystracelog = godebug.New("#gofsystracelog") |
| gofsystracestack = godebug.New("#gofsystracestack") |
| ) |
| |
| func init() { |
| if gofsystrace.Value() != "1" { |
| return |
| } |
| doTrace = true |
| if f := gofsystracelog.Value(); f != "" { |
| // Note: No buffering on writes to this file, so no need to worry about closing it at exit. |
| var err error |
| traceFile, err = os.OpenFile(f, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) |
| if err != nil { |
| log.Fatal(err) |
| } |
| } else { |
| traceFile = os.Stderr |
| } |
| } |
| |
| // OverlayFile is the -overlay flag value. |
| // It names a file containing the JSON for an overlayJSON struct. |
| var OverlayFile string |
| |
| // overlayJSON is the format for the -overlay file. |
| type overlayJSON struct { |
| // Replace maps file names observed by Go tools |
| // to the actual files that should be used when those are read. |
| // If the actual name is "", the file should appear to be deleted. |
| Replace map[string]string |
| } |
| |
| // overlay is a list of replacements to be applied, sorted by cmp of the from field. |
| // cmp sorts the filepath.Separator less than any other byte so that x is always |
| // just before any children x/a, x/b, and so on, before x.go. (This would not |
| // be the case with byte-wise sorting, which would produce x, x.go, x/a.) |
| // The sorting lets us find the relevant overlay entry quickly even if it is for a |
| // parent of the path being searched. |
| var overlay []replace |
| |
| // A replace represents a single replaced path. |
| type replace struct { |
| // from is the old path being replaced. |
| // It is an absolute path returned by abs. |
| from string |
| |
| // to is the replacement for the old path. |
| // It is an absolute path returned by abs. |
| // If it is the empty string, the old path appears deleted. |
| // Otherwise the old path appears to be the file named by to. |
| // If to ends in a trailing slash, the overlay code below treats |
| // it as a directory replacement, akin to a bind mount. |
| // However, our processing of external overlay maps removes |
| // such paths by calling abs, except for / or C:\. |
| to string |
| } |
| |
| var binds []replace |
| |
| // Bind makes the virtual file system use dir as if it were mounted at mtpt, |
| // like Plan 9's “bind” or Linux's “mount --bind”, or like os.Symlink |
| // but without the symbolic link. |
| // |
| // For now, the behavior of using Bind on multiple overlapping |
| // mountpoints (for example Bind("x", "/a") and Bind("y", "/a/b")) |
| // is undefined. |
| func Bind(dir, mtpt string) { |
| if dir == "" || mtpt == "" { |
| panic("Bind of empty directory") |
| } |
| binds = append(binds, replace{abs(mtpt), abs(dir)}) |
| } |
| |
| // cwd returns the current directory, caching it on first use. |
| var cwd = sync.OnceValue(cwdOnce) |
| |
| func cwdOnce() string { |
| wd, err := os.Getwd() |
| if err != nil { |
| // Note: cannot import base, so using log.Fatal. |
| log.Fatalf("cannot determine current directory: %v", err) |
| } |
| return wd |
| } |
| |
| // abs returns the absolute form of path, for looking up in the overlay map. |
| // For the most part, this is filepath.Abs and filepath.Clean, |
| // except that Windows requires special handling, as always. |
| func abs(path string) string { |
| if path == "" { |
| return "" |
| } |
| if filepath.IsAbs(path) { |
| return filepath.Clean(path) |
| } |
| |
| dir := cwd() |
| if vol := filepath.VolumeName(dir); vol != "" && (path[0] == '\\' || path[0] == '/') { |
| // path is volume-relative, like `\Temp`. |
| // Connect to volume name to make absolute path. |
| // See go.dev/issue/8130. |
| return filepath.Join(vol, path) |
| } |
| |
| return filepath.Join(dir, path) |
| } |
| |
| func searchcmp(r replace, t string) int { |
| return cmp(r.from, t) |
| } |
| |
| // info is a summary of the known information about a path |
| // being looked up in the virtual file system. |
| type info struct { |
| abs string |
| deleted bool |
| replaced bool |
| dir bool // must be dir |
| file bool // must be file |
| actual string |
| } |
| |
| // stat returns info about the path in the virtual file system. |
| func stat(path string) info { |
| apath := abs(path) |
| if path == "" { |
| return info{abs: apath, actual: path} |
| } |
| |
| // Apply bind replacements before applying overlay. |
| replaced := false |
| for _, r := range binds { |
| if str.HasFilePathPrefix(apath, r.from) { |
| // apath is below r.from. |
| // Replace prefix with r.to and fall through to overlay. |
| apath = r.to + apath[len(r.from):] |
| path = apath |
| replaced = true |
| break |
| } |
| if str.HasFilePathPrefix(r.from, apath) { |
| // apath is above r.from. |
| // Synthesize a directory in case one does not exist. |
| return info{abs: apath, replaced: true, dir: true, actual: path} |
| } |
| } |
| |
| // Binary search for apath to find the nearest relevant entry in the overlay. |
| i, ok := slices.BinarySearchFunc(overlay, apath, searchcmp) |
| if ok { |
| // Exact match; overlay[i].from == apath. |
| r := overlay[i] |
| if r.to == "" { |
| // Deleted. |
| return info{abs: apath, deleted: true} |
| } |
| if strings.HasSuffix(r.to, string(filepath.Separator)) { |
| // Replacement ends in slash, denoting directory. |
| // Note that this is impossible in current overlays since we call abs |
| // and it strips the trailing slashes. But we could support it in the future. |
| return info{abs: apath, replaced: true, dir: true, actual: path} |
| } |
| // Replaced file. |
| return info{abs: apath, replaced: true, file: true, actual: r.to} |
| } |
| if i < len(overlay) && str.HasFilePathPrefix(overlay[i].from, apath) { |
| // Replacement for child path; infer existence of parent directory. |
| return info{abs: apath, replaced: true, dir: true, actual: path} |
| } |
| if i > 0 && str.HasFilePathPrefix(apath, overlay[i-1].from) { |
| // Replacement for parent. |
| r := overlay[i-1] |
| if strings.HasSuffix(r.to, string(filepath.Separator)) { |
| // Parent replaced by directory; apply replacement in our path. |
| // Note that this is impossible in current overlays since we call abs |
| // and it strips the trailing slashes. But we could support it in the future. |
| p := r.to + apath[len(r.from)+1:] |
| return info{abs: apath, replaced: true, actual: p} |
| } |
| // Parent replaced by file; path is deleted. |
| return info{abs: apath, deleted: true} |
| } |
| return info{abs: apath, replaced: replaced, actual: path} |
| } |
| |
| // children returns a sequence of (name, info) |
| // for all the children of the directory i |
| // implied by the overlay. |
| func (i *info) children() iter.Seq2[string, info] { |
| return func(yield func(string, info) bool) { |
| // Build list of directory children implied by the binds. |
| // Binds are not sorted, so just loop over them. |
| var dirs []string |
| for _, m := range binds { |
| if str.HasFilePathPrefix(m.from, i.abs) && m.from != i.abs { |
| name := m.from[len(i.abs)+1:] |
| if i := strings.IndexByte(name, filepath.Separator); i >= 0 { |
| name = name[:i] |
| } |
| dirs = append(dirs, name) |
| } |
| } |
| if len(dirs) > 1 { |
| slices.Sort(dirs) |
| str.Uniq(&dirs) |
| } |
| |
| // Loop looking for next possible child in sorted overlay, |
| // which is previous child plus "\x00". |
| target := i.abs + string(filepath.Separator) + "\x00" |
| for { |
| // Search for next child: first entry in overlay >= target. |
| j, _ := slices.BinarySearchFunc(overlay, target, func(r replace, t string) int { |
| return cmp(r.from, t) |
| }) |
| |
| Loop: |
| // Skip subdirectories with deleted children (but not direct deleted children). |
| for j < len(overlay) && overlay[j].to == "" && str.HasFilePathPrefix(overlay[j].from, i.abs) && strings.Contains(overlay[j].from[len(i.abs)+1:], string(filepath.Separator)) { |
| j++ |
| } |
| if j >= len(overlay) { |
| // Nothing found at all. |
| break |
| } |
| r := overlay[j] |
| if !str.HasFilePathPrefix(r.from, i.abs) { |
| // Next entry in overlay is beyond the directory we want; all done. |
| break |
| } |
| |
| // Found the next child in the directory. |
| // Yield it and its info. |
| name := r.from[len(i.abs)+1:] |
| actual := r.to |
| dir := false |
| if j := strings.IndexByte(name, filepath.Separator); j >= 0 { |
| // Child is multiple levels down, so name must be a directory, |
| // and there is no actual replacement. |
| name = name[:j] |
| dir = true |
| actual = "" |
| } |
| deleted := !dir && r.to == "" |
| ci := info{ |
| abs: filepath.Join(i.abs, name), |
| deleted: deleted, |
| replaced: !deleted, |
| dir: dir || strings.HasSuffix(r.to, string(filepath.Separator)), |
| actual: actual, |
| } |
| for ; len(dirs) > 0 && dirs[0] < name; dirs = dirs[1:] { |
| if !yield(dirs[0], info{abs: filepath.Join(i.abs, dirs[0]), replaced: true, dir: true}) { |
| return |
| } |
| } |
| if len(dirs) > 0 && dirs[0] == name { |
| dirs = dirs[1:] |
| } |
| if !yield(name, ci) { |
| return |
| } |
| |
| // Next target is first name after the one we just returned. |
| target = ci.abs + "\x00" |
| |
| // Optimization: Check whether the very next element |
| // is the next child. If so, skip the binary search. |
| if j+1 < len(overlay) && cmp(overlay[j+1].from, target) >= 0 { |
| j++ |
| goto Loop |
| } |
| } |
| |
| for _, dir := range dirs { |
| if !yield(dir, info{abs: filepath.Join(i.abs, dir), replaced: true, dir: true}) { |
| return |
| } |
| } |
| } |
| } |
| |
| // Init initializes the overlay, if one is being used. |
| func Init() error { |
| if overlay != nil { |
| // already initialized |
| return nil |
| } |
| |
| if OverlayFile == "" { |
| return nil |
| } |
| |
| Trace("ReadFile", OverlayFile) |
| b, err := os.ReadFile(OverlayFile) |
| if err != nil { |
| return fmt.Errorf("reading overlay: %v", err) |
| } |
| return initFromJSON(b) |
| } |
| |
| func initFromJSON(js []byte) error { |
| var ojs overlayJSON |
| if err := json.Unmarshal(js, &ojs); err != nil { |
| return fmt.Errorf("parsing overlay JSON: %v", err) |
| } |
| |
| seen := make(map[string]string) |
| var list []replace |
| for _, from := range slices.Sorted(maps.Keys(ojs.Replace)) { |
| if from == "" { |
| return fmt.Errorf("empty string key in overlay map") |
| } |
| afrom := abs(from) |
| if old, ok := seen[afrom]; ok { |
| return fmt.Errorf("duplicate paths %s and %s in overlay map", old, from) |
| } |
| seen[afrom] = from |
| list = append(list, replace{from: afrom, to: abs(ojs.Replace[from])}) |
| } |
| |
| slices.SortFunc(list, func(x, y replace) int { return cmp(x.from, y.from) }) |
| |
| for i, r := range list { |
| if r.to == "" { // deleted |
| continue |
| } |
| // have file for r.from; look for child file implying r.from is a directory |
| prefix := r.from + string(filepath.Separator) |
| for _, next := range list[i+1:] { |
| if !strings.HasPrefix(next.from, prefix) { |
| break |
| } |
| if next.to != "" { |
| // found child file |
| return fmt.Errorf("inconsistent files %s and %s in overlay map", r.from, next.from) |
| } |
| } |
| } |
| |
| overlay = list |
| return nil |
| } |
| |
| // IsDir returns true if path is a directory on disk or in the |
| // overlay. |
| func IsDir(path string) (bool, error) { |
| Trace("IsDir", path) |
| |
| switch info := stat(path); { |
| case info.dir: |
| return true, nil |
| case info.deleted, info.replaced: |
| return false, nil |
| } |
| |
| info, err := os.Stat(path) |
| if err != nil { |
| return false, err |
| } |
| return info.IsDir(), nil |
| } |
| |
| // errNotDir is used to communicate from ReadDir to IsGoDir |
| // that the argument is not a directory, so that IsGoDir doesn't |
| // return an error. |
| var errNotDir = errors.New("not a directory") |
| |
| // osReadDir is like os.ReadDir corrects the error to be errNotDir |
| // if the problem is that name exists but is not a directory. |
| func osReadDir(name string) ([]fs.DirEntry, error) { |
| dirs, err := os.ReadDir(name) |
| if err != nil && !os.IsNotExist(err) { |
| if info, err := os.Stat(name); err == nil && !info.IsDir() { |
| return nil, &fs.PathError{Op: "ReadDir", Path: name, Err: errNotDir} |
| } |
| } |
| return dirs, err |
| } |
| |
| // ReadDir reads the named directory in the virtual file system. |
| func ReadDir(name string) ([]fs.DirEntry, error) { |
| Trace("ReadDir", name) |
| |
| info := stat(name) |
| if info.deleted { |
| return nil, &fs.PathError{Op: "read", Path: name, Err: fs.ErrNotExist} |
| } |
| if !info.replaced { |
| return osReadDir(name) |
| } |
| if info.file { |
| return nil, &fs.PathError{Op: "read", Path: name, Err: errNotDir} |
| } |
| |
| // Start with normal disk listing. |
| dirs, err := osReadDir(info.actual) |
| if err != nil && !os.IsNotExist(err) && !errors.Is(err, errNotDir) { |
| return nil, err |
| } |
| dirErr := err |
| |
| // Merge disk listing and overlay entries in map. |
| all := make(map[string]fs.DirEntry) |
| for _, d := range dirs { |
| all[d.Name()] = d |
| } |
| for cname, cinfo := range info.children() { |
| if cinfo.dir { |
| all[cname] = fs.FileInfoToDirEntry(fakeDir(cname)) |
| continue |
| } |
| if cinfo.deleted { |
| delete(all, cname) |
| continue |
| } |
| |
| // Overlay is not allowed to have targets that are directories. |
| // And we hide symlinks, although it's not clear it helps callers. |
| cinfo, err := os.Stat(cinfo.actual) |
| if err != nil { |
| all[cname] = fs.FileInfoToDirEntry(missingFile(cname)) |
| continue |
| } |
| if cinfo.IsDir() { |
| return nil, &fs.PathError{Op: "read", Path: name, Err: fmt.Errorf("overlay maps child %s to directory", cname)} |
| } |
| all[cname] = fs.FileInfoToDirEntry(fakeFile{cname, cinfo}) |
| } |
| |
| // Rebuild list using same storage. |
| dirs = dirs[:0] |
| for _, d := range all { |
| dirs = append(dirs, d) |
| } |
| slices.SortFunc(dirs, func(x, y fs.DirEntry) int { return strings.Compare(x.Name(), y.Name()) }) |
| |
| if len(dirs) == 0 { |
| return nil, dirErr |
| } |
| return dirs, nil |
| } |
| |
| // Actual returns the actual file system path for the named file. |
| // It returns the empty string if name has been deleted in the virtual file system. |
| func Actual(name string) string { |
| info := stat(name) |
| if info.deleted { |
| return "" |
| } |
| if info.dir || info.replaced { |
| return info.actual |
| } |
| return name |
| } |
| |
| // Replaced reports whether the named file has been modified |
| // in the virtual file system compared to the OS file system. |
| func Replaced(name string) bool { |
| info := stat(name) |
| return info.deleted || info.replaced && !info.dir |
| } |
| |
| // DirContainsReplacement reports whether the named directory is affected by a replacement, |
| // either because a parent directory has been replaced, it has been replaced, or a file or |
| // directory under it has been replaced. |
| // It is meant to be used to detect cases where GOMODCACHE has been replaced. That replacement |
| // is not supported (GOMODCACHE is meant to be immutable) and the caller will use the |
| // information to return an error. |
| func DirContainsReplacement(name string) (string, bool) { |
| apath := abs(name) |
| |
| // Check the overlay using similar logic to what stat uses. |
| i, ok := slices.BinarySearchFunc(overlay, apath, searchcmp) |
| if ok { |
| // The named directory itself has been replaced. |
| return overlay[i].from, true |
| } |
| if i < len(overlay) && str.HasFilePathPrefix(overlay[i].from, apath) { |
| // A file or directory contained in the named directory has been replaced. |
| return overlay[i].from, true |
| } |
| if i > 0 && str.HasFilePathPrefix(apath, overlay[i-1].from) { |
| // A parent of the named directory has been replaced. |
| return overlay[i-1].from, true |
| } |
| return "", false |
| } |
| |
| // Open opens the named file in the virtual file system. |
| // It must be an ordinary file, not a directory. |
| func Open(name string) (*os.File, error) { |
| Trace("Open", name) |
| |
| bad := func(msg string) (*os.File, error) { |
| return nil, &fs.PathError{ |
| Op: "Open", |
| Path: name, |
| Err: errors.New(msg), |
| } |
| } |
| |
| info := stat(name) |
| if info.deleted { |
| return bad("deleted in overlay") |
| } |
| if info.dir { |
| return bad("cannot open directory in overlay") |
| } |
| if info.replaced { |
| name = info.actual |
| } |
| |
| return os.Open(name) |
| } |
| |
| // ReadFile reads the named file from the virtual file system |
| // and returns the contents. |
| func ReadFile(name string) ([]byte, error) { |
| f, err := Open(name) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() |
| |
| return io.ReadAll(f) |
| } |
| |
| // IsGoDir reports whether the named directory in the virtual file system |
| // is a directory containing one or more Go source files. |
| func IsGoDir(name string) (bool, error) { |
| Trace("IsGoDir", name) |
| fis, err := ReadDir(name) |
| if os.IsNotExist(err) || errors.Is(err, errNotDir) { |
| return false, nil |
| } |
| if err != nil { |
| return false, err |
| } |
| |
| var firstErr error |
| for _, d := range fis { |
| if d.IsDir() || !strings.HasSuffix(d.Name(), ".go") { |
| continue |
| } |
| if d.Type().IsRegular() { |
| return true, nil |
| } |
| |
| // d is a non-directory, non-regular .go file. |
| // Stat to see if it is a symlink, which we allow. |
| if actual := Actual(filepath.Join(name, d.Name())); actual != "" { |
| fi, err := os.Stat(actual) |
| if err == nil && fi.Mode().IsRegular() { |
| return true, nil |
| } |
| if err != nil && firstErr == nil { |
| firstErr = err |
| } |
| } |
| } |
| |
| // No go files found in directory. |
| return false, firstErr |
| } |
| |
| // Lstat returns a FileInfo describing the named file in the virtual file system. |
| // It does not follow symbolic links |
| func Lstat(name string) (fs.FileInfo, error) { |
| Trace("Lstat", name) |
| return overlayStat("lstat", name, os.Lstat) |
| } |
| |
| // Stat returns a FileInfo describing the named file in the virtual file system. |
| // It follows symbolic links. |
| func Stat(name string) (fs.FileInfo, error) { |
| Trace("Stat", name) |
| return overlayStat("stat", name, os.Stat) |
| } |
| |
| // overlayStat implements lstat or Stat (depending on whether os.Lstat or os.Stat is passed in). |
| func overlayStat(op, path string, osStat func(string) (fs.FileInfo, error)) (fs.FileInfo, error) { |
| info := stat(path) |
| if info.deleted { |
| return nil, &fs.PathError{Op: op, Path: path, Err: fs.ErrNotExist} |
| } |
| if info.dir { |
| return fakeDir(filepath.Base(path)), nil |
| } |
| if info.replaced { |
| // To keep the data model simple, if the overlay contains a symlink we |
| // always stat through it (using Stat, not Lstat). That way we don't need to |
| // worry about the interaction between Lstat and directories: if a symlink |
| // in the overlay points to a directory, we reject it like an ordinary |
| // directory. |
| ainfo, err := os.Stat(info.actual) |
| if err != nil { |
| return nil, err |
| } |
| if ainfo.IsDir() { |
| return nil, &fs.PathError{Op: op, Path: path, Err: fmt.Errorf("overlay maps to directory")} |
| } |
| return fakeFile{name: filepath.Base(path), real: ainfo}, nil |
| } |
| return osStat(path) |
| } |
| |
| // fakeFile provides an fs.FileInfo implementation for an overlaid file, |
| // so that the file has the name of the overlaid file, but takes all |
| // other characteristics of the replacement file. |
| type fakeFile struct { |
| name string |
| real fs.FileInfo |
| } |
| |
| func (f fakeFile) Name() string { return f.name } |
| func (f fakeFile) Size() int64 { return f.real.Size() } |
| func (f fakeFile) Mode() fs.FileMode { return f.real.Mode() } |
| func (f fakeFile) ModTime() time.Time { return f.real.ModTime() } |
| func (f fakeFile) IsDir() bool { return f.real.IsDir() } |
| func (f fakeFile) Sys() any { return f.real.Sys() } |
| |
| func (f fakeFile) String() string { |
| return fs.FormatFileInfo(f) |
| } |
| |
| // missingFile provides an fs.FileInfo for an overlaid file where the |
| // destination file in the overlay doesn't exist. It returns zero values |
| // for the fileInfo methods other than Name, set to the file's name, and Mode |
| // set to ModeIrregular. |
| type missingFile string |
| |
| func (f missingFile) Name() string { return string(f) } |
| func (f missingFile) Size() int64 { return 0 } |
| func (f missingFile) Mode() fs.FileMode { return fs.ModeIrregular } |
| func (f missingFile) ModTime() time.Time { return time.Unix(0, 0) } |
| func (f missingFile) IsDir() bool { return false } |
| func (f missingFile) Sys() any { return nil } |
| |
| func (f missingFile) String() string { |
| return fs.FormatFileInfo(f) |
| } |
| |
| // fakeDir provides an fs.FileInfo implementation for directories that are |
| // implicitly created by overlaid files. Each directory in the |
| // path of an overlaid file is considered to exist in the overlay filesystem. |
| type fakeDir string |
| |
| func (f fakeDir) Name() string { return string(f) } |
| func (f fakeDir) Size() int64 { return 0 } |
| func (f fakeDir) Mode() fs.FileMode { return fs.ModeDir | 0500 } |
| func (f fakeDir) ModTime() time.Time { return time.Unix(0, 0) } |
| func (f fakeDir) IsDir() bool { return true } |
| func (f fakeDir) Sys() any { return nil } |
| |
| func (f fakeDir) String() string { |
| return fs.FormatFileInfo(f) |
| } |
| |
| func cmp(x, y string) int { |
| for i := 0; i < len(x) && i < len(y); i++ { |
| xi := int(x[i]) |
| yi := int(y[i]) |
| if xi == filepath.Separator { |
| xi = -1 |
| } |
| if yi == filepath.Separator { |
| yi = -1 |
| } |
| if xi != yi { |
| return xi - yi |
| } |
| } |
| return len(x) - len(y) |
| } |