From 8e9c79b29f525359779db26714f98469582b1d48 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 1 Nov 2026 11:14:13 +0000 Subject: [PATCH] Skip global PAX metadata in TAR readers --- extract_test.go | 19 ++++++++ stream_tar.go | 19 +++++--- tar.go | 3 ++ tar_pax_test.go | 122 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 tar_pax_test.go diff --git a/extract_test.go b/extract_test.go index 931fc9a..bd9d3cc 100644 --- a/extract_test.go +++ b/extract_test.go @@ -49,6 +49,25 @@ func TestExtractAllTarGz(t *testing.T) { assertFileContent(t, filepath.Join(dir, "lib", "util.js"), "module.exports = {};") } +func TestExtractAllSkipsGlobalPAXHeaders(t *testing.T) { + data := gzipTar(t, tarWithGlobalPAXHeaders(t, "repo-main/")) + r, err := OpenBytes("test.tar.gz", data) + if err != nil { + t.Fatal(err) + } + defer func() { _ = r.Close() }() + dir := t.TempDir() + if err := ExtractAll(r, dir); err != nil { + t.Fatal(err) + } + entries, err := os.ReadDir(dir) + if err != nil || len(entries) != 1 || entries[0].Name() != "repo-main" { + t.Fatalf("extracted root = %v, %v", entries, err) + } + assertFileContent(t, filepath.Join(dir, "repo-main", "README.md"), "body: README.md") + assertFileContent(t, filepath.Join(dir, "repo-main", "pax_global_header"), "body: pax_global_header") +} + func TestExtractAllCreatesTargetDir(t *testing.T) { reader, err := OpenBytes("test.zip", createTestZip()) if err != nil { diff --git a/stream_tar.go b/stream_tar.go index a4a2ee3..a8519e5 100644 --- a/stream_tar.go +++ b/stream_tar.go @@ -21,14 +21,19 @@ func newTarStream(ctx context.Context, content io.Reader, compression string) (* } func (t *tarStream) Next() (*StreamEntry, error) { - h, err := t.reader.Next() - if err != nil { - return nil, err + for { + h, err := t.reader.Next() + if err != nil { + return nil, err + } + if h.Typeflag == tar.TypeXGlobalHeader { + continue + } + return &StreamEntry{ + FileInfo: fileInfoFromTar(h), Linkname: h.Linkname, + IsHardlink: h.Typeflag == tar.TypeLink, + }, nil } - return &StreamEntry{ - FileInfo: fileInfoFromTar(h), Linkname: h.Linkname, - IsHardlink: h.Typeflag == tar.TypeLink, - }, nil } func (t *tarStream) Read(p []byte) (int, error) { diff --git a/tar.go b/tar.go index 59bd0bc..ffeeda2 100644 --- a/tar.go +++ b/tar.go @@ -61,6 +61,9 @@ func openTarWithInitialEntryCount(raw []byte, compression string, initialEntryCo if err != nil { return nil, fmt.Errorf("reading tar: %w", err) } + if header.Typeflag == tar.TypeXGlobalHeader { + continue + } if err := checkArchiveEntryCount(initialEntryCount + len(files) + 1); err != nil { return nil, err } diff --git a/tar_pax_test.go b/tar_pax_test.go new file mode 100644 index 0000000..cfa94b3 --- /dev/null +++ b/tar_pax_test.go @@ -0,0 +1,122 @@ +package archives + +import ( + "archive/tar" + "bytes" + "io" + "reflect" + "testing" +) + +func tarWithGlobalPAXHeaders(t *testing.T, prefix string) []byte { + t.Helper() + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + for _, name := range []string{"README.md", "pax_global_header", ""} { + if err := tw.WriteHeader(&tar.Header{ + Name: "pax_global_header", + Typeflag: tar.TypeXGlobalHeader, + PAXRecords: map[string]string{"comment": "0123456789abcdef0123456789abcdef01234567"}, + }); err != nil { + t.Fatal(err) + } + if name != "" { + writeTarFile(t, tw, prefix+name, "body: "+name, 0o644) + } + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + +func TestTarSkipsGlobalPAXHeaders(t *testing.T) { + raw := tarWithGlobalPAXHeaders(t, "") + for _, name := range []string{"test.tar", "test.tar.gz", "test.tar.xz", "test.tar.zst", "test.gem", "test.conda"} { + t.Run(name, func(t *testing.T) { + data := streamTestArchive(t, name, raw) + r, err := OpenBytes(name, data) + if err != nil { + t.Fatal(err) + } + defer func() { _ = r.Close() }() + files, err := r.List() + if err != nil { + t.Fatal(err) + } + var paths []string + for _, file := range files { + paths = append(paths, file.Path) + body, err := r.Extract(file.Path) + if err != nil { + t.Fatal(err) + } + got, err := io.ReadAll(body) + _ = body.Close() + if err != nil || string(got) != "body: "+file.Path { + t.Fatalf("Extract(%q) = %q, %v", file.Path, got, err) + } + } + if !reflect.DeepEqual(paths, []string{"README.md", "pax_global_header"}) { + t.Fatalf("paths = %v", paths) + } + }) + } +} + +func TestStreamSkipsGlobalPAXHeaders(t *testing.T) { + raw := tarWithGlobalPAXHeaders(t, "") + for _, name := range []string{"test.tar", "test.tar.gz", "test.tar.xz", "test.tar.zst", "test.gem", "test.conda"} { + t.Run(name, func(t *testing.T) { + data := streamTestArchive(t, name, raw) + s, err := OpenStream(name, bytes.NewReader(data), StreamOptions{MaxEntries: 2}) + if err != nil { + t.Fatal(err) + } + defer func() { _ = s.Close() }() + for _, path := range []string{"README.md", "pax_global_header"} { + entry, err := s.Next() + if err != nil { + t.Fatal(err) + } + if entry.Path != path { + t.Fatalf("path = %q, want %q", entry.Path, path) + } + body, err := io.ReadAll(s) + if err != nil || string(body) != "body: "+path { + t.Fatalf("Read(%q) = %q, %v", path, body, err) + } + } + if _, err := s.Next(); err != io.EOF { + t.Fatalf("end = %v, want EOF", err) + } + }) + } +} + +func TestBrowserRootWithGlobalPAXHeaders(t *testing.T) { + data := gzipTar(t, tarWithGlobalPAXHeaders(t, "repo-main/")) + r, err := OpenBytes("test.tar.gz", data) + if err != nil { + t.Fatal(err) + } + defer func() { _ = r.Close() }() + files, err := r.List() + if err != nil { + t.Fatal(err) + } + if root := CommonRoot(files); root != "repo-main/" { + t.Fatalf("CommonRoot = %q", root) + } + reopen := func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(data)), nil } + b, err := OpenBrowser("test.tar.gz", bytes.NewReader(data), reopen, BrowseOptions{DetectRoot: true}) + if err != nil { + t.Fatal(err) + } + files, err = b.ListDir("") + if err != nil || len(files) != 2 || files[0].Path != "README.md" || files[1].Path != "pax_global_header" { + t.Fatalf("ListDir = %+v, %v", files, err) + } + assertBrowsedBody(t, b, "README.md", "body: README.md") + assertBrowsedBody(t, b, "pax_global_header", "body: pax_global_header") +}