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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,39 @@ buffers that input within `MaxInputBytes`; `OpenStreamBytes(name, data, options)
reuses an existing byte slice without copying it. Keep that slice unchanged
until `Close`. TAR and gem can consume a forward-only input directly.

For an existing file or another `io.ReaderAt`, use
`OpenStreamReaderAt(name, input, size, options)` or
`OpenBrowserReaderAt(name, input, size, browseOptions)` to avoid copying ZIP
and conda input. Each has a `Context` variant. The size is the complete compressed
archive size and must fit `MaxInputBytes`. Keep the source open and unchanged
until browsing and all extracted files finish. Closing a stream or extracted
file does not close the source.

```go
file, err := os.Open("package.zip")
if err != nil {
return err
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return err
}
browser, err := archives.OpenBrowserReaderAt("package.zip", file, info.Size(), archives.BrowseOptions{})
if err != nil {
return err
}
files, err := browser.List()
```

The random-access APIs apply the same central directory checks as the byte-slice
APIs and accept the same archives. They skip unread ZIP bodies without decoding or checking their CRCs.
Declared sizes of skipped entries still count against expansion limits.
Read a member through EOF to check its CRC. These APIs return `Stream` or
`Browser`, not the `Reader` interface. For whole-artifact hashing, hash the
original source separately. The existing forward-only and byte-slice APIs keep
their behavior.

When content detection finds gzip, bzip2, xz or zstd, the decompressed prefix
determines whether to read TAR entries or expose one member named `content`.
The single member has `Size: -1` in streams and browser listings because its size
Expand Down
9 changes: 9 additions & 0 deletions browse.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type Browser struct {
reopen func() (io.ReadCloser, error)
first io.Reader
data []byte
random io.ReaderAt
size int64
}

// OpenBrowser prepares an archive for browsing. The caller owns content and
Expand Down Expand Up @@ -75,6 +77,13 @@ func (b *Browser) openStream() (*Stream, func() error, error) {
if err := b.ctx.Err(); err != nil {
return nil, nil, err
}
if b.random != nil {
stream, err := OpenStreamReaderAtContext(b.ctx, b.filename, b.random, b.size, b.options.Limits)
if err != nil {
return nil, nil, err
}
return stream, stream.Close, nil
}
if b.data != nil {
stream, err := OpenStreamBytesContext(b.ctx, b.filename, b.data, b.options.Limits)
if err != nil {
Expand Down
80 changes: 80 additions & 0 deletions random_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package archives

import (
"archive/zip"
"context"
"errors"
"fmt"
"io"
)

// OpenStreamReaderAt opens ZIP or conda input without buffering the archive.
// size is the complete compressed input size. The caller owns content and must
// keep it open and unchanged until the stream is closed. Unread ZIP entries are
// skipped without decoding; their declared sizes still count against limits.
func OpenStreamReaderAt(filename string, content io.ReaderAt, size int64, options StreamOptions) (*Stream, error) {
return OpenStreamReaderAtContext(context.Background(), filename, content, size, options)
}

// OpenStreamReaderAtContext is OpenStreamReaderAt with cancellation before and
// after each source read. Cancellation does not close or interrupt the input.
func OpenStreamReaderAtContext(ctx context.Context, filename string, content io.ReaderAt, size int64, options StreamOptions) (*Stream, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
options, err := options.defaults()
if err != nil {
return nil, err
}
if size < 0 {
return nil, fmt.Errorf("negative archive size: %d", size)
}
if size > options.MaxInputBytes {
return nil, ErrInputLimit
}
input := contextReaderAt{ctx: ctx, reader: content}
format, _, err := streamFormat(filename, io.NewSectionReader(input, 0, size))
if err != nil {
return nil, err
}
if format != formatZIP && format != formatConda {
return nil, fmt.Errorf("%w: random-access archive format: %s", errors.ErrUnsupported, filename)
}
if err := checkZipEntryCountLimitContext(ctx, input, size, options.MaxEntries); err != nil {
return nil, err
}
reader, err := zip.NewReader(input, size)
if err != nil {
return nil, fmt.Errorf("opening zip: %w", err)
}
if len(reader.File) > options.MaxEntries {
return nil, ErrEntryLimit
}
reader.RegisterDecompressor(zip.Deflate, newFlateReader)
var source streamSource = &zipStream{ctx: ctx, reader: reader, skipUnread: true}
if format == formatConda {
source = &condaStream{outer: &Stream{ctx: ctx, source: source, options: containerOptions(options)}}
}
return &Stream{ctx: ctx, source: source, options: options}, nil
}

// OpenBrowserReaderAt prepares ZIP or conda input for repeated browsing without
// copying it. The caller owns content and must keep it open and unchanged until
// browsing and all extracted files finish. Limits apply to each pass. This API
// does not implement Reader or whole-artifact hashing; hash the source separately.
func OpenBrowserReaderAt(filename string, content io.ReaderAt, size int64, options BrowseOptions) (*Browser, error) {
return OpenBrowserReaderAtContext(context.Background(), filename, content, size, options)
}

// OpenBrowserReaderAtContext is OpenBrowserReaderAt with cancellation across
// every pass, including reads from extracted files.
func OpenBrowserReaderAtContext(ctx context.Context, filename string, content io.ReaderAt, size int64, options BrowseOptions) (*Browser, error) {
stream, err := OpenStreamReaderAtContext(ctx, filename, content, size, options.Limits)
if err != nil {
return nil, err
}
if err := stream.Close(); err != nil {
return nil, err
}
return &Browser{ctx: ctx, filename: filename, options: options, random: content, size: size}, nil
}
Loading
Loading