pac1

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 3, 2026 License: MIT Imports: 19 Imported by: 0

README

pak1

Go package for reading, extracting, and packing Arma Reforger PAC1 archives (.pak).

Current status

Implemented:

  • parse FORM/PAC1 container and top-level chunks (HEAD, DATA, FILE);
  • recursive FILE tree parsing to flat file entries;
  • read entry payloads (raw and zlib);
  • extraction to directory with worker pool;
  • packing .pak from stream-based inputs;
  • transactional editor workflow (OpenEditor + staged ops + Commit);
  • compression policy by pathrules.

API quick start

r, err := pak1.Open("data.pak")
if err != nil {
  return err
}
defer r.Close()

first, ok := r.EntryAt(0)
if !ok {
  return nil
}

data, err := r.ReadEntry(first.Path)
if err != nil {
  return err
}
_ = data

Zero-copy metadata iteration:

for i := range r.EntryCount() {
  entry, ok := r.EntryAt(i)
  if !ok {
    break
  }
  _ = entry
}

Extract selected entries:

err = r.Extract(ctx, "out", pak1.ExtractOptions{
  MaxWorkers: 4,
})

Pack from inputs:

res, err := pak1.PackFile(ctx, "out.pak", inputs, pak1.PackOptions{
  Compress: []pathrules.Rule{
    {Action: pathrules.ActionInclude, Pattern: "*.txt"},
  },
})
if err != nil {
  return err
}
_ = res

Edit existing archive:

editor, err := pak1.OpenEditor("addon.pak", pak1.EditOptions{BackupKeep: 1})
if err != nil {
  return err
}

if err := editor.Replace(pak1.Input{
  Path: "scripts/main.c",
  Open: func() (io.ReadCloser, error) { return os.Open("scripts/main.c") },
}); err != nil {
  return err
}

if err := editor.DeleteDir("obsolete"); err != nil {
  return err
}

_, err = editor.Commit(ctx)
if err != nil {
  return err
}

Documentation

Overview

Package pak1 provides read, extract, pack, and transactional edit operations for Arma Reforger PAC1 (".pak") archives.

The package parses FORM/PAC1 container layout and exposes file metadata from the FILE tree, including payload offsets, sizes, compression markers, and entry modification time.

Typical workflow:

Example (open and read):

r, err := pak1.Open("data.pak")
if err != nil {
	return err
}
defer r.Close()

first, ok := r.EntryAt(0)
if !ok {
	return nil
}

payload, err := r.ReadEntry(first.Path)
if err != nil {
	return err
}
_ = payload

Example (extract):

err = r.Extract(ctx, "out", pak1.ExtractOptions{
	MaxWorkers: 4,
})
if err != nil {
	return err
}

Example (pack):

res, err := pak1.PackFile(ctx, "out.pak", inputs, pak1.PackOptions{})
if err != nil {
	return err
}
_ = res

Example (transactional edit):

editor, err := pak1.OpenEditor("addon.pak", pak1.EditOptions{BackupKeep: 1})
if err != nil {
	return err
}

if err := editor.DeleteDir("obsolete"); err != nil {
	return err
}

_, err = editor.Commit(ctx)
if err != nil {
	return err
}

Index

Constants

View Source
const (
	DefaultWriteBuffer     = 16 * 1024 * 1024
	DefaultMinCompressSize = 512
	DefaultMaxCompressSize = 16 * 1024 * 1024
)

Default packer tuning values.

View Source
const FileExtension = ".pak"

FileExtension is the PAK file extension.

Variables

View Source
var (
	// ErrInvalidHeader means the file is missing or has an invalid FORM/PAC1 header.
	ErrInvalidHeader = errors.New("invalid PAK file: missing or bad FORM/PAC1 header")
	// ErrInvalidChunk means one or more IFF chunks are malformed.
	ErrInvalidChunk = errors.New("invalid PAK chunk")
	// ErrMissingDataChunk means DATA chunk is required but not found.
	ErrMissingDataChunk = errors.New("missing DATA chunk")
	// ErrMissingFileChunk means FILE chunk is required but not found.
	ErrMissingFileChunk = errors.New("missing FILE chunk")
	// ErrNilReader means the reader is nil.
	ErrNilReader = errors.New("reader is nil")
	// ErrNilWriter means the writer is nil.
	ErrNilWriter = errors.New("writer is nil")
	// ErrReaderAtRequired means operation requires io.ReaderAt support.
	ErrReaderAtRequired = errors.New("readerAt is required")
	// ErrClosed means reader/resource is already closed.
	ErrClosed = errors.New("reader or resource already closed")
	// ErrEntryNotFound means archive entry path is not present.
	ErrEntryNotFound = errors.New("entry not found")
	// ErrInvalidEntryPath means one of input paths is empty or invalid after normalization.
	ErrInvalidEntryPath = errors.New("invalid entry path")
	// ErrDuplicateEntryPath means two entries resolve to the same logical path.
	ErrDuplicateEntryPath = errors.New("duplicate entry path")
	// ErrUnsupportedCompression means entry uses unknown or unsupported compression type.
	ErrUnsupportedCompression = errors.New("unsupported compression type")
	// ErrInvalidEntryOffset means entry payload offset/length is outside DATA chunk bounds.
	ErrInvalidEntryOffset = errors.New("invalid entry offset")
	// ErrInvalidExtractPath means archive entry path is invalid for extraction destination.
	ErrInvalidExtractPath = errors.New("invalid extract path")
	// ErrExtractPathOutsideRoot means resolved extraction path escapes destination root.
	ErrExtractPathOutsideRoot = errors.New("extract path escapes destination root")
	// ErrEmptyInputs means pack operation got no inputs.
	ErrEmptyInputs = errors.New("no inputs provided for pack")
	// ErrSizeOverflow means value exceeds uint32 or supported file format limit.
	ErrSizeOverflow = errors.New("size exceeds PAK format limits")
	// ErrInvalidCompressPattern means one or more compression rules are invalid.
	ErrInvalidCompressPattern = errors.New("invalid compress rules")
)

Sentinel errors for PAK operations. Use errors.Is in callers.

Functions

func NormalizePath

func NormalizePath(raw string) string

NormalizePath converts an archive path to canonical slash-separated form.

func ReadHead

func ReadHead(path string) ([]byte, error)

ReadHead opens archive and returns raw HEAD chunk payload.

func ReadHeadFromReaderAt

func ReadHeadFromReaderAt(ra io.ReaderAt, size int64) ([]byte, error)

ReadHeadFromReaderAt parses archive header and returns raw HEAD chunk payload.

func ResolveSeriesPaths

func ResolveSeriesPaths(basePath string) ([]string, error)

ResolveSeriesPaths expands "name.pak" to [name.pak, name001.pak, name002.pak, ...]. Existing siblings with exactly three decimal digits are appended in numeric order.

Types

type ChunkInfo

type ChunkInfo struct {
	// Type is four-character chunk id, for example DATA.
	Type string `json:"type" yaml:"type"`
	// Length is chunk body size in bytes.
	Length uint32 `json:"length" yaml:"length"`
	// Offset is absolute file offset where chunk header starts.
	Offset int64 `json:"offset" yaml:"offset"`
}

ChunkInfo describes one top-level IFF chunk.

type CompressionType

type CompressionType uint32

CompressionType is entry compression marker from FILE metadata.

const (
	// CompressionNone means raw, uncompressed payload.
	CompressionNone CompressionType = 0
	// CompressionZlib means zlib/deflate payload.
	CompressionZlib CompressionType = 0x106
)

Supported compression type values.

type EditOptions

type EditOptions struct {
	// PackOptions are applied for added/replaced entries during commit.
	PackOptions PackOptions `json:"pack_options,omitzero" yaml:"pack_options,omitzero"`
	// BackupKeep controls how many backup generations are kept after successful commit.
	// 0 means remove backup, 1 keeps only "<archive>.bak", N keeps ".bak" + ".bak.1..N-1".
	BackupKeep int `json:"backup_keep,omitempty" yaml:"backup_keep,omitempty"`
}

EditOptions configures file-based archive edit flow.

type Editor

type Editor struct {
	// contains filtered or unexported fields
}

Editor accumulates archive edit operations and applies them on Commit.

func OpenEditor

func OpenEditor(path string, opts EditOptions) (*Editor, error)

OpenEditor creates staged editor for file-based archive rewrite workflow.

func (*Editor) Add

func (e *Editor) Add(inputs ...Input) error

Add stages creation of new entries. Fails on duplicates during Commit.

func (*Editor) Commit

func (e *Editor) Commit(ctx context.Context) (*PackResult, error)

Commit applies staged operations transactionally and returns pack result.

func (*Editor) Delete

func (e *Editor) Delete(paths ...string) error

Delete stages exact-path removal operations.

func (*Editor) DeleteDir

func (e *Editor) DeleteDir(prefixes ...string) error

DeleteDir stages directory-prefix removal operations.

func (*Editor) Replace

func (e *Editor) Replace(inputs ...Input) error

Replace stages replacement of existing entries by path.

type EntryInfo

type EntryInfo struct {
	// ModTime is per-entry modification time from FILE metadata.
	// Stored in archive as uint32 little-endian Unix seconds.
	ModTime time.Time `json:"mod_time,omitzero" yaml:"mod_time,omitzero"`
	// Path is canonical archive path, slash-separated.
	Path string `json:"path" yaml:"path"`
	// Offset is absolute payload offset in .pak file.
	Offset uint32 `json:"offset" yaml:"offset"`
	// CompressedSize is stored payload length in bytes.
	CompressedSize uint32 `json:"compressed_size" yaml:"compressed_size"`
	// OriginalSize is uncompressed payload length for compressed entries.
	OriginalSize uint32 `json:"original_size,omitempty" yaml:"original_size,omitempty"`
	// CompressionType is entry compression marker.
	CompressionType CompressionType `json:"compression_type,omitempty" yaml:"compression_type,omitempty"`
	// Unknown1 is first 4-byte opaque metadata field from FILE node.
	Unknown1 [4]byte `json:"unknown1,omitempty" yaml:"unknown1,omitempty"`
}

EntryInfo describes one file entry from FILE tree.

func ListEntries

func ListEntries(path string) ([]EntryInfo, error)

ListEntries opens archive, parses FILE tree, and returns entries.

func ListEntriesFromReaderAt

func ListEntriesFromReaderAt(ra io.ReaderAt, size int64) ([]EntryInfo, error)

ListEntriesFromReaderAt parses archive and returns entries.

func ListEntriesFromReaderAtWithOptions

func ListEntriesFromReaderAtWithOptions(ra io.ReaderAt, size int64, opts ReaderOptions) ([]EntryInfo, error)

ListEntriesFromReaderAtWithOptions parses archive with explicit options and returns entries.

func ListEntriesWithOptions

func ListEntriesWithOptions(path string, opts ReaderOptions) ([]EntryInfo, error)

ListEntriesWithOptions opens archive with explicit options and returns entries.

func (EntryInfo) IsCompressed

func (e EntryInfo) IsCompressed() bool

IsCompressed reports whether entry payload must be decompressed.

func (EntryInfo) LogicalSize

func (e EntryInfo) LogicalSize() uint32

LogicalSize returns original size for compressed entries, otherwise stored size.

type EntrySourceInfo

type EntrySourceInfo struct {
	// ArchivePath is source archive path where Entry was resolved.
	ArchivePath string `json:"archive_path" yaml:"archive_path"`
	// Entry is resolved entry metadata.
	Entry EntryInfo `json:"entry" yaml:"entry"`
}

EntrySourceInfo binds one entry to concrete archive path in a multi-pack set.

func ListEntriesSet

func ListEntriesSet(paths []string) ([]EntrySourceInfo, error)

ListEntriesSet merges entries from several archives in given order. If the same normalized path appears multiple times, later archive wins.

func ListEntriesSetWithOptions

func ListEntriesSetWithOptions(paths []string, opts ReaderOptions) ([]EntrySourceInfo, error)

ListEntriesSetWithOptions merges entries from archives with reader options. If the same normalized path appears multiple times, later archive wins.

func ListSeriesEntries

func ListSeriesEntries(basePath string) ([]EntrySourceInfo, error)

ListSeriesEntries resolves archive series paths and returns merged entries.

func ListSeriesEntriesWithOptions

func ListSeriesEntriesWithOptions(basePath string, opts ReaderOptions) ([]EntrySourceInfo, error)

ListSeriesEntriesWithOptions resolves archive series paths and returns merged entries.

type ExtractOptions

type ExtractOptions struct {
	// OnEntryDone is called after one entry is fully written to disk.
	OnEntryDone func(entry EntryInfo, written int64, outputPath string) `json:"-" yaml:"-"`
	// Entries limits extraction to selected metadata list; nil means all entries.
	Entries []EntryInfo `json:"-" yaml:"-"`
	// MaxWorkers is number of extraction workers (zero means GOMAXPROCS).
	MaxWorkers int `json:"max_workers,omitempty" yaml:"max_workers,omitempty"`
	// RawNames disables sanitization and uses raw normalized archive names.
	RawNames bool `json:"raw_names,omitempty" yaml:"raw_names,omitempty"`
	// RawPayload skips decompression and writes stored payload bytes.
	RawPayload bool `json:"raw_payload,omitempty" yaml:"raw_payload,omitempty"`
}

ExtractOptions configures archive extraction behavior.

type Input

type Input struct {
	// ModTime is optional source timestamp written to FILE metadata.
	ModTime time.Time `json:"mod_time" yaml:"mod_time"`
	// Open returns raw source payload stream.
	Open func() (io.ReadCloser, error) `json:"-" yaml:"-"`
	// Path is destination path inside archive.
	Path string `json:"path" yaml:"path"`
	// SizeHint is expected source size in bytes (zero when unknown).
	SizeHint int64 `json:"size_hint,omitempty" yaml:"size_hint,omitempty"`
}

Input describes one source stream to be packed into archive.

type PackEntryProgress

type PackEntryProgress struct {
	// Path is entry path written to archive.
	Path string `json:"path" yaml:"path"`
	// Offset is payload offset in resulting archive.
	Offset uint32 `json:"offset" yaml:"offset"`
	// CompressedSize is stored payload size in bytes.
	CompressedSize uint32 `json:"compressed_size" yaml:"compressed_size"`
	// OriginalSize is original source size for compressed payload entries.
	OriginalSize uint32 `json:"original_size,omitempty" yaml:"original_size,omitempty"`
	// CompressionType is stored compression marker.
	CompressionType CompressionType `json:"compression_type,omitempty" yaml:"compression_type,omitempty"`
	// CompressionCandidate reports whether compression path was selected.
	CompressionCandidate bool `json:"compression_candidate,omitempty" yaml:"compression_candidate,omitempty"`
	// Compressed reports whether compressed payload was actually written.
	Compressed bool `json:"compressed,omitempty" yaml:"compressed,omitempty"`
}

PackEntryProgress contains one completed entry write event from pack flow.

type PackOptions

type PackOptions struct {
	// OnEntryDone is called after one entry is fully written.
	OnEntryDone func(entry PackEntryProgress) `json:"-" yaml:"-"`
	// Head stores opaque HEAD chunk payload. Empty means default header is used.
	Head []byte `json:"head,omitempty" yaml:"head,omitempty"`
	// Compress defines ordered path rules for compression candidate selection.
	Compress []pathrules.Rule `json:"compress,omitempty" yaml:"compress,omitempty"`
	// CompressMatcherOptions controls compression path rule matching.
	CompressMatcherOptions pathrules.MatcherOptions `json:"compress_matcher_options,omitzero" yaml:"compress_matcher_options,omitzero"`
	// WriterBufferSize is buffered writer size in bytes.
	WriterBufferSize int `json:"writer_buffer_size,omitempty" yaml:"writer_buffer_size,omitempty"`
	// MinCompressSize disables compression for smaller entries.
	MinCompressSize uint32 `json:"min_compress_size,omitempty" yaml:"min_compress_size,omitempty"`
	// MaxCompressSize disables compression for larger entries.
	MaxCompressSize uint32 `json:"max_compress_size,omitempty" yaml:"max_compress_size,omitempty"`
}

PackOptions configures pack behavior.

type PackResult

type PackResult struct {
	// WrittenEntries is number of entries written to archive.
	WrittenEntries int `json:"written_entries" yaml:"written_entries"`
	// DataSize is DATA chunk body length in bytes.
	DataSize int64 `json:"data_size" yaml:"data_size"`
	// FileIndexSize is FILE chunk body length in bytes.
	FileIndexSize int64 `json:"file_index_size" yaml:"file_index_size"`
	// RawBytes is total bytes written for uncompressed payload entries.
	RawBytes int64 `json:"raw_bytes,omitempty" yaml:"raw_bytes,omitempty"`
	// CompressedBytes is total bytes written for compressed payload entries.
	CompressedBytes int64 `json:"compressed_bytes,omitempty" yaml:"compressed_bytes,omitempty"`
	// CompressedEntries is number of entries written as compressed payload.
	CompressedEntries int `json:"compressed_entries,omitempty" yaml:"compressed_entries,omitempty"`
	// SkippedCompressionEntries is number of candidates stored raw.
	SkippedCompressionEntries int `json:"skipped_compression_entries,omitempty" yaml:"skipped_compression_entries,omitempty"`
	// Duration is end-to-end pack duration.
	Duration time.Duration `json:"duration,omitempty" yaml:"duration,omitempty"`
}

PackResult contains pack output statistics.

func Pack

func Pack(ctx context.Context, out io.WriteSeeker, inputs []Input, opts PackOptions) (*PackResult, error)

Pack writes PAC1 archive to output from provided inputs.

func PackFile

func PackFile(ctx context.Context, outPath string, inputs []Input, opts PackOptions) (*PackResult, error)

PackFile writes PAC1 archive to output path.

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader provides read-only access to parsed PAC1 archive.

func NewReaderFromReaderAt

func NewReaderFromReaderAt(ra io.ReaderAt, size int64) (*Reader, error)

NewReaderFromReaderAt parses PAC1 from existing ReaderAt and known size.

func NewReaderFromReaderAtWithOptions

func NewReaderFromReaderAtWithOptions(ra io.ReaderAt, size int64, opts ReaderOptions) (*Reader, error)

NewReaderFromReaderAtWithOptions parses PAC1 from existing ReaderAt and known size with options.

func Open

func Open(path string) (*Reader, error)

Open opens PAC1 file by path and parses chunk/index structures.

func OpenWithOptions

func OpenWithOptions(path string, opts ReaderOptions) (*Reader, error)

OpenWithOptions opens PAC1 file by path and parses chunk/index structures with options.

func (*Reader) Chunks

func (r *Reader) Chunks() []ChunkInfo

Chunks returns parsed top-level chunks in source order.

func (*Reader) Close

func (r *Reader) Close() error

Close closes underlying file when Reader owns it.

func (*Reader) Entries

func (r *Reader) Entries() []EntryInfo

Entries returns copy of parsed entries.

func (*Reader) EntryAt

func (r *Reader) EntryAt(index int) (EntryInfo, bool)

EntryAt returns one parsed entry by index.

func (*Reader) EntryCount

func (r *Reader) EntryCount() int

EntryCount returns number of parsed entries without copying metadata slice.

func (*Reader) Extract

func (r *Reader) Extract(ctx context.Context, dstDir string, opts ExtractOptions) error

Extract writes selected entries to dstDir. Extraction is parallelized by MaxWorkers.

func (*Reader) ForEachEntry

func (r *Reader) ForEachEntry(callback func(entry EntryInfo) bool)

ForEachEntry iterates parsed entries in source order until callback returns false.

func (*Reader) Head

func (r *Reader) Head() []byte

Head returns copy of raw HEAD chunk body.

func (*Reader) OpenEntry

func (r *Reader) OpenEntry(name string) (io.ReadCloser, error)

OpenEntry opens named entry and returns logical payload stream.

func (*Reader) OpenEntryInfo

func (r *Reader) OpenEntryInfo(info EntryInfo) (io.ReadCloser, error)

OpenEntryInfo opens entry payload stream by already resolved metadata.

func (*Reader) OpenRawEntry

func (r *Reader) OpenRawEntry(name string) (io.ReadCloser, error)

OpenRawEntry opens named entry and returns stored payload stream without decompression.

func (*Reader) OpenRawEntryInfo

func (r *Reader) OpenRawEntryInfo(info EntryInfo) (io.ReadCloser, error)

OpenRawEntryInfo opens raw entry payload stream by already resolved metadata.

func (*Reader) ReadEntry

func (r *Reader) ReadEntry(name string) ([]byte, error)

ReadEntry reads full logical entry content into memory.

func (*Reader) ReadRawEntry

func (r *Reader) ReadRawEntry(name string) ([]byte, error)

ReadRawEntry reads full stored entry payload into memory.

type ReaderOptions

type ReaderOptions struct {
	// EntryPathPrefix keeps entries under prefix (or exact path match).
	EntryPathPrefix string `json:"entry_path_prefix,omitempty" yaml:"entry_path_prefix,omitempty"`
	// MinEntryOriginalSize keeps entries with logical size >= threshold.
	MinEntryOriginalSize uint32 `json:"min_entry_original_size,omitempty" yaml:"min_entry_original_size,omitempty"`
	// RawOnUnknownCompression returns raw entry payload for unsupported compression.
	RawOnUnknownCompression bool `json:"raw_on_unknown_compression,omitempty" yaml:"raw_on_unknown_compression,omitempty"`
}

ReaderOptions configures parse and payload-open behavior.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL