Documentation
¶
Overview ¶
Package hfsplus is a pure-Go, CGO-free read/write driver for the HFS+ (Mac OS Extended) on-disk format, including its HFSX (case-sensitive) variant.
HFS+ stores every multi-byte field big-endian. The volume header lives at byte offset 1024 and carries the block size, block counts, and the special fork descriptors for the catalog, extents-overflow, and allocation files. File and directory metadata live in the catalog B-tree, keyed by the parent CNID plus the UTF-16 node name; file contents are addressed by allocation blocks via up to eight inline extents per fork, spilling into the extents-overflow B-tree for fragmented files.
The package implements the full shared github.com/go-filesystems/interface Filesystem contract:
- Open / OpenFile open a volume read-only.
- OpenWritable / OpenFileWritable open it for read/write; the whole image is held in memory, mutated in place, and flushed by Sync.
- Format (and the lower-level Mkfs) lay down a fresh, empty HFS+/HFSX volume in pure Go — no host tooling — that passes fsck_hfs -n clean and mounts read/write on macOS.
- WriteFile, MkDir, DeleteFile, DeleteDir, Rename mutate the catalog B-tree (insert/delete with node splitting and tree-height growth), manage the allocation bitmap, and keep the volume-header counters in sync. The optional Labeller (SetLabel), Symlinker (Symlink), and Truncater (Truncate) capabilities are implemented too.
Every write path is validated against the native macOS tooling: fsck_hfs -n reports the volume clean and macOS mounts the image read/write and reads the exact files and bytes the Go side wrote, in both directions (Go-formatted → macOS-read and macOS-created → Go-written → macOS-read). The cross-arch, big-endian (s390x) round-trip runs in pure Go on every architecture.
Case-folding: case-insensitive name comparison implements Apple's FastUnicodeCompare for the practical character set (ASCII, Latin-1, Latin Extended-A, and the ignorable-NUL handling fsck requires) rather than embedding the full 8 KiB fold table; exotic case-folding corner cases outside that range fall back to identity ordering.
Out of scope (deliberate non-goals, as with the btrfs/xfs siblings):
- Journaling, decmpfs compression, resource forks, and indirect-node hardlink following are not implemented; see the README Status section.
Fragmented data forks (more than eight extents), catalog and extents-overflow B-tree growth, and node-underflow rebalancing/merging on delete ARE implemented: a written fork fills its eight inline extents and spills the remainder into the extents-overflow tree, the B-tree files grow their backing forks when their node reservation is exhausted, and deletion rebalances/merges underflowing nodes and frees emptied ones.
Index ¶
- Constants
- Variables
- func Format(path string, sizeBytes int64, cfg FormatConfig) (filesystem.Filesystem, error)
- func FormatAppleDmg(path string, sizeBytes int64, cfg FormatConfig) (filesystem.Filesystem, error)
- func Mkfs(sizeBytes int64, cfg FormatConfig) ([]byte, error)
- type FormatConfig
- type Volume
- func (v *Volume) Bytes() []byte
- func (v *Volume) CaseSensitive() bool
- func (v *Volume) Close() error
- func (v *Volume) DeleteDir(p string) error
- func (v *Volume) DeleteFile(p string) error
- func (v *Volume) FinderInfo(p string) ([finderInfoLen]byte, error)
- func (v *Volume) Label() string
- func (v *Volume) ListDir(path string) ([]filesystem.DirEntry, error)
- func (v *Volume) MkDir(p string, perm os.FileMode) error
- func (v *Volume) ReadFile(path string) ([]byte, error)
- func (v *Volume) ReadLink(path string) (string, error)
- func (v *Volume) Rename(oldPath, newPath string) error
- func (v *Volume) SetFinderInfo(p string, info [finderInfoLen]byte) error
- func (v *Volume) SetLabel(label string) error
- func (v *Volume) Stat(path string) (filesystem.Stat, error)
- func (v *Volume) Symlink(target, linkPath string) error
- func (v *Volume) Sync() error
- func (v *Volume) Truncate(p string, newSize int64) error
- func (v *Volume) VolumeHeader() *volumeHeader
- func (v *Volume) WriteFile(p string, data []byte, perm os.FileMode) error
Constants ¶
const ( // FinderFlagHasCustomIcon marks a folder or volume as carrying its own // icon — for a volume root, that is what makes the Finder look for // .VolumeIcon.icns and draw it instead of the generic disk. Writing the // file without the flag does nothing at all. FinderFlagHasCustomIcon = uint16(0x0400) // FinderFlagIsInvisible hides an entry, which is how a disk image keeps // its .background folder out of the window it decorates. FinderFlagIsInvisible = uint16(0x4000) )
Finder flags worth naming. The rest are documented in TN1150 and in CarbonCore/Finder.h; these are the ones a disk-image builder needs.
const ( // FinderFlagsOffset is where the flags word lives inside the 32 bytes // FinderInfo returns: DInfo.frFlags for a folder, FInfo.fdFlags for a // file. Big-endian, like everything else in HFS+. FinderFlagsOffset = 8 )
The Finder's own 32 bytes, which every catalog record carries and which nothing here could read or write until now.
HFS+ (Apple TN1150) puts them at the same place in both record kinds: a 16-byte userInfo at record offset 48 followed by a 16-byte finderInfo at 64. For a file those are FInfo/FXInfo, for a folder DInfo/DXInfo, and the two differ in what the first eight bytes mean — but the FLAGS word sits at +8 of userInfo either way, so it is addressable without knowing which kind a record is.
Variables ¶
var ( // ErrReadOnly is returned by every mutating method when the volume was // opened read-only (Open / OpenFile). Open it writable (OpenWritable / // OpenFileWritable / Format) to mutate it. ErrReadOnly = errors.New("hfsplus: filesystem is read-only") // ErrBadHeader is returned when the volume header at offset 1024 lacks a // recognized HFS+ ("H+") or HFSX ("HX") signature. ErrBadHeader = errors.New("hfsplus: no valid volume header") // ErrNotFound is returned when a path component cannot be located in the // catalog. ErrNotFound = errors.New("hfsplus: path not found") // ErrNotDirectory is returned when ListDir targets a non-directory. ErrNotDirectory = errors.New("hfsplus: not a directory") // ErrNotRegular is returned when ReadFile targets a non-regular file. ErrNotRegular = errors.New("hfsplus: not a regular file") // ErrNotSymlink is returned by ReadLink when the target is not a symlink. ErrNotSymlink = errors.New("hfsplus: not a symbolic link") // ErrCorrupt is returned when an on-disk structure fails a sanity check. ErrCorrupt = errors.New("hfsplus: corrupt image") // ErrUnsupported is returned for on-disk features the driver does not yet // decode/encode (e.g. compressed forks). ErrUnsupported = errors.New("hfsplus: unsupported feature") // ErrNoSpace is returned by the write path when the volume has no free // allocation blocks (or no contiguous run) to satisfy a request. ErrNoSpace = errors.New("hfsplus: no space left on volume") // ErrExists is returned by mutators when the target path already exists. ErrExists = errors.New("hfsplus: path already exists") // ErrNotEmpty is returned by DeleteDir when the directory still has // children. ErrNotEmpty = errors.New("hfsplus: directory not empty") )
Sentinel errors. Compare with errors.Is so wrapped errors keep matching.
Functions ¶
func Format ¶
func Format(path string, sizeBytes int64, cfg FormatConfig) (filesystem.Filesystem, error)
Format creates a fresh, empty HFS+ (or HFSX) volume image at path of sizeBytes bytes using the pure-Go formatter (Mkfs), then opens it read/write. Pure Go, CGO-free, big-endian — works on every architecture.
The produced image passes `fsck_hfs -n` clean on macOS and mounts read/write; on every platform Open/OpenWritable round-trip it. The returned Volume is writable: WriteFile/MkDir/DeleteFile/DeleteDir/Rename mutate it and flush back to path.
The signature matches the apfs sibling (Format(path, sizeBytes, cfg)).
func FormatAppleDmg ¶
func FormatAppleDmg(path string, sizeBytes int64, cfg FormatConfig) (filesystem.Filesystem, error)
FormatAppleDmg is the optional darwin-only alternative that shells out to the native hdiutil to author a real HFS+ image (the same tool that produced the read-path fixtures). It is provided as a parity escape hatch alongside the primary pure-Go Format, mirroring the apfs sibling's FormatAppleDmg. On non-darwin platforms it returns ErrUnsupported.
func Mkfs ¶
func Mkfs(sizeBytes int64, cfg FormatConfig) ([]byte, error)
Mkfs lays down a valid empty HFS+/HFSX volume of sizeBytes bytes into a freshly-allocated byte slice and returns it. Pure Go, big-endian, no host tooling — runs on every architecture. The returned image passes fsck_hfs -n on macOS and can be opened with Open/OpenWritable.
Types ¶
type FormatConfig ¶
type FormatConfig struct {
// Label is the volume name. Defaults to "GOTEST" when empty.
Label string
// CaseSensitive requests an HFSX (case-sensitive) volume instead of plain
// case-insensitive HFS+.
CaseSensitive bool
}
FormatConfig configures Format/Mkfs.
type Volume ¶
type Volume struct {
// contains filtered or unexported fields
}
Volume is an opened HFS+ (or HFSX) volume. When opened read-only (Open / OpenFile) the mutating methods return ErrReadOnly. When opened writable (OpenWritable / OpenFileWritable / Format) the whole image is held in an in-memory byte slice that the write path edits in place; Sync (and the mutators, which Sync implicitly) flush the bytes back to the backing io.WriterAt when one is present.
func Open ¶
Open parses an HFS+ volume from rs. The caller retains ownership of rs unless it implements io.Closer (then Close releases it). Pass size = -1 if unknown.
func OpenFileWritable ¶
OpenFileWritable opens the image at path for read/write. The whole image is read into memory; mutations are flushed back to the file by Sync (and implicitly by every mutator).
func OpenWritable ¶
OpenWritable opens an HFS+ image held entirely in img for read/write. The volume edits img in place; callers can retrieve the mutated bytes with Bytes() or, if wa is non-nil, flush them with Sync. Pass wa = nil for a purely in-memory writable volume.
func (*Volume) Bytes ¶
Bytes returns the current (possibly mutated) image bytes for a writable volume, or nil for a read-only one. The slice aliases the volume's internal buffer; copy it if you need a stable snapshot.
func (*Volume) CaseSensitive ¶
CaseSensitive reports whether the volume is HFSX with binary key comparison.
func (*Volume) DeleteFile ¶
DeleteFile removes the regular file (or symlink) at path.
func (*Volume) FinderInfo ¶ added in v0.2.0
FinderInfo returns the 32 Finder bytes of the entry at p.
p may be "/" for the volume root, which is the case that matters for a volume icon.
func (*Volume) ListDir ¶
func (v *Volume) ListDir(path string) ([]filesystem.DirEntry, error)
ListDir enumerates the directory at path.
func (*Volume) ReadLink ¶
ReadLink returns the target of a symbolic link. HFS+ stores the target as the data-fork contents of a file whose BSD mode marks it S_IFLNK.
func (*Volume) Rename ¶
Rename moves/renames oldPath to newPath. Both parents must exist; newPath must not already exist. The data fork and CNID are preserved (catalog key change + thread parent/name update).
func (*Volume) SetFinderInfo ¶ added in v0.2.0
SetFinderInfo replaces the 32 Finder bytes of the entry at p.
The record is re-keyed rather than patched in place, because the catalog is a B-tree and its writer owns node layout; delete-then-insert with the same key is how SetLabel already does it.
func (*Volume) SetLabel ¶
SetLabel renames the volume. The label lives as the root folder's catalog key (parent=1, name=label); SetLabel rewrites that key (and the root thread name) and is reflected by the reader and by macOS.
func (*Volume) Stat ¶
func (v *Volume) Stat(path string) (filesystem.Stat, error)
Stat resolves path and returns mode, size, and the CNID as a pseudo-inode.
func (*Volume) Symlink ¶
Symlink creates a symbolic link at linkPath pointing at target. HFS+ stores the target as the data fork of an S_IFLNK file.
func (*Volume) Sync ¶
Sync flushes the in-memory image back to the backing store, if any. It is a no-op for read-only or purely in-memory volumes.
func (*Volume) Truncate ¶
Truncate resizes the regular file at path to newSize bytes. Growing reallocates a larger contiguous run (zero-filled); shrinking reallocates a smaller run. Both rewrite the file's single inline extent.
func (*Volume) VolumeHeader ¶
func (v *Volume) VolumeHeader() *volumeHeader
VolumeHeader exposes the decoded volume header (owned by Volume).
func (*Volume) WriteFile ¶
WriteFile creates or overwrites the regular file at path with data. The data fork is allocated across the volume's free allocation blocks: a single contiguous run when one is available, otherwise multiple fragments. The first numInlineExtents runs are stored in the inline extent descriptors and any remaining runs are inserted into the extents-overflow B-tree, so an arbitrarily fragmented file (more than numInlineExtents extents) round-trips correctly.
