Skip to content

Commit 1697b7e

Browse files
committed
Hello world
0 parents  commit 1697b7e

10 files changed

Lines changed: 1111 additions & 0 deletions

File tree

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Andrew Nesbitt
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# archives
2+
3+
A Go library for reading and browsing archive files in memory. Supports ZIP, TAR (with gzip, bzip2, xz compression), and Ruby gem formats.
4+
5+
## Installation
6+
7+
```bash
8+
go get github.com/git-pkgs/archives
9+
```
10+
11+
## Usage
12+
13+
```go
14+
package main
15+
16+
import (
17+
"fmt"
18+
"os"
19+
20+
"github.com/git-pkgs/archives"
21+
)
22+
23+
func main() {
24+
f, _ := os.Open("package.tar.gz")
25+
defer f.Close()
26+
27+
reader, _ := archives.Open("package.tar.gz", f)
28+
defer reader.Close()
29+
30+
// List all files
31+
files, _ := reader.List()
32+
for _, fi := range files {
33+
fmt.Println(fi.Path, fi.Size)
34+
}
35+
36+
// List a specific directory
37+
dirFiles, _ := reader.ListDir("src")
38+
for _, fi := range dirFiles {
39+
fmt.Println(fi.Name, fi.IsDir)
40+
}
41+
42+
// Extract a file
43+
rc, _ := reader.Extract("README.md")
44+
defer rc.Close()
45+
// read from rc...
46+
}
47+
```
48+
49+
### Prefix stripping
50+
51+
Some package formats wrap content in a directory (npm uses `package/`). `OpenWithPrefix` strips a path prefix from all entries:
52+
53+
```go
54+
reader, _ := archives.OpenWithPrefix("pkg.tgz", f, "package/")
55+
// files are now accessible without the package/ prefix
56+
```
57+
58+
## Supported formats
59+
60+
- `.zip`, `.jar`, `.whl`, `.nupkg`, `.egg` (ZIP-based)
61+
- `.tar`, `.tar.gz`, `.tgz`, `.tar.bz2`, `.tar.xz`
62+
- `.gem` (Ruby gems with nested data.tar.gz)
63+
64+
## License
65+
66+
MIT

archives.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Package archives provides in-memory archive reading and browsing capabilities.
2+
//
3+
// It supports multiple archive formats including:
4+
// - ZIP (.zip, .jar, .whl, .nupkg)
5+
// - TAR (.tar, .tar.gz, .tgz, .tar.bz2, .tar.xz)
6+
// - GEM (.gem - Ruby gems with nested tar structure)
7+
//
8+
// The package is designed to work entirely in memory without writing to disk,
9+
// making it suitable for browsing cached artifacts on-demand.
10+
package archives
11+
12+
import (
13+
"fmt"
14+
"io"
15+
"path"
16+
"strings"
17+
"time"
18+
)
19+
20+
// FileInfo represents metadata about a file in an archive.
21+
type FileInfo struct {
22+
Path string // Full path within archive
23+
Name string // Base name
24+
Size int64 // Uncompressed size in bytes
25+
ModTime time.Time // Modification time
26+
IsDir bool // Whether this is a directory
27+
Mode uint32 // File mode/permissions
28+
CompressedSize int64 // Compressed size (if available)
29+
}
30+
31+
// Reader provides methods to browse and extract files from archives.
32+
type Reader interface {
33+
// List returns all files in the archive.
34+
List() ([]FileInfo, error)
35+
36+
// ListDir returns files in a specific directory path.
37+
// Use "" or "/" for root directory.
38+
ListDir(dirPath string) ([]FileInfo, error)
39+
40+
// Extract reads a specific file from the archive.
41+
// Returns io.ReadCloser for the file content.
42+
Extract(filePath string) (io.ReadCloser, error)
43+
44+
// Close releases resources associated with the reader.
45+
Close() error
46+
}
47+
48+
// Open creates an archive reader for the given content.
49+
// The filename is used to detect the archive format.
50+
// The content reader will be read entirely into memory.
51+
func Open(filename string, content io.Reader) (Reader, error) {
52+
format := detectFormat(filename)
53+
if format == "" {
54+
return nil, fmt.Errorf("unsupported archive format: %s", filename)
55+
}
56+
57+
var reader Reader
58+
var err error
59+
60+
switch format {
61+
case "zip":
62+
reader, err = openZip(content)
63+
case "tar":
64+
reader, err = openTar(content, "")
65+
case "tar.gz", "tgz":
66+
reader, err = openTar(content, "gzip")
67+
case "tar.bz2":
68+
reader, err = openTar(content, "bzip2")
69+
case "tar.xz":
70+
reader, err = openTar(content, "xz")
71+
case "gem":
72+
reader, err = openGem(content)
73+
default:
74+
return nil, fmt.Errorf("unsupported format: %s", format)
75+
}
76+
77+
return reader, err
78+
}
79+
80+
// OpenWithPrefix opens an archive and strips the given prefix from all paths.
81+
// This is useful for npm packages which wrap content in a "package/" directory.
82+
func OpenWithPrefix(filename string, content io.Reader, stripPrefix string) (Reader, error) {
83+
reader, err := Open(filename, content)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
if stripPrefix == "" {
89+
return reader, nil
90+
}
91+
92+
return &prefixStripper{
93+
reader: reader,
94+
prefix: stripPrefix,
95+
}, nil
96+
}
97+
98+
// detectFormat determines archive format from filename extension.
99+
func detectFormat(filename string) string {
100+
filename = strings.ToLower(filename)
101+
102+
// Check for compound extensions first
103+
if strings.HasSuffix(filename, ".tar.gz") {
104+
return "tar.gz"
105+
}
106+
if strings.HasSuffix(filename, ".tar.bz2") {
107+
return "tar.bz2"
108+
}
109+
if strings.HasSuffix(filename, ".tar.xz") {
110+
return "tar.xz"
111+
}
112+
113+
// Check simple extensions
114+
ext := path.Ext(filename)
115+
switch ext {
116+
case ".zip", ".jar", ".whl", ".nupkg", ".egg":
117+
return "zip"
118+
case ".tar":
119+
return "tar"
120+
case ".tgz":
121+
return "tgz"
122+
case ".gem":
123+
return "gem"
124+
default:
125+
return ""
126+
}
127+
}
128+
129+
// normalizeDir normalizes directory path for consistent comparison.
130+
func normalizeDir(dirPath string) string {
131+
dirPath = strings.TrimSpace(dirPath)
132+
dirPath = strings.Trim(dirPath, "/")
133+
if dirPath == "" {
134+
return ""
135+
}
136+
return dirPath + "/"
137+
}
138+
139+
// isInDir checks if filePath is directly in dirPath (not in subdirectories).
140+
func isInDir(filePath, dirPath string) bool {
141+
dirPath = normalizeDir(dirPath)
142+
143+
// Normalize file path by trimming trailing slash
144+
filePath = strings.TrimSuffix(filePath, "/")
145+
146+
// Root directory
147+
if dirPath == "" {
148+
// File is in root if it has no slashes
149+
parts := strings.Split(filePath, "/")
150+
return len(parts) == 1
151+
}
152+
153+
// Check if file starts with directory path
154+
if !strings.HasPrefix(filePath+"/", dirPath) {
155+
return false
156+
}
157+
158+
// Get relative path
159+
rel := strings.TrimPrefix(filePath, strings.TrimSuffix(dirPath, "/"))
160+
rel = strings.TrimPrefix(rel, "/")
161+
162+
// Should have no more slashes
163+
return !strings.Contains(rel, "/")
164+
}

0 commit comments

Comments
 (0)