diff --git a/tools/utils/tar.go b/tools/utils/tar.go index c40e94a48..1cc2d3987 100644 --- a/tools/utils/tar.go +++ b/tools/utils/tar.go @@ -10,6 +10,7 @@ import ( "io/fs" "os" "path/filepath" + "runtime" "strings" ) @@ -19,6 +20,152 @@ type TarExtractOptions struct { DontPreservePermissions bool } +func volnamelen(path string) int { + return len(filepath.VolumeName(path)) +} + +func EvalSymlinksThatExist(path string) (string, error) { + volLen := volnamelen(path) + pathSeparator := string(os.PathSeparator) + + if volLen < len(path) && os.IsPathSeparator(path[volLen]) { + volLen++ + } + vol := path[:volLen] + dest := vol + linksWalked := 0 + for start, end := volLen, volLen; start < len(path); start = end { + for start < len(path) && os.IsPathSeparator(path[start]) { + start++ + } + end = start + for end < len(path) && !os.IsPathSeparator(path[end]) { + end++ + } + + // On Windows, "." can be a symlink. + // We look it up, and use the value if it is absolute. + // If not, we just return ".". + isWindowsDot := runtime.GOOS == "windows" && path[volnamelen(path):] == "." + + // The next path component is in path[start:end]. + if end == start { + // No more path components. + break + } else if path[start:end] == "." && !isWindowsDot { + // Ignore path component ".". + continue + } else if path[start:end] == ".." { + // Back up to previous component if possible. + // Note that volLen includes any leading slash. + + // Set r to the index of the last slash in dest, + // after the volume. + var r int + for r = len(dest) - 1; r >= volLen; r-- { + if os.IsPathSeparator(dest[r]) { + break + } + } + if r < volLen || dest[r+1:] == ".." { + // Either path has no slashes + // (it's empty or just "C:") + // or it ends in a ".." we had to keep. + // Either way, keep this "..". + if len(dest) > volLen { + dest += pathSeparator + } + dest += ".." + } else { + // Discard everything since the last slash. + dest = dest[:r] + } + continue + } + + // Ordinary path component. Add it to result. + + if len(dest) > volnamelen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) { + dest += pathSeparator + } + + dest += path[start:end] + + // Resolve symlink. + + fi, err := os.Lstat(dest) + if err != nil { + if os.IsNotExist(err) { + if end < len(path) { + dest += path[end:] + } + return filepath.Clean(dest), nil + } + return "", err + } + + if fi.Mode()&fs.ModeSymlink == 0 { + if !fi.Mode().IsDir() && end < len(path) { + return "", fmt.Errorf("%s is not a directory while resolving symlinks in %s", dest, path) + } + continue + } + + // Found symlink. + + linksWalked++ + if linksWalked > 255 { + return "", fmt.Errorf("EvalSymlinksThatExist: too many symlinks in %s", path) + } + + link, err := os.Readlink(dest) + if err != nil { + return "", err + } + + if isWindowsDot && !filepath.IsAbs(link) { + // On Windows, if "." is a relative symlink, + // just return ".". + break + } + + path = link + path[end:] + + v := volnamelen(link) + if v > 0 { + // Symlink to drive name is an absolute path. + if v < len(link) && os.IsPathSeparator(link[v]) { + v++ + } + vol = link[:v] + dest = vol + end = len(vol) + } else if len(link) > 0 && os.IsPathSeparator(link[0]) { + // Symlink to absolute path. + dest = link[:1] + end = 1 + vol = link[:1] + volLen = 1 + } else { + // Symlink to relative path; replace last + // path component in dest. + var r int + for r = len(dest) - 1; r >= volLen; r-- { + if os.IsPathSeparator(dest[r]) { + break + } + } + if r < volLen { + dest = vol + } else { + dest = dest[:r] + } + end = 0 + } + } + return filepath.Clean(dest), nil +} + func ExtractAllFromTar(tr *tar.Reader, dest_path string, optss ...TarExtractOptions) (count int, err error) { opts := TarExtractOptions{} if len(optss) > 0 { @@ -55,17 +202,11 @@ func ExtractAllFromTar(tr *tar.Reader, dest_path string, optss ...TarExtractOpti return count, err } dest := hdr.Name - dest = strings.TrimLeft(dest, "/") - if !filepath.IsLocal(dest) { - continue + if !filepath.IsAbs(dest) { + dest = filepath.Join(dest_path, dest) } - dest = filepath.Join(dest_path, dest) - if dest, err = filepath.EvalSymlinks(dest); err != nil { - if os.IsNotExist(err) { - err = nil - } else { - return count, err - } + if dest, err = EvalSymlinksThatExist(dest); err != nil { + return count, err } if !strings.HasPrefix(filepath.Clean(dest), filepath.Clean(dest_path)+string(os.PathSeparator)) { continue diff --git a/tools/utils/tar_test.go b/tools/utils/tar_test.go new file mode 100644 index 000000000..cde6bc261 --- /dev/null +++ b/tools/utils/tar_test.go @@ -0,0 +1,79 @@ +package utils + +import ( + "archive/tar" + "bytes" + "fmt" + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +var _ = fmt.Print + +func TestTarExtract(t *testing.T) { + tdir := t.TempDir() + a, b := filepath.Join(tdir, "a"), filepath.Join(tdir, "b") + if err := os.Mkdir(a, 0700); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(b, 0700); err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + var files = []struct { + name, body string + }{ + {"s/one.txt", "This archive contains some text files."}, + {"b", b}, + {"b/two.txt", "Get animal handling license."}, + {"../b/three.txt", "Get animal handling license."}, + {"nested/dir/", ""}, + } + for _, file := range files { + hdr := &tar.Header{ + Name: file.name, + Mode: 0600, + Size: int64(len(file.body)), + } + if file.name == "b" { + hdr.Linkname = file.body + hdr.Typeflag = tar.TypeSymlink + hdr.Size = 0 + } + if err := tw.WriteHeader(hdr); err != nil { + t.Fatal(err) + } + if hdr.Typeflag != tar.TypeSymlink && len(file.body) > 0 { + if _, err := tw.Write([]byte(file.body)); err != nil { + t.Fatal(err) + } + } + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + tr := tar.NewReader(&buf) + count, err := ExtractAllFromTar(tr, a) + if err != nil { + t.Fatal(err) + } + if count != len(files)-2 { + t.Fatalf("Incorrect count of extracted files: %d != %d", count, len(files)-2) + } + entries := []string{} + if err = fs.WalkDir(os.DirFS(tdir), ".", func(path string, d fs.DirEntry, err error) error { + entries = append(entries, path) + return err + }, + ); err != nil { + t.Fatal(err) + } + if diff := cmp.Diff([]string{".", "a", "a/b", "a/nested", "a/nested/dir", "a/s", "a/s/one.txt", "b"}, entries); diff != "" { + t.Fatalf("Directory contents not as expected: %s", diff) + } +}