Documentation
¶
Index ¶
- Variables
- func Format(path string, sizeBytes int64, cfg FormatConfig) (filesystem.Filesystem, error)
- func FormatAppleDmg(path string, sizeBytes int64, cfg FormatConfig) error
- func FormatContainer(path string, sizeBytes int64, volumeLabel string) error
- func FormatContainerEncrypted(path string, sizeBytes int64, volumeLabel string, passphrase []byte) error
- func FormatContainerEncryptedGPT(path string, totalSize int64, volumeLabel string, passphrase []byte) error
- func Open(imagePath string, partIndex int) (filesystem.Filesystem, error)
- func OpenFDE(imagePath string, passphrase []byte, partIndex int) (filesystem.Filesystem, error)
- func OpenFromBlockDevice(dev BlockRW, partIndex int) (filesystem.Filesystem, error)
- func OpenWithKeys(imagePath string, partIndex int, keys ...string) (filesystem.Filesystem, error)
- type BlockRW
- type Container
- func (c *Container) AddVolume(label string) (int, error)
- func (c *Container) Close() error
- func (c *Container) Commit() error
- func (c *Container) Grow(newSizeBytes int64) error
- func (c *Container) OpenSnapshot(snap Snapshot) (*Volume, error)
- func (c *Container) OpenVolume(index int) (*Volume, error)
- func (c *Container) Resize(newSizeBytes int64) error
- func (c *Container) SetVerifyHashes(on bool)
- func (c *Container) Shrink(newSizeBytes int64) error
- func (c *Container) Volumes() []VolumeInfo
- type FDEConfig
- type FormatConfig
- type Inode
- type Sibling
- type Snapshot
- type Volume
- func (v *Volume) CreateBlockDevice(parentOID uint64, name string, perm uint16, rdev uint32) (uint64, error)
- func (v *Volume) CreateCharDevice(parentOID uint64, name string, perm uint16, rdev uint32) (uint64, error)
- func (v *Volume) CreateDirectory(parentOID uint64, name string, perm uint16) (uint64, error)
- func (v *Volume) CreateFifo(parentOID uint64, name string, perm uint16) (uint64, error)
- func (v *Volume) CreateFile(parentOID uint64, name string, data []byte) (uint64, error)
- func (v *Volume) CreateHardlink(targetOID, newParentOID uint64, newName string) error
- func (v *Volume) CreateSnapshot(name string) (uint64, error)
- func (v *Volume) CreateSocket(parentOID uint64, name string, perm uint16) (uint64, error)
- func (v *Volume) CreateSparseFile(parentOID uint64, name string, size uint64) (uint64, error)
- func (v *Volume) CreateSymlink(parentOID uint64, name, target string) (uint64, error)
- func (v *Volume) DebugWalkInodes(visit func(oid uint64, val []byte)) error
- func (v *Volume) DeleteDirectory(parentOID uint64, name string) error
- func (v *Volume) DeleteFile(parentOID uint64, name string) error
- func (v *Volume) DeleteSnapshot(name string) error
- func (v *Volume) FileReaderAt(inode Inode) (io.ReaderAt, error)
- func (v *Volume) FindInode(oid uint64) (Inode, error)
- func (v *Volume) ListInodes() ([]Inode, error)
- func (v *Volume) ListSiblings(owner Inode) ([]Sibling, error)
- func (v *Volume) ListSnapshots() ([]Snapshot, error)
- func (v *Volume) ListXAttrs(owner Inode) ([]XAttr, error)
- func (v *Volume) LookupInodeRawValue(oid uint64) ([]byte, error)
- func (v *Volume) LookupInodeRecord(oid uint64) (Inode, error)
- func (v *Volume) LookupSnapshotByName(name string) (Snapshot, error)
- func (v *Volume) Name() string
- func (v *Volume) OverwriteFile(oid uint64, newData []byte) error
- func (v *Volume) ReadFile(inode Inode) ([]byte, error)
- func (v *Volume) ReadFileTransparent(inode Inode) ([]byte, error)
- func (v *Volume) ReadXAttrStream(x XAttr) ([]byte, error)
- func (v *Volume) Rename(oldParentOID uint64, oldName string, newParentOID uint64, newName string) error
- func (v *Volume) SetSuppressSnapshotGuard(on bool)
- func (v *Volume) SetXAttr(oid uint64, name string, payload []byte) error
- func (v *Volume) SetXAttrStream(targetOID uint64, name string, payload []byte) error
- func (v *Volume) TruncateFile(oid uint64, newSize uint64) error
- func (v *Volume) WriteFile(inode Inode, data []byte) error
- func (v *Volume) WriteFileInPlace(inode Inode, data []byte) error
- func (v *Volume) XAttrStreamReaderAt(x XAttr) (io.ReaderAt, error)
- type VolumeInfo
- type XAttr
Constants ¶
This section is empty.
Variables ¶
var ErrHasSnapshot = errors.New("apfs: refusing to mutate a volume with active snapshots (would corrupt the frozen view)")
ErrHasSnapshot is returned by every writer-side entry point on a volume whose APSB reports `apfs_num_snapshots > 0`. The frozen snapshot view shares physical blocks (FS-tree root, extent-ref tree, etc.) with the live volume, so an in-place mutation would corrupt the snapshot. Until copy-on-write is implemented for every mutating path, callers must remove the snapshot first OR explicitly suppress the guard via `Volume.SetSuppressSnapshotGuard(true)`.
var ErrNoHeader = errors.New("apfs: no header")
ErrNoHeader is returned by Open when the file is neither a real APFS container nor (on darwin) a hdiutil-mountable image.
var ErrReadOnly = errors.New("apfs: container is read-only")
ErrReadOnly is returned by write paths when the container was opened without write capability (e.g. via OpenContainer which opens the file O_RDONLY, or via OpenContainerFromBackend with a read-only backend).
var ErrResizeUnsupported = errors.New("apfs: resize crosses chunk boundary (not implemented)")
ErrResizeUnsupported is returned when a Grow/Shrink would require allocating a fresh chunk_info_block (i.e. cross a 128 MiB chunk boundary). The single-chunk regime covers every test container in this package; a future iteration will lift the restriction.
var ErrShrinkUnsupported = errors.New("apfs: shrink would lose allocated extents (relocation not implemented)")
ErrShrinkUnsupported is returned by Shrink when at least one block at or above the requested new boundary is still marked allocated in the spaceman bitmap. Relocating those extents downward would require a pure-Go defragmenter, which is out of scope here.
var ErrUnsupported = errors.New("apfs: feature not implemented in this iteration")
ErrUnsupported is returned for code paths the parser knows exist on disk but does not yet implement (compressed extents, hashed FS-tree, etc.).
Functions ¶
func Format ¶
func Format(path string, sizeBytes int64, cfg FormatConfig) (filesystem.Filesystem, error)
Format creates a fresh APFS container at `path` and returns a filesystem.Filesystem wrapping its first volume. The file is auto-created if missing. For encrypted containers, populate cfg.Encryption with the passphrase.
func FormatAppleDmg ¶
func FormatAppleDmg(path string, sizeBytes int64, cfg FormatConfig) error
FormatAppleDmg creates a real Apple DMG formatted as APFS using `hdiutil`. This is used by diskimage on macOS to produce images mountable by native tools.
func FormatContainer ¶
FormatContainer writes a fresh, empty APFS container to the file at path. The file must already exist and be at least formatMetadataBlocks * 4 KiB in size; FormatContainer writes the metadata blocks at offsets 0 through (formatMetadataBlocks-1) * 4096 and leaves the remainder zeroed.
The returned container is the layout described in the package-level comment of format.go. Open the file with OpenContainer to verify it is a valid APFS volume; ListInodes will return an empty slice and ListSnapshots a nil slice.
func FormatContainerEncrypted ¶
func FormatContainerEncrypted(path string, sizeBytes int64, volumeLabel string, passphrase []byte) error
FormatContainerEncrypted writes a fresh APFS container at path with FileVault-style software encryption enabled, protected by passphrase. The returned container has nx_flags |= NX_CRYPTO_SW set and a container + volume keybag pair the kext-style unlock walk (passphrase → PBKDF2-derived key → KEK → VEK) can recover.
The container is byte-compatible with apfsfde.Open at the keybag-chain level — see TestFormatContainerEncrypted_Roundtrips. It is NOT yet expected to mount via `hdiutil attach -stdinpass` because the volume metadata blocks (APSB, OMAPs, FS-tree root, …) are still plaintext; adding the AES-XTS-VEK layer on top is the next iteration.
func FormatContainerEncryptedGPT ¶
func FormatContainerEncryptedGPT(path string, totalSize int64, volumeLabel string, passphrase []byte) error
FormatContainerEncryptedGPT writes an Apple_APFS-GPT-wrapped FileVault-style encrypted container to path. The output is a single file totalSize bytes large with a protective MBR + primary GPT at the head, the APFS container starting at byte 1 MiB (LBA 2048), and a GPT backup at the tail. apfs.kext recognises the Apple_APFS (7C3457EF-…) partition GUID in the GPT and binds the synthesised container's physical store correctly — without it the kext attaches the raw image but the container scheme device shows `+0 B` capacity and no inner volumes.
The APFS container itself is exactly what FormatContainerEncrypted produces. totalSize must accommodate the GPT overhead (~1 MiB at the head + ~16 KiB at the tail) plus the formatMetadataBlocks-class minimum APFS container size.
func Open ¶
func Open(imagePath string, partIndex int) (filesystem.Filesystem, error)
Open opens an existing APFS container at `path`. Resolution order:
- Try real APFS (pure Go, all platforms).
- Try FileVault-encrypted real APFS (requires a passphrase, see OpenWithKeys for that variant).
- On darwin: hdiutil-mount the image and proxy operations to the mountpoint.
Returns ErrNoHeader when nothing matches.
The partIndex parameter is accepted for API compatibility but currently ignored (the pure-Go reader uses the container's first volume; GPT-partitioned images are handled transparently by OpenContainerAuto in fs.go).
func OpenFDE ¶
func OpenFDE(imagePath string, passphrase []byte, partIndex int) (filesystem.Filesystem, error)
OpenFDE opens a FileVault 2-encrypted APFS container at imagePath, unlocking it with passphrase, and returns a Filesystem backed by the decrypted container.
If imagePath does not look like a FileVault container, an error is returned; the caller should fall back to Open.
func OpenFromBlockDevice ¶
func OpenFromBlockDevice(dev BlockRW, partIndex int) (filesystem.Filesystem, error)
OpenFromBlockDevice opens an APFS container from any read-write block device satisfying BlockRW. Useful for QCOW2 or memory- backed backends. For FileVault-encrypted devices, use OpenFDE instead.
func OpenWithKeys ¶
func OpenWithKeys(imagePath string, partIndex int, keys ...string) (filesystem.Filesystem, error)
OpenWithKeys opens an APFS container, trying the supplied keys against the FileVault keybag (if encrypted). For unencrypted images the keys are ignored. Falls back to hdiutil-mount on darwin.
Types ¶
type BlockRW ¶
type BlockRW interface {
ReadAt(p []byte, off int64) (int, error)
WriteAt(p []byte, off int64) (int, error)
Close() error
}
BlockRW is the minimal interface for an arbitrary read-write block device accepted by OpenFromBlockDevice. Kept for source-level compatibility with callers that still pass custom backends.
type Container ¶
type Container struct {
// contains filtered or unexported fields
}
Container is an opened APFS container. It does not hold any keys — callers must unlock the underlying device with go-fde/apfs (or supply a non-encrypted reader) before passing it here.
func OpenContainer ¶
OpenContainer opens a real APFS container at path read-only.
func OpenContainerAuto ¶
OpenContainerAuto opens an APFS container at path read-only, auto- detecting whether the file is naked APFS (NX SB at offset 0) or GPT-wrapped (NX SB inside the Apple_APFS partition). Use this when you don't know up-front whether you're looking at the output of our `FormatContainer` (naked) or Apple's `hdiutil create -fs APFS` (GPT).
func OpenContainerFromBackend ¶
OpenContainerFromBackend opens an APFS container from any ReadAt-capable backend. If the backend additionally satisfies containerWriter (WriteAt), write APIs are enabled. The caller retains ownership of the backend (Close on the returned container will not close it).
func OpenContainerRW ¶
OpenContainerRW opens an APFS container at path read-write so callers can invoke the mutating APIs (WriteFileInPlace, ...). Read paths behave identically to OpenContainer.
func OpenContainerRWAuto ¶
OpenContainerRWAuto is OpenContainerAuto with read+write capability. When the image is GPT-wrapped, both reads and writes are offset into the Apple_APFS partition; the GPT header and protective MBR are untouched (writes outside the APFS partition would corrupt them).
func (*Container) AddVolume ¶
AddVolume adds a new volume to an open APFS container. Returns the new volume's index in the container's fs_oid array (0-based; the existing first volume is at index 0). Caller does NOT need to invoke Commit afterward — the changes are persisted in place at block 0 + the current desc-area NX SB copy + the container OMAP leaf.
Limit: the container starts with one volume from FormatContainer and can grow to a total of 100. Each additional volume needs 6 fresh metadata blocks; AddVolume returns an error when the chunk bitmap can't supply 6 contiguous free blocks past the format-time metadata.
func (*Container) Close ¶
Close releases the underlying file descriptor when one was opened by OpenContainer; OpenContainerFromBackend is a no-op.
func (*Container) Commit ¶
Commit promotes the in-memory state to a new on-disk checkpoint. Callers that have run CreateFile / WriteFile / etc. must Commit before macOS will mount the result; without a Commit the mutations live only at the FS-tree level and the (older) checkpoint that fsck uses to validate the container does not see them.
The Commit cascade:
- Compute the next checkpoint's xid (= current xid + 1).
- Compute the next slots in the desc + data ring buffers.
- Write a fresh SPACEMAN, REAPER, FQ_IP and FQ_MAIN at the new data slots, all carrying the new xid.
- Write a CheckpointMap at the next desc slot mapping each ephemeral OID to its new paddr.
- Write a new block-0 NX SB pointing at the new checkpoint, then replicate it at the desc slot AFTER the CheckpointMap.
All writes are sealed (Fletcher64) before WriteAt; the underlying backend's WriteAt is called sequentially in cascade order so a crash before block 0 is updated leaves the previous checkpoint intact.
func (*Container) Grow ¶
Grow extends the container to at least newSizeBytes. The growth is rejected when newSizeBytes is not strictly larger than the current container, when it would require a new spaceman chunk, or when the container is read-only. On success the NX superblock, spaceman, and chunk_info_block are all updated and the backing storage is extended where the backend supports Truncate.
func (*Container) OpenSnapshot ¶
OpenSnapshot returns a read-only Volume that exposes the volume as it was at the snapshot's transaction id. The frozen APSB is resolved through the container OMAP with xid = snap.XID, and every subsequent virtual-oid resolution inside that volume is similarly clamped via Volume.xidLimit so the snapshot's FS-tree, OMAP and snap_meta tree all read their frozen state.
func (*Container) OpenVolume ¶
OpenVolume materialises the volume at the given index of Volumes(). It resolves the APSB through the container omap, then loads the volume's own omap and FS-tree root.
func (*Container) Resize ¶
Resize is a convenience dispatcher: it computes the direction from the current container size and forwards to Grow or Shrink. A no-op (newSizeBytes equal to the current size) returns nil.
func (*Container) SetVerifyHashes ¶
SetVerifyHashes toggles SHA-256 verification of hashed B-tree children. When enabled, every traversal that descends a hashed internal node validates the child block's hash against the 32-byte digest stored after the child OID in the parent's value. Mismatches surface as errors from FindInode, LookupInodeRecord, ListInodes, ListSnapshots, etc.
Apple uses hashed B-trees for sealed (signed) volumes such as the macOS system volume; non-hashed trees are silently exempt.
func (*Container) Shrink ¶
Shrink reduces the container to exactly newSizeBytes. The operation is rejected when newSizeBytes is not strictly smaller than the current container, when any block ≥ newBlocks is allocated, when it would require a new spaceman chunk (i.e. shrink to less than formatMetadataBlocks * blockSize), or when the container is read-only. On success the spaceman, chunk_info_block, and NX superblock all advertise the smaller geometry and (for a Truncate-capable backend) the underlying file is trimmed.
func (*Container) Volumes ¶
func (c *Container) Volumes() []VolumeInfo
Volumes lists the volumes declared in the NX superblock fs_oid array. Names are NOT decoded here (that requires opening the volume).
type FDEConfig ¶
type FDEConfig struct {
Passphrase string
}
FDEConfig holds the passphrase for FileVault-encrypted format.
type FormatConfig ¶
FormatConfig configures Format. The Encryption field accepts an `*FDEConfig` to produce a FileVault-encrypted APFS container.
type Inode ¶
type Inode struct {
ID uint64 // file system object identifier
ParentID uint64
Name string // populated when discovered through a parent's directory record
Mode uint16 // file mode (POSIX bits)
Size uint64 // logical file size (J_DSTREAM.size)
IsDir bool
// contains filtered or unexported fields
}
Inode is the minimal projection of a J_INODE_VAL record exposed by this iteration of the parser.
type Sibling ¶
type Sibling struct {
OwnerID uint64 // the inode this sibling refers to
SiblingID uint64
ParentID uint64
Name string
}
Sibling is one J_SIBLING_LINK record: an alternate (parent, name) path for the inode it belongs to (i.e., a hard link).
type Snapshot ¶
type Snapshot struct {
XID uint64
APSBOID uint64 // sblock_oid: the frozen APSB to open for read access
Name string
CreateTime uint64
ChangeTime uint64
Inum uint64
Flags uint32
}
Snapshot is one entry from the volume's snapshot metadata tree. It corresponds to a J_SNAP_META record (apfs_snap_meta_val): the frozen transaction id (XID), human-readable name, and the OID of the volume superblock captured by the snapshot.
type Volume ¶
type Volume struct {
// contains filtered or unexported fields
}
Volume is an opened volume inside a container.
func (*Volume) CreateBlockDevice ¶
func (v *Volume) CreateBlockDevice(parentOID uint64, name string, perm uint16, rdev uint32) (uint64, error)
CreateBlockDevice creates a block-device node under (parentOID, name). `rdev` is the encoded device number (major / minor pair packed via the platform's `mkdev` macro — the kernel decodes it into st_rdev on stat(2)). The inode carries an INO_EXT_TYPE_RDEV xfield.
func (*Volume) CreateCharDevice ¶
func (v *Volume) CreateCharDevice(parentOID uint64, name string, perm uint16, rdev uint32) (uint64, error)
CreateCharDevice creates a character-device node — same as CreateBlockDevice but with `mode = S_IFCHR` and drec type DT_CHR.
func (*Volume) CreateDirectory ¶
CreateDirectory creates a new directory inode under parentOID with the given name and POSIX permission bits. Returns the new directory's inode oid. The directory starts empty (nchildren = 0); its parent's nchildren is incremented to reflect the new dentry.
parentOID may be APFS_ROOT_DIR_PARENT (1) or APFS_ROOT_DIR_INO_NUM (2) to bind the dentry under the canonical root directory; both rebind to oid 2.
func (*Volume) CreateFifo ¶
CreateFifo creates a named pipe under (parentOID, name) with the given permission bits. Returns the new FIFO's inode oid. FIFOs have no content blocks; the inode + drec are sufficient.
func (*Volume) CreateFile ¶
CreateFile inserts a regular file under parentOID with the given name and content. Returns the new inode's object id on success.
Preconditions: the FS-tree root must currently be a single leaf node (true immediately after FormatContainer and for small populated volumes), and the parent oid must reference an existing directory (or be 1, the synthetic root). The container must be opened with write capability (OpenContainerRW).
func (*Volume) CreateHardlink ¶
CreateHardlink adds a second name (alias) for an existing file at targetOID under newParentOID. After the call the file has nlink=2: it is reachable both through its original drec (the one CreateFile installed) and through the freshly added drec named newName under newParentOID. Both names show the same inode number to the kernel (`stat` returns the same st_ino), and removing either link decrements nlink — the inode persists until nlink reaches 0.
Limits in this iteration:
- the target's nlink must be exactly 1 (single-name file). The 1→2 transition retroactively creates J_SIBLING_LINK records for both the existing primary drec and the new alias.
- both the original drec and the new alias must live in the same leaf so the in-place upsert path stays simple. This is the case when the existing drec's parent is the root dir AND newParentOID is also the root dir, which is the common test workload.
func (*Volume) CreateSnapshot ¶
CreateSnapshot adds a snapshot named `name` to the current volume. Returns the snapshot's xid (which Apple uses both as the on-disk identifier and as the OMAP key for resolving the frozen APSB later via OpenSnapshot).
The snapshot's xid is taken from the container's nextXID counter, matching Apple's convention of stamping a snapshot with the xid the container will hand out at the next Commit. The Commit cascade then promotes this xid into the live state.
func (*Volume) CreateSocket ¶
CreateSocket creates a UNIX-domain socket node. Same shape as a FIFO but with `mode = S_IFSOCK` and drec type DT_SOCK.
func (*Volume) CreateSparseFile ¶
CreateSparseFile creates an empty (all-zero) regular file under (parentOID, name) with the given declared logical size. The file reads as N bytes of zeros without consuming any physical blocks. Returns the new inode's oid.
Use case: pre-allocating a file's logical size without paying for the storage, e.g. for a sparse VM disk image. Subsequent `OverwriteFile` calls would replace the sparse hole with real data (the existing OverwriteFile path doesn't yet handle hole-to-real transitions; that would need additional extent-replacement logic).
func (*Volume) CreateSymlink ¶
CreateSymlink creates a symbolic-link inode under parentOID with the given name. The target path is stored as the embedded payload of a `com.apple.fs.symlink` xattr — Apple's documented convention for APFS symlinks. Returns the new symlink's inode oid.
Mode is fixed to S_IFLNK | 0o777 (the canonical UNIX symlink mode); timestamps come from `time.Now()` and owner/group from `os.Geteuid()/Getegid()` to match what `apfs.kext` writes when the host user creates a symlink through the mounted volume.
func (*Volume) DebugWalkInodes ¶
DebugWalkInodes calls visit(oid, rawJInodeVal) for every J_INODE record in the FS-tree, walking multi-level trees via traverseFSTree.
func (*Volume) DeleteDirectory ¶
DeleteDirectory removes an empty directory at (parentOID, name). Like POSIX `rmdir(2)`, refuses non-empty directories — counts every J_DIR_REC under the target oid first and errors if the count is non-zero. Refuses to remove the canonical root or private-dir oids.
On success: drops the directory's J_INODE (+ any J_XATTR records it owned), drops the J_DIR_REC under (parentOID, name), refreshes the parent's nchildren, and decrements `apfs_num_directories` (APSB +0xC0).
func (*Volume) DeleteFile ¶
DeleteFile removes the file at (parentOID, name) from the volume. For nlink==1 files: all four (inode, drec, file_extent, dstream_id) records are removed; the file's extent blocks are freed; the parent's nchildren is decremented; APSB counters (apfs_num_files, apfs_fs_alloc_count) are updated. For nlink>1 files: only this name's drec + its matching J_SIBLING_LINK + J_SIBLING_MAP records are dropped, and the inode's nlink is decremented in place. The inode, its extents, xattrs and dstream_id stay because the other names still reference them.
func (*Volume) DeleteSnapshot ¶
DeleteSnapshot removes the snapshot named `name` from the volume. Returns os.ErrNotExist when no snapshot of that name exists.
The snapshot's frozen APSB block is freed, the J_SNAP_NAME + J_SNAP_META records are dropped from the snap-meta tree, and `apsb.apfs_num_snapshots` is decremented. If `name` was the most-recent snapshot, the volume OMAP's `om_most_recent_snap` is rolled back to the new maximum xid (or 0 when no snapshots remain).
func (*Volume) FileReaderAt ¶
FileReaderAt returns an io.ReaderAt that streams the bytes of the regular file at `inode` on demand without buffering the whole payload. Bounded to `inode.Size`: reads past the end return `io.EOF`. Sparse holes return zeros without consuming I/O.
The returned reader holds a snapshot of the inode's extent list at the time of the call; subsequent writes to the file will not be visible.
Returns an error if `inode` is a directory.
func (*Volume) FindInode ¶
FindInode locates an inode by object id and returns a fully populated Inode (Mode, Size, IsDir, ParentID, Name and dataExtents).
Implementation: two B-tree seeks via seekAndIterate, both O(log n + k) where k is the number of records visited.
- Phase 1 seeks (oid, type=0) and walks forward while j_key.oid == oid, gathering J_INODE_VAL, J_FILE_EXTENT (and could trivially gather xattrs / sibling links — those expose dedicated APIs already).
- Phase 2 seeks (parent_id, jTypeDirRec) and walks forward while the j_key prefix stays at that (parent_id, type), looking for the drec whose value's file_id field matches our oid; that drec carries the directory entry name.
Requires the FS-tree leaves to be sorted in canonical APFS order (by compareFSKey) — synthetic test images built with this package's helpers honour that automatically.
func (*Volume) ListInodes ¶
ListInodes walks the entire FS-tree and returns every J_INODE_VAL projected through Inode. Names and data extents discovered in the same traversal are folded into the matching inode. This is now a full traversal — every leaf contributes, regardless of B-tree height.
func (*Volume) ListSiblings ¶
ListSiblings walks the FS-tree and returns every J_SIBLING_LINK record that names inode owner.ID. Each sibling is a hard-link path (parent + name) pointing at owner.
func (*Volume) ListSnapshots ¶
ListSnapshots opens the volume's snapshot metadata tree and returns every J_SNAP_META record it contains. Returns an empty slice when the volume has no snapshots (apfs_snap_meta_tree_oid = 0).
func (*Volume) ListXAttrs ¶
ListXAttrs walks the FS-tree and returns every J_XATTR record attached to inode owner.ID. Stream xattrs are reported with empty EmbeddedValue and non-zero StreamID; fetch their payload via `ReadXAttrStream` or `XAttrStreamReaderAt`.
func (*Volume) LookupInodeRawValue ¶
LookupInodeRawValue returns the raw J_INODE_VAL bytes for the inode with the given oid. Used by debug helpers; production code should prefer LookupInodeRecord / FindInode.
func (*Volume) LookupInodeRecord ¶
LookupInodeRecord locates the J_INODE_VAL for the given oid using B-tree binary search through the FS-tree (O(log n) reads instead of the linear scan performed by FindInode). The returned Inode has Mode, Size, IsDir and ParentID populated; Name and dataExtents are NOT populated (those records live under different keys in the tree). Use FindInode when full inode information is required.
func (*Volume) LookupSnapshotByName ¶
LookupSnapshotByName resolves a snapshot by its human-readable name.
Fast path (Apple-spec-compliant images): the snapshot metadata tree is expected to carry a J_SNAP_NAME record alongside every J_SNAP_META. J_SNAP_NAME records sort alphabetically by name within their (oid=0, type=jTypeSnapName) range, so a single seekAndIterate finds the entry in O(log n) and yields the matching XID; a second seekAndIterate then resolves the J_SNAP_META at that XID to populate the full Snapshot.
Fallback path (synthetic test images that only carry J_SNAP_META): if the fast path returns no match, ListSnapshots is scanned linearly. This keeps the helper compatible with images built incrementally without the J_SNAP_NAME side records.
Returns os.ErrNotExist when neither path turns up a match.
func (*Volume) OverwriteFile ¶
OverwriteFile replaces the entire content of the file at `oid` with `newData`. The file's logical size becomes `len(newData)`. The file must be a regular file with at least one existing extent.
Allocation policy:
- newData fits in the existing extents' total capacity: payload is written across them in logical-offset order, the partial tail of the boundary extent is zeroed, and the inode size is updated. No new extents are allocated, no extents are freed.
- newData EXCEEDS existing capacity: the head fills the existing extents, then a single fresh contiguous extent is allocated at logical offset = old total capacity for the tail. The new J_FILE_EXTENT is inserted, chunk bitmap + ci_free_count + sm_free_count + extent-ref tree + apfs_fs_alloc_count are all updated, and the inode's J_DSTREAM `alloced_size` is bumped.
- newData is smaller than the existing logical size: the inode's size is reduced; trailing extent blocks stay allocated. Use `TruncateFile(oid, len(newData))` afterwards if you also want to free the no-longer-used blocks.
Multi-extent files are supported on both the in-place and the grow paths.
func (*Volume) ReadFile ¶
ReadFile reads the contents of a regular file by concatenating every J_FILE_EXTENT for the inode in logical-offset order. Sparse holes (gaps between extents) are zero-filled. The trailing zero region implied by inode.Size > sum(extent.length) is also zero-filled.
func (*Volume) ReadFileTransparent ¶
ReadFileTransparent reads a regular file and decompresses it on the fly when the file carries a com.apple.decmpfs xattr (transparent file compression). For uncompressed files it falls back to ReadFile.
Supported decmpfs types:
inline — 1 (uncompressed), 3 (zlib), 7 (LZVN), 11 (LZFSE) rsrc-fork — 4 (zlib), 5 (raw), 8 (LZVN), 12 (LZFSE)
Resource-fork variants automatically fetch the file's com.apple.ResourceFork xattr (embedded or stream).
func (*Volume) ReadXAttrStream ¶
ReadXAttrStream returns the payload of an extended attribute stored as a stream (xattrFlagDataStream). For embedded xattrs it returns the payload already in x.EmbeddedValue. It collects every J_FILE_EXTENT keyed by the stream's xattr_obj_id, sorts them by logical offset, and concatenates (zero-filling sparse holes, trimming to x.StreamSize).
func (*Volume) Rename ¶
func (v *Volume) Rename(oldParentOID uint64, oldName string, newParentOID uint64, newName string) error
Rename moves the entry at (oldParentOID, oldName) to (newParentOID, newName). The two (parent, name) pairs must differ (we reject the no-op case). Both parents may be either APFS_ROOT_DIR_PARENT (1) or APFS_ROOT_DIR_INO_NUM (2); they're rebound to oid 2 either way.
Overwrite semantics: if `(newParentOID, newName)` already exists AND points at a regular file with nlink==1, that file is deleted (records dropped, extents freed, APSB counters updated) before the rename proceeds — matching POSIX `rename(2)` for the regular-file case. Overwriting a directory or a multi-link inode is rejected.
Limit: single-link source inodes only (nlink == 1). Multi-link rename requires updating the corresponding J_SIBLING_LINK record's stored (parent_id, name) and is left as follow-up work.
func (*Volume) SetSuppressSnapshotGuard ¶
SetSuppressSnapshotGuard toggles the snapshot-write guard on this volume handle. The default (false) is the safe choice: writers return ErrHasSnapshot when num_snapshots > 0. Setting to true tells the package "I know what I'm doing — proceed with in-place writes even if it corrupts the snapshot". Used by test fixtures that need post-snapshot writes for byte-diff diagnostics.
func (*Volume) SetXAttr ¶
SetXAttr sets (or replaces) an embedded extended attribute on the inode at oid. Payload sizes up to a few hundred bytes are typical for xattrs like `com.apple.FinderInfo` (32 bytes), `com.apple.metadata:*` (a few hundred bytes), and `com.apple.quarantine` (variable). For payloads that don't fit in a single FS-tree leaf alongside the rest of the inode's records, callers should fall back to a stream xattr; that path isn't exposed by this writer yet.
Replace semantics: if a J_XATTR record with the same (oid, name) already exists, its value is overwritten. Reads via `ListXAttrs` after a Commit see the new payload.
func (*Volume) SetXAttrStream ¶
SetXAttrStream sets (or replaces) a stream-mode extended attribute on the inode at `targetOID`. Use this for large xattr payloads (Time Machine's `com.apple.metadata:_kTimeMachineSnapshotMetadata`, quarantine attributes for big files, etc.) that don't fit inline in a single FS-tree leaf alongside the rest of the inode's records. The payload is written to a fresh extent at the volume's next free block; the extent + a J_DSTREAM_ID + a J_XATTR record (with the stream flag) are inserted into the FS-tree under a fresh `xattr_obj_id`.
If a stream xattr with the same name already exists, its old payload extent is freed, its extent-ref record is removed, and its J_FILE_EXTENT + J_DSTREAM_ID records (under the previous xattr_obj_id) are dropped before the new ones are inserted. The J_XATTR record itself is replaced via upsert semantics. An existing embedded xattr of the same name is rejected — call SetXAttr (or delete first) for that case.
func (*Volume) TruncateFile ¶
TruncateFile sets the file at `oid` to exactly `newSize` bytes.
Semantics:
- newSize ≥ existing logical size: only the inode's size field is bumped. The file becomes sparse past the existing extents — reads past them return zero. No new extents are allocated.
- newSize < existing logical size: the inode's size is reduced AND extents that fall entirely past `newSize` are freed (chunk bitmap, ci_free_count, sm_free_count, extent-ref tree, and apfs_fs_alloc_count are all updated). When `newSize` lands in the middle of an extent, that extent is shrunk to the smallest block-aligned size that still contains `newSize`; the tail blocks of that extent are freed individually.
fsck tolerates the case where the surviving extent's last block contains bytes past `newSize`: the documented invariant is `alloced_size ≥ size`, not `alloced_size == size`.
func (*Volume) WriteFile ¶
WriteFile is iteration B of the read/write roadmap: it performs the in-place data overwrite of WriteFileInPlace AND patches the inode's J_DSTREAM.size field on disk so subsequent reads see len(data) as the file's logical size. The FS-tree leaf carrying the J_INODE_VAL is re-emitted to the same physical block (the inode value's length is unchanged), so this call does not trigger a checkpoint cascade.
Returns ErrReadOnly when the container has no write capability; returns the same capacity / sparsity errors as WriteFileInPlace; and returns an error when the on-disk inode value has no J_DSTREAM xfield to update (most regular files do).
func (*Volume) WriteFileInPlace ¶
WriteFileInPlace overwrites the contents of inode with data, writing directly into the physical extents already allocated to the file. Returns ErrReadOnly when the container has no write capability; returns a descriptive error when the file's extent layout cannot accommodate the requested write.
The caller is expected to read inode via FindInode (which populates dataExtents); a stale Inode whose extents no longer match the on-disk layout will silently corrupt unrelated blocks. Read first, write second, in the same session, with no intervening mutation.
func (*Volume) XAttrStreamReaderAt ¶
XAttrStreamReaderAt returns an io.ReaderAt that streams the bytes of the stream-extent xattr `x` on demand. Bounded to `x.StreamSize`. For embedded xattrs (no stream id) the returned reader wraps `x.EmbeddedValue` via `bytes.Reader`-like semantics: a single in-memory copy with the same ReaderAt interface.
type VolumeInfo ¶
type VolumeInfo struct {
Index uint32
OID uint64 // virtual oid of the APSB
Name string // populated lazily by OpenVolume
}
VolumeInfo describes a volume found inside a container.
type XAttr ¶
type XAttr struct {
OwnerID uint64
Name string
Flags uint16
EmbeddedValue []byte
StreamID uint64 // valid when Flags & xattrFlagDataStream != 0
StreamSize uint64
}
XAttr is one extended-attribute record decoded from the FS-tree. EmbeddedValue is non-nil when the attribute payload is stored inline in the J_XATTR_VAL record (xattrFlagDataEmbedded). Stream xattrs (whose data lives in a separate J_DSTREAM_ID chain) leave EmbeddedValue nil and expose StreamID + StreamSize so the caller can fetch them later.
Source Files
¶
- addvolume.go
- apfs.go
- apfs_fde.go
- btree.go
- commit.go
- compress.go
- create.go
- delete.go
- driver.go
- driver_mount.go
- extent_ref.go
- extent_ref_l3.go
- extent_ref_multilevel.go
- format.go
- format_encrypted.go
- fs.go
- gpt.go
- gpt_wrap.go
- multilevel.go
- multilevel_l3.go
- obj.go
- overwrite.go
- read_streaming.go
- rename.go
- resize.go
- snap_meta_multilevel.go
- snapshot_create.go
- snapshot_delete.go
- snapshot_guard.go
- sparse.go
- special_files.go
- timestamps.go
- write.go
- xattr_stream.go
