Skip to content

Commit abc9b6e

Browse files
ForestEckhardtsophiewigmore
authored andcommitted
Adds logic to untar hard links
-Zip does not support hard links so this is a consideration only needed for TAR
1 parent 3f1daee commit abc9b6e

7 files changed

+137
-53
lines changed

vacation/init_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
func TestVacation(t *testing.T) {
1111
suite := spec.New("vacation", spec.Report(report.Terminal{}))
1212
suite("Archive", testArchive)
13+
suite("LinkSorting", testLinkSorting)
1314
suite("NopArchive", testNopArchive)
14-
suite("SymlinkSorting", testSymlinkSorting)
1515
suite("TarArchive", testTarArchive)
1616
suite("TarBzip2Archive", testTarBzip2Archive)
1717
suite("TarGzipArchive", testTarGzipArchive)

vacation/symlink_sorting.go vacation/link_sorting.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import (
66
"strings"
77
)
88

9-
type symlink struct {
9+
type link struct {
1010
name string
1111
path string
1212
}
1313

14-
func sortSymlinks(symlinks []symlink) ([]symlink, error) {
14+
func sortLinks(symlinks []link) ([]link, error) {
1515
// Create a map of all of the symlink names and where they are pointing to to
1616
// act as a quasi-graph
1717
index := map[string]string{}
@@ -40,7 +40,7 @@ func sortSymlinks(symlinks []symlink) ([]symlink, error) {
4040
// Iterate over the symlink map for every link that is found this ensures
4141
// that all symlinks that can be created will be created and any that are
4242
// left over are cyclically dependent
43-
var links []symlink
43+
var links []link
4444
maxIterations := len(index)
4545
for i := 0; i < maxIterations; i++ {
4646
for path, name := range index {
@@ -50,7 +50,7 @@ func sortSymlinks(symlinks []symlink) ([]symlink, error) {
5050
continue
5151
}
5252

53-
links = append(links, symlink{
53+
links = append(links, link{
5454
name: name,
5555
path: path,
5656
})
@@ -65,7 +65,7 @@ func sortSymlinks(symlinks []symlink) ([]symlink, error) {
6565
// Check to see if there are any symlinks left in the map which would
6666
// indicate a cyclical dependency
6767
if len(index) > 0 {
68-
return nil, fmt.Errorf("failed: max iterations reached: this symlink graph contains a cycle")
68+
return nil, fmt.Errorf("failed: max iterations reached: this link graph contains a cycle")
6969
}
7070

7171
return links, nil

vacation/symlink_sorting_test.go vacation/link_sorting_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
. "github.com/onsi/gomega"
1616
)
1717

18-
func testSymlinkSorting(t *testing.T, context spec.G, it spec.S) {
18+
func testLinkSorting(t *testing.T, context spec.G, it spec.S) {
1919
var (
2020
Expect = NewWithT(t).Expect
2121
)

vacation/tar_archive.go

+24-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ func (ta TarArchive) Decompress(destination string) error {
3030
// metadata.
3131
directories := map[string]interface{}{}
3232

33-
var symlinks []symlink
33+
var symlinks []link
34+
var links []link
3435

3536
tarReader := tar.NewReader(ta.reader)
3637
for {
@@ -106,17 +107,25 @@ func (ta TarArchive) Decompress(destination string) error {
106107
return err
107108
}
108109

110+
case tar.TypeLink:
111+
// Collect all of the headers for links so that they can be verified
112+
// after all other files are written
113+
links = append(links, link{
114+
name: hdr.Linkname,
115+
path: path,
116+
})
117+
109118
case tar.TypeSymlink:
110119
// Collect all of the headers for symlinks so that they can be verified
111120
// after all other files are written
112-
symlinks = append(symlinks, symlink{
121+
symlinks = append(symlinks, link{
113122
name: hdr.Linkname,
114123
path: path,
115124
})
116125
}
117126
}
118127

119-
symlinks, err := sortSymlinks(symlinks)
128+
symlinks, err := sortLinks(symlinks)
120129
if err != nil {
121130
return err
122131
}
@@ -134,6 +143,18 @@ func (ta TarArchive) Decompress(destination string) error {
134143
}
135144
}
136145

146+
links, err = sortLinks(links)
147+
if err != nil {
148+
return err
149+
}
150+
151+
for _, link := range links {
152+
err := os.Link(linknameFullPath(link.path, link.name), link.path)
153+
if err != nil {
154+
return fmt.Errorf("failed to extract link: %s", err)
155+
}
156+
}
157+
137158
return nil
138159
}
139160

vacation/tar_archive_test.go

+74-12
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ func testTarArchive(t *testing.T, context spec.G, it spec.S) {
5151
_, err = tw.Write([]byte{})
5252
Expect(err).NotTo(HaveOccurred())
5353

54+
Expect(tw.WriteHeader(&tar.Header{Name: "hardlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeLink, Linkname: "first"})).To(Succeed())
55+
_, err = tw.Write([]byte{})
56+
Expect(err).NotTo(HaveOccurred())
57+
5458
nestedFile := filepath.Join("some-dir", "some-other-dir", "some-file")
5559
Expect(tw.WriteHeader(&tar.Header{Name: nestedFile, Mode: 0755, Size: int64(len(nestedFile))})).To(Succeed())
5660
_, err = tw.Write([]byte(nestedFile))
@@ -80,10 +84,11 @@ func testTarArchive(t *testing.T, context spec.G, it spec.S) {
8084
Expect(err).NotTo(HaveOccurred())
8185
Expect(files).To(ConsistOf([]string{
8286
filepath.Join(tempDir, "first"),
87+
filepath.Join(tempDir, "hardlink"),
8388
filepath.Join(tempDir, "second"),
84-
filepath.Join(tempDir, "third"),
8589
filepath.Join(tempDir, "some-dir"),
8690
filepath.Join(tempDir, "symlink"),
91+
filepath.Join(tempDir, "third"),
8792
}))
8893

8994
info, err := os.Stat(filepath.Join(tempDir, "first"))
@@ -96,6 +101,10 @@ func testTarArchive(t *testing.T, context spec.G, it spec.S) {
96101
data, err := os.ReadFile(filepath.Join(tempDir, "symlink"))
97102
Expect(err).NotTo(HaveOccurred())
98103
Expect(data).To(Equal([]byte(`first`)))
104+
105+
data, err = os.ReadFile(filepath.Join(tempDir, "hardlink"))
106+
Expect(err).NotTo(HaveOccurred())
107+
Expect(data).To(Equal([]byte(`first`)))
99108
})
100109

101110
it("unpackages the archive into the path but also strips the first component", func() {
@@ -235,26 +244,54 @@ func testTarArchive(t *testing.T, context spec.G, it spec.S) {
235244
})
236245
})
237246

247+
context("when there is a symlink cycle", func() {
248+
var cyclicalSymlinkTar vacation.TarArchive
249+
250+
it.Before(func() {
251+
var err error
252+
253+
buffer := bytes.NewBuffer(nil)
254+
tw := tar.NewWriter(buffer)
255+
256+
Expect(tw.WriteHeader(&tar.Header{Name: "a-symlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: "b-symlink"})).To(Succeed())
257+
_, err = tw.Write([]byte{})
258+
Expect(err).NotTo(HaveOccurred())
259+
260+
Expect(tw.WriteHeader(&tar.Header{Name: "b-symlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: "a-symlink"})).To(Succeed())
261+
_, err = tw.Write([]byte{})
262+
Expect(err).NotTo(HaveOccurred())
263+
264+
Expect(tw.Close()).To(Succeed())
265+
266+
cyclicalSymlinkTar = vacation.NewTarArchive(bytes.NewReader(buffer.Bytes()))
267+
})
268+
269+
it("returns an error", func() {
270+
err := cyclicalSymlinkTar.Decompress(tempDir)
271+
Expect(err).To(MatchError(ContainSubstring("failed: max iterations reached: this link graph contains a cycle")))
272+
})
273+
})
274+
238275
context("when it tries to symlink to a file that does not exist", func() {
239-
var zipSlipSymlinkTar vacation.TarArchive
276+
var symlinkNotExistTar vacation.TarArchive
240277

241278
it.Before(func() {
242279
var err error
243280

244281
buffer := bytes.NewBuffer(nil)
245282
tw := tar.NewWriter(buffer)
246283

247-
Expect(tw.WriteHeader(&tar.Header{Name: "symlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: filepath.Join("..", "some-file")})).To(Succeed())
284+
Expect(tw.WriteHeader(&tar.Header{Name: "symlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: "some-file"})).To(Succeed())
248285
_, err = tw.Write([]byte{})
249286
Expect(err).NotTo(HaveOccurred())
250287

251288
Expect(tw.Close()).To(Succeed())
252289

253-
zipSlipSymlinkTar = vacation.NewTarArchive(bytes.NewReader(buffer.Bytes()))
290+
symlinkNotExistTar = vacation.NewTarArchive(bytes.NewReader(buffer.Bytes()))
254291
})
255292

256293
it("returns an error", func() {
257-
err := zipSlipSymlinkTar.Decompress(tempDir)
294+
err := symlinkNotExistTar.Decompress(tempDir)
258295
Expect(err).To(MatchError(ContainSubstring("failed to evaluate symlink")))
259296
Expect(err).To(MatchError(ContainSubstring("no such file or directory")))
260297
})
@@ -290,31 +327,56 @@ func testTarArchive(t *testing.T, context spec.G, it spec.S) {
290327
})
291328
})
292329

293-
context("when there is a symlink cycle", func() {
294-
var cyclicalSymlinkTar vacation.TarArchive
330+
context("when there is a link cycle", func() {
331+
var cyclicalLinkTar vacation.TarArchive
295332

296333
it.Before(func() {
297334
var err error
298335

299336
buffer := bytes.NewBuffer(nil)
300337
tw := tar.NewWriter(buffer)
301338

302-
Expect(tw.WriteHeader(&tar.Header{Name: "a-symlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: "b-symlink"})).To(Succeed())
339+
Expect(tw.WriteHeader(&tar.Header{Name: "a-link", Mode: 0755, Size: int64(0), Typeflag: tar.TypeLink, Linkname: "b-link"})).To(Succeed())
303340
_, err = tw.Write([]byte{})
304341
Expect(err).NotTo(HaveOccurred())
305342

306-
Expect(tw.WriteHeader(&tar.Header{Name: "b-symlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: "a-symlink"})).To(Succeed())
343+
Expect(tw.WriteHeader(&tar.Header{Name: "b-link", Mode: 0755, Size: int64(0), Typeflag: tar.TypeLink, Linkname: "a-link"})).To(Succeed())
344+
_, err = tw.Write([]byte{})
345+
Expect(err).NotTo(HaveOccurred())
346+
347+
Expect(tw.Close()).To(Succeed())
348+
349+
cyclicalLinkTar = vacation.NewTarArchive(bytes.NewReader(buffer.Bytes()))
350+
})
351+
352+
it("returns an error", func() {
353+
err := cyclicalLinkTar.Decompress(tempDir)
354+
Expect(err).To(MatchError(ContainSubstring("failed: max iterations reached: this link graph contains a cycle")))
355+
})
356+
})
357+
358+
context("when it tries to symlink to a file that does not exist", func() {
359+
var linkNotExistTar vacation.TarArchive
360+
361+
it.Before(func() {
362+
var err error
363+
364+
buffer := bytes.NewBuffer(nil)
365+
tw := tar.NewWriter(buffer)
366+
367+
Expect(tw.WriteHeader(&tar.Header{Name: "link", Mode: 0755, Size: int64(0), Typeflag: tar.TypeLink, Linkname: "some-file"})).To(Succeed())
307368
_, err = tw.Write([]byte{})
308369
Expect(err).NotTo(HaveOccurred())
309370

310371
Expect(tw.Close()).To(Succeed())
311372

312-
cyclicalSymlinkTar = vacation.NewTarArchive(bytes.NewReader(buffer.Bytes()))
373+
linkNotExistTar = vacation.NewTarArchive(bytes.NewReader(buffer.Bytes()))
313374
})
314375

315376
it("returns an error", func() {
316-
err := cyclicalSymlinkTar.Decompress(tempDir)
317-
Expect(err).To(MatchError(ContainSubstring("failed: max iterations reached: this symlink graph contains a cycle")))
377+
err := linkNotExistTar.Decompress(tempDir)
378+
Expect(err).To(MatchError(ContainSubstring("failed to extract link")))
379+
Expect(err).To(MatchError(ContainSubstring("no such file or directory")))
318380
})
319381
})
320382
})

vacation/zip_archive.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (z ZipArchive) Decompress(destination string) error {
4343
return fmt.Errorf("failed to create zip reader: %w", err)
4444
}
4545

46-
var symlinks []symlink
46+
var symlinks []link
4747
for _, f := range zr.File {
4848
// Clean the name in the header to prevent './filename' being stripped to
4949
// 'filename' also to skip if the destination it the destination directory
@@ -87,7 +87,7 @@ func (z ZipArchive) Decompress(destination string) error {
8787

8888
// Collect all of the headers for symlinks so that they can be verified
8989
// after all other files are written
90-
symlinks = append(symlinks, symlink{
90+
symlinks = append(symlinks, link{
9191
name: string(linkname),
9292
path: path,
9393
})
@@ -123,7 +123,7 @@ func (z ZipArchive) Decompress(destination string) error {
123123
}
124124
}
125125

126-
symlinks, err = sortSymlinks(symlinks)
126+
symlinks, err = sortLinks(symlinks)
127127
if err != nil {
128128
return err
129129
}

0 commit comments

Comments
 (0)