From 7e293d6ef90a29162cb82dc0d333ebf4e544e8e6 Mon Sep 17 00:00:00 2001 From: miyuko Date: Sat, 7 Feb 2026 13:05:34 +0000 Subject: [PATCH] Normalize archive member names. --- src/extract.go | 32 +++++++++++++++++++------------- src/manifest.go | 3 +++ src/patch.go | 2 +- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/extract.go b/src/extract.go index e3b3a6f..0f1f9d6 100644 --- a/src/extract.go +++ b/src/extract.go @@ -11,6 +11,7 @@ import ( "io" "math" "os" + "path" "strings" "github.com/c2h5oh/datasize" @@ -61,6 +62,16 @@ func (err UnresolvedRefError) Error() string { return fmt.Sprintf("%d unresolved blob references", len(err.missing)) } +func normalizeArchiveMemberName(fileName string) string { + // Strip the leading slash and any extraneous path segments. + fileName = path.Clean(fileName) + fileName = strings.TrimPrefix(fileName, "/") + if fileName == "." { + fileName = "" + } + return fileName +} + // Returns a map of git hash to entry. If `manifest` is nil, returns an empty map. func indexManifestByGitHash(manifest *Manifest) map[string]*Entry { index := map[string]*Entry{} @@ -110,15 +121,10 @@ func ExtractTar(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* return nil, err } - // For some reason, GNU tar includes any leading `.` path segments in archive filenames, - // unless there is a `..` path segment anywhere in the input filenames. - fileName := header.Name - for { - if strippedName, found := strings.CutPrefix(fileName, "./"); found { - fileName = strippedName - } else { - break - } + fileName := normalizeArchiveMemberName(header.Name) + if fileName == "" { + // This must be the root directory. It will be filled in by EnsureLeadingDirectories. + continue } switch header.Typeflag { @@ -200,8 +206,9 @@ func ExtractZip(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* missing := []string{} manifest := NewManifest() for _, file := range archive.File { + normalizedName := normalizeArchiveMemberName(file.Name) if strings.HasSuffix(file.Name, "/") { - AddDirectory(manifest, file.Name) + AddDirectory(manifest, normalizedName) } else { fileReader, err := file.Open() if err != nil { @@ -216,10 +223,10 @@ func ExtractZip(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* if file.Mode()&os.ModeSymlink != 0 { entry := addSymlinkOrBlobReference( - manifest, file.Name, string(fileData), index, &missing) + manifest, normalizedName, string(fileData), index, &missing) dataBytesRecycled += entry.GetOriginalSize() } else { - AddFile(manifest, file.Name, fileData) + AddFile(manifest, normalizedName, fileData) dataBytesTransferred += int64(len(fileData)) } } @@ -240,4 +247,3 @@ func ExtractZip(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* return manifest, nil } - diff --git a/src/manifest.go b/src/manifest.go index 024db67..b6e3788 100644 --- a/src/manifest.go +++ b/src/manifest.go @@ -151,6 +151,9 @@ func AddProblem(manifest *Manifest, pathName, format string, args ...any) error func EnsureLeadingDirectories(manifest *Manifest) { for name := range manifest.Contents { for dir := path.Dir(name); dir != "." && dir != ""; dir = path.Dir(dir) { + if dir == "/" { + panic("malformed manifest (paths must not be rooted in /)") + } if _, exists := manifest.Contents[dir]; !exists { AddDirectory(manifest, dir) } diff --git a/src/patch.go b/src/patch.go index 3c8b2c9..4ea5d01 100644 --- a/src/patch.go +++ b/src/patch.go @@ -70,7 +70,7 @@ func ApplyTarPatch(manifest *Manifest, reader io.Reader, parents CreateParentsMo return err } - segments := strings.Split(strings.TrimRight(header.Name, "/"), "/") + segments := strings.Split(normalizeArchiveMemberName(header.Name), "/") fileName := segments[len(segments)-1] node := root for index, segment := range segments[:len(segments)-1] {