Skip to content

HFS+ Filesystem

opencode-agent[bot] edited this page May 11, 2026 · 1 revision

HFS+ Filesystem

JNode's implementation of the HFS+ (Hierarchical File System Plus) filesystem used by classic Mac OS X.

Overview

HFS+ is a journaling filesystem originally developed by Apple for Mac OS 8.1 and later. JNode provides a read-only implementation supporting both standard HFS+ (magic 0x482b) and HFSX (magic 0x4858, with case-sensitive filenames).

Key Components

Class Purpose
HfsPlusFileSystem Main filesystem implementation extending AbstractFileSystem
HfsPlusFileSystemType Factory implementing BlockDeviceFileSystemType, detects HFS+/HFSX volumes
SuperBlock Volume header at sector 2 (1024 bytes), contains magic, version, block size, allocation file descriptors
Catalog B-Tree catalog file storing all file and directory records
HfsPlusEntry Entry implementation bridging VFS to HFS+ catalog records
HfsPlusDirectory Directory traversal via catalog B-Tree lookups
HfsPlusFile File access with fork data (data fork, resource fork)
Extent Extent overflow B-Tree for additional block allocations beyond the 8 extent limit
Attributes Extended attributes B-Tree for security.xattr and compression attributes

How It Works

Volume Header (SuperBlock)

The volume header is located at byte offset 1024 (after the 512-byte boot sector plus 512 bytes padding). It contains:

  • Magic number: 0x482b (HFS+) or 0x4858 (HFSX)
  • Version: 4 (HFS+) or 5 (HFSX)
  • Block size: typically 4096 bytes
  • Fork descriptors: pointers to the four special B-Tree files
    • Allocation file (bitmap) — CNID 0x01
    • Extents overflow file — CNID 0x02
    • Catalog file — CNID 0x05
    • Attributes file — CNID 0x0A
  • Journal info block: pointer to journal if journaled
  • Free block count: available allocation blocks
  • File/folder counts: total entries

Volume header detection in HfsPlusFileSystemType.supports() reads sector 2 and checks the magic at offset 0 (big-endian).

Mount Flow

HfsPlusFileSystemType.create(device, readOnly)
  → HfsPlusFileSystem(device, readOnly, this)
    → read()
      → new SuperBlock(this, false)     // Load volume header
      → check HFSPLUS_VOL_JOURNALED_BIT // Force read-only for journaled
      → new Extent(this)                // Extent overflow B-Tree
      → new Catalog(this)               // Catalog B-Tree
      → new Attributes(this)            // Extended attributes B-Tree
      → getRootEntry()                   // Returns root (CNID 2)

Catalog B-Tree

The catalog is a B-Tree storing all files and directories. Each key is a CatalogKey (parent CNID + Unicode name), and each leaf record contains either:

  • CatalogFolder — directory record with dates, permissions, folder ID
  • CatalogFile — file record with fork data, dates, Finder info
  • CatalogThread — reverse pointer from CNID to parent/name for efficient lookups by ID

The root entry (CNID 2, HFSPLUS_ROOT_CNID) is looked up via catalog.getRecord(CatalogNodeId.HFSPLUS_POR_CNID) where POR_CNID is the parent-of-root (CNID 1).

Directory Traversal

HfsPlusDirectory.getEntry(name) searches the catalog B-Tree:

  1. Start at root node (from BTHeaderRecord.getRootNode())
  2. Navigate index nodes until reaching a leaf node
  3. Binary search leaf for key (parentCNID, name)
  4. Return HfsPlusEntry wrapping the found LeafRecord

File Data Access

HfsPlusFile reads file data via:

  1. Direct extents (up to 8): stored in CatalogFile.bsdInfo.forkData
  2. Overflow extents: looked up in the Extent B-Tree by (fileID, forkType, startBlock)

Both use HfsPlusForkData to describe the allocation: start block, block count, clump size.

Private Data Directories

HFS+ stores hard-linked files and directories in hidden system directories:

  • \0\0\0\0HFS+ Private Data — hard-linked file data (CNID 8)
  • .HFS+ Private Directory Data\r — hard-linked directory records (CNID 9)

getPrivateDataDirectory() and getPrivateDirectoryDataDirectory() lazily resolve these on access.

Gotchas

  • Read-only enforcement: Journaled volumes are mounted read-only regardless of the readOnly flag
  • Write support stubbed: create() and writeAllocationFile() contain FIXME comments
  • No write-back caching: No dirty-bit tracking or consistency recovery
  • Hard link handling: Requires scanning the private data directory to resolve link targets
  • Unicode normalization: HFSX supports case-sensitive names; JNode does not normalize Unicode
  • No journal replay: Mounting a dirty journaled volume as read-only skips journal recovery

Clone this wiki locally