Skip to content

Commit 665606c

Browse files
committed
tar: add filter option to extraction
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
1 parent 9d32b13 commit 665606c

3 files changed

Lines changed: 80 additions & 5 deletions

File tree

tar/options.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ type tarOpts struct {
3434
// and Untar reads one.
3535
skipGzip bool
3636

37-
// filter is called for each file or directory during archiving.
38-
// If it returns true, the entry is excluded from the archive.
37+
// filter is called for each entry during archiving or extraction.
38+
// If it returns true, the entry is excluded.
3939
filter func(path string, fi os.FileInfo) bool
4040
}
4141

@@ -63,9 +63,10 @@ func WithSkipGzip() Option {
6363
}
6464
}
6565

66-
// WithFilter sets a predicate called for each file or directory during
67-
// archiving. Entries for which fn returns true are excluded from the
68-
// archive. Passing this option to Untar has no effect.
66+
// WithFilter sets a predicate called for each entry during archiving
67+
// or extraction. Entries for which fn returns true are excluded. During
68+
// Tar the path is the absolute filesystem path; during Untar it is the
69+
// slash-separated name from the tar header.
6970
func WithFilter(fn func(path string, fi os.FileInfo) bool) Option {
7071
return func(t *tarOpts) {
7172
t.filter = fn

tar/untar.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const (
3939
// By default, r is expected to be gzip-compressed; use WithSkipGzip to
4040
// read a plain tar stream. Extraction is capped at DefaultMaxUntarSize
4141
// bytes; use WithMaxUntarSize to raise, lower, or disable the limit.
42+
// Use WithFilter to skip entries by name or FileInfo during extraction.
4243
// Entries with paths that escape dir are rejected. Symlinks fail
4344
// extraction unless WithSkipSymlinks is set, in which case they are
4445
// silently dropped.
@@ -116,6 +117,10 @@ func Untar(r io.Reader, dir string, inOpts ...Option) (err error) {
116117
fi := f.FileInfo()
117118
mode := fi.Mode()
118119

120+
if opts.filter != nil && opts.filter(f.Name, fi) {
121+
continue
122+
}
123+
119124
switch {
120125
case mode.IsRegular():
121126
// Make the directory. This is redundant because it should

tar/untar_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"os"
2626
"path"
2727
"path/filepath"
28+
"strings"
2829
"testing"
2930
)
3031

@@ -380,3 +381,71 @@ func tgzWithSymlinks(src string, buf io.Writer) error {
380381
}
381382
return nil
382383
}
384+
385+
func TestUntar_withFilter(t *testing.T) {
386+
// Build a gzipped tar with two files.
387+
var buf bytes.Buffer
388+
gw := gzip.NewWriter(&buf)
389+
tw := tar.NewWriter(gw)
390+
for name, data := range map[string]string{"keep.txt": "keep", "skip.log": "skip"} {
391+
tw.WriteHeader(&tar.Header{Name: name, Size: int64(len(data)), Mode: 0o644})
392+
tw.Write([]byte(data))
393+
}
394+
tw.Close()
395+
gw.Close()
396+
397+
dst := t.TempDir()
398+
filter := func(p string, _ os.FileInfo) bool {
399+
return filepath.Ext(p) == ".log"
400+
}
401+
if err := Untar(&buf, dst, WithMaxUntarSize(-1), WithFilter(filter)); err != nil {
402+
t.Fatalf("Untar: %v", err)
403+
}
404+
405+
if _, err := os.Stat(filepath.Join(dst, "skip.log")); err == nil {
406+
t.Error("filtered file skip.log should not have been extracted")
407+
}
408+
got, err := os.ReadFile(filepath.Join(dst, "keep.txt"))
409+
if err != nil {
410+
t.Fatalf("read keep.txt: %v", err)
411+
}
412+
if string(got) != "keep" {
413+
t.Errorf("keep.txt: got %q, want %q", string(got), "keep")
414+
}
415+
}
416+
417+
func TestUntar_withFilterDirectory(t *testing.T) {
418+
// Build a gzipped tar with entries under two directories.
419+
var buf bytes.Buffer
420+
gw := gzip.NewWriter(&buf)
421+
tw := tar.NewWriter(gw)
422+
tw.WriteHeader(&tar.Header{Name: "skip/", Mode: 0o755, Typeflag: tar.TypeDir})
423+
s := "secret"
424+
tw.WriteHeader(&tar.Header{Name: "skip/secret.txt", Size: int64(len(s)), Mode: 0o644})
425+
tw.Write([]byte(s))
426+
tw.WriteHeader(&tar.Header{Name: "keep/", Mode: 0o755, Typeflag: tar.TypeDir})
427+
k := "public"
428+
tw.WriteHeader(&tar.Header{Name: "keep/data.txt", Size: int64(len(k)), Mode: 0o644})
429+
tw.Write([]byte(k))
430+
tw.Close()
431+
gw.Close()
432+
433+
dst := t.TempDir()
434+
filter := func(p string, _ os.FileInfo) bool {
435+
return strings.HasPrefix(p, "skip/")
436+
}
437+
if err := Untar(&buf, dst, WithMaxUntarSize(-1), WithFilter(filter)); err != nil {
438+
t.Fatalf("Untar: %v", err)
439+
}
440+
441+
if _, err := os.Stat(filepath.Join(dst, "skip", "secret.txt")); err == nil {
442+
t.Error("skip/secret.txt should not have been extracted")
443+
}
444+
got, err := os.ReadFile(filepath.Join(dst, "keep", "data.txt"))
445+
if err != nil {
446+
t.Fatalf("read keep/data.txt: %v", err)
447+
}
448+
if string(got) != "public" {
449+
t.Errorf("keep/data.txt: got %q, want %q", string(got), "public")
450+
}
451+
}

0 commit comments

Comments
 (0)