Skip to content

Commit 4986e08

Browse files
committed
Handle symlinks and hardlinks better
Previously (and probably right now for cases I haven't considered), we would fail to traverse symlinked directories properly. This attempts to handle that in a performant-but-probably-not-entirely-correct way by looking at parent directories and traversing any symlinks if the file doesn't exist at the originally requested path. Signed-off-by: Jon Johnson <[email protected]>
1 parent 309a6e7 commit 4986e08

File tree

4 files changed

+113
-12
lines changed

4 files changed

+113
-12
lines changed

go.mod

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
11
module github.com/jonjohnsonjr/targz
22

3-
go 1.22.0
4-
5-
require (
6-
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
7-
golang.org/x/sync v0.7.0 // indirect
8-
)
3+
go 1.23.0

go.sum

-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +0,0 @@
1-
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
2-
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
3-
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
4-
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=

tarfs/tarfs.go

+39-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"fmt"
2525
"io"
2626
"io/fs"
27+
"iter"
2728
"path"
2829
"strings"
2930
"testing/iotest"
@@ -144,6 +145,18 @@ func (fsys *FS) Readlink(name string) (string, error) {
144145
return "", fmt.Errorf("Readlink(%q): file is not a link", name)
145146
}
146147

148+
func dirs(name string) iter.Seq[string] {
149+
return func(yield func(string) bool) {
150+
for i, v := range name {
151+
if v == '/' {
152+
if !yield(name[0:i]) {
153+
return
154+
}
155+
}
156+
}
157+
}
158+
}
159+
147160
// arbitrary number stolen from filepath.EvalSymlinks
148161
// this seems to be 40 in linux (MAXSYMLINKS), which might be more reasonable
149162
const maxHops = 255
@@ -156,14 +169,38 @@ func (fsys *FS) open(name string, hops int) (fs.File, error) {
156169

157170
e, err := fsys.Entry(name)
158171
if err != nil {
172+
if errors.Is(err, fs.ErrNotExist) {
173+
// Deal with symlinked dirs.
174+
for dir := range dirs(name) {
175+
e, err := fsys.Entry(dir)
176+
if err != nil {
177+
continue
178+
}
179+
180+
if e.Header.Typeflag != tar.TypeSymlink {
181+
continue
182+
}
183+
184+
// We need to rewrite what comes after the symlinked dir.
185+
rest := strings.TrimPrefix(name, dir)
186+
187+
link := e.Header.Linkname
188+
if path.IsAbs(link) {
189+
return fsys.open(normalize(path.Join(link, rest)), hops+1)
190+
}
191+
192+
return fsys.open(path.Join(e.dir, link, rest), hops+1)
193+
}
194+
}
195+
159196
return nil, err
160197
}
161198

162199
switch e.Header.Typeflag {
163200
case tar.TypeSymlink, tar.TypeLink:
164201
link := e.Header.Linkname
165-
if path.IsAbs(link) {
166-
return fsys.open(link, hops+1)
202+
if path.IsAbs(link) || e.Header.Typeflag == tar.TypeLink {
203+
return fsys.open(normalize(link), hops+1)
167204
}
168205

169206
return fsys.open(path.Join(e.dir, link), hops+1)

tarfs/tarfs_test.go

+73
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package tarfs
22

33
import (
4+
"archive/tar"
5+
"bytes"
6+
"io/fs"
47
"os"
58
"testing"
69
"testing/fstest"
@@ -37,3 +40,73 @@ func TestFS(t *testing.T) {
3740
t.Fatal(err)
3841
}
3942
}
43+
44+
func TestSymlinkedDirs(t *testing.T) {
45+
buf := &bytes.Buffer{}
46+
47+
tw := tar.NewWriter(buf)
48+
49+
want := "pretend this is a binary"
50+
51+
tw.WriteHeader(&tar.Header{
52+
Name: "usr",
53+
Typeflag: tar.TypeDir,
54+
})
55+
tw.WriteHeader(&tar.Header{
56+
Name: "usr/bin",
57+
Typeflag: tar.TypeDir,
58+
})
59+
tw.WriteHeader(&tar.Header{
60+
Name: "usr/bin/binary",
61+
Typeflag: tar.TypeReg,
62+
Size: int64(len(want)),
63+
})
64+
tw.Write([]byte(want))
65+
tw.WriteHeader(&tar.Header{
66+
Name: "weird",
67+
Typeflag: tar.TypeDir,
68+
})
69+
tw.WriteHeader(&tar.Header{
70+
Name: "weird/linked",
71+
Typeflag: tar.TypeSymlink,
72+
Linkname: "/usr/bin",
73+
})
74+
tw.WriteHeader(&tar.Header{
75+
Name: "weird/absolute",
76+
Typeflag: tar.TypeDir,
77+
})
78+
tw.WriteHeader(&tar.Header{
79+
Name: "weird/absolute/binary",
80+
Typeflag: tar.TypeSymlink,
81+
Linkname: "/weird/linked/binary",
82+
})
83+
tw.WriteHeader(&tar.Header{
84+
Name: "weird/relative",
85+
Typeflag: tar.TypeDir,
86+
})
87+
tw.WriteHeader(&tar.Header{
88+
Name: "weird/relative/binary",
89+
Typeflag: tar.TypeSymlink,
90+
Linkname: "../linked/binary",
91+
})
92+
93+
if err := tw.Close(); err != nil {
94+
t.Fatal(err)
95+
}
96+
97+
fsys, err := New(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
98+
if err != nil {
99+
t.Fatal(err)
100+
}
101+
102+
for _, name := range []string{
103+
"weird/linked/binary",
104+
"weird/absolute/binary",
105+
} {
106+
if b, err := fs.ReadFile(fsys, name); err != nil {
107+
t.Fatalf("ReadFile(%q): %v", name, err)
108+
} else if string(b) != want {
109+
t.Fatalf("want %q, got %q", want, b)
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)