Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,12 @@ for _, f := range result.Files {

## Supported formats

- `.zip`, `.jar`, `.whl`, `.nupkg`, `.egg` (ZIP-based)
- `.tar`, `.tar.gz`, `.tgz`, `.tar.bz2`, `.tar.xz`
- `.zip`, `.jar`, `.whl`, `.nupkg`, `.egg`, `.vsix` (ZIP-based)
- `.tar`, `.tar.gz`, `.tgz`, `.crate`, `.tar.bz2`, `.tar.xz`
- `.gem` (Ruby gems with nested data.tar.gz)
- `.apk` (routed by content: Android packages open as ZIP, Alpine packages open as gzipped tar)

Filenames without a recognised extension are opened by inspecting the first bytes for a ZIP, tar, gzip, bzip2, or xz signature.

## License

Expand Down
16 changes: 11 additions & 5 deletions archives.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// Package archives provides in-memory archive reading and browsing capabilities.
//
// It supports multiple archive formats including:
// - ZIP (.zip, .jar, .whl, .nupkg)
// - TAR (.tar, .tar.gz, .tgz, .tar.bz2, .tar.xz)
// - ZIP (.zip, .jar, .whl, .nupkg, .egg, .vsix)
// - TAR (.tar, .tar.gz, .tgz, .crate, .tar.bz2, .tar.xz)
// - GEM (.gem - Ruby gems with nested tar structure)
//
// The .apk extension is routed by content since Android packages are ZIP
// and Alpine packages are gzipped tar. Filenames without a recognised
// extension are opened by inspecting the first bytes.
//
// The package is designed to work entirely in memory without writing to disk,
// making it suitable for browsing cached artifacts on-demand.
package archives
Expand Down Expand Up @@ -205,14 +209,16 @@ func detectFormat(filename string) string {
return formatTarXZ
}

// Check simple extensions
// Check simple extensions. .apk is deliberately absent: Alpine packages
// are gzipped tarballs and Android packages are zips, so it falls
// through to content sniffing.
ext := path.Ext(filename)
switch ext {
case ".zip", ".jar", ".whl", ".nupkg", ".egg":
case ".zip", ".jar", ".whl", ".nupkg", ".egg", ".vsix":
return formatZIP
case ".tar":
return formatTAR
case ".tgz":
case ".tgz", ".crate":
return formatTGZ
case ".gem":
return formatGem
Expand Down
47 changes: 47 additions & 0 deletions archives_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func TestDetectFormat(t *testing.T) {
{"package.tar.bz2", "tar.bz2"},
{"package.tar.xz", "tar.xz"},
{"package.gem", "gem"},
{"package.vsix", "zip"},
{"package.crate", "tgz"},
{"package.apk", ""}, // Ambiguous: routed by content sniff
{"unknown.txt", ""},
{"Package.ZIP", "zip"}, // Case insensitive
{"package.TAR.GZ", "tar.gz"},
Expand Down Expand Up @@ -357,6 +360,50 @@ func TestOpenDetectsExtensionlessArchives(t *testing.T) {
}
}

func TestOpenRegistryArtifactExtensions(t *testing.T) {
tests := []struct {
filename string
data []byte
want string
}{
{"serde-1.0.0.crate", createTestTarGz(), "*archives.tarReader"},
{"extension-1.0.0.vsix", createTestZip(), "*archives.zipReader"},
{"android.apk", createTestZip(), "*archives.zipReader"},
{"alpine.apk", createTestTarGz(), "*archives.tarReader"},
}

for _, test := range tests {
t.Run(test.filename+"/Open", func(t *testing.T) {
reader, err := Open(test.filename, bytes.NewReader(test.data))
if err != nil {
t.Fatal(err)
}
defer func() { _ = reader.Close() }()
if got := fmt.Sprintf("%T", reader); got != test.want {
t.Fatalf("reader = %s, want %s", got, test.want)
}
files, err := reader.List()
if err != nil {
t.Fatal(err)
}
if len(files) == 0 {
t.Fatal("archive contains no files")
}
})

t.Run(test.filename+"/OpenBytes", func(t *testing.T) {
reader, err := OpenBytes(test.filename, test.data)
if err != nil {
t.Fatal(err)
}
defer func() { _ = reader.Close() }()
if got := fmt.Sprintf("%T", reader); got != test.want {
t.Fatalf("reader = %s, want %s", got, test.want)
}
})
}
}

func TestOpenLimitsUnsupportedContentRead(t *testing.T) {
content := bytes.NewReader(bytes.Repeat([]byte("x"), contentSniffSize*4))
_, err := Open("artifact", content)
Expand Down