go-apfs-v2

module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 29, 2026 License: MIT

README

apfs

A cross-platform, self-contained pure-Go toolkit for Apple disk images. It reads APFS and HFS+ file systems directly from .dmg files, raw images and containers without a partition map — without mounting, without kernel extensions, and without macOS — and can also build and repack disk images.

It parses on-disk structures directly, which makes it useful for data recovery, forensic analysis, backup verification, security auditing, and CI pipelines that need to inspect or produce .dmg payloads on Linux, macOS or Windows.

$ apfs info Firefox.dmg
HFS+ volume:
  UUID               2bc924f8-...
  Size               500.6 MB (525378560 bytes)
  Volumes            1
Volume 0: Firefox
  Contents           116 files, 54 directories, 1 symlinks

$ apfs list -R Firefox.dmg | head -3
.DS_Store
Firefox.app
Firefox.app/Contents

$ apfs extract Firefox.dmg -C ./out --verify

Features

Capability APFS HFS+ / HFSX
Read: info, list, cat, extract, inspect
Mount read-only (FUSE, Linux/macOS)
FileVault unlock (AES-128-XTS, AES-256-XTS)
Transparent compression: read and write
Repack a DMG losslessly (pack)
Build a DMG from a directory (pack <dir>)
Create a formatted volume (create)
Symlinks, hard links, extended attributes, resource forks
APFS snapshots: create, list, revert (snapshot)
Several volumes per container, and volume groups
Reproducible (byte-identical) output

Both file systems can be written as well as read: pack <dir> builds a populated volume (files, symlinks, nested directories) and create formats an empty one. Created containers are validated against Apple's fsck_apfs and hdiutil and against Linux apfsck — tools that did not write them, which is the only way to know the output is right.

Every write command is reproducible: run it twice on the same input and you get two byte-identical images. That makes an image content-addressable, makes "did the vendor change anything?" a hash comparison, and lets an attestation over the output mean something. --source-date-epoch (or the standard SOURCE_DATE_EPOCH) pins the build timestamp and clamps source modification times to it; --uuid pins the volume identity. See docs/reproducible-output.md.

Image formats read: UDIF DMGs compressed with zlib (UDZO), bzip2 (UDBZ), ADC, LZFSE (ULFO) or LZMA (ULMO); GPT-partitioned and Apple-Partition-Map layouts; and raw file system images. Images are detected by content, not by file extension.

Install

Signed archives for macOS, Linux and Windows on amd64 and arm64 are attached to each release. The checksum file is signed with cosign keylessly, so verifying it needs no key of ours:

cosign verify-blob \
  --bundle apfs_<version>_checksums.txt.sigstore.json \
  --certificate-identity-regexp 'https://github.com/deploymenttheory/go-apfs-v2/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  apfs_<version>_checksums.txt

shasum -a 256 -c apfs_<version>_checksums.txt --ignore-missing

Or build it yourself:

go install github.com/deploymenttheory/go-apfs-v2/cmd/apfs@latest

# From a clone
git clone https://github.com/deploymenttheory/go-apfs-v2
cd go-apfs-v2
go build -o apfs ./cmd/apfs

One static binary with the full read and write feature set for both file systems. No cgo, so it cross-compiles to Linux, macOS and Windows on amd64 and arm64 and needs nothing installed alongside it.

Command reference

Every command takes the image path as its first argument. Global flags (below) apply to all commands.

info — container and volume summary
apfs info IMAGE [--hierarchy] [--bodyfile FILE] [--md5] [--entry ID] [--file-path PATH]

Prints the file system type (APFS or HFS+), container/volume UUIDs, sizes, file/directory/symlink counts, and — for APFS — each volume's role and volume group. With -o json, emits a single document including a fileSystem field.

apfs info image.dmg
apfs info -o json image.dmg | jq -r '.volumes[0].name'
apfs info -v "Macintosh HD" image.dmg          # select a volume by name/UUID
apfs info -v system image.dmg                  # ...or by role
apfs info --hierarchy image.dmg                # full tree (APFS, text)
apfs info --bodyfile out.body image.dmg        # Sleuth Kit bodyfile

The forensic flags (--hierarchy, --bodyfile, --md5, --entry, --file-path) are APFS-only and text-only.

Volume roles

A volume's role (apfs_role) says what it is for — system, data, recovery, preboot, update and so on. Without it a macOS installer or system image is illegible: several similarly-named volumes with no indication which one holds the OS.

-v/--volume accepts a role token anywhere it accepts an index, name or UUID, so -v system finds the OS volume without knowing its name. Name and UUID are matched first, so a volume literally named system still wins; -v role:system skips that and matches only on role. A role matching several volumes is an error naming the candidates rather than an arbitrary pick.

In JSON, role is the lowercase token, roleName the display name, and roleValue the raw apfs_role — always present, so a value this build does not recognize is visible rather than silently dropped. volumeGroupId is the volume group (apfs_volume_group_id), the system/data pairing macOS has used since Catalina. All three are omitted when unset.

Note that roles are a single value, never a combination: 0x00c0 is update, not data|baseband.

list — list directory contents
apfs list IMAGE [PATH] [-l|--long] [-R|--recursive]

Lists a directory (default: the volume root), sorted by name. With -o json, emits one JSON object per line (path, name, type, size, mode, mtime, inode, symlink target) for streaming with jq.

apfs list image.dmg
apfs list image.dmg /Applications --long
apfs list -R image.dmg | wc -l
apfs list -o json -R image.dmg | jq -r 'select(.type=="symlink") | "\(.path) -> \(.target)"'
cat — write file contents to stdout
apfs cat IMAGE PATH...

Streams one or more files to stdout, for pipelines.

apfs cat image.dmg /etc/hosts
apfs cat image.dmg /App.app/Contents/Info.plist | plutil -p -
extract — extract files to the local file system
apfs extract IMAGE [PATH] -C DIR [-r|--recursive] [--pattern REGEX]
                   [--preserve-meta] [--xattrs] [--verify]
                   [--symlinks auto|real|file]

Extracts the whole volume (or a subtree given PATH) to DIR, preserving symlinks, decompressing transparently-compressed files, and optionally restoring permissions/timestamps (--preserve-meta), restoring extended attributes (--xattrs) and verifying content against source checksums (--verify).

Symlink handling (--symlinks): auto (default) creates a real symlink where the OS allows it and otherwise writes the link target into a regular file (useful on Windows without the symlink privilege); real always creates a symlink and fails if refused; file always writes the target as a file. Names the destination OS cannot store (e.g. trailing spaces on Windows) are sanitized and reported. Exit code 6 signals a partial extraction.

apfs extract image.dmg -C ./out
apfs extract image.dmg /Applications/Some.app -C ./out --recursive
apfs extract image.dmg -C ./out --pattern '\.plist$'
apfs extract image.dmg -C ./out --preserve-meta --verify
inspect — low-level structural inspection
apfs inspect IMAGE                 # structural walk (superblock, checkpoints, object maps, volumes)
apfs inspect IMAGE block N         # decode one block by physical address (decimal or 0x hex)
apfs inspect IMAGE fstree          # interactively explore the file-system tree

Debugging/forensics view of the on-disk structures. Text output only.

fstree mode accepts --fstree-root and --omap-root to override the physical addresses it would otherwise resolve, which is how a container whose superblock no longer points at a readable tree can still be walked.

apfs inspect image.dmg
apfs inspect image.dmg block 0
apfs inspect image.dmg block 0x1b0c1
apfs inspect image.dmg fstree
mount — mount read-only via FUSE
apfs mount IMAGE MOUNTPOINT

Mounts an APFS image read-only using FUSE (Linux and macOS with macFUSE). On Windows it exits with code 5; use extract instead. Press Ctrl+C to unmount.

apfs mount image.dmg /mnt/apfs
pack — build a DMG from a directory, or repack a DMG
apfs pack SOURCE OUT.dmg [--fs hfs+|apfs] [--volname NAME] [--compression lzfse|lzma|zlib|none] [--chunk-size KiB]
                         [--strict] [--decompress]

Two modes, chosen by what SOURCE is:

  • SOURCE is a directory → its contents are written into a new volume and wrapped in a DMG (the inverse of extract). --fs chooses the file system: hfs+ (default) or apfs.
  • SOURCE is a DMG → it is repacked losslessly: the exact block layout is preserved and the chunks recompressed. The result is not byte-identical to the original (different compressors produce different container bytes), but the raw file system image round-trips bit-for-bit and mounts under both this tool and macOS. Repacking the same input twice is byte-identical.
apfs pack ./mytree out.dmg --volname "My Data"          # directory -> HFS+ DMG
apfs pack ./mytree out.dmg --fs apfs --volname "My Data" # directory -> APFS DMG
apfs pack original.dmg repacked.dmg                     # repack a DMG (LZFSE)
apfs pack original.dmg smaller.dmg --compression lzma   # smallest output
apfs pack original.dmg fast.dmg --compression none      # no compression
apfs pack ./mytree out.dmg --fs apfs --decompress        # store compressed files in full

The DMG chunk compressor is chosen with --compression: lzfse (default — Apple's modern codec, the best speed/ratio balance and what recent hdiutil produces), lzma (ULMO — the smallest output, at the cost of speed), zlib (UDZO — the widely-compatible classic) or none. Each codec stores any chunk it cannot shrink raw, so the choice never grows an incompressible image. All four are reproducible: the same input and codec give byte-identical output.

A source file with transparent compression (decmpfs) keeps it, on either file system: the compressed bytes are copied across as they stand, so the volume holds what the source did and takes no more room. --decompress writes those files out in full instead, which is what you want if the image is bound for a reader that does not understand decmpfs — one that ignores the attribute sees an empty file rather than a large one.

create — format an empty volume
apfs create OUT.dmg --fs hfs+|apfs [--volname NAME] [--size MiB] [--case-sensitive]
                    [--role ROLE] [--volume-group UUID]

Creates a new DMG containing a freshly formatted, empty volume (the format operation). Both --fs hfs+ and --fs apfs are supported.

apfs create blank.dmg --fs hfs+ --volname Scratch --size 16
apfs create blank.dmg --fs apfs --volname Data
apfs create system.dmg --fs apfs --volname "Macintosh HD" --role system
apfs create group.dmg  --fs apfs --volname "Macintosh HD" --volume-group "$(uuidgen)"

--volume-group writes a complete group: a system volume with the given name and a data volume called " - Data", both carrying the group identifier. A group is a system/data pair, so one flag makes both halves and --role may not be combined with it. The container is sized for two volumes automatically, since APFS allows one volume per 512 MiB.

--role and --volume-group are APFS-only, and cannot be combined: a group gives each of its halves a role already.

A snapshot capturing the new volume can be created at the same time with --snapshot NAME (APFS only). This applies to pack too.

Reproducible output

create, pack and snapshot create produce byte-identical images for identical input, with no flags required.

[--source-date-epoch SECONDS] [--uuid UUID] [--volume-uuid UUID] [--container-uuid UUID]
apfs pack ./tree a.dmg --source-date-epoch 1700000000
SOURCE_DATE_EPOCH=1700000000 apfs pack ./tree b.dmg
shasum -a 256 a.dmg b.dmg   # identical

--source-date-epoch pins the build timestamp and clamps source modification times newer than it, per the reproducible-builds convention. It resolves as --source-date-epoch > SOURCE_DATE_EPOCH > APFS_SOURCE_DATE_EPOCH > config file — the bare variable outranks the APFS_ form, the one deliberate exception to this tool's usual precedence, because build systems set the standard name.

Images are built into a temporary file beside the output rather than in memory, so image size is not bounded by RAM. --temp-dir puts that scratch file somewhere else; the system temporary directory is deliberately not the default, because it is often small and on many Linux systems is RAM-backed. create and pack check there is room before they start, rather than failing part-way through.

--uuid pins the volume UUID; for APFS the container UUID is derived from it. Full detail, including the residual sources of variance, is in docs/reproducible-output.md.

snapshot — create, list and revert APFS snapshots
apfs snapshot list   IMAGE
apfs snapshot create IMAGE --name NAME [-O OUT.dmg] [--force]
apfs snapshot revert IMAGE --name NAME [-O OUT.dmg] [--force]
apfs snapshot verify IMAGE

Real, spec-compliant APFS snapshots — recognized by macOS (diskutil apfs listSnapshots), validated with fsck_apfs/apfsck. Snapshots are an APFS concept (HFS+ has none). A snapshot captures a volume at a point in time, for data-forensics and backup-verification workflows.

  • list — show each APFS volume's snapshots (name, xid, creation time). With -o json, one record per snapshot.
  • create — rebuild the image with an added snapshot of the current state. The writer rebuilds the volume from its contents (so the result is not a byte-for-byte copy of the source). To protect evidence, the result goes to -O/--output by default; overwriting the source in place requires --force.
  • revert — mark the volume to roll back to a snapshot on next mount (the spec's revert_to_xid mechanism, patched in place with a resealed checksum). Same output safety.
  • verify — confirm every snapshot's frozen superblock and metadata read back.
apfs snapshot create evidence.dmg --name baseline -O evidence-snap.dmg
apfs snapshot list   evidence-snap.dmg
apfs snapshot revert evidence-snap.dmg --name baseline -O reverted.dmg

One snapshot per image is supported today; multiple snapshots require distinct transaction ids and incremental checkpoints (planned).

Global flags

Flag Description
-o, --output Output format: text or json (default text).
-v, --volume STR Select a volume by index, name or UUID (default: first).
-p, --password STR Password for encrypted (FileVault) volumes.
--password-stdin Read the password from standard input.
--recovery-password STR Recovery password for encrypted volumes.
--offset N Byte offset of the container in the image (expert; overrides detection).
-q, --quiet Suppress progress and non-essential messages.
--verbose Verbose diagnostics on stderr.

Configuration precedence: command-line flag > APFS_<FLAG> environment variable (e.g. APFS_OUTPUT=json, APFS_PASSWORD=…) > optional config file at ~/.config/apfs/config.yaml.

Data is written to stdout; progress and diagnostics go to stderr, so pipelines stay clean.

Exit codes

Code Meaning
0 Success
1 Generic runtime error
2 Usage error (bad flags or arguments)
3 Image not found or not a recognizable APFS/HFS+ image
4 Authentication required or failed
5 Feature not supported on this platform / build
6 Partial result (e.g. some files skipped during extraction)

Using it as a library

Volumes implement io/fs.FS, so they plug into any code that consumes a file system:

import (
	"io/fs"
	"github.com/deploymenttheory/go-apfs-v2/pkg/apfs"
)

container, closer, _ := apfs.OpenImage("image.dmg", nil)
defer closer.Close()
defer container.Free()

vol, _ := container.VolumeBySelector("0")
data, _ := fs.ReadFile(vol, "Applications/Some.app/Contents/Info.plist")

Key packages: pkg/apfs (APFS reader), pkg/hfsplus (HFS+ reader and writer), pkg/disk (DMG/UDIF reader and writer, partition tables), and pkg/apfswrite (APFS container writer).

Licensing

This repository is MIT-licensed in full, including pkg/apfswrite (the APFS writer). That package began as a study of mkapfs (GPL-2.0), used as a reference for the format's behaviour, and was then reimplemented independently from Apple's Apple File System Reference; it shares none of mkapfs's source expression. See NOTICE and pkg/apfswrite/README.md for the full provenance.

Development

go build ./...            # build everything
go test ./pkg/... ./internal/...   # unit tests
go test -v ./acceptance/  # acceptance suite against the committed fixtures

The acceptance tests live in their own root package, acceptance/, which holds no production code. A test goes there if something outside our own code decides whether it passed — the real apfs command run as a subprocess, or a tool that ships with the OS such as fsck_apfs, apfsck or hdiutil. Tests that only check our code against itself stay in the package they test. Some of them run against a published application DMG and are skipped unless APFS_ACCEPTANCE_DMG is set; see go doc ./acceptance.

CI runs the build matrix and the test suite on Linux, macOS and Windows. Correctness is cross-checked against the platforms' own tools: created HFS+ volumes are validated with fsck_hfs and mounted with hdiutil; created APFS containers with fsck_apfs (macOS) and apfsck (Linux). Real-world acceptance runs against published vendor DMGs (Firefox for HFS+, Zed for APFS), and on macOS every extraction is compared byte-for-byte against an hdiutil mount of the same image.

Acknowledgements

The design and on-disk handling draw on libfsapfs, blacktop/go-apfs, apfsprogs and Apple's Apple File System Reference. See NOTICE for details and licenses.

Directories

Path Synopsis
Package acceptance holds the acceptance tests for this repository.
Package acceptance holds the acceptance tests for this repository.
cmd
apfs command
APFS Tools - Unified CLI for APFS container operations
APFS Tools - Unified CLI for APFS container operations
internal
cli
apfs cat IMAGE PATH...
apfs cat IMAGE PATH...
decmpfs
Package decmpfs decodes Apple's transparent file compression.
Package decmpfs decodes Apple's transparent file compression.
hostmeta
Package hostmeta reads the file metadata the Go standard library does not expose: extended attributes, hard-link identity and BSD flags.
Package hostmeta reads the file metadata the Go standard library does not expose: extended attributes, hard-link identity and BSD flags.
hostwalk
Package hostwalk walks a host directory tree into a file-system writer's entry tree, recording everything it cannot carry across.
Package hostwalk walks a host directory tree into a file-system writer's entry tree, recording everything it cannot carry across.
imagefile
Package imagefile provides a temporary-file-backed io.WriterAt for building a raw file system image without holding it in memory.
Package imagefile provides a temporary-file-backed io.WriterAt for building a raw file system image without holding it in memory.
tools
Bodyfile output functions for APFS
Bodyfile output functions for APFS
pkg
apfs
Public entry points for opening APFS containers from images and readers.
Public entry points for opening APFS containers from images and readers.
apfswrite
Package apfswrite builds APFS containers from scratch in pure Go.
Package apfswrite builds APFS containers from scratch in pure Go.
disk
Apple Data Compression (ADC) decompressor.
Apple Data Compression (ADC) decompressor.
exitcode
Package exitcode defines the exit-code contract of the apfs command.
Package exitcode defines the exit-code contract of the apfs command.
fidelity
Package fidelity describes what a directory-to-volume write cannot carry across.
Package fidelity describes what a directory-to-volume write cannot carry across.
hfsplus
Extended attributes: the attributes file, an HFS+ special file holding a third B-tree keyed by (file id, attribute name).
Extended attributes: the attributes file, an HFS+ special file holding a third B-tree keyed by (file id, attribute name).
scripts
casefold command
Command casefold derives the HFS+ case-folding table by observing macOS.
Command casefold derives the HFS+ case-folding table by observing macOS.
normalize command
Command normalize derives the HFS+ name-normalization exclusion list by observing macOS.
Command normalize derives the HFS+ name-normalization exclusion list by observing macOS.

Jump to

Keyboard shortcuts

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