← Back to list

To Understand Apple File System (APFS) Better, I Made a Driver in Python

Supporting Read, Write, and Encryption

Aaron Beckley in Data Science Collective · 2025-12-28 23:07 · 4 claps · 16.7 min read paywalled
#python #apple #apf #filesystem #guides-and-tutorials
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity

To Understand Apple File System (APFS) Better, I Made a Driver in Python

Supporting Read, Write, and Encryption

If you read my previous article about my experience with data loss on APFS, you know the full story: an external HDD with APFS formatting became unreadable after a kernel panic on my iMac. That drive contained irreplaceable data that I couldn’t afford to lose.

What followed was a 30-day nightmare of trying every recovery tool I could find: Disk Utility, fsck_apfs, UFS Explorer, R-Studio. Some tools couldn’t even see the drive. Others could see it but couldn’t read the filesystem. I was throwing solutions at the problem without understanding what the problem actually was.

Eventually, Disk Drill’s quick scan recovered my files with the directory structure intact. But the experience left me determined to never again be at the mercy of recovery tools I didn’t understand. I decided that if I could understand how APFS actually stores data, I could make better decisions about recovery strategies and potentially build my own tools.

I opened a hex editor and started reading the raw bytes.

This is the story of how I built a Python driver that can read and write APFS disk images from scratch. More importantly, it’s a guide to understanding APFS from the ground up — because understanding how your data is stored is the first step toward protecting it.

The Problem That Started It All

I started with a simple experiment:

Can I read an APFS disk image without mounting it? Can I parse the raw structures myself?

I created a test APFS disk image and opened it in a hex editor. At offset 32, four bytes spelled out “NXSB” — the magic number for an APFS container superblock.

This discovery opened up a whole world of understanding. If I could parse these structures myself, I could understand how APFS works, how recovery tools operate, and potentially build my own tools.

The result is a complete system: low-level drivers for reading and writing APFS, encryption support for both DMG-level and APFS native encryption, and a cross-platform GUI that ties it all together into a practical tool. The GUI demonstrates everything in action — you can browse encrypted APFS images, extract files, and even write new ones, all while seeing the underlying structures at work.

Image of the apfs_gui.py running on MacOS

Image of the apfs_gui.py running on MacOS

The complete codebase is available on GitHub: here.

What Makes APFS Different?

APFS represents a significant departure from traditional filesystems. Its design choices enable features that weren’t possible with earlier MacOS file systems.

Copy-on-Write (CoW): When you modify a file, APFS doesn’t overwrite the old data. Instead, it writes the changes to new blocks and updates pointers. This means your old data is still there until it’s garbage collected.

B-Trees for Everything: Unlike traditional filesystems that use directory tables or inode lists, APFS stores all metadata in B-trees. Files, directories, file extents (where the data actually lives), extended attributes — all of it goes into B-trees. This makes lookups fast and consistent.

Encryption Built-In: APFS has native encryption support using AES-XTS. Your password doesn’t directly encrypt your files — instead, there’s a sophisticated key hierarchy that protects your data even if someone gets physical access to your disk.

Space Sharing: Multiple volumes can share the same physical space. You can have a 100GB container with two 100GB volumes, and as long as their combined usage stays under 100GB, everything works fine.

Snapshots: Because of copy-on-write, APFS can create instant snapshots of your entire filesystem. No copying data, just updating some pointers.

These features make APFS incredibly powerful, but they also make it complex. Let’s see how it all fits together.

The Architecture: Containers, Volumes, and Object Maps

APFS uses a hierarchical structure. At the outermost level, you have a Container — similar to a physical disk partition, but capable of holding multiple volumes that share the same physical space.

Inside the container, you have:

  • A Container Superblock that describes the container itself
  • An Object Map (OMAP) that translates virtual object IDs to physical block addresses
  • One or more Volumes, each with their own filesystem

Each volume has:

  • A Volume Superblock with volume-specific informatio
  • A Root B-Tree that contains all the files and directories
  • A Keybag (if encrypted) that holds the encryption keys

Here’s what that looks like visually:

The first challenge in building my driver was: how do I even find where the container starts?

Opening the File: The First Steps

When you open an APFS disk image, you might be opening:

  • A raw APFS container (just the APFS data, no partition table)
  • A GPT-partitioned disk (with an APFS partition somewhere inside)
  • An MBR-partitioned disk (less common, but possible)

My driver needed to handle all of these cases. Here’s how it works:


def _detect_partition_table(self):
    """Detect if the image has a GPT partition table and find the APFS partition."""
    # First, check if this is a raw APFS container (NXSB at offset 32)
    self.disk.seek(32)
    magic = self.disk.read(4)
    if magic == NX_MAGIC:  # NX_MAGIC = b'NXSB'
        print("Detected: Raw APFS container (no partition table)")
        self.partition_offset = 0
        return

    # Check for GPT header at LBA 1 (sector 512)
    self.disk.seek(512)
    gpt_sig = self.disk.read(8)

    if gpt_sig == b'EFI PART':
        print("Detected: GPT partition table")
        self._parse_gpt()

Once we know where the container starts, we can read the container superblock. This structure contains metadata about the entire APFS container:

  • How big blocks are (usually 4096 bytes)
  • How many blocks are in the container
  • Where to find the object map
  • Which volumes exist
  • Where the checkpoint data is stored

Every APFS object — whether it’s a superblock, a B-tree node, or a data block — starts with a 32-byte header. This header contains:

  • A checksum (Fletcher-64 algorithm) to detect corruption
  • An Object ID (OID) that uniquely identifies this object
  • A Transaction ID (XID) for the copy-on-write system
  • The object type and flags

The checksum is particularly important. APFS uses Fletcher-64, which is designed to catch errors efficiently:

def fletcher64(data: bytes) -> int:
    """Calculate Fletcher-64 checksum for APFS blocks."""
    sum1, sum2 = 0, 0
    for i in range(0, len(data), 4):
        word = struct.unpack('<I', data[i:i+4])[0]
        sum1 = (sum1 + word) % 0xFFFFFFFF
        sum2 = (sum2 + sum1) % 0xFFFFFFFF
    check1 = 0xFFFFFFFF - ((sum1 + sum2) % 0xFFFFFFFF)
    check2 = 0xFFFFFFFF - ((sum1 + check1) % 0xFFFFFFFF)
    return (check2 << 32) | check1

Every time we read a block, we verify its checksum. If it doesn’t match, something is corrupted.

The Object Map: Virtual to Physical Translation

Here’s where APFS gets interesting. Most objects in APFS are virtual — they have an Object ID (OID), but that OID doesn’t directly tell you where the object is stored on disk. Instead, you need to look it up in the Object Map.

The Object Map is itself a B-tree that maps:

  • Virtual OID + Transaction ID → Physical block address

This indirection is what makes copy-on-write possible. When you modify a file, APFS creates new blocks with new OIDs, updates the object map, and the old blocks remain untouched. This is how snapshots work — they just point to different versions of the object map.

The reading driver found here uses a simplified approach for OID resolution: it performs a linear search through a single node of the object map B-tree. This works for small object maps but doesn’t scale. However, for directory listing and file reading, it does implement proper recursive B-tree traversal — when scanning for directory records or file extents, it checks if nodes are internal or leaf, follows child pointers, and recursively descends the tree. The writing driver found here implements more sophisticated B-tree operations, including finding the correct leaf node for insertion and handling node splits when the tree grows.

B-Trees: The Heart of APFS

APFS uses B-trees for everything. There’s no separate directory table or inode list. Instead:

  • Inode records (type 3): Store file metadata (permissions, timestamps, size)
  • Directory records (type 9): Link a filename to an inode
  • File extent records (type 8): Map logical file offsets to physical blocks
  • Extended attribute records (type 4): Store file metadata beyond the standard inode

All of these live in the same B-tree, distinguished by their record type.

A B-tree node has a specific layout:

The keys and values are stored separately, with the Table of Contents acting as an index. This design allows for efficient insertion and deletion without moving large amounts of data around.

Parsing B-tree nodes requires careful attention to offset calculations. TOC offsets are relative to the start of the key/value areas, not absolute positions — a detail that cost me hours of debugging.

Reading a File: Following the Trail

Let’s trace through what happens when you read a file. Say you want to read /Users/test/document.txt.

Step 1: Navigate the Path

We start at the root directory (inode 2, by convention). We need to look up “Users” in the directory B-tree. Directory records are keyed by:

  • Parent inode ID (2, for root)
  • Filename hash (a CRC32C hash of the normalized filename)

The filename hashing is interesting. APFS normalizes filenames using Unicode NFD (Normalization Form Decomposed), then converts to UTF-32, then hashes with CRC32C. This ensures that “Document.txt” and “document.txt” hash to the same value (case-insensitive matching).

def hash_filename(name: str, case_insensitive: bool = True) -> int:
    """Calculate APFS filename hash using CRC32C on UTF-32 NFD-normalized data."""
    normalized = unicodedata.normalize('NFD', name)
    if case_insensitive:
        normalized = normalized.lower()

    utf32_bytes = b''.join(struct.pack('<I', ord(c)) for c in normalized)

    crc = 0xFFFFFFFF
    for byte in utf32_bytes:
        crc ^= byte
        for _ in range(8):
            if crc & 1:
                crc = (crc >> 1) ^ 0x82F63B78
            else:
                crc >>= 1

    return crc & 0x3FFFFF  # Low 22 bitspy

Once we find the directory record for “Users”, we get its inode number. Then we repeat the process for “test”, then “document.txt”.

Step 2: Find the File Extents

Now we have the inode for document.txt. But the inode doesn’t contain the file data — it just has metadata. To find the actual data, we look for file extent records.

File extent records are keyed by:

  • Inode ID + logical offset within the file

The value contains:

  • Physical block number
  • Length of the extent
  • Crypto ID (if encrypted)

A file might be stored in multiple extents (fragmented). We need to read all of them and piece them together.

Step 3: Read the Blocks

Finally, we read the physical blocks. If the volume is encrypted, we decrypt each block as we read it (more on that later).

def _read_file_data(self, inode: int) -> bytes:
    """Read file data by inode number."""
    extents = self._find_file_extents(inode)

    if not extents:
        return b''

    # Read all extents
    data = bytearray()
    for logical_offset, phys_block, length in sorted(extents):
        for block_offset in range(0, length, self.block_size):
            block_num = phys_block + (block_offset // self.block_size)
            block_data = self._read_block(block_num)

            # If encrypted, decrypt the block
            if self.aes_xts:
                block_data = self.aes_xts.decrypt(block_data, block_num)

            remaining = length - block_offset
            data.extend(block_data[:min(remaining, self.block_size)])

    return bytes(data)

The process is straightforward in concept, but the implementation requires careful handling of fragmented files, encrypted volumes, B-tree traversal, and edge cases.

Writing Files: The Copy-on-Write Dance

Reading is one thing. Writing is where APFS really shows its sophistication.

Because of copy-on-write, we never overwrite existing blocks. Instead, we:

  1. Allocate new blocks

  2. Write the new data

  3. Create new B-tree nodes with updated pointers

  4. Update the object map

  5. Increment the transaction ID

Let’s say we want to create a new file called hello.txt with the contents “Hello, APFS!”.

Step 1: Allocate an Inode

First, we need a new inode. We allocate a new OID (Object ID) and create an inode record in the B-tree:

def _create_inode(self, mode: int, size: int) -> int:
    """Create a new inode and return its OID."""
    new_ino = self._allocate_oid()

    # Build inode value with all the metadata
    now = int(time.time() * 1e9)  # Nanoseconds since epoch
    inode_val = struct.pack(
        '<QQQQQQIIIIIIHHHQ',
        parent_id,      # Parent directory (root = 2)
        private_id,     # Private ID
        now,            # Create time
        now,            # Mod time
        now,            # Change time
        now,            # Access time
        0,              # Internal flags
        1,              # nlink (number of hard links)
        0,              # Protection class
        0,              # Write generation
        0,              # BSD flags
        501,            # UID
        20,             # GID
        mode,           # File mode (permissions + type)
        0,              # Padding
        size            # File size
    )

    # Insert into B-tree
    key = struct.pack('<Q', (JOBJ_TYPE_INODE << 60) | new_ino)
    self._insert_btree_record(key, inode_val)

    return new_ino

Step 2: Create a Directory Record

Now we need to link the filename to the inode. This is a directory record:

def _create_drec(self, parent_ino: int, name: str, file_ino: int):
    """Create a directory record linking filename to inode."""
    name_hash = hash_filename(name)

    # Build drec key: parent inode + name hash + name
    name_bytes = name.encode('utf-8')
    name_len_hash = (len(name_bytes) << 22) | name_hash
    drec_key = struct.pack(
        '<QI',
        (JOBJ_TYPE_DIR_REC << 60) | parent_ino,
        name_len_hash
    ) + name_bytes + b'\x00'

    # Build drec value: file inode + timestamp + flags
    now = int(time.time() * 1e9)
    drec_val = struct.pack('<QQH', file_ino, now, DT_REG)

    # Insert into B-tree
    self._insert_btree_record(drec_key, drec_val)

Step 3: Write the File Data

Now we write the actual file content. We allocate blocks, write the data, and create extent records:

def _write_file_data(self, inode: int, data: bytes):
    """Write file data to allocated extents."""
    # Allocate blocks for data
    data_blocks = (len(data) + self.block_size - 1) // self.block_size
    phys_blocks = self._allocate_blocks(data_blocks)

    # Write data to blocks
    for i, block_num in enumerate(phys_blocks):
        offset = i * self.block_size
        block_data = data[offset:offset + self.block_size]

        # Pad to block size
        if len(block_data) < self.block_size:
            block_data = block_data.ljust(self.block_size, b'\x00')

        # Encrypt if needed
        if self.aes_xts:
            block_data = self.aes_xts.encrypt(block_data, block_num)

        self._write_block(block_num, block_data)

    # Create extent records linking logical offsets to physical blocks
    for i, block_num in enumerate(phys_blocks):
        logical_offset = i * self.block_size
        length = min(self.block_size, len(data) - logical_offset)

        # Build extent key: inode + logical offset
        extent_key = struct.pack(
            '<QQ',
            (JOBJ_TYPE_FILE_EXTENT << 60) | inode,
            logical_offset
        )

        # Build extent value: length + physical block + crypto ID
        len_flags = length
        extent_val = struct.pack('<QQQ', len_flags, block_num, 0)

        # Insert into B-tree
        self._insert_btree_record(extent_key, extent_val)

Step 4: Update Everything

Every block we modify needs a new checksum and transaction ID. This is what makes the copy-on-write system work — the old blocks remain untouched, and the new blocks have higher transaction IDs.

def _update_block(self, block_num: int, data: bytearray):
    """Update a block with new checksum and XID."""
    # Update transaction ID
    struct.pack_into('<Q', data, 16, self.next_xid)

    # Calculate new checksum
    checksum_data = b'\x00' * 8 + bytes(data[8:])
    checksum = fletcher64(checksum_data)
    struct.pack_into('<Q', data, 0, checksum)

    # Write block
    self._write_block(block_num, bytes(data))

The beauty of this system is that if something goes wrong during the write, the old data is still there. We can just discard the new blocks and everything is back to the previous state.

Encryption: A Multi-Layer Defense

One of the most fascinating parts of APFS is its encryption system. When you enable FileVault on macOS, your entire volume is encrypted using AES-XTS, but the way the keys are managed is elegant.

The Key Hierarchy

Your password doesn’t directly encrypt your files. Instead, there’s a chain:

  1. Password → PBKDF2 (with salt and iterations) → KEK (Key Encryption Key)

  2. KEK → RFC 3394 AES Key Wrap → VEK (Volume Encryption Key)

  3. VEK → AES-XTS → Your File Data

The VEK is a 32-byte key split into two 16-byte parts:

  • First 16 bytes: Data encryption key
  • Second 16 bytes: Tweak encryption key (for XTS mode)

Keybags: Where Keys Live

Keys are stored in encrypted keybags. There are two types:

  1. Container Keybag: Encrypted with the container’s UUID. Contains references to volume keybags and sometimes the VEK directly.

  2. Volume Keybag: Encrypted with the volume’s UUID. Contains the KEK (wrapped with the password-derived key) and the VEK (wrapped with the KEK).

When you enter your password, the driver:

  1. Finds the keybag location in the container superblock

  2. Decrypts the container keybag using the container UUID

  3. Finds the volume keybag reference

  4. Decrypts the volume keybag using the volume UUID

  5. Extracts the KEK blob (which contains salt, iterations, and wrapped KEK)

  6. Derives the KEK from your password using PBKDF2

  7. Unwraps the KEK using RFC 3394

  8. Unwraps the VEK using the KEK

  9. Initializes AES-XTS with the VEK

Here’s what that looks like in code:

def _derive_vek(self, kek_info: Dict, vek_data: bytes):
    """Derive VEK from password using PBKDF2 and RFC 3394 unwrap."""
    # Step 1: Derive KEK from password
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=kek_info['salt'],
        iterations=kek_info['iterations'],
        backend=default_backend()
    )
    derived_key = kdf.derive(self.password.encode('utf-8'))

    # Step 2: Unwrap KEK using RFC 3394
    key_manager = KeyManager()
    unwrapped_kek = key_manager.unwrap_key(kek_info['wrapped_kek'], derived_key)

    if unwrapped_kek:
        # Step 3: Unwrap VEK using KEK
        wrapped_vek = self._parse_vek_blob(vek_data)
        if wrapped_vek:
            self.volume_key = key_manager.unwrap_key(wrapped_vek, unwrapped_kek)
            if self.volume_key:
                self.aes_xts = AesXts(self.volume_key)

AES-XTS: The Encryption Mode

APFS uses AES-XTS (XEX-based Tweaked CodeBook mode with ciphertext Stealing). This is a disk encryption mode designed for sector-based storage.

XTS works by:

  1. Encrypting a tweak value (usually the sector number) with a tweak key

  2. XORing the plaintext with the tweak

  3. Encrypting with the data key

  4. XORing the result with the tweak again

This ensures that identical data in different sectors encrypts to different ciphertext, which is crucial for security.

class AesXts:
    """AES-XTS encryption/decryption for APFS."""

    def decrypt(self, ciphertext: bytes, block_no: int, sector_size: int = 512) -> bytes:
        """Decrypt data using AES-XTS."""
        result = bytearray()
        cs_factor = 4096 // sector_size  # 8 sectors per 4KB block
        sector_no = block_no * cs_factor

        for sector_start in range(0, len(ciphertext), sector_size):
            sector = ciphertext[sector_start:sector_start + sector_size]

            # Initialize tweak for this sector
            tweak_input = struct.pack('<QQ', sector_no, 0)
            tweak = bytearray(self._aes_encrypt_block(self.key2, tweak_input))

            # Decrypt each 16-byte block in the sector
            for i in range(0, len(sector), 16):
                block = sector[i:i+16]
                xored = bytes(a ^ b for a, b in zip(block, tweak))
                decrypted = self._aes_decrypt_block(self.key1, xored)
                result.extend(bytes(a ^ b for a, b in zip(decrypted, tweak)))
                self._multiply_tweak(tweak)  # Multiply by x in GF(2^128)

            sector_no += 1

        return bytes(result)

The _multiply_tweak function implements multiplication by x in the Galois field GF(2¹²⁸). This is part of the XTS specification and ensures that each 16-byte block within a sector has a unique tweak value.

What I Learned (And What I’m Still Learning)

Building this driver taught me more about filesystems than any textbook could, specifically because I couldn’t find much material on this subject. I am very thankful to the previous work done by other people on github as their code was invaluable. Here are some of my key insights:

Copy-on-write is powerful: The ability to create instant snapshots, roll back changes, and maintain data integrity comes from this one design decision.

Encryption is layered: The key hierarchy in APFS shows how modern encryption systems protect data at multiple levels. Even if someone gets your disk, they still need your password to derive the keys.

B-trees are fundamental: B-trees appear throughout systems software — databases, filesystems, and various in-memory data structures rely on them.

What Works

The complete system consists of two main drivers:

apfs_driver_full.py (Reading Driver):

  • Read files from unencrypted volumes
  • Read files from encrypted volumes (with password)
  • List directories recursively with proper B-tree traversal
  • Handle DMG-level encryption (via dmg_decryptor.py)
  • Handle APFS native encryption (FileVault)
  • Encrypt/decrypt with AES-XTS
  • Navigate paths and read file extents

**apfs_writer.py (Writing Driver):**

  • Write new files to the root directory
  • Allocate blocks and create file extents
  • Insert records into B-trees with proper leaf finding
  • Handle B-tree node splitting when nodes fill up
  • Update object maps for copy-on-write
  • Manage transaction IDs and checksums
  • Create checkpoint descriptors (though macOS compatibility is still a work in progress)

What’s Still Challenging

Some things are harder than they look:

Checkpoint Management: APFS uses a checkpoint system for atomic transactions. The writing driver apfs_writer.py implements checkpoint creation — it writes all ephemeral objects to the checkpoint data area and creates checkpoint maps. However, full compatibility with macOS’s checkpoint validation is still a work in progress. Files written by the driver may not always mount on macOS, though they can be read back by the reading driver.

B-Tree Splitting: The writing driver implements B-tree node splitting — when a leaf node fills up, it splits into two leaves and creates a new internal root node. This handles the common case, but complex multi-level splits and tree rebalancing are simplified. For most practical purposes, the implementation works, but it’s not as robust as a possible production filesystem.

Space Management: The space manager uses bitmaps to track free blocks. The writing driver uses a simplified heuristic to find free blocks (scanning for zero-filled blocks) rather than parsing the full space manager bitmap structure. This works for small writes but doesn’t scale to large volumes.

macOS Compatibility: Files written by the driver may not mount on macOS because some metadata might be missing or checkpoint descriptors may not pass macOS’s strict validation. The reading driver can read the files back, confirming the structures are correct, but macOS’s mount process has additional checks that aren’t fully implemented yet.

Try It Yourself

The complete project is available on GitHub here. It includes a cross-platform GUI application that demonstrates everything we’ve discussed. You can launch it with:

python apfs_gui.py

The GUI provides a visual interface for browsing APFS images, handling both DMG-level encryption (automatically detecting encrcdsa format) and supports APFS native encryption. It lets you navigate directories, preview files, extract them to your local filesystem, and even write new files to images.

For programmatic access, you can use the drivers directly. The reading driver apfs_driver_full.py handles all read operations:

from apfs_driver_full import APFSDriver

# Open an APFS image
with APFSDriver('my_disk.dmg') as driver:
    # List volumes
    volumes = driver.list_volumes()
    for vol in volumes:
        print(f"Volume: {vol['name']}")
        print(f"  Files: {vol['files']}")
        print(f"  Directories: {vol['directories']}")

    # List root directory
    entries = driver.list_directory('/')
    for entry in entries:
        type_char = 'd' if entry.is_directory else '-'
        print(f"{type_char} {entry.name}")

    # Read a file
    data = driver.read_file('/Users/test/document.txt')
    print(data.decode('utf-8'))

For encrypted volumes, the reading driver handles key derivation automatically:

# Open encrypted volume with password
with APFSDriver('encrypted.dmg', password='mypassword') as driver:
    # The driver automatically:
    # 1. Finds the keybag
    # 2. Derives KEK from password using PBKDF2
    # 3. Unwraps VEK using RFC 3394
    # 4. Initializes AES-XTS
    # 5. Decrypts blocks on-the-fly as you read
    data = driver.read_file('/secret.txt')
    print(data.decode('utf-8'))

For writing files, use the writing driver apfs_writer.py:

# Open encrypted volume with password
with APFSDriver('encrypted.dmg', password='mypassword') as driver:
    # The driver automatically:
    # 1. Finds the keybag
    # 2. Derives KEK from password using PBKDF2
    # 3. Unwraps VEK using RFC 3394
    # 4. Initializes AES-XTS
    # 5. Decrypts blocks on-the-fly as you read
    data = driver.read_file('/secret.txt')
    print(data.decode('utf-8'))

The GUI also handles DMG-level encryption (the encrcdsa format used by “hdiutil create -encryption”). When you open an encrypted DMG, it automatically detects the encryption, prompts for a password, decrypts to a temporary file, and then opens the APFS container inside.

Important Warning: Always work on copies of disk images. Writing to APFS can corrupt data if done incorrectly. This is educational code, not production software.

The Bigger Picture: From Helplessness to Understanding

Building this driver wasn’t just about understanding APFS — it was about reclaiming control after a devastating 30-day recovery process. When my external drive became unreadable after that kernel panic, I felt completely helpless. I didn’t understand what had happened, I couldn’t fix it myself, and I had to try tool after tool, hoping one would work.

I spent days trying Disk Utility, fsck_apfs, UFS Explorer, and R-Studio. Some couldn’t even see the drive. Others could see it but couldn’t read the filesystem. I was throwing solutions at the problem without understanding what the problem actually was.

When Disk Drill’s quick scan finally worked and recovered my files with the directory structure intact, I was relieved.

Now, I understand how APFS stores data. I know how copy-on-write works, how snapshots are created, how encryption protects files, and how B-trees organize everything. More importantly, I know what to look for when something goes wrong and which recovery strategy to use.

APFS represents years of engineering work by Apple. Features like copy-on-write, snapshots, and encryption aren’t just nice-to-haves — they’re fundamental to how modern operating systems protect and manage data. But they’re also complex, and that complexity can make recovery difficult if you don’t understand what’s happening.

My implementation is far from complete, but it demonstrates the core concepts. More importantly, it gave me the knowledge I needed to feel in control again. If you’ve ever lost data or worried about losing it, I encourage you to explore how your filesystem works. Understanding is the first step toward prevention and recovery.

The next time you save a file on your Mac, you’ll know what’s really happening under the hood.

Further Reading

If you want to dive deeper:

Thanks for reading!

Aaron Beckley


메타데이터
post_id
1a045debbf78
slug
to-understand-apple-file-system-apfs-better-i-made-a-driver-in-python-1a045debbf78
url
https://medium.com/data-science-collective/to-understand-apple-file-system-apfs-better-i-made-a-driver-in-python-1a045debbf78
canonical_url
https://medium.com/data-science-collective/to-understand-apple-file-system-apfs-better-i-made-a-driver-in-python-1a045debbf78
author_url
https://medium.com/@aaronbeckley
status
ok
fetched_at
2026-07-13 20:57:02