To Understand Apple File System (APFS) Better, I Made a Driver in Python
Supporting Read, Write, and Encryption
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
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:
-
Allocate new blocks
-
Write the new data
-
Create new B-tree nodes with updated pointers
-
Update the object map
-
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:
-
Password → PBKDF2 (with salt and iterations) → KEK (Key Encryption Key)
-
KEK → RFC 3394 AES Key Wrap → VEK (Volume Encryption Key)
-
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:
-
Container Keybag: Encrypted with the container’s UUID. Contains references to volume keybags and sometimes the VEK directly.
-
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:
-
Finds the keybag location in the container superblock
-
Decrypts the container keybag using the container UUID
-
Finds the volume keybag reference
-
Decrypts the volume keybag using the volume UUID
-
Extracts the KEK blob (which contains salt, iterations, and wrapped KEK)
-
Derives the KEK from your password using PBKDF2
-
Unwraps the KEK using RFC 3394
-
Unwraps the VEK using the KEK
-
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:
-
Encrypting a tweak value (usually the sector number) with a tweak key
-
XORing the plaintext with the tweak
-
Encrypting with the data key
-
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:
- APFS-rw-driver-python — The complete codebase for this project
- Apple File System Reference— The official documentation (when available)
- apfs-fuse— A FUSE implementation of APFS
- drat— APFS reverse engineering tools
- dissect.apfs— A Python APFS library
- RFC 3394— The AES Key Wrap Algorithm specification
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