hfsplus

package
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 Imports: 19 Imported by: 0

Documentation

Overview

Extended attributes: the attributes file, an HFS+ special file holding a third B-tree keyed by (file id, attribute name).

Attribute values live in one of three record shapes. A small value sits inside its own record; a larger one gets a fork of its own, described by a fork-data record; and a fork too fragmented for its eight inline extents spills into extents records. Note those extents records live in *this* tree, keyed by the same file id and name with a non-zero start block -- not in the extents overflow file, whose keys only ever name a data or resource fork.

Writing the attributes file: encoding attribute B-tree keys and records.

The header constants here are not guesses. They were read off a volume macOS created (`testdata/cli/hfs-basic.dmg`), whose attributes B-tree header carries maxKeyLength 266, attributes 0x06 and keyCompareType 0x00 -- the last notably *not* one of the catalog's compare types, because attribute names are compared as plain UTF-16 code units.

B-tree reading: header parsing and leaf-chain traversal over a fork.

Adapted from blacktop/go-apfs's hfsplus package (Apache-2.0); the leaf walk here follows the FirstLeafNode/FLink sibling chain instead of recursing through index nodes, so each node is read exactly once.

B-tree writing: serialisation of catalog and extents-overflow B-tree nodes for the HFS+/HFSX writer. Produces node byte images with the standard descriptor, packed records and a downward-growing offset table (see TN1150). All multi-byte fields are big-endian.

The catalog tree is built as a bottom-up B-tree: leaf records are packed in key order into leaf nodes, then index levels are added until a single root node remains. This handles trees of any size (single-leaf trees keep treeDepth == 1).

Transparent compression: reading a file whose content is stored compressed rather than in its data fork.

A compressed file carries the UF_COMPRESSED flag, an empty data fork, and a com.apple.decmpfs attribute whose 16-byte header names the compression type and the size the file presents. The compressed bytes live either immediately after that header, for a small file, or in the file's resource fork, chunked into 64 KiB blocks.

None of that is specific to HFS+ -- APFS inherited it unchanged -- so the decoding lives in internal/decmpfs and this file only supplies the two things that differ: where the attribute comes from, and where the resource fork is.

io/fs adapter: Volume implements fs.FS, fs.ReadDirFS, fs.StatFS and fs.ReadFileFS, so volumes can be walked with fs.WalkDir, tested with testing/fstest and passed to anything that consumes an fs.FS.

Symlinks are never followed: Stat and Open return the link itself, like archive-backed filesystems. Use Readlink to resolve targets.

Writing hard links.

HFS+ represents several names for one file indirectly. The content lives in an "indirect node" file called iNodeNNNN inside a private directory at the volume root, and each visible name is a catalog record carrying the 'hlnk' file type, the 'hfs+' creator, and the indirect node's catalog id in the BSD info's special field. The indirect node puts the number of names in that same field.

The shape here follows a volume macOS created rather than the prose: its private directory carries the folder-count flag, invisible and name-locked Finder flags, a Finder location of (0x4000, 0x4000) and a mode with no permission bits at all, and its indirect node holds the file's extended attributes as well as its content.

Name normalization.

HFS+ stores names decomposed, so a name given with a precomposed character such as "ü" (U+00FC) is stored as "u" followed by a combining diaeresis. A writer that stores the precomposed form produces a name macOS will not find, because it looks for the decomposed one.

The decomposition is NFD with an exclusion list; see normalize_table.go for what is excluded and how that was established.

On-disk structure definitions for HFS Plus / HFSX volumes.

The catalog and B-tree type definitions in this file are adapted from blacktop/go-apfs's hfsplus package (Apache-2.0): https://github.com/blacktop/go-apfs/tree/main/pkg/disk/hfsplus

Reference: Apple Technote TN1150 "HFS Plus Volume Format" https://developer.apple.com/library/archive/technotes/tn/tn1150.html

Checking a caller-supplied Entry tree before any of it is written.

Package hfsplus is a pure-Go, read-only HFS Plus / HFSX file system engine. It parses the volume header and catalog B-tree, reads file content through the data fork's inline extents plus the extents overflow B-tree, and resolves symlinks and hard links.

Catalog and on-disk type definitions are adapted from blacktop/go-apfs's hfsplus package (Apache-2.0); file-data reading, extents overflow support, link resolution and the io/fs adapter are new.

HFS+ / HFSX volume writer: serialises an in-memory directory tree into a raw, mountable HFSX volume image (bytes).

By default it produces a case-sensitive HFSX volume (signature "HX", version 5, catalog keyCompareType 0xBC / kHFSBinaryCompare), unjournaled, where catalog ordering is a plain lexicographic compare of the big-endian UTF-16 name code units. CreateOptions.CaseInsensitive instead produces plain HFS+ ("H+", version 4, keyCompareType 0xCF), whose catalog is ordered through the fold table in casefold_table.go.

Files carry their data fork, their resource fork and their extended attributes; the attributes file is emitted only when something needs it, so a volume with no attributes is byte-identical to one this writer produced before it could emit one.

All multi-byte on-disk fields are big-endian. Reuses the on-disk structs from types.go for serialisation.

Index

Constants

View Source
const (
	// HFSPlusVersion is the only valid version for 'H+' volumes.
	HFSPlusVersion uint16 = 4
	// HFSXVersion is the first valid version for 'HX' volumes.
	HFSXVersion uint16 = 5

	// Indirect node files (hard links) have this Finder type/creator.
	HardLinkFileType uint32 = 0x686C6E6B // 'hlnk'
	HFSPlusCreator   uint32 = 0x6866732B // 'hfs+'

	// Directory hard links have this Finder type/creator.
	DirLinkFileType uint32 = 0x66647270 // 'fdrp'
	DirLinkCreator  uint32 = 0x4D414353 // 'MACS'

	// Symbolic links have this Finder type/creator.
	SymLinkFileType uint32 = 0x736C6E6B // 'slnk'
	SymLinkCreator  uint32 = 0x72686170 // 'rhap'
)
View Source
const (
	HFSVolumeUnmountedMask    = 0x00000100
	HFSVolumeJournaledMask    = 0x00002000
	HFSVolumeInconsistentMask = 0x00004000
	HFSVolumeSoftwareLockMask = 0x00008000
)

HFS Plus volume attribute masks (subset).

View Source
const HFSPlusExtentDensity = 8

HFSPlusExtentDensity is the number of extents stored inline per fork.

Variables

View Source
var DefaultTime = time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)

DefaultTime is the timestamp used when CreateOptions.FixedTime is unset. It is a fixed value rather than the wall clock so that identical input produces identical bytes: an image built twice is byte-for-byte the same.

Functions

func CanWriteXattr added in v0.2.0

func CanWriteXattr(name string, value []byte) bool

CanWriteXattr reports whether this writer can carry an extended attribute.

It exists so a caller walking a source tree can tell in advance what will survive, rather than discovering it afterwards. It must agree exactly with what the writer does: an attribute accepted here and then silently discarded would make the fidelity report claim a loss did not happen.

func CreateImage

func CreateImage(w io.WriterAt, sizeBytes int64, volumeName string, root *Entry, opts *CreateOptions) error

CreateImage writes a raw HFSX volume image built from root into w. When sizeBytes is 0 a minimum size that fits all metadata and data is computed; otherwise sizeBytes (rounded down to a whole number of blocks) is used and must be large enough. volumeName becomes the root folder's catalog name.

func CreateImageFromDir

func CreateImageFromDir(w io.WriterAt, sizeBytes int64, volumeName, srcDir string, opts *CreateOptions) error

CreateImageFromDir walks srcDir into an Entry tree and writes an HFS+ image built from it. srcDir's own name is not used; its contents become the volume root's children.

The conversion is lossy: device nodes, FIFOs and sockets are skipped, and extended attributes, resource forks, ACLs, BSD flags and hard links are not carried across. This function discards the account of what was lost; call EntryTreeFromDir directly to see it.

Types

type BSDInfo

type BSDInfo struct {
	OwnerID    uint32
	GroupID    uint32
	AdminFlags uint8
	OwnerFlags uint8
	FileMode   uint16
	// Special is a union: iNodeNum for hard links, linkCount for indirect
	// node files, rawDevice for block/character devices.
	Special uint32
}

BSDInfo holds the POSIX permission info of a catalog record.

type BTHeaderKeyCompareType

type BTHeaderKeyCompareType uint8

BTHeaderKeyCompareType selects the catalog key comparison algorithm.

const (
	HFSCaseFolding   BTHeaderKeyCompareType = 0xCF // case-insensitive (HFS+)
	HFSBinaryCompare BTHeaderKeyCompareType = 0xBC // case-sensitive (HFSX)
)

type BTHeaderRec

type BTHeaderRec struct {
	TreeDepth      uint16
	RootNode       uint32
	LeafRecords    uint32
	FirstLeafNode  uint32
	LastLeafNode   uint32
	NodeSize       uint16
	MaxKeyLength   uint16
	TotalNodes     uint32
	FreeNodes      uint32
	Reserved1      uint16
	ClumpSize      uint32
	BtreeType      uint8
	KeyCompareType BTHeaderKeyCompareType
	Attributes     uint32
	Reserved3      [16]uint32
}

BTHeaderRec is the first record of a B-tree header node (106 bytes).

type BTNodeDescriptor

type BTNodeDescriptor struct {
	FLink      uint32 // next node at this level
	BLink      uint32 // previous node at this level
	Kind       BTreeNodeKind
	Height     uint8
	NumRecords uint16
	Reserved   uint16
}

BTNodeDescriptor is the 14-byte descriptor at the start of every node.

type BTreeNodeKind

type BTreeNodeKind int8

BTreeNodeKind is the kind byte of a B-tree node descriptor.

const (
	BTLeafNodeKind   BTreeNodeKind = -1
	BTIndexNodeKind  BTreeNodeKind = 0
	BTHeaderNodeKind BTreeNodeKind = 1
	BTMapNodeKind    BTreeNodeKind = 2
)

func (BTreeNodeKind) String

func (kind BTreeNodeKind) String() string

type CatalogFlags

type CatalogFlags uint16

CatalogFlags are the flag bits of catalog file/folder records.

const (
	HFSFileLockedMask     CatalogFlags = 0x0001
	HFSThreadExistsMask   CatalogFlags = 0x0002
	HFSHasAttributesMask  CatalogFlags = 0x0004
	HFSHasSecurityMask    CatalogFlags = 0x0008
	HFSHasFolderCountMask CatalogFlags = 0x0010
	HFSHasLinkChainMask   CatalogFlags = 0x0020
	HFSHasChildLinkMask   CatalogFlags = 0x0040
	HFSHasDateAddedMask   CatalogFlags = 0x0080
)

type CatalogKey

type CatalogKey struct {
	KeyLength uint16
	ParentID  CatalogNodeID
	NodeName  UniStr255
}

CatalogKey is the key of a catalog B-tree record.

type CatalogNodeID

type CatalogNodeID uint32

CatalogNodeID identifies a file or folder in the catalog.

const (
	HFSRootParentID     CatalogNodeID = 1 // parent of the root folder
	HFSRootFolderID     CatalogNodeID = 2 // the root folder itself
	HFSExtentsFileID    CatalogNodeID = 3 // extents overflow file
	HFSCatalogFileID    CatalogNodeID = 4 // catalog file
	HFSBadBlockFileID   CatalogNodeID = 5 // bad allocation block file
	HFSAllocationFileID CatalogNodeID = 6 // allocation bitmap file
	HFSStartupFileID    CatalogNodeID = 7 // startup file
	HFSAttributesFileID CatalogNodeID = 8 // attributes file

	HFSFirstUserCatalogNodeID CatalogNodeID = 16
)

type CreateOptions

type CreateOptions struct {
	// BlockSize is the allocation block size in bytes (default 4096). Must be
	// a power of two >= 512.
	BlockSize uint32
	// FixedTime is the timestamp written to the volume header's create, modify
	// and checked dates, and the default for entries that carry no ModTime. The
	// zero value selects DefaultTime, so an image is byte-identical for
	// identical input without the caller doing anything.
	FixedTime time.Time
	// ClampModTimes applies the SOURCE_DATE_EPOCH rule to entry modification
	// times: an Entry.ModTime later than the resolved FixedTime is written as
	// FixedTime, while earlier times are preserved. It has no effect on entries
	// that supply no ModTime.
	ClampModTimes bool
	// CaseInsensitive selects a case-insensitive HFS+ volume (signature "H+",
	// version 4, catalog keyCompareType 0xCF) instead of the case-sensitive
	// HFSX default. Names then compare through the fold table in
	// casefold_table.go, which is what macOS itself does.
	CaseInsensitive bool

	// VolumeUUID pins the volume's identifier. Only its first eight bytes reach
	// disk, because the HFS+ volume identifier is the 64-bit pair
	// FinderInfo[6]/FinderInfo[7]. The zero value derives a stable identifier
	// from the volume name and the resolved timestamp.
	VolumeUUID [16]byte
}

CreateOptions tunes image creation. The zero value is valid.

type Entry

type Entry struct {
	Name     string
	Mode     os.FileMode // dir/symlink/perm bits
	ModTime  time.Time
	UID, GID uint32
	Data     []byte   // file content, or symlink target bytes
	Children []*Entry // directory children (writer sorts them)

	// ResourceFork is the file's resource fork, empty when it has none. On
	// HFS+ this is a fork of the catalog record rather than an extended
	// attribute, even though macOS presents it as com.apple.ResourceFork.
	ResourceFork []byte

	// Xattrs are the entry's extended attributes. Directories may carry them
	// too. com.apple.ResourceFork does not belong here -- it is a fork, and
	// goes in ResourceFork above.
	Xattrs map[string][]byte

	// LinkGroup marks entries that are several names for one file. Every name
	// sharing a value is written as a hard link to a single copy of the
	// content; zero means the entry has only one name. The value itself is
	// arbitrary and does not reach the disk.
	LinkGroup uint64
}

Entry is one node of the directory tree to be written. A directory has Children; a regular file or symlink carries its bytes in Data (for a symlink, Data is the target path).

func EntryTreeFromDir

func EntryTreeFromDir(srcDir string, opts *WalkOptions) (*Entry, *fidelity.Report, error)

EntryTreeFromDir walks srcDir into an Entry tree and reports everything it could not represent. srcDir's own name is dropped; its contents become the returned root's children.

The returned Report is never nil. A caller wanting only the tree can ignore it, but it is returned rather than hidden because a lossy conversion that does not say so is the failure mode this exists to prevent.

Extended attributes are carried, whatever their size: a small value lives inside its record in the attributes file and a larger one gets an allocation extent of its own. A resource fork is carried too, in the catalog record's resource fork where HFS+ actually keeps it. com.apple.decmpfs is the exception -- it declares content this writer does not produce -- and is reported as dropped. Several names for one file are written as hard links to one copy of the content, rather than as copies.

type ExtentDescriptor

type ExtentDescriptor struct {
	StartBlock uint32
	BlockCount uint32
}

ExtentDescriptor describes one contiguous run of allocation blocks.

type ExtentRecord

ExtentRecord is the inline extent list of a fork (or one extents overflow leaf record).

type FinderFileInfo

type FinderFileInfo struct {
	FileType    uint32 // OSType
	FileCreator uint32 // OSType
	FinderFlags uint16
	Location    Point
	Opaque      uint16
}

FinderFileInfo is the Finder info of a file record.

type FinderOpaqueInfo

type FinderOpaqueInfo struct {
	Opaque [16]int8
}

FinderOpaqueInfo is the extended Finder info blob.

type FolderInfo

type FolderInfo struct {
	WindowBounds Rect
	FinderFlags  uint16
	Location     Point
	Opaque       uint16
}

FolderInfo is the Finder info of a folder record.

type ForkData

type ForkData struct {
	LogicalSize uint64
	ClumpSize   uint32
	TotalBlocks uint32
	Extents     ExtentRecord
}

ForkData describes the size and initial extents of a file fork.

type HFSPlusCatalogFile

type HFSPlusCatalogFile struct {
	RecordType       RecordType
	Flags            CatalogFlags
	Reserved1        uint32
	FileID           CatalogNodeID
	CreateDate       hfsTime
	ContentModDate   hfsTime
	AttributeModDate hfsTime
	AccessDate       hfsTime
	BackupDate       hfsTime
	BSDInfo          BSDInfo
	UserInfo         FinderFileInfo
	FinderInfo       FinderOpaqueInfo
	TextEncoding     uint32
	Reserved2        uint32
	DataFork         ForkData
	ResourceFork     ForkData
}

HFSPlusCatalogFile is an HFS Plus catalog file record (248 bytes).

type HFSPlusCatalogFolder

type HFSPlusCatalogFolder struct {
	RecordType       RecordType
	Flags            CatalogFlags
	Valence          uint32
	FolderID         CatalogNodeID
	CreateDate       hfsTime
	ContentModDate   hfsTime
	AttributeModDate hfsTime
	AccessDate       hfsTime
	BackupDate       hfsTime
	BSDInfo          BSDInfo
	UserInfo         FolderInfo
	FinderInfo       FinderOpaqueInfo
	TextEncoding     uint32
	FolderCount      uint32
}

HFSPlusCatalogFolder is an HFS Plus catalog folder record (88 bytes).

type Point

type Point struct {
	V int16
	H int16
}

Point and Rect are classic Finder geometry types.

type RecordType

type RecordType int16

RecordType discriminates catalog leaf records.

const (
	HFSPlusFolderRecord       RecordType = 0x0001
	HFSPlusFileRecord         RecordType = 0x0002
	HFSPlusFolderThreadRecord RecordType = 0x0003
	HFSPlusFileThreadRecord   RecordType = 0x0004
)

type Rect

type Rect struct {
	Top    int16
	Left   int16
	Bottom int16
	Right  int16
}

type Signature

type Signature uint16

Signature identifies the volume format in the volume header.

const (
	HFSPlusSigWord Signature = 0x482B // "H+"
	HFSXSigWord    Signature = 0x4858 // "HX"
)

func (Signature) String

func (s Signature) String() string

type UniStr255

type UniStr255 struct {
	Length  uint16
	UniChar []uint16
}

UniStr255 is an HFS Plus Unicode (UTF-16BE) string, stored fully decomposed and in canonical order.

func (*UniStr255) String

func (us *UniStr255) String() string

type Volume

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

Volume is a read-only HFS Plus / HFSX volume. It implements fs.FS, fs.ReadDirFS, fs.StatFS and fs.ReadFileFS (see fs.go).

func New

func New(device io.ReaderAt) (*Volume, error)

New parses the HFS Plus / HFSX volume whose partition starts at offset 0 of device, and loads the catalog.

func (*Volume) CaseSensitive

func (v *Volume) CaseSensitive() bool

CaseSensitive reports whether catalog names are compared case-sensitively (HFSX with binary compare).

func (*Volume) Created

func (v *Volume) Created() time.Time

Created returns the volume creation date.

func (*Volume) FileCount

func (v *Volume) FileCount() uint32

FileCount returns the number of files from the volume header.

func (*Volume) FolderCount

func (v *Volume) FolderCount() uint32

FolderCount returns the number of folders from the volume header (not including the root folder).

func (*Volume) Header

func (v *Volume) Header() VolumeHeader

Header returns a copy of the parsed volume header.

func (*Volume) Modified

func (v *Volume) Modified() time.Time

Modified returns the volume's last modification date.

func (*Volume) Name

func (v *Volume) Name() string

Name returns the volume name (the root folder's catalog name).

func (*Volume) Open

func (v *Volume) Open(name string) (fs.File, error)

Open implements fs.FS.

func (*Volume) ReadDir

func (v *Volume) ReadDir(name string) ([]fs.DirEntry, error)

ReadDir implements fs.ReadDirFS. Entries are sorted by filename as the interface requires.

func (*Volume) ReadFile

func (v *Volume) ReadFile(name string) ([]byte, error)

ReadFile implements fs.ReadFileFS.

func (v *Volume) Readlink(name string) (string, error)

Readlink returns the target of the symbolic link at name.

func (*Volume) Stat

func (v *Volume) Stat(name string) (fs.FileInfo, error)

Stat implements fs.StatFS. Symlinks are not followed.

func (*Volume) UUID

func (v *Volume) UUID() string

UUID returns the 64-bit volume identifier from finderInfo[6..7] as a hex string, or "" when the volume has none.

func (*Volume) Xattrs added in v0.2.0

func (v *Volume) Xattrs(name string) (map[string][]byte, error)

Xattrs returns the extended attributes of the file at name.

A file's resource fork is reported as com.apple.ResourceFork, which is what macOS presents it as, even though HFS+ stores it as a fork of the catalog record rather than in the attributes file.

com.apple.decmpfs is returned like any other attribute rather than being filtered out. A caller writing these attributes back onto a decompressed copy of the file must drop it, since it would describe content the copy no longer holds.

type VolumeHeader

type VolumeHeader struct {
	Signature          Signature
	Version            uint16
	Attributes         uint32
	LastMountedVersion [4]byte
	JournalInfoBlock   uint32
	CreateDate         hfsTime
	ModifyDate         hfsTime
	BackupDate         hfsTime
	CheckedDate        hfsTime
	FileCount          uint32
	FolderCount        uint32
	BlockSize          uint32
	TotalBlocks        uint32
	FreeBlocks         uint32
	NextAllocation     uint32
	RsrcClumpSize      uint32
	DataClumpSize      uint32
	NextCatalogID      CatalogNodeID
	WriteCount         uint32
	EncodingsBitmap    uint64
	FinderInfo         [8]uint32
	AllocationFile     ForkData
	ExtentsFile        ForkData
	CatalogFile        ForkData
	AttributesFile     ForkData
	StartupFile        ForkData
}

VolumeHeader is the 512-byte HFS Plus volume header at offset 1024.

type WalkOptions

type WalkOptions struct {
	// Xattrs reads each entry's extended attributes so they can be counted,
	// and carried when this writer can represent them. It costs a syscall or
	// two per entry, so it is opt-in; without it the report says nothing about
	// attributes rather than saying there were none.
	//
	// A resource fork reaches the walk as an extended attribute even though
	// HFS+ stores it as a fork, so carrying one needs this set.
	Xattrs bool

	// Decompress writes a transparently compressed file out in full instead of
	// carrying its compression across. The default is to carry it: the
	// compressed bytes are what the source held, copying them is cheaper than
	// decompressing, and the result takes less room.
	//
	// Compression can only be carried when Xattrs is set, because that is where
	// a compressed file keeps its content.
	Decompress bool

	// Warn, when non-nil, is called once for each thing the walk cannot carry
	// across, as it is found. The library never writes to stderr itself —
	// deciding whether a warning is worth showing, and how many, belongs to the
	// caller.
	Warn func(path string, kind fidelity.Kind, detail string)
}

WalkOptions tunes EntryTreeFromDir.

Jump to

Keyboard shortcuts

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