Skip to content

Commit 8829a25

Browse files
committed
RebaseArchiveEntries: fix archive path rebasing
Handle rebasing from the archive root explicitly so that both relative and absolute entry names are joined to `newBase` with exactly one path separator. Also restrict rebasing to complete path prefixes. The previous use of `strings.Replace` could replace `oldBase` outside the beginning of the name or as part of another path component, such as rebasing `origin` in `original/file`. Apply the same handling to hardlink targets, while preserving the remainder of archive paths without cleaning or canonicalizing them. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent c583b20 commit 8829a25

2 files changed

Lines changed: 141 additions & 12 deletions

File tree

copy.go

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -316,19 +316,40 @@ func PrepareArchiveCopy(srcContent io.Reader, srcInfo, dstInfo CopyInfo) (dstDir
316316
}
317317
}
318318

319+
// newNameRebaser returns a function that replaces oldBase with newBase at the
320+
// beginning of POSIX-style archive entry names. It converts oldBase and newBase
321+
// to forward-slash form and trims trailing slashes.
322+
//
323+
// When rebasing from the archive root, the returned function removes all
324+
// leading slashes from names. It otherwise preserves the remainder verbatim
325+
// and does not clean or canonicalize paths.
326+
func newNameRebaser(oldBase, newBase string) func(string) string {
327+
oldBase = strings.TrimRight(filepath.ToSlash(oldBase), "/")
328+
newBase = strings.TrimRight(filepath.ToSlash(newBase), "/")
329+
330+
if oldBase == "" {
331+
return func(name string) string {
332+
name = strings.TrimLeft(name, "/")
333+
if newBase == "" {
334+
return name
335+
}
336+
return newBase + "/" + name
337+
}
338+
}
339+
340+
return func(name string) string {
341+
suffix, ok := strings.CutPrefix(name, oldBase)
342+
if !ok || suffix != "" && !strings.HasPrefix(suffix, "/") {
343+
return name
344+
}
345+
return newBase + suffix
346+
}
347+
}
348+
319349
// RebaseArchiveEntries rewrites the given srcContent archive replacing
320350
// an occurrence of oldBase with newBase at the beginning of entry names.
321351
func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.ReadCloser {
322-
oldBase = filepath.ToSlash(oldBase)
323-
newBase = filepath.ToSlash(newBase)
324-
325-
if oldBase == "/" {
326-
// If oldBase specifies the root directory, use an empty string as
327-
// oldBase instead so that newBase doesn't replace the path separator
328-
// that all paths will start with.
329-
oldBase = ""
330-
}
331-
352+
rebase := newNameRebaser(oldBase, newBase)
332353
rebased, w := io.Pipe()
333354

334355
go func() {
@@ -356,9 +377,9 @@ func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.Read
356377
//
357378
// To fix, set the format to PAX here. See docker/for-linux issue #484.
358379
hdr.Format = tar.FormatPAX
359-
hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1)
380+
hdr.Name = rebase(hdr.Name)
360381
if hdr.Typeflag == tar.TypeLink {
361-
hdr.Linkname = strings.Replace(hdr.Linkname, oldBase, newBase, 1)
382+
hdr.Linkname = rebase(hdr.Linkname)
362383
}
363384

364385
if err = rebasedTar.WriteHeader(hdr); err != nil {

copy_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,47 @@ func TestRebaseArchiveEntriesPlatformPaths(t *testing.T) {
2020
wantName string
2121
wantLinkName string
2222
}{
23+
{
24+
name: "rebase relative name from root",
25+
oldBase: "/",
26+
newBase: "prefix",
27+
headerName: "foo/bar",
28+
wantName: "prefix/foo/bar",
29+
},
30+
{
31+
name: "rebase absolute name from root",
32+
oldBase: "/",
33+
newBase: "prefix",
34+
headerName: "/foo/bar",
35+
wantName: "prefix/foo/bar",
36+
},
37+
{
38+
name: "rebase name with multiple leading slashes from root",
39+
oldBase: "/",
40+
newBase: "prefix",
41+
headerName: "///foo/bar",
42+
wantName: "prefix/foo/bar",
43+
},
44+
{
45+
name: "root with multiple trailing slashes",
46+
oldBase: "///",
47+
newBase: "prefix",
48+
headerName: "/foo/bar",
49+
wantName: "prefix/foo/bar",
50+
},
51+
{
52+
name: "rebase absolute name from root to empty",
53+
oldBase: "/",
54+
newBase: "",
55+
headerName: "/foo/bar",
56+
wantName: "foo/bar",
57+
},
58+
{
59+
name: "rebase absolute name from root to empty",
60+
oldBase: "/",
61+
headerName: "/foo/bar",
62+
wantName: "foo/bar",
63+
},
2364
{
2465
name: "regular file",
2566
oldBase: filepath.Join("origin", "subdir"),
@@ -34,6 +75,55 @@ func TestRebaseArchiveEntriesPlatformPaths(t *testing.T) {
3475
headerName: "origin/subdir/file",
3576
wantName: "dest/target/file",
3677
},
78+
{
79+
name: "name equals old base",
80+
oldBase: "origin",
81+
newBase: "dest",
82+
headerName: "origin",
83+
wantName: "dest",
84+
},
85+
{
86+
name: "old base not at beginning",
87+
oldBase: "origin",
88+
newBase: "dest",
89+
headerName: "other/origin/file",
90+
wantName: "other/origin/file",
91+
},
92+
{
93+
name: "old base is partial path component",
94+
oldBase: "origin",
95+
newBase: "dest",
96+
headerName: "original/file",
97+
wantName: "original/file",
98+
},
99+
{
100+
name: "old base with multiple trailing slashes",
101+
oldBase: "origin///",
102+
newBase: "dest",
103+
headerName: "origin/file",
104+
wantName: "dest/file",
105+
},
106+
{
107+
name: "new base with multiple trailing slashes",
108+
oldBase: "origin",
109+
newBase: "dest///",
110+
headerName: "origin/file",
111+
wantName: "dest/file",
112+
},
113+
{
114+
name: "preserve unclean name",
115+
oldBase: "/",
116+
newBase: "prefix",
117+
headerName: "foo/../bar",
118+
wantName: "prefix/foo/../bar",
119+
},
120+
{
121+
name: "do not rebase partial path component match",
122+
oldBase: "foo",
123+
newBase: "prefix",
124+
headerName: "foobar/baz",
125+
wantName: "foobar/baz",
126+
},
37127
{
38128
name: "hardlink",
39129
oldBase: filepath.Join("origin", "subdir"),
@@ -52,6 +142,24 @@ func TestRebaseArchiveEntriesPlatformPaths(t *testing.T) {
52142
wantName: "dest/target/link",
53143
wantLinkName: "dest/target/file",
54144
},
145+
{
146+
name: "hardlink rebase relative names from root",
147+
oldBase: "/",
148+
newBase: "prefix",
149+
headerName: "link",
150+
linkName: "target",
151+
wantName: "prefix/link",
152+
wantLinkName: "prefix/target",
153+
},
154+
{
155+
name: "hardlink rebase absolute names from root",
156+
oldBase: "/",
157+
newBase: "prefix",
158+
headerName: "/link",
159+
linkName: "/target",
160+
wantName: "prefix/link",
161+
wantLinkName: "prefix/target",
162+
},
55163
}
56164

57165
for _, tc := range cases {

0 commit comments

Comments
 (0)