Skip to content

Commit ade2afc

Browse files
authored
zip: add support for symlink extraction (#39)
Signed-off-by: Jakub Wójtowicz <jakub.wojtowic@gmail.com>
1 parent 39a94fd commit ade2afc

File tree

3 files changed

+73
-1
lines changed

3 files changed

+73
-1
lines changed

testdata/symlinks.zip

812 Bytes
Binary file not shown.

zip.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"io/fs"
1010
"log"
11+
"os"
1112
"path"
1213
"strings"
1314

@@ -215,10 +216,16 @@ func (z Zip) Extract(ctx context.Context, sourceArchive io.Reader, handleFile Fi
215216
}
216217

217218
info := f.FileInfo()
219+
linkTarget, err := z.getLinkTarget(f)
220+
if err != nil {
221+
return fmt.Errorf("getting link target for file %d: %s: %w", i, f.Name, err)
222+
}
223+
218224
file := FileInfo{
219225
FileInfo: info,
220226
Header: f.FileHeader,
221227
NameInArchive: f.Name,
228+
LinkTarget: linkTarget,
222229
Open: func() (fs.File, error) {
223230
openedFile, err := f.Open()
224231
if err != nil {
@@ -228,7 +235,7 @@ func (z Zip) Extract(ctx context.Context, sourceArchive io.Reader, handleFile Fi
228235
},
229236
}
230237

231-
err := handleFile(ctx, file)
238+
err = handleFile(ctx, file)
232239
if errors.Is(err, fs.SkipAll) {
233240
break
234241
} else if errors.Is(err, fs.SkipDir) && file.IsDir() {
@@ -264,6 +271,33 @@ func (z Zip) decodeText(hdr *zip.FileHeader) {
264271
}
265272
}
266273

274+
func (z Zip) getLinkTarget(f *zip.File) (string, error) {
275+
info := f.FileInfo()
276+
// Exit early if not a symlink
277+
if info.Mode()&os.ModeSymlink == 0 {
278+
return "", nil
279+
}
280+
281+
// Open the file and read the link target
282+
file, err := f.Open()
283+
if err != nil {
284+
return "", err
285+
}
286+
defer file.Close()
287+
288+
const maxLinkTargetSize = 32768
289+
linkTargetBytes, err := io.ReadAll(io.LimitReader(file, maxLinkTargetSize))
290+
if err != nil {
291+
return "", err
292+
}
293+
294+
if len(linkTargetBytes) == maxLinkTargetSize {
295+
return "", fmt.Errorf("link target is too large: %d bytes", len(linkTargetBytes))
296+
}
297+
298+
return string(linkTargetBytes), nil
299+
}
300+
267301
// Insert appends the listed files into the provided Zip archive stream.
268302
// If the filename already exists in the archive, it will be replaced.
269303
func (z Zip) Insert(ctx context.Context, into io.ReadWriteSeeker, files []FileInfo) error {

zip_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package archives
2+
3+
import (
4+
"context"
5+
"os"
6+
"reflect"
7+
"sort"
8+
"testing"
9+
)
10+
11+
func TestZip_ExtractZipWithSymlinks(t *testing.T) {
12+
zipFile, err := os.Open("testdata/symlinks.zip")
13+
if err != nil {
14+
t.Errorf("failed to open zip file: %v", err)
15+
}
16+
defer zipFile.Close()
17+
18+
zip := Zip{}
19+
extractedFiles := []string{}
20+
zip.Extract(context.Background(), zipFile, func(ctx context.Context, file FileInfo) error {
21+
extractedFiles = append(extractedFiles, file.Name())
22+
if file.Name() == "symlinked" {
23+
if file.LinkTarget != "../a/hello" {
24+
t.Errorf("expected symlink target to be '../a/hello', got %s", file.LinkTarget)
25+
}
26+
}
27+
return nil
28+
})
29+
30+
if len(extractedFiles) != 5 {
31+
t.Errorf("expected 5 files to be extracted, got %d", len(extractedFiles))
32+
}
33+
sort.Strings(extractedFiles)
34+
expectedFiles := []string{"a", "b", "hello", "symlinked", "zip_test"}
35+
if !reflect.DeepEqual(extractedFiles, expectedFiles) {
36+
t.Errorf("expected files to be %v, got %v", expectedFiles, extractedFiles)
37+
}
38+
}

0 commit comments

Comments
 (0)