unrealpak

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 8, 2026 License: MIT Imports: 14 Imported by: 0

README

go-unrealpak

Pure-Go reader and writer for Unreal Engine v11 (PakFile_Version_Fnv64BugFix) .pak archives, plus a CLI. No cgo, no external dependencies.

Existing tooling is Rust (repak) or Python (u4pak); this exists because a Go mod manager needed to read and write these archives in-process.

Scope

Read stored (uncompressed) and Zlib entries
Write stored entries only
Oodle not supported — indexes read fine, but reading an Oodle payload returns an error naming the entry
Encryption not supported; an encrypted index is rejected
Versions v10+ (the path-hash index format); v11 is what it writes

The format documentation is in docs/format.md, decoded empirically and verified against 173,078 entries across 34 real paks.

CLI

go install github.com/DonovanMods/go-unrealpak/cmd/unrealpak@latest
unrealpak info    <pak>                          # mount point, entry count, total size, index hash
unrealpak list    <pak> [--json]                 # entries with sizes, sorted by path
unrealpak cat     <pak> <path>                   # one entry's bytes to stdout
unrealpak extract <pak> <dir> [--filter <glob>]  # entries to dir, + a .unrealpak.json sidecar
unrealpak build   <dir> <pak> [--mount <mount>]  # pack dir; mount defaults to the sidecar's

Flags may appear before or after the positional arguments.

extract records the source mount point in .unrealpak.json so a plain extractbuild cycle round-trips without flags. build refuses to guess a mount point: a wrong one produces a pak that loads and silently does nothing.

extract also refuses any entry path that would escape the output directory, since entry paths in an archive you did not build yourself are untrusted input.

Library

r, err := unrealpak.Open("pakchunk0-WindowsNoEditor.pak")
if err != nil {
    return err
}
defer r.Close()

fmt.Println(r.MountPoint(), len(r.Files()))

data, err := r.ReadFile("Engine/Config/Base.ini")
w, err := unrealpak.Create("out_P.pak", unrealpak.WithMountPoint("../../../Game/Content/"))
if err != nil {
    return err
}
if err := w.AddFile("data/Thing.json", payload); err != nil {
    return err
}
return w.Close()

Mount points and entry paths are whatever you supply — no game's conventions are baked in.

Testing

go test -race ./...

Most reader tests read paks this package's own writer produced. To also exercise real cooker output (Zlib block lists, 1 MiB alignment padding, a populated pruned directory index), point UNREALPAK_TEST_PAK at a shipped .pak:

UNREALPAK_TEST_PAK=/path/to/game/Content/Paks/pakchunk0-WindowsNoEditor.pak go test -run RealPak -v

That test skips when the variable is unset.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrUnsupportedFormat = errors.New("unrealpak: unsupported pak feature")

ErrUnsupportedFormat indicates the pak uses a feature this package deliberately does not support (compression, encryption, exotic FString encodings) rather than a genuine parse failure. Callers should fail loudly on this, not silently degrade (repo precedent: #95).

Functions

This section is empty.

Types

type FileEntry

type FileEntry struct {
	Path string // Mount-relative path, e.g. "Icarus/Content/Data/AI-D_AIGrowth.json"
	Size int64  // Uncompressed size in bytes
}

FileEntry describes one file inside a pak, as returned by Reader.Files.

type Option

type Option func(*Writer)

Option configures a Writer at construction time (see Create). The set is deliberately tiny and additive: new options can be introduced without breaking existing Create(path) call sites, since opts is variadic.

func WithMountPoint

func WithMountPoint(mountPoint string) Option

WithMountPoint overrides the mount point Writer stamps into the primary index (see defaultMountPoint). This package stays game-agnostic — it has no built-in notion of any specific game's directory layout — so a caller that knows what its target game's mod loader expects supplies it here, rather than this package guessing or hard-coding one game's convention.

type Reader

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

Reader provides read access to an unencrypted UE4-range pak. Stored entries and Zlib-compressed entries are readable; any other compression method is a loud ErrUnsupportedFormat.

func Open

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

Open parses path's footer and index. It does not read file contents — call ReadFile for that (Task 3).

func (*Reader) Close

func (r *Reader) Close() error

Close releases the underlying file handle.

func (*Reader) Files

func (r *Reader) Files() []FileEntry

Files returns every file this pak's index describes.

func (*Reader) IndexHash

func (r *Reader) IndexHash() string

IndexHash returns the pak's footer-recorded primary-index SHA1 as a lowercase hex string — a cheap, stable fingerprint of the pak's content (Open already reads and verifies this region; IndexHash reads no further bytes and never hashes the pak's actual file payloads). Any content or layout change to the pak changes its primary index and therefore this hash, making it a reliable "has this pak changed" signal without the cost of hashing the whole (often multi-gigabyte) file.

func (*Reader) MountPoint

func (r *Reader) MountPoint() string

MountPoint returns this pak's primary-index MountPoint string — where the engine roots Files' mount-relative paths once the pak is mounted. Real paks vary this: Icarus's own data.pak declares an absolute cook-machine path, while mod paks conventionally declare a relative "../../../..." form (see Writer's WithMountPoint, #178).

func (*Reader) ReadFile

func (r *Reader) ReadFile(path string) ([]byte, error)

ReadFile returns the bytes of the entry at mount-relative path.

On-disk entry data is preceded by a full FPakEntry header — 53 bytes for a stored entry, plus a block table for a compressed one — and the index's offset points at that header, not the payload. The header is re-read and cross-checked rather than trusted: its method and sizes must agree with the index, and its Hash must match the on-disk payload's SHA1. Real paks satisfy all of this (verified across a whole install), so a disagreement means corruption or a layout this package misread.

type Writer

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

Writer produces a stored (uncompressed), unencrypted version-11 pak carrying the full three-part index: primary index, path-hash index and full directory index, then the 221-byte footer.

AddFile buffers content in memory and Close emits everything sorted by path, so identical inputs produce byte-identical output regardless of AddFile call order. Mod paks are small — Icarus's entire base data.pak is 2.4 MB — so buffering costs little, and deterministic output is worth more: it makes the round-trip test able to assert on bytes and keeps compiled paks stable across recompiles.

func Create

func Create(path string, opts ...Option) (*Writer, error)

Create opens path for writing. Call AddFile for each entry, then Close. With no options, the written pak uses defaultMountPoint, matching every existing caller's prior behavior exactly.

func (*Writer) AddFile

func (w *Writer) AddFile(mountPath string, data []byte) error

AddFile records one entry. Nothing reaches disk until Close.

func (*Writer) Close

func (w *Writer) Close() error

Close assembles the data section and all three index structures, writes them with the footer, and closes the file.

Directories

Path Synopsis
cmd
unrealpak command
Command unrealpak inspects and builds Unreal Engine v11 .pak archives.
Command unrealpak inspects and builds Unreal Engine v11 .pak archives.

Jump to

Keyboard shortcuts

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