diff --git a/archives_test.go b/archives_test.go index 4bd8225..6094d9a 100644 --- a/archives_test.go +++ b/archives_test.go @@ -6,6 +6,7 @@ import ( "bytes" "compress/gzip" "io" + "os" "strings" "testing" "time" @@ -382,6 +383,145 @@ func TestOpenWithPrefix(t *testing.T) { } } +// createTestZipWithDirEntries creates a zip with explicit directory entries, +// like GitHub zipball downloads produce. +func createTestZipWithDirEntries() []byte { + buf := new(bytes.Buffer) + w := zip.NewWriter(buf) + + // Add explicit directory entry (GitHub zipballs do this) + header := &zip.FileHeader{ + Name: "project-abc123/", + Method: zip.Store, + Modified: time.Date(2026, 3, 30, 8, 14, 47, 0, time.UTC), + } + header.SetMode(0755 | os.ModeDir) + _, _ = w.CreateHeader(header) + + // Add explicit src/ subdirectory entry + srcHeader := &zip.FileHeader{ + Name: "project-abc123/src/", + Method: zip.Store, + Modified: time.Date(2026, 3, 30, 8, 14, 47, 0, time.UTC), + } + srcHeader.SetMode(0755 | os.ModeDir) + _, _ = w.CreateHeader(srcHeader) + + // Add files inside that directory + files := []struct { + name string + content string + }{ + {"project-abc123/README.md", "# Test"}, + {"project-abc123/src/main.go", "package main"}, + {"project-abc123/src/util.go", "package main"}, + } + + for _, file := range files { + f, _ := w.Create(file.name) + _, _ = f.Write([]byte(file.content)) + } + + _ = w.Close() + return buf.Bytes() +} + +// createTestTarGzWithDirEntries creates a tar.gz with explicit directory entries, +// like GitHub tarball downloads produce. +func createTestTarGzWithDirEntries() []byte { + buf := new(bytes.Buffer) + gw := gzip.NewWriter(buf) + tw := tar.NewWriter(gw) + + // Add explicit directory entries + for _, dir := range []string{"project-abc123/", "project-abc123/src/"} { + _ = tw.WriteHeader(&tar.Header{ + Typeflag: tar.TypeDir, + Name: dir, + Mode: 0755, + ModTime: time.Date(2026, 3, 30, 8, 14, 47, 0, time.UTC), + }) + } + + files := []struct { + name string + content string + }{ + {"project-abc123/README.md", "# Test"}, + {"project-abc123/src/main.go", "package main"}, + {"project-abc123/src/util.go", "package main"}, + } + + for _, file := range files { + _ = tw.WriteHeader(&tar.Header{ + Name: file.name, + Size: int64(len(file.content)), + Mode: 0644, + ModTime: time.Date(2026, 3, 30, 8, 14, 47, 0, time.UTC), + }) + _, _ = tw.Write([]byte(file.content)) + } + + _ = tw.Close() + _ = gw.Close() + return buf.Bytes() +} + +func assertNoDuplicates(t *testing.T, label string, files []FileInfo) { + t.Helper() + seen := map[string]int{} + for _, f := range files { + seen[f.Path]++ + } + for path, count := range seen { + if count > 1 { + t.Errorf("%s: %d entries for %q, want 1", label, count, path) + } + } +} + +func TestZipListDirNoDuplicatesWithExplicitDirEntries(t *testing.T) { + data := createTestZipWithDirEntries() + reader, err := openZip(bytes.NewReader(data)) + if err != nil { + t.Fatalf("openZip failed: %v", err) + } + defer func() { _ = reader.Close() }() + + files, err := reader.ListDir("") + if err != nil { + t.Fatalf("ListDir root failed: %v", err) + } + assertNoDuplicates(t, "ListDir root", files) + + files, err = reader.ListDir("project-abc123/") + if err != nil { + t.Fatalf("ListDir subdir failed: %v", err) + } + assertNoDuplicates(t, "ListDir project-abc123/", files) +} + +func TestTarListDirNoDuplicatesWithExplicitDirEntries(t *testing.T) { + data := createTestTarGzWithDirEntries() + reader, err := openTar(bytes.NewReader(data), "gzip") + if err != nil { + t.Fatalf("openTar failed: %v", err) + } + defer func() { _ = reader.Close() }() + + files, err := reader.ListDir("") + if err != nil { + t.Fatalf("ListDir root failed: %v", err) + } + assertNoDuplicates(t, "ListDir root", files) + + files, err = reader.ListDir("project-abc123/") + if err != nil { + t.Fatalf("ListDir subdir failed: %v", err) + } + assertNoDuplicates(t, "ListDir project-abc123/", files) +} + func TestGetStripPrefixNpm(t *testing.T) { // Create npm-style archive buf := new(bytes.Buffer) diff --git a/tar.go b/tar.go index 6c50884..a69633b 100644 --- a/tar.go +++ b/tar.go @@ -100,6 +100,9 @@ func (t *tarReader) ListDir(dirPath string) ([]FileInfo, error) { // Check if this file/dir is directly in the requested directory if isInDir(path, dirPath) { + if f.info.IsDir { + seenDirs[path] = true + } files = append(files, f.info) continue } diff --git a/zip.go b/zip.go index 09e1bd9..45fad14 100644 --- a/zip.go +++ b/zip.go @@ -54,6 +54,9 @@ func (z *zipReader) ListDir(dirPath string) ([]FileInfo, error) { // Check if this file/dir is directly in the requested directory if isInDir(path, dirPath) { + if f.FileInfo().IsDir() { + seenDirs[path] = true + } files = append(files, fileInfoFromZip(f)) continue }