apfs-0.2.4/.cargo_vcs_info.json 0000644 00000000142 10461020230 0012007 0 ustar {
"git": {
"sha1": "ea84058be933ce51e7b8251b302a3646daf8e12f"
},
"path_in_vcs": "apfs"
} apfs-0.2.4/CHANGELOG.md 0000644 0000000 0000000 00000003021 10461020230 0012365 0 ustar 0000000 0000000 # Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).
## [0.2.4] - 2026-04-12
### Changed
- Include `LICENSE` file in the published crate
## [0.2.3] - 2026-02-21
### Fixed
- Use checked arithmetic and fallible indexing in APFS xfield parsing to prevent panics on malformed images
## [0.2.2] - 2026-02-16
### Changed
- Rust edition upgraded from 2021 to 2024
### Fixed
- Clippy fixes for Rust 2024 edition
- Removed `#[allow(clippy::too_many_arguments)]` by refactoring B-tree traversal parameters into `BTreeParams` struct
## [0.2.1] - 2026-02-16
### Fixed
- Clippy warnings: `empty_line_after_doc_comments`, `unnecessary_cast`, `too_many_arguments` allow
- Formatting drift in benchmark and source files
## [0.2.0] - 2026-02-11
### Changed
- Fixture-dependent tests now use `#[ignore]` instead of silent path-exists guards
### Added
- Self-contained unit tests for Fletcher-64 checksum, superblock magic validation,
DrecVal parsing, and FileExtentVal length masking
## [0.1.0] - 2026-02-10
### Added
- APFS container and volume superblock parsing
- Fletcher-64 checksum verification
- Checkpoint descriptor scanning
- Object Map B-tree resolution
- Catalog B-tree traversal (inodes, directory records, file extents)
- `ApfsForkReader` with `Read + Seek` streaming I/O
- Directory listing, file reading, recursive walk
- Path resolution (Unix-style paths)
apfs-0.2.4/Cargo.lock 0000644 00000035447 10461020230 0010002 0 ustar # This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "apfs"
version = "0.2.4"
dependencies = [
"byteorder",
"criterion",
"thiserror",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
apfs-0.2.4/Cargo.toml 0000644 00000002251 10461020230 0010010 0 ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "apfs"
version = "0.2.4"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Read-only APFS (Apple File System) parser"
readme = "README.md"
keywords = [
"apfs",
"apple",
"macos",
"filesystem",
]
categories = [
"parsing",
"filesystem",
]
license = "MIT"
repository = "https://github.com/Dil4rd/dpp"
resolver = "2"
[lib]
name = "apfs"
path = "src/lib.rs"
[[bench]]
name = "parse_benchmark"
path = "benches/parse_benchmark.rs"
harness = false
[dependencies.byteorder]
version = "1.5"
[dependencies.thiserror]
version = "2"
[dev-dependencies.criterion]
version = "0.5"
features = ["html_reports"]
apfs-0.2.4/Cargo.toml.orig 0000644 0000000 0000000 00000000704 10461020230 0013450 0 ustar 0000000 0000000 [package]
name = "apfs"
version = "0.2.4"
edition = "2024"
description = "Read-only APFS (Apple File System) parser"
license = "MIT"
repository = "https://github.com/Dil4rd/dpp"
keywords = ["apfs", "apple", "macos", "filesystem"]
categories = ["parsing", "filesystem"]
[dependencies]
byteorder = "1.5"
thiserror = "2"
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "parse_benchmark"
harness = false
apfs-0.2.4/LICENSE 0000644 0000000 0000000 00000002047 10461020230 0011570 0 ustar 0000000 0000000 MIT License
Copyright (c) 2026 Dil4rd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
apfs-0.2.4/README.md 0000644 0000000 0000000 00000013072 10461020230 0012042 0 ustar 0000000 0000000
# apfs
**Cross-platform Rust library for reading Apple File System (APFS) containers**
[](https://crates.io/crates/apfs)
[](https://docs.rs/apfs)
[](LICENSE)

Parse APFS volumes from raw disk images on any platform — no kernel drivers or FUSE required.
**Pure Rust, zero unsafe** — works everywhere Rust compiles.
---
## Why apfs?
**apfs is a standalone pure-Rust library for reading APFS filesystems with full B-tree traversal and object map resolution.**
| Feature | **apfs** | fal-backend-apfs | apfs-fuse | libfsapfs |
|---------|:--------:|:----------------:|:---------:|:---------:|
| Pure Rust | ✓ | ✓ | ❌ (C++) | ❌ (C) |
| Standalone | ✓ | ❌ (fal ecosystem) | ❌ (FUSE) | ❌ (system lib) |
| Generic `Read+Seek` | ✓ | ❌ | ❌ | ❌ |
| Streaming reads | ✓ | ❌ | ✓ | ✓ |
| Container parsing | ✓ | ✓ | ✓ | ✓ |
| Object map resolution | ✓ | ✓ | ✓ | ✓ |
| Catalog B-tree | ✓ | ✓ | ✓ | ✓ |
| Checkpoint scanning | ✓ | partial | ✓ | ✓ |
| Fletcher-64 checksums | ✓ | ✓ | ✓ | ✓ |
| Encryption | ❌ | ❌ | ✓ | ✓ |
| Compression | ❌ | ❌ | ✓ | ✓ |
| Permissive license | MIT | MIT | GPL-2.0 | LGPL-3.0 |
\* Only `byteorder` and `thiserror` — no compression, no FFI, no system libs.
## Features
| | |
|---|---|
| **List directories** | Browse filesystem tree with names, sizes, timestamps |
| **Read files** | Extract file contents into memory or stream to a writer |
| **Streaming I/O** | `ApfsForkReader` provides `Read+Seek` access without buffering |
| **File metadata** | BSD permissions, creation/modification dates, inode info |
| **Recursive walk** | Walk entire filesystem tree with full paths |
| **Path resolution** | Navigate by Unix-style paths (`/Applications/Upscayl.app/Contents/Info.plist`) |
| **Checksums** | Fletcher-64 verification on all on-disk objects |
| **Checkpoint scanning** | Finds latest valid container superblock |
### Format Support
| Feature | Support | Notes |
|---------|:-------:|-------|
| Read-only volumes | ✓ | Full directory listing, file reading, metadata |
| Multiple volumes | First only | Reads the first non-empty volume in the container |
| Encryption | ❌ | Encrypted volumes not supported |
| Snapshots | ❌ | Snapshot browsing not supported |
| Clones | ❌ | Clone resolution not supported |
| Compression | ❌ | Compressed extents not supported |
## Quick Start
### Open and Browse
```rust
use apfs::ApfsVolume;
use std::fs::File;
use std::io::BufReader;
let file = File::open("container.raw")?;
let mut vol = ApfsVolume::open(BufReader::new(file))?;
// Volume info
let info = vol.volume_info();
println!("{}: {} files, {} dirs", info.name, info.num_files, info.num_directories);
// List root directory
for entry in vol.list_directory("/")? {
println!("{:?} {:>12} {}", entry.kind, entry.size, entry.name);
}
```
### Read a File
```rust
// Read into memory
let data = vol.read_file("/Applications/Upscayl.app/Contents/Info.plist")?;
// Or stream to a writer (low memory)
let mut out = File::create("Info.plist")?;
vol.read_file_to("/Applications/Upscayl.app/Contents/Info.plist", &mut out)?;
```
### Walk Entire Filesystem
```rust
for entry in vol.walk()? {
if entry.entry.kind == apfs::EntryKind::File {
println!("{}: {} bytes", entry.path, entry.entry.size);
}
}
```
### Streaming File Access
```rust
use std::io::Read;
let mut reader = vol.open_file("/large-file.bin")?;
let mut buf = [0u8; 4096];
let n = reader.read(&mut buf)?;
```
### File Metadata
```rust
let stat = vol.stat("/.DS_Store")?;
println!("Size: {} bytes", stat.size);
println!("Owner: {}:{}", stat.uid, stat.gid);
println!("Mode: 0o{:o}", stat.mode);
```
## Architecture
```
Container (NXSB)
├── Checkpoint descriptor area → latest valid superblock
├── Container OMAP → resolves volume OIDs to physical blocks
└── Volume (APSB)
├── Volume OMAP → resolves catalog OIDs to physical blocks
└── Catalog B-tree (virtual, keyed by OID then type)
├── Inodes (type 3) — file/directory metadata
├── Xattrs (type 4) — extended attributes
├── File extents (type 8) — physical data locations
└── Directory records (type 9) — name → inode mapping
```
### Modules
| Module | Description |
|--------|-------------|
| `fletcher` | Fletcher-64 checksum computation and verification |
| `object` | 32-byte object header parsing, type constants |
| `superblock` | Container (NXSB) and volume (APSB) superblock parsing, checkpoint scanning |
| `omap` | Object Map B-tree lookup — virtual OID to physical block |
| `btree` | Generic APFS B-tree node parsing, search, and range scan |
| `catalog` | Catalog record types: inodes, directory records, file extents, path resolution |
| `extents` | File data reading from physical extents, `ApfsForkReader` |
## Limitations
- **Read-only** — no write support
- **No encryption** — cannot read FileVault or per-file encrypted volumes
- **No compression** — transparent compression (lzvn, lzfse, zlib) not decompressed
- **No snapshots** — snapshot browsing not implemented
- **Single volume** — reads only the first volume in a multi-volume container
- **No extended attributes** — xattr values not exposed through the public API
## License
MIT
apfs-0.2.4/benches/parse_benchmark.rs 0000644 0000000 0000000 00000005364 10461020230 0015671 0 ustar 0000000 0000000 use criterion::{Criterion, criterion_group, criterion_main};
use std::io::BufReader;
fn open_appfs() -> Option> {
let path = std::path::Path::new("../tests/appfs.raw");
if !path.exists() {
return None;
}
let file = std::fs::File::open(path).ok()?;
Some(BufReader::new(file))
}
fn bench_open(c: &mut Criterion) {
if open_appfs().is_none() {
eprintln!("Skipping benchmarks - appfs.raw not found");
return;
}
c.bench_function("apfs_open", |b| {
b.iter(|| {
let reader = open_appfs().unwrap();
let _vol = apfs::ApfsVolume::open(reader).unwrap();
})
});
}
fn bench_list_root(c: &mut Criterion) {
let reader = match open_appfs() {
Some(r) => r,
None => return,
};
let mut vol = apfs::ApfsVolume::open(reader).unwrap();
c.bench_function("apfs_list_root", |b| {
b.iter(|| {
let _entries = vol.list_directory("/").unwrap();
})
});
}
fn bench_walk_all(c: &mut Criterion) {
if open_appfs().is_none() {
return;
}
c.bench_function("apfs_walk_all", |b| {
b.iter(|| {
let reader = open_appfs().unwrap();
let mut vol = apfs::ApfsVolume::open(reader).unwrap();
let _entries = vol.walk().unwrap();
})
});
}
fn bench_stat(c: &mut Criterion) {
let reader = match open_appfs() {
Some(r) => r,
None => return,
};
let mut vol = apfs::ApfsVolume::open(reader).unwrap();
// Find a file path to stat
let walk = vol.walk().unwrap();
let file_path = walk
.iter()
.find(|e| e.entry.kind == apfs::EntryKind::File && e.entry.size > 0)
.map(|e| e.path.clone());
if let Some(path) = file_path {
c.bench_function("apfs_stat", |b| {
b.iter(|| {
let _stat = vol.stat(&path).unwrap();
})
});
}
}
fn bench_read_small_file(c: &mut Criterion) {
let reader = match open_appfs() {
Some(r) => r,
None => return,
};
let mut vol = apfs::ApfsVolume::open(reader).unwrap();
// Find a small file to read
let walk = vol.walk().unwrap();
let file_path = walk
.iter()
.find(|e| {
e.entry.kind == apfs::EntryKind::File && e.entry.size > 0 && e.entry.size < 100_000
})
.map(|e| e.path.clone());
if let Some(path) = file_path {
c.bench_function("apfs_read_small_file", |b| {
b.iter(|| {
let _data = vol.read_file(&path).unwrap();
})
});
}
}
criterion_group!(
benches,
bench_open,
bench_list_root,
bench_walk_all,
bench_stat,
bench_read_small_file
);
criterion_main!(benches);
apfs-0.2.4/src/btree.rs 0000644 0000000 0000000 00000040222 10461020230 0013016 0 ustar 0000000 0000000 use byteorder::{LittleEndian, ReadBytesExt};
use std::io::{Cursor, Read, Seek};
use crate::error::{ApfsError, Result};
use crate::object::{self, ObjectHeader};
use crate::omap;
// B-tree node flags (from btn_flags)
pub const BTNODE_ROOT: u16 = 0x0001;
pub const BTNODE_LEAF: u16 = 0x0002;
pub const BTNODE_FIXED_KV_SIZE: u16 = 0x0004;
// BTreeInfo flags
pub const BTREE_PHYSICAL: u32 = 0x0001;
/// B-tree node header — 56 bytes after the object header.
#[derive(Debug, Clone)]
pub struct BTreeNodeHeader {
pub btn_flags: u16,
pub btn_level: u16,
pub btn_nkeys: u32,
pub btn_table_space_off: u16,
pub btn_table_space_len: u16,
pub btn_free_space_off: u16,
pub btn_free_space_len: u16,
pub btn_free_list_off: u16,
pub btn_free_list_len: u16,
pub btn_key_free_list_off: u16,
pub btn_key_free_list_len: u16,
pub btn_val_free_list_off: u16,
pub btn_val_free_list_len: u16,
}
impl BTreeNodeHeader {
pub const SIZE: usize = 24;
pub fn parse(data: &[u8]) -> Result {
if data.len() < Self::SIZE {
return Err(ApfsError::InvalidBTree(
"btree node header too short".into(),
));
}
let mut cursor = Cursor::new(data);
Ok(BTreeNodeHeader {
btn_flags: cursor.read_u16::()?,
btn_level: cursor.read_u16::()?,
btn_nkeys: cursor.read_u32::()?,
btn_table_space_off: cursor.read_u16::()?,
btn_table_space_len: cursor.read_u16::()?,
btn_free_space_off: cursor.read_u16::()?,
btn_free_space_len: cursor.read_u16::()?,
btn_free_list_off: cursor.read_u16::()?,
btn_free_list_len: cursor.read_u16::()?,
btn_key_free_list_off: cursor.read_u16::()?,
btn_key_free_list_len: cursor.read_u16::()?,
btn_val_free_list_off: cursor.read_u16::()?,
btn_val_free_list_len: cursor.read_u16::()?,
})
}
pub fn is_leaf(&self) -> bool {
self.btn_flags & BTNODE_LEAF != 0
}
pub fn is_root(&self) -> bool {
self.btn_flags & BTNODE_ROOT != 0
}
pub fn is_fixed_kv(&self) -> bool {
self.btn_flags & BTNODE_FIXED_KV_SIZE != 0
}
}
/// BTreeInfo — 40 bytes at the end of a root node (before the footer).
#[derive(Debug, Clone)]
pub struct BTreeInfo {
pub bt_fixed: BTreeInfoFixed,
pub bt_longest_key: u32,
pub bt_longest_val: u32,
pub bt_key_count: u64,
pub bt_node_count: u64,
}
#[derive(Debug, Clone)]
pub struct BTreeInfoFixed {
pub bt_flags: u32,
pub bt_node_size: u32,
pub bt_key_size: u32,
pub bt_val_size: u32,
}
impl BTreeInfo {
pub const SIZE: usize = 40;
pub fn parse(data: &[u8]) -> Result {
if data.len() < Self::SIZE {
return Err(ApfsError::InvalidBTree("btree info too short".into()));
}
let mut cursor = Cursor::new(data);
let bt_flags = cursor.read_u32::()?;
let bt_node_size = cursor.read_u32::()?;
let bt_key_size = cursor.read_u32::()?;
let bt_val_size = cursor.read_u32::()?;
let bt_longest_key = cursor.read_u32::()?;
let bt_longest_val = cursor.read_u32::()?;
let bt_key_count = cursor.read_u64::()?;
let bt_node_count = cursor.read_u64::()?;
Ok(BTreeInfo {
bt_fixed: BTreeInfoFixed {
bt_flags,
bt_node_size,
bt_key_size,
bt_val_size,
},
bt_longest_key,
bt_longest_val,
bt_key_count,
bt_node_count,
})
}
}
/// A Table of Contents entry (fixed-size KV: 4 bytes, variable-size: 8 bytes)
#[derive(Debug, Clone)]
pub struct TocEntry {
pub key_off: u16,
pub key_len: u16, // 0 for fixed-size KV
pub val_off: u16,
pub val_len: u16, // 0 for fixed-size KV
}
/// A parsed APFS B-tree node with extracted key-value pairs.
pub struct BTreeNode {
pub header: ObjectHeader,
pub node_header: BTreeNodeHeader,
pub toc: Vec,
pub block_data: Vec,
pub key_area_off: usize, // Absolute offset within block_data where key area starts
pub val_area_end: usize, // Absolute offset within block_data where val area ends
pub info: Option,
}
impl BTreeNode {
/// Parse a B-tree node from a raw block.
pub fn parse(block: &[u8]) -> Result {
let header = ObjectHeader::parse(block)?;
let node_header = BTreeNodeHeader::parse(&block[ObjectHeader::SIZE..])?;
let toc_start =
ObjectHeader::SIZE + BTreeNodeHeader::SIZE + node_header.btn_table_space_off as usize;
let fixed_kv = node_header.is_fixed_kv();
// Key area starts right after the table space
let key_area_off = ObjectHeader::SIZE
+ BTreeNodeHeader::SIZE
+ node_header.btn_table_space_off as usize
+ node_header.btn_table_space_len as usize;
// Parse BTreeInfo if this is a root node (it's at the end of the value area)
let info = if node_header.is_root() {
let info_start = block.len() - BTreeInfo::SIZE;
Some(BTreeInfo::parse(&block[info_start..])?)
} else {
None
};
// Value area end: for root nodes, it's before BTreeInfo; for non-root, it's end of block
let val_area_end = if node_header.is_root() {
block.len() - BTreeInfo::SIZE
} else {
block.len()
};
// Parse TOC entries
let mut toc = Vec::with_capacity(node_header.btn_nkeys as usize);
let mut cursor = Cursor::new(&block[toc_start..]);
for _ in 0..node_header.btn_nkeys {
if fixed_kv {
let key_off = cursor.read_u16::()?;
let val_off = cursor.read_u16::()?;
toc.push(TocEntry {
key_off,
key_len: 0,
val_off,
val_len: 0,
});
} else {
let key_off = cursor.read_u16::()?;
let key_len = cursor.read_u16::()?;
let val_off = cursor.read_u16::()?;
let val_len = cursor.read_u16::()?;
toc.push(TocEntry {
key_off,
key_len,
val_off,
val_len,
});
}
}
Ok(BTreeNode {
header,
node_header,
toc,
block_data: block.to_vec(),
key_area_off,
val_area_end,
info,
})
}
/// Get the key bytes for a given TOC index.
pub fn key(&self, index: usize, fixed_key_size: u32) -> Result<&[u8]> {
let entry = &self.toc[index];
let start = self.key_area_off + entry.key_off as usize;
let len = if self.node_header.is_fixed_kv() {
fixed_key_size as usize
} else {
entry.key_len as usize
};
let end = start + len;
if end > self.block_data.len() {
return Err(ApfsError::InvalidBTree(format!(
"key out of bounds: start={}, len={}, block_size={}",
start,
len,
self.block_data.len()
)));
}
Ok(&self.block_data[start..end])
}
/// Get the value bytes for a given TOC index.
///
/// val_off is an offset from val_area_end to the START of the value data.
/// i.e., value bytes are at block_data[val_area_end - val_off .. val_area_end - val_off + len].
///
/// For internal (non-leaf) nodes, the value is always an oid_t (u64, 8 bytes).
pub fn value(&self, index: usize, fixed_val_size: u32) -> Result<&[u8]> {
let entry = &self.toc[index];
let len = if !self.node_header.is_leaf() {
// Internal node values are always an oid_t (8 bytes)
8
} else if self.node_header.is_fixed_kv() {
fixed_val_size as usize
} else {
entry.val_len as usize
};
let val_off = entry.val_off as usize;
let start = self.val_area_end - val_off;
let end = start + len;
if end > self.block_data.len() || start < self.key_area_off {
return Err(ApfsError::InvalidBTree(format!(
"value out of bounds: start={}, len={}, val_area_end={}, block_size={}",
start,
len,
self.val_area_end,
self.block_data.len()
)));
}
Ok(&self.block_data[start..end])
}
/// For index nodes, get the child OID at a given index.
/// The value for index nodes is always an oid_t (u64, 8 bytes).
pub fn child_oid(&self, index: usize) -> Result {
let val = self.value(index, 8)?;
if val.len() < 8 {
return Err(ApfsError::InvalidBTree("child oid too short".into()));
}
Ok(u64::from_le_bytes([
val[0], val[1], val[2], val[3], val[4], val[5], val[6], val[7],
]))
}
}
/// Tree-shape constants passed through recursive B-tree traversal.
struct BTreeParams {
block_size: u32,
fixed_key_size: u32,
fixed_val_size: u32,
omap_root: Option,
}
/// Resolve a child OID to a physical block number.
/// If `omap_root` is Some, the OID is virtual and needs OMAP resolution.
/// If `omap_root` is None, the OID is already a physical block number.
fn resolve_child_oid(
reader: &mut R,
child_oid: u64,
block_size: u32,
omap_root: Option,
) -> Result {
match omap_root {
Some(omap) => omap::omap_lookup(reader, omap, block_size, child_oid),
None => Ok(child_oid),
}
}
/// Look up a key in a B-tree.
///
/// `compare_fn` takes key bytes and returns Ordering of the node key relative to the search key:
/// - Less: node key < search key
/// - Equal: match
/// - Greater: node key > search key
///
/// `omap_root`: Some(block) for virtual B-trees (catalog), None for physical (OMAP).
///
/// Returns the raw value bytes if found.
pub fn btree_lookup(
reader: &mut R,
root_block: u64,
block_size: u32,
fixed_key_size: u32,
fixed_val_size: u32,
compare_fn: &F,
omap_root: Option,
) -> Result