Skip to content

Commit 9dbcc2e

Browse files
Ryan Moranryanmoran
Ryan Moran
authored andcommitted
Extract out symlink sorting logic
1 parent f01f936 commit 9dbcc2e

File tree

3 files changed

+104
-140
lines changed

3 files changed

+104
-140
lines changed

vacation/symlink_sorting.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package vacation
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"strings"
7+
)
8+
9+
type symlink struct {
10+
name string
11+
path string
12+
}
13+
14+
func sortSymlinks(symlinks []symlink) ([]symlink, error) {
15+
// Create a map of all of the symlink names and where they are pointing to to
16+
// act as a quasi-graph
17+
index := map[string]string{}
18+
for _, s := range symlinks {
19+
index[filepath.Clean(s.path)] = s.name
20+
}
21+
22+
// Check to see if the link name lies on the path of another symlink in
23+
// the table or if it is another symlink in the table
24+
//
25+
// Example:
26+
// path = dir/file
27+
// a-symlink -> dir
28+
// b-symlink -> a-symlink
29+
// c-symlink -> a-symlink/file
30+
shouldSkipLink := func(linkname, linkpath string) bool {
31+
sln := strings.Split(linkname, "/")
32+
for j := 0; j < len(sln); j++ {
33+
if _, ok := index[linknameFullPath(linkpath, filepath.Join(sln[:j+1]...))]; ok {
34+
return true
35+
}
36+
}
37+
return false
38+
}
39+
40+
// Iterate over the symlink map for every link that is found this ensures
41+
// that all symlinks that can be created will be created and any that are
42+
// left over are cyclically dependent
43+
var links []symlink
44+
maxIterations := len(index)
45+
for i := 0; i < maxIterations; i++ {
46+
for path, name := range index {
47+
// If there is a match either of the symlink or it is on the path then
48+
// skip the creation of this symlink for now
49+
if shouldSkipLink(name, path) {
50+
continue
51+
}
52+
53+
links = append(links, symlink{
54+
name: name,
55+
path: path,
56+
})
57+
58+
// Remove the created symlink from the symlink table so that its
59+
// dependent symlinks can be created in the next iteration
60+
delete(index, path)
61+
break
62+
}
63+
}
64+
65+
// Check to see if there are any symlinks left in the map which would
66+
// indicate a cyclical dependency
67+
if len(index) > 0 {
68+
return nil, fmt.Errorf("failed: max iterations reached: this symlink graph contains a cycle")
69+
}
70+
71+
return links, nil
72+
}

vacation/tar_archive.go

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

33-
// Struct and slice to collect symlinks and create them after all files have
34-
// been created
35-
type header struct {
36-
linkname string
37-
path string
38-
}
39-
40-
var symlinkHeaders []header
33+
var symlinks []symlink
4134

4235
tarReader := tar.NewReader(ta.reader)
4336
for {
@@ -116,76 +109,29 @@ func (ta TarArchive) Decompress(destination string) error {
116109
case tar.TypeSymlink:
117110
// Collect all of the headers for symlinks so that they can be verified
118111
// after all other files are written
119-
symlinkHeaders = append(symlinkHeaders, header{
120-
linkname: hdr.Linkname,
121-
path: path,
112+
symlinks = append(symlinks, symlink{
113+
name: hdr.Linkname,
114+
path: path,
122115
})
123116
}
124117
}
125118

126-
// Create a map of all of the symlink names and where they are pointing to to
127-
// act as a quasi-graph
128-
symlinkMap := map[string]string{}
129-
for _, h := range symlinkHeaders {
130-
symlinkMap[filepath.Clean(h.path)] = h.linkname
119+
symlinks, err := sortSymlinks(symlinks)
120+
if err != nil {
121+
return err
131122
}
132123

133-
// Iterate over the symlink map for every link that is found this ensures
134-
// that all symlinks that can be created will be created and any that are
135-
// left over are cyclically dependent
136-
maxIterations := len(symlinkMap)
137-
for i := 0; i < maxIterations; i++ {
138-
for path, linkname := range symlinkMap {
139-
// Check to see if the linkname lies on the path of another symlink in
140-
// the table or if it is another symlink in the table
141-
//
142-
// Example:
143-
// path = dir/file
144-
// a-symlink -> dir
145-
// b-symlink -> a-symlink
146-
// c-symlink -> a-symlink/file
147-
//
148-
// If there is a match either of the symlink or it is on the path then
149-
// skip the creation of this symlink for now
150-
shouldSkipLink := func() bool {
151-
sln := strings.Split(linkname, "/")
152-
for j := 0; j < len(sln); j++ {
153-
if _, ok := symlinkMap[linknameFullPath(path, filepath.Join(sln[:j+1]...))]; ok {
154-
return true
155-
}
156-
}
157-
return false
158-
}
159-
160-
if shouldSkipLink() {
161-
continue
162-
}
163-
164-
// If the linkname is not an existing link in the symlink table then we
165-
// can attempt the make the link
166-
167-
// Check to see if the file that will be linked to is valid for symlinking
168-
_, err := filepath.EvalSymlinks(linknameFullPath(path, linkname))
169-
if err != nil {
170-
return fmt.Errorf("failed to evaluate symlink %s: %w", path, err)
171-
}
172-
173-
// Create the symlink
174-
err = os.Symlink(linkname, path)
175-
if err != nil {
176-
return fmt.Errorf("failed to extract symlink: %s", err)
177-
}
178-
179-
// Remove the created symlink from the symlink table so that its
180-
// dependent symlinks can be created in the next iteration
181-
delete(symlinkMap, path)
124+
for _, link := range symlinks {
125+
// Check to see if the file that will be linked to is valid for symlinking
126+
_, err := filepath.EvalSymlinks(linknameFullPath(link.path, link.name))
127+
if err != nil {
128+
return fmt.Errorf("failed to evaluate symlink %s: %w", link.path, err)
182129
}
183-
}
184130

185-
// Check to see if there are any symlinks left in the map which would
186-
// indicate a cyclical dependency
187-
if len(symlinkMap) > 0 {
188-
return fmt.Errorf("failed: max iterations reached: this symlink graph contains a cycle")
131+
err = os.Symlink(link.name, link.path)
132+
if err != nil {
133+
return fmt.Errorf("failed to extract symlink: %s", err)
134+
}
189135
}
190136

191137
return nil

vacation/zip_archive.go

+16-70
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,6 @@ func NewZipArchive(inputReader io.Reader) ZipArchive {
2323
// Decompress reads from ZipArchive and writes files into the destination
2424
// specified.
2525
func (z ZipArchive) Decompress(destination string) error {
26-
// Struct and slice to collect symlinks and create them after all files have
27-
// been created
28-
type header struct {
29-
linkname string
30-
path string
31-
}
32-
33-
var symlinkHeaders []header
3426

3527
// Use an os.File to buffer the zip contents. This is needed because
3628
// zip.NewReader requires an io.ReaderAt so that it can jump around within
@@ -51,6 +43,7 @@ func (z ZipArchive) Decompress(destination string) error {
5143
return fmt.Errorf("failed to create zip reader: %w", err)
5244
}
5345

46+
var symlinks []symlink
5447
for _, f := range zr.File {
5548
// Clean the name in the header to prevent './filename' being stripped to
5649
// 'filename' also to skip if the destination it the destination directory
@@ -94,9 +87,9 @@ func (z ZipArchive) Decompress(destination string) error {
9487

9588
// Collect all of the headers for symlinks so that they can be verified
9689
// after all other files are written
97-
symlinkHeaders = append(symlinkHeaders, header{
98-
linkname: string(linkname),
99-
path: path,
90+
symlinks = append(symlinks, symlink{
91+
name: string(linkname),
92+
path: path,
10093
})
10194

10295
default:
@@ -130,69 +123,22 @@ func (z ZipArchive) Decompress(destination string) error {
130123
}
131124
}
132125

133-
// Create a map of all of the symlink names and where they are pointing to to
134-
// act as a quasi-graph
135-
symlinkMap := map[string]string{}
136-
for _, h := range symlinkHeaders {
137-
symlinkMap[filepath.Clean(h.path)] = h.linkname
126+
symlinks, err = sortSymlinks(symlinks)
127+
if err != nil {
128+
return err
138129
}
139130

140-
// Iterate over the symlink map for every link that is found this ensures
141-
// that all symlinks that can be created will be created and any that are
142-
// left over are cyclically dependent
143-
maxIterations := len(symlinkMap)
144-
for i := 0; i < maxIterations; i++ {
145-
for path, linkname := range symlinkMap {
146-
// Check to see if the linkname lies on the path of another symlink in
147-
// the table or if it is another symlink in the table
148-
//
149-
// Example:
150-
// path = dir/file
151-
// a-symlink -> dir
152-
// b-symlink -> a-symlink
153-
// c-symlink -> a-symlink/file
154-
//
155-
// If there is a match either of the symlink or it is on the path then
156-
// skip the creation of this symlink for now
157-
shouldSkipLink := func() bool {
158-
sln := strings.Split(linkname, "/")
159-
for j := 0; j < len(sln); j++ {
160-
if _, ok := symlinkMap[linknameFullPath(path, filepath.Join(sln[:j+1]...))]; ok {
161-
return true
162-
}
163-
}
164-
return false
165-
}
166-
167-
if shouldSkipLink() {
168-
continue
169-
}
170-
171-
// If the linkname is not an existing link in the symlink table then we
172-
// can attempt the make the link
173-
174-
// Check to see if the file that will be linked to is valid for symlinking
175-
_, err := filepath.EvalSymlinks(linknameFullPath(path, linkname))
176-
if err != nil {
177-
return fmt.Errorf("failed to evaluate symlink %s: %w", path, err)
178-
}
179-
180-
// Create the symlink
181-
err = os.Symlink(linkname, path)
182-
if err != nil {
183-
return fmt.Errorf("failed to unzip symlink: %s", err)
184-
}
185-
186-
// Remove the created symlink from the symlink table so that its
187-
// dependent symlinks can be created in the next iteration
188-
delete(symlinkMap, path)
131+
for _, link := range symlinks {
132+
// Check to see if the file that will be linked to is valid for symlinking
133+
_, err := filepath.EvalSymlinks(linknameFullPath(link.path, link.name))
134+
if err != nil {
135+
return fmt.Errorf("failed to evaluate symlink %s: %w", link.path, err)
189136
}
190-
}
191137

192-
// Check to see if there are any symlinks left in the map which would
193-
// indicate a cyclical dependency
194-
if len(symlinkMap) > 0 {
195-
return fmt.Errorf("failed: max iterations reached: this symlink graph contains a cycle")
138+
err = os.Symlink(link.name, link.path)
139+
if err != nil {
140+
return fmt.Errorf("failed to unzip symlink: %s", err)
141+
}
196142
}
197143

198144
return nil

0 commit comments

Comments
 (0)