aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/cacheitem/restore_directory.go
diff options
context:
space:
mode:
author简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
committer简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
commitdd84b9d64fb98746a230cd24233ff50a562c39c9 (patch)
treeb583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/cacheitem/restore_directory.go
parent0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff)
downloadHydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz
HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip
Diffstat (limited to 'cli/internal/cacheitem/restore_directory.go')
-rw-r--r--cli/internal/cacheitem/restore_directory.go144
1 files changed, 144 insertions, 0 deletions
diff --git a/cli/internal/cacheitem/restore_directory.go b/cli/internal/cacheitem/restore_directory.go
new file mode 100644
index 0000000..4704d66
--- /dev/null
+++ b/cli/internal/cacheitem/restore_directory.go
@@ -0,0 +1,144 @@
+package cacheitem
+
+import (
+ "archive/tar"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/vercel/turbo/cli/internal/turbopath"
+)
+
+// restoreDirectory restores a directory.
+func restoreDirectory(dirCache *cachedDirTree, anchor turbopath.AbsoluteSystemPath, header *tar.Header) (turbopath.AnchoredSystemPath, error) {
+ processedName, err := canonicalizeName(header.Name)
+ if err != nil {
+ return "", err
+ }
+
+ // We need to traverse `processedName` from base to root split at
+ // `os.Separator` to make sure we don't end up following a symlink
+ // outside of the restore path.
+
+ // Create the directory.
+ if err := safeMkdirAll(dirCache, anchor, processedName, header.Mode); err != nil {
+ return "", err
+ }
+
+ return processedName, nil
+}
+
+type cachedDirTree struct {
+ anchorAtDepth []turbopath.AbsoluteSystemPath
+ prefix []turbopath.RelativeSystemPath
+}
+
+func (cr *cachedDirTree) getStartingPoint(path turbopath.AnchoredSystemPath) (turbopath.AbsoluteSystemPath, []turbopath.RelativeSystemPath) {
+ pathSegmentStrings := strings.Split(path.ToString(), string(os.PathSeparator))
+ pathSegments := make([]turbopath.RelativeSystemPath, len(pathSegmentStrings))
+ for index, pathSegmentString := range pathSegmentStrings {
+ pathSegments[index] = turbopath.RelativeSystemPathFromUpstream(pathSegmentString)
+ }
+
+ i := 0
+ for i = 0; i < len(cr.prefix) && i < len(pathSegments); i++ {
+ if pathSegments[i] != cr.prefix[i] {
+ break
+ }
+ }
+
+ // 0: root anchor, can't remove it.
+ cr.anchorAtDepth = cr.anchorAtDepth[:i+1]
+
+ // 0: first prefix.
+ cr.prefix = cr.prefix[:i]
+
+ return cr.anchorAtDepth[i], pathSegments[i:]
+}
+
+func (cr *cachedDirTree) Update(anchor turbopath.AbsoluteSystemPath, newSegment turbopath.RelativeSystemPath) {
+ cr.anchorAtDepth = append(cr.anchorAtDepth, anchor)
+ cr.prefix = append(cr.prefix, newSegment)
+}
+
+// safeMkdirAll creates all directories, assuming that the leaf node is a directory.
+// FIXME: Recheck the symlink cache before creating a directory.
+func safeMkdirAll(dirCache *cachedDirTree, anchor turbopath.AbsoluteSystemPath, processedName turbopath.AnchoredSystemPath, mode int64) error {
+ // Iterate through path segments by os.Separator, appending them onto the anchor.
+ // Check to see if that path segment is a symlink with a target outside of anchor.
+
+ // Pull the iteration starting point from thie directory cache.
+ calculatedAnchor, pathSegments := dirCache.getStartingPoint(processedName)
+ for _, segment := range pathSegments {
+ calculatedAnchor, checkPathErr := checkPath(anchor, calculatedAnchor, segment)
+ // We hit an existing directory or absolute path that was invalid.
+ if checkPathErr != nil {
+ return checkPathErr
+ }
+
+ // Otherwise we continue and check the next segment.
+ dirCache.Update(calculatedAnchor, segment)
+ }
+
+ // If we have made it here we know that it is safe to call os.MkdirAll
+ // on the Join of anchor and processedName.
+ //
+ // This could _still_ error, but we don't care.
+ return processedName.RestoreAnchor(anchor).MkdirAll(os.FileMode(mode))
+}
+
+// checkPath ensures that the resolved path (if restoring symlinks).
+// It makes sure to never traverse outside of the anchor.
+func checkPath(originalAnchor turbopath.AbsoluteSystemPath, accumulatedAnchor turbopath.AbsoluteSystemPath, segment turbopath.RelativeSystemPath) (turbopath.AbsoluteSystemPath, error) {
+ // Check if the segment itself is sneakily an absolute path...
+ // (looking at you, Windows. CON, AUX...)
+ if filepath.IsAbs(segment.ToString()) {
+ return "", errTraversal
+ }
+
+ // Find out if this portion of the path is a symlink.
+ combinedPath := accumulatedAnchor.Join(segment)
+ fileInfo, err := combinedPath.Lstat()
+
+ // Getting an error here means we failed to stat the path.
+ // Assume that means we're safe and continue.
+ if err != nil {
+ return combinedPath, nil
+ }
+
+ // Find out if we have a symlink.
+ isSymlink := fileInfo.Mode()&os.ModeSymlink != 0
+
+ // If we don't have a symlink it's safe.
+ if !isSymlink {
+ return combinedPath, nil
+ }
+
+ // Check to see if the symlink targets outside of the originalAnchor.
+ // We don't do eval symlinks because we could find ourself in a totally
+ // different place.
+
+ // 1. Get the target.
+ linkTarget, readLinkErr := combinedPath.Readlink()
+ if readLinkErr != nil {
+ return "", readLinkErr
+ }
+
+ // 2. See if the target is absolute.
+ if filepath.IsAbs(linkTarget) {
+ absoluteLinkTarget := turbopath.AbsoluteSystemPathFromUpstream(linkTarget)
+ if originalAnchor.HasPrefix(absoluteLinkTarget) {
+ return absoluteLinkTarget, nil
+ }
+ return "", errTraversal
+ }
+
+ // 3. Target is relative (or absolute Windows on a Unix device)
+ relativeLinkTarget := turbopath.RelativeSystemPathFromUpstream(linkTarget)
+ computedTarget := accumulatedAnchor.UntypedJoin(linkTarget)
+ if computedTarget.HasPrefix(originalAnchor) {
+ // Need to recurse and make sure the target doesn't link out.
+ return checkPath(originalAnchor, accumulatedAnchor, relativeLinkTarget)
+ }
+ return "", errTraversal
+}