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
- Variables
- func CanWriteXattr(name string, value []byte) bool
- func CreateImage(w io.WriterAt, sizeBytes int64, volumeName string, root *Entry, ...) error
- func CreateImageFromDir(w io.WriterAt, sizeBytes int64, volumeName, srcDir string, opts *CreateOptions) error
- type BSDInfo
- type BTHeaderKeyCompareType
- type BTHeaderRec
- type BTNodeDescriptor
- type BTreeNodeKind
- type CatalogFlags
- type CatalogKey
- type CatalogNodeID
- type CreateOptions
- type Entry
- type ExtentDescriptor
- type ExtentRecord
- type FinderFileInfo
- type FinderOpaqueInfo
- type FolderInfo
- type ForkData
- type HFSPlusCatalogFile
- type HFSPlusCatalogFolder
- type Point
- type RecordType
- type Rect
- type Signature
- type UniStr255
- type Volume
- func (v *Volume) CaseSensitive() bool
- func (v *Volume) Created() time.Time
- func (v *Volume) FileCount() uint32
- func (v *Volume) FolderCount() uint32
- func (v *Volume) Header() VolumeHeader
- func (v *Volume) Modified() time.Time
- func (v *Volume) Name() string
- func (v *Volume) Open(name string) (fs.File, error)
- func (v *Volume) ReadDir(name string) ([]fs.DirEntry, error)
- func (v *Volume) ReadFile(name string) ([]byte, error)
- func (v *Volume) Readlink(name string) (string, error)
- func (v *Volume) Stat(name string) (fs.FileInfo, error)
- func (v *Volume) UUID() string
- func (v *Volume) Xattrs(name string) (map[string][]byte, error)
- type VolumeHeader
- type WalkOptions
Constants ¶
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' )
const ( HFSVolumeUnmountedMask = 0x00000100 HFSVolumeJournaledMask = 0x00002000 HFSVolumeInconsistentMask = 0x00004000 HFSVolumeSoftwareLockMask = 0x00008000 )
HFS Plus volume attribute masks (subset).
const HFSPlusExtentDensity = 8
HFSPlusExtentDensity is the number of extents stored inline per fork.
Variables ¶
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
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 ¶
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 ¶
ExtentDescriptor describes one contiguous run of allocation blocks.
type ExtentRecord ¶
type ExtentRecord [HFSPlusExtentDensity]ExtentDescriptor
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 ¶
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 RecordType ¶
type RecordType int16
RecordType discriminates catalog leaf records.
const ( HFSPlusFolderRecord RecordType = 0x0001 HFSPlusFileRecord RecordType = 0x0002 HFSPlusFolderThreadRecord RecordType = 0x0003 HFSPlusFileThreadRecord RecordType = 0x0004 )
type UniStr255 ¶
UniStr255 is an HFS Plus Unicode (UTF-16BE) string, stored fully decomposed and in canonical order.
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 ¶
New parses the HFS Plus / HFSX volume whose partition starts at offset 0 of device, and loads the catalog.
func (*Volume) CaseSensitive ¶
CaseSensitive reports whether catalog names are compared case-sensitively (HFSX with binary compare).
func (*Volume) FolderCount ¶
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) ReadDir ¶
ReadDir implements fs.ReadDirFS. Entries are sorted by filename as the interface requires.
func (*Volume) UUID ¶
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
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.