├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── docs └── figure-1.png ├── rust-toolchain.toml └── src ├── app.rs ├── cli.rs ├── ctx.rs ├── db.rs ├── fs_index.rs ├── main.rs └── utils ├── mod.rs ├── perf_timer.rs └── string_pool.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Cargo 2 | /target 3 | 4 | # Visual Studio Code 5 | /.vscode/launch.json 6 | 7 | # macOS files 8 | .DS_Store -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "allocator-api2" 18 | version = "0.2.16" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 21 | 22 | [[package]] 23 | name = "anstream" 24 | version = "0.5.0" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" 27 | dependencies = [ 28 | "anstyle", 29 | "anstyle-parse", 30 | "anstyle-query", 31 | "anstyle-wincon", 32 | "colorchoice", 33 | "utf8parse", 34 | ] 35 | 36 | [[package]] 37 | name = "anstyle" 38 | version = "1.0.3" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" 41 | 42 | [[package]] 43 | name = "anstyle-parse" 44 | version = "0.2.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 47 | dependencies = [ 48 | "utf8parse", 49 | ] 50 | 51 | [[package]] 52 | name = "anstyle-query" 53 | version = "1.0.0" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 56 | dependencies = [ 57 | "windows-sys 0.48.0", 58 | ] 59 | 60 | [[package]] 61 | name = "anstyle-wincon" 62 | version = "2.1.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" 65 | dependencies = [ 66 | "anstyle", 67 | "windows-sys 0.48.0", 68 | ] 69 | 70 | [[package]] 71 | name = "anyhow" 72 | version = "1.0.75" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 75 | 76 | [[package]] 77 | name = "autocfg" 78 | version = "1.1.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 81 | 82 | [[package]] 83 | name = "base64" 84 | version = "0.21.4" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" 87 | 88 | [[package]] 89 | name = "bitflags" 90 | version = "2.4.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 93 | 94 | [[package]] 95 | name = "cfg-if" 96 | version = "1.0.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 99 | 100 | [[package]] 101 | name = "clap" 102 | version = "4.4.4" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" 105 | dependencies = [ 106 | "clap_builder", 107 | "clap_derive", 108 | ] 109 | 110 | [[package]] 111 | name = "clap_builder" 112 | version = "4.4.4" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" 115 | dependencies = [ 116 | "anstream", 117 | "anstyle", 118 | "clap_lex", 119 | "strsim", 120 | ] 121 | 122 | [[package]] 123 | name = "clap_derive" 124 | version = "4.4.2" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" 127 | dependencies = [ 128 | "heck", 129 | "proc-macro2", 130 | "quote", 131 | "syn", 132 | ] 133 | 134 | [[package]] 135 | name = "clap_lex" 136 | version = "0.5.1" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" 139 | 140 | [[package]] 141 | name = "colorchoice" 142 | version = "1.0.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 145 | 146 | [[package]] 147 | name = "console" 148 | version = "0.15.7" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" 151 | dependencies = [ 152 | "encode_unicode", 153 | "lazy_static", 154 | "libc", 155 | "unicode-width", 156 | "windows-sys 0.45.0", 157 | ] 158 | 159 | [[package]] 160 | name = "deranged" 161 | version = "0.3.8" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" 164 | 165 | [[package]] 166 | name = "encode_unicode" 167 | version = "0.3.6" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 170 | 171 | [[package]] 172 | name = "fallible-iterator" 173 | version = "0.2.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 176 | 177 | [[package]] 178 | name = "fallible-streaming-iterator" 179 | version = "0.1.9" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 182 | 183 | [[package]] 184 | name = "hashbrown" 185 | version = "0.12.3" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 188 | 189 | [[package]] 190 | name = "hashbrown" 191 | version = "0.14.0" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 194 | dependencies = [ 195 | "ahash", 196 | "allocator-api2", 197 | ] 198 | 199 | [[package]] 200 | name = "hashlink" 201 | version = "0.8.4" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" 204 | dependencies = [ 205 | "hashbrown 0.14.0", 206 | ] 207 | 208 | [[package]] 209 | name = "heck" 210 | version = "0.4.1" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 213 | 214 | [[package]] 215 | name = "ibackupextractor" 216 | version = "0.1.0" 217 | dependencies = [ 218 | "anyhow", 219 | "clap", 220 | "console", 221 | "fallible-iterator", 222 | "indicatif", 223 | "plist", 224 | "readonly", 225 | "rusqlite", 226 | ] 227 | 228 | [[package]] 229 | name = "indexmap" 230 | version = "1.9.3" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 233 | dependencies = [ 234 | "autocfg", 235 | "hashbrown 0.12.3", 236 | ] 237 | 238 | [[package]] 239 | name = "indicatif" 240 | version = "0.17.6" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "0b297dc40733f23a0e52728a58fa9489a5b7638a324932de16b41adc3ef80730" 243 | dependencies = [ 244 | "console", 245 | "instant", 246 | "number_prefix", 247 | "portable-atomic", 248 | "unicode-width", 249 | ] 250 | 251 | [[package]] 252 | name = "instant" 253 | version = "0.1.12" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 256 | dependencies = [ 257 | "cfg-if", 258 | ] 259 | 260 | [[package]] 261 | name = "itoa" 262 | version = "1.0.9" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 265 | 266 | [[package]] 267 | name = "lazy_static" 268 | version = "1.4.0" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 271 | 272 | [[package]] 273 | name = "libc" 274 | version = "0.2.148" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" 277 | 278 | [[package]] 279 | name = "libsqlite3-sys" 280 | version = "0.26.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" 283 | dependencies = [ 284 | "pkg-config", 285 | "vcpkg", 286 | ] 287 | 288 | [[package]] 289 | name = "line-wrap" 290 | version = "0.1.1" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" 293 | dependencies = [ 294 | "safemem", 295 | ] 296 | 297 | [[package]] 298 | name = "memchr" 299 | version = "2.6.3" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 302 | 303 | [[package]] 304 | name = "number_prefix" 305 | version = "0.4.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 308 | 309 | [[package]] 310 | name = "once_cell" 311 | version = "1.18.0" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 314 | 315 | [[package]] 316 | name = "pkg-config" 317 | version = "0.3.27" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 320 | 321 | [[package]] 322 | name = "plist" 323 | version = "1.5.0" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" 326 | dependencies = [ 327 | "base64", 328 | "indexmap", 329 | "line-wrap", 330 | "quick-xml", 331 | "serde", 332 | "time", 333 | ] 334 | 335 | [[package]] 336 | name = "portable-atomic" 337 | version = "1.4.3" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "31114a898e107c51bb1609ffaf55a0e011cf6a4d7f1170d0015a165082c0338b" 340 | 341 | [[package]] 342 | name = "proc-macro2" 343 | version = "1.0.67" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 346 | dependencies = [ 347 | "unicode-ident", 348 | ] 349 | 350 | [[package]] 351 | name = "quick-xml" 352 | version = "0.29.0" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" 355 | dependencies = [ 356 | "memchr", 357 | ] 358 | 359 | [[package]] 360 | name = "quote" 361 | version = "1.0.33" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 364 | dependencies = [ 365 | "proc-macro2", 366 | ] 367 | 368 | [[package]] 369 | name = "readonly" 370 | version = "0.2.11" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "b8f439da1766942fe069954da6058b2e6c1760eb878bae76f5be9fc29f56f574" 373 | dependencies = [ 374 | "proc-macro2", 375 | "quote", 376 | "syn", 377 | ] 378 | 379 | [[package]] 380 | name = "rusqlite" 381 | version = "0.29.0" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" 384 | dependencies = [ 385 | "bitflags", 386 | "fallible-iterator", 387 | "fallible-streaming-iterator", 388 | "hashlink", 389 | "libsqlite3-sys", 390 | "smallvec", 391 | ] 392 | 393 | [[package]] 394 | name = "safemem" 395 | version = "0.3.3" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 398 | 399 | [[package]] 400 | name = "serde" 401 | version = "1.0.188" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 404 | dependencies = [ 405 | "serde_derive", 406 | ] 407 | 408 | [[package]] 409 | name = "serde_derive" 410 | version = "1.0.188" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 413 | dependencies = [ 414 | "proc-macro2", 415 | "quote", 416 | "syn", 417 | ] 418 | 419 | [[package]] 420 | name = "smallvec" 421 | version = "1.11.1" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 424 | 425 | [[package]] 426 | name = "strsim" 427 | version = "0.10.0" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 430 | 431 | [[package]] 432 | name = "syn" 433 | version = "2.0.37" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" 436 | dependencies = [ 437 | "proc-macro2", 438 | "quote", 439 | "unicode-ident", 440 | ] 441 | 442 | [[package]] 443 | name = "time" 444 | version = "0.3.28" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" 447 | dependencies = [ 448 | "deranged", 449 | "itoa", 450 | "serde", 451 | "time-core", 452 | "time-macros", 453 | ] 454 | 455 | [[package]] 456 | name = "time-core" 457 | version = "0.1.1" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" 460 | 461 | [[package]] 462 | name = "time-macros" 463 | version = "0.2.14" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" 466 | dependencies = [ 467 | "time-core", 468 | ] 469 | 470 | [[package]] 471 | name = "unicode-ident" 472 | version = "1.0.12" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 475 | 476 | [[package]] 477 | name = "unicode-width" 478 | version = "0.1.11" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 481 | 482 | [[package]] 483 | name = "utf8parse" 484 | version = "0.2.1" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 487 | 488 | [[package]] 489 | name = "vcpkg" 490 | version = "0.2.15" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 493 | 494 | [[package]] 495 | name = "version_check" 496 | version = "0.9.4" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 499 | 500 | [[package]] 501 | name = "windows-sys" 502 | version = "0.45.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 505 | dependencies = [ 506 | "windows-targets 0.42.2", 507 | ] 508 | 509 | [[package]] 510 | name = "windows-sys" 511 | version = "0.48.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 514 | dependencies = [ 515 | "windows-targets 0.48.5", 516 | ] 517 | 518 | [[package]] 519 | name = "windows-targets" 520 | version = "0.42.2" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 523 | dependencies = [ 524 | "windows_aarch64_gnullvm 0.42.2", 525 | "windows_aarch64_msvc 0.42.2", 526 | "windows_i686_gnu 0.42.2", 527 | "windows_i686_msvc 0.42.2", 528 | "windows_x86_64_gnu 0.42.2", 529 | "windows_x86_64_gnullvm 0.42.2", 530 | "windows_x86_64_msvc 0.42.2", 531 | ] 532 | 533 | [[package]] 534 | name = "windows-targets" 535 | version = "0.48.5" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 538 | dependencies = [ 539 | "windows_aarch64_gnullvm 0.48.5", 540 | "windows_aarch64_msvc 0.48.5", 541 | "windows_i686_gnu 0.48.5", 542 | "windows_i686_msvc 0.48.5", 543 | "windows_x86_64_gnu 0.48.5", 544 | "windows_x86_64_gnullvm 0.48.5", 545 | "windows_x86_64_msvc 0.48.5", 546 | ] 547 | 548 | [[package]] 549 | name = "windows_aarch64_gnullvm" 550 | version = "0.42.2" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 553 | 554 | [[package]] 555 | name = "windows_aarch64_gnullvm" 556 | version = "0.48.5" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 559 | 560 | [[package]] 561 | name = "windows_aarch64_msvc" 562 | version = "0.42.2" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 565 | 566 | [[package]] 567 | name = "windows_aarch64_msvc" 568 | version = "0.48.5" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 571 | 572 | [[package]] 573 | name = "windows_i686_gnu" 574 | version = "0.42.2" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 577 | 578 | [[package]] 579 | name = "windows_i686_gnu" 580 | version = "0.48.5" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 583 | 584 | [[package]] 585 | name = "windows_i686_msvc" 586 | version = "0.42.2" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 589 | 590 | [[package]] 591 | name = "windows_i686_msvc" 592 | version = "0.48.5" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 595 | 596 | [[package]] 597 | name = "windows_x86_64_gnu" 598 | version = "0.42.2" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 601 | 602 | [[package]] 603 | name = "windows_x86_64_gnu" 604 | version = "0.48.5" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 607 | 608 | [[package]] 609 | name = "windows_x86_64_gnullvm" 610 | version = "0.42.2" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 613 | 614 | [[package]] 615 | name = "windows_x86_64_gnullvm" 616 | version = "0.48.5" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 619 | 620 | [[package]] 621 | name = "windows_x86_64_msvc" 622 | version = "0.42.2" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 625 | 626 | [[package]] 627 | name = "windows_x86_64_msvc" 628 | version = "0.48.5" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 631 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ibackupextractor" 3 | description = "A simple tool for extracting files from iOS backup archive." 4 | version = "0.1.0" 5 | edition = "2021" 6 | 7 | [profile.release] 8 | strip = true 9 | opt-level = "z" 10 | lto = true 11 | codegen-units = 1 12 | panic = "abort" 13 | 14 | [dependencies] 15 | anyhow = "1" 16 | readonly = "0.2" 17 | fallible-iterator = "0.2" 18 | rusqlite = "0.29" 19 | plist = "1" 20 | console = "0.15" 21 | indicatif = "0.17" 22 | clap = { version = "4", features = ["derive"] } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Cyandev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iBackupExtractor 2 | 3 | A simple tool for extracting files from iOS backup archive. 4 | 5 | iOS backup files are not stored with their original directory layouts. Retrieving a particular file from the app sandbox can be difficult. This tool can extract all the files from a backup archive, and then you can view the sandbox filesystem as it was originally stored in your iPhone or iPad. 6 | 7 | To save disk usage and speed up the extraction process, the extracted files are symbolic links to the original files in the backup archive. 8 | 9 | ## Install 10 | 11 | ### Download From GitHub Releases 12 | 13 | For Mac users, you can download the pre-built binaries directly from [releases](https://github.com/unixzii/ibackupextractor/releases) page. 14 | 15 | ### Build Locally 16 | 17 | To build this locally, a nightly toolchain is required. You can install it using `rustup`: 18 | 19 | ``` 20 | rustup toolchain install nightly 21 | ``` 22 | 23 | Then, you can build the executable: 24 | 25 | ``` 26 | cargo +nightly build --release 27 | ``` 28 | 29 | ## Usage 30 | 31 | Locate the backup archive you want to extract. Generally, you can find it under `/Users/cyandev/Library/Application Support/MobileSync/Backup`. **The archive is a directory that contains `Manifest.db` file.** 32 | 33 | ### List Domains 34 | 35 | Backup files are grouped by domains, and you need to specify a domain name when extracting. To list all the domains available, run the command below: 36 | 37 | ``` 38 | ibackupextractor -l /path/to/your_backup_archive 39 | ``` 40 | 41 | ### Extract a Specified Domain 42 | 43 | To extract files, you need to specify a domain name and a destination path (an empty directory is recommended): 44 | 45 | ``` 46 | ibackupextractor -o /path/to/dest_dir /path/to/your_backup_archive SomeDomain 47 | ``` 48 | 49 | The extraction process can take minutes to finish, depends on the number of files. 50 | 51 | In addition to the default symbolic-link mode, you can also change to copy mode by specifying `-c` flag. In copy mode, all files are copied to the destination path, and then you can delete the original backup archive freely if you want. 52 | 53 | ## FAQ 54 | 55 | ### How to create a proper backup archive? 56 | 57 | This tool can only handle the backup archives that are unencrypted. To backup without encryption, uncheck the following option before starting: 58 | 59 | ![Disable Encryption](./docs/figure-1.png) 60 | 61 | ### Will this tool modify the original backup archive? 62 | 63 | No, the tool will not write to any file in the backup archive. 64 | 65 | ## License 66 | 67 | MIT 68 | -------------------------------------------------------------------------------- /docs/figure-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unixzii/ibackupextractor/7d487b7f51718cd7ad01bfb4db12c1cd1997ecdc/docs/figure-1.png -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::{Context, Result}; 4 | 5 | use crate::cli::Args; 6 | use crate::ctx::Context as AppContext; 7 | use crate::db::BackupManifest; 8 | use crate::utils; 9 | 10 | mod progress_bar { 11 | use std::sync::mpsc::{channel, Receiver, Sender}; 12 | use std::thread::{Builder as ThreadBuilder, JoinHandle}; 13 | use std::time::Duration; 14 | 15 | use indicatif::{ProgressBar, ProgressStyle}; 16 | 17 | use crate::ctx::ProgressEvent; 18 | 19 | pub struct ControlPort { 20 | tx: Sender>, 21 | join_handle: Option>, 22 | } 23 | 24 | impl ControlPort { 25 | pub fn send(&self, event: ProgressEvent) { 26 | self.tx.send(Some(event)).unwrap(); 27 | } 28 | } 29 | 30 | impl Drop for ControlPort { 31 | fn drop(&mut self) { 32 | self.tx.send(None).unwrap(); 33 | self.join_handle.take().unwrap().join().unwrap(); 34 | } 35 | } 36 | 37 | fn thread_main(rx: Receiver>) { 38 | let spinner_style = ProgressStyle::with_template("{spinner} [{bar:20.white}] {msg}") 39 | .unwrap() 40 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ") 41 | .progress_chars("=> "); 42 | 43 | let progress_bar = ProgressBar::new(100); 44 | progress_bar.set_style(spinner_style); 45 | 46 | loop { 47 | let Ok(event) = rx.recv_timeout(Duration::from_millis(200)) else { 48 | // No event at this time, tick the progress bar to keep 49 | // the animation running. 50 | progress_bar.tick(); 51 | continue; 52 | }; 53 | 54 | let Some(event) = event else { 55 | // No more event, exit. 56 | break; 57 | }; 58 | 59 | update_progress_bar(&progress_bar, event); 60 | } 61 | 62 | progress_bar.finish_and_clear(); 63 | } 64 | 65 | fn update_progress_bar(progress_bar: &ProgressBar, event: ProgressEvent) { 66 | match event { 67 | ProgressEvent::Querying => { 68 | progress_bar.set_message("Querying database..."); 69 | } 70 | ProgressEvent::Indexing { indexed, total } => { 71 | progress_bar 72 | .set_message(format!("Creating file system index... ({indexed}/{total})")); 73 | progress_bar.set_length(total as u64); 74 | progress_bar.set_position(indexed as u64); 75 | } 76 | ProgressEvent::Extracting { extracted, total } => { 77 | progress_bar.set_message(format!("Extracting files... ({extracted}/{total})")); 78 | progress_bar.set_length(total as u64); 79 | progress_bar.set_position(extracted as u64); 80 | } 81 | } 82 | } 83 | 84 | pub fn make() -> ControlPort { 85 | let (tx, rx) = channel(); 86 | 87 | let join_handle = ThreadBuilder::new() 88 | .name("ProgressUIThread".to_owned()) 89 | .spawn(move || thread_main(rx)) 90 | .unwrap(); 91 | 92 | ControlPort { 93 | tx, 94 | join_handle: Some(join_handle), 95 | } 96 | } 97 | } 98 | 99 | pub fn run(args: Args) -> Result<()> { 100 | let backup_dir = args.backup_dir; 101 | 102 | let manifest_path = backup_dir.join("Manifest.db"); 103 | let mut manifest = 104 | BackupManifest::open(manifest_path).context("failed to open the manifest database")?; 105 | 106 | let context = AppContext::new(&backup_dir, &mut manifest, args.copy); 107 | if args.list_domains { 108 | let timer = utils::PerfTimer::new(); 109 | let domains = context.list_domains().context("failed to list domains")?; 110 | timer.finish(); 111 | 112 | for domain in domains { 113 | println!("{domain}"); 114 | } 115 | } else { 116 | let timer = utils::PerfTimer::new(); 117 | let pb_port = progress_bar::make(); 118 | context 119 | .extract_file( 120 | args.domain.as_ref().expect("domain should not be empty"), 121 | args.out_dir 122 | .as_ref() 123 | .map(|p| p as &Path) 124 | .expect("out_dir should not be empty"), 125 | |event| { 126 | pb_port.send(event); 127 | }, 128 | ) 129 | .context("failed to extract files")?; 130 | 131 | // Dispose the progress bar first to prevent it from being 132 | // clobbered by the timer message. 133 | drop(pb_port); 134 | 135 | timer.finish(); 136 | } 137 | 138 | Ok(()) 139 | } 140 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | 5 | #[derive(Parser, Debug)] 6 | #[command(version, about)] 7 | pub struct Args { 8 | /// Path of the backup archive. 9 | pub backup_dir: PathBuf, 10 | 11 | /// Domain of the files to extract. 12 | #[arg(required = true, conflicts_with = "list_domains")] 13 | pub domain: Option, 14 | 15 | /// Path of the destination directory for extracted files. 16 | #[arg(short, required = true, conflicts_with = "list_domains")] 17 | pub out_dir: Option, 18 | 19 | /// List all the domains. 20 | #[arg(short)] 21 | pub list_domains: bool, 22 | 23 | /// Copy the files instead of creating symbolic links. 24 | #[arg(short, conflicts_with = "list_domains")] 25 | pub copy: bool, 26 | } 27 | 28 | pub fn parse_args() -> Args { 29 | Args::parse() 30 | } 31 | -------------------------------------------------------------------------------- /src/ctx.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use anyhow::{Context as AnyhowContext, Result}; 5 | 6 | use crate::db::{BackupManifest, ManifestFileType}; 7 | use crate::fs_index::FileSystemIndex; 8 | use crate::utils::string_pool::StringPool; 9 | 10 | pub struct Context<'p, 'd> { 11 | backup_dir: &'p Path, 12 | manifest: &'d mut BackupManifest, 13 | copy_mode: bool, 14 | } 15 | 16 | impl<'p, 'd> Context<'p, 'd> { 17 | pub fn new(backup_dir: &'p Path, manifest: &'d mut BackupManifest, copy_mode: bool) -> Self { 18 | Self { 19 | backup_dir, 20 | manifest, 21 | copy_mode, 22 | } 23 | } 24 | 25 | pub fn list_domains(&self) -> Result> { 26 | self.manifest.query_domains() 27 | } 28 | 29 | pub fn extract_file(&self, domain: &str, dest_dir: &Path, progress_cb: F) -> Result<()> 30 | where 31 | F: FnMut(ProgressEvent), 32 | { 33 | let mut progress_cb = progress_cb; 34 | 35 | let string_pool = StringPool::new(); 36 | let mut file_system_index = FileSystemIndex::new(&string_pool); 37 | 38 | progress_cb(ProgressEvent::Querying); 39 | let files = self 40 | .manifest 41 | .query_files(domain) 42 | .context("failed to query files from database")?; 43 | 44 | for (idx, file) in files.iter().enumerate() { 45 | if file.file_type != ManifestFileType::File { 46 | continue; 47 | } 48 | if file.file_id.len() != 40 { 49 | // TODO: handle this error, maybe the database is corrupted. 50 | continue; 51 | } 52 | 53 | file_system_index 54 | .add_file(&file.relative_path, file.file_id.clone()) 55 | .with_context(|| format!("failed to index file: {file:?}"))?; 56 | 57 | progress_cb(ProgressEvent::Indexing { 58 | indexed: idx + 1, 59 | total: files.len(), 60 | }); 61 | } 62 | 63 | let total_file_count = file_system_index.file_count(); 64 | let mut extracted_file_count = 0; 65 | file_system_index.walk_files(|path, file_id| -> Result<()> { 66 | let dest_file_path = dest_dir.join(path); 67 | let dir = dest_file_path.parent().expect("path should have a parent"); 68 | if !dir.exists() { 69 | fs::create_dir_all(dir).with_context(|| { 70 | format!("failed to create directory: {}", dir.to_string_lossy()) 71 | })?; 72 | } else if !dir.is_dir() { 73 | return Err(anyhow!( 74 | "file already exists but not a directory: {}", 75 | dir.to_string_lossy() 76 | )); 77 | } 78 | 79 | self.write_file(&dest_file_path, file_id).with_context(|| { 80 | format!( 81 | "failed to create file: {}", 82 | dest_file_path.to_string_lossy() 83 | ) 84 | })?; 85 | 86 | extracted_file_count += 1; 87 | progress_cb(ProgressEvent::Extracting { 88 | extracted: extracted_file_count, 89 | total: total_file_count, 90 | }); 91 | 92 | Ok(()) 93 | })?; 94 | 95 | Ok(()) 96 | } 97 | } 98 | 99 | impl<'p, 'd> Context<'p, 'd> { 100 | fn write_file(&self, file_path: &Path, file_id: &str) -> Result<()> { 101 | let original_file_path = self.original_file_path(file_id); 102 | 103 | if self.copy_mode { 104 | fs::copy(original_file_path, file_path)?; 105 | } else { 106 | #[cfg(unix)] 107 | std::os::unix::fs::symlink(original_file_path, file_path)?; 108 | #[cfg(windows)] 109 | panic!("symbolic link mode is not supported on Windows"); 110 | } 111 | Ok(()) 112 | } 113 | 114 | fn original_file_path(&self, file_id: &str) -> PathBuf { 115 | let bucket = &file_id[0..2]; 116 | self.backup_dir.join(bucket).join(file_id) 117 | } 118 | } 119 | 120 | #[derive(Debug)] 121 | pub enum ProgressEvent { 122 | Querying, 123 | Indexing { indexed: usize, total: usize }, 124 | Extracting { extracted: usize, total: usize }, 125 | } 126 | -------------------------------------------------------------------------------- /src/db.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::Path; 3 | 4 | use anyhow::{Error as AnyhowError, Result}; 5 | use fallible_iterator::FallibleIterator; 6 | use rusqlite::Connection as SqliteConnection; 7 | 8 | pub struct BackupManifest { 9 | db_conn: SqliteConnection, 10 | } 11 | 12 | impl BackupManifest { 13 | pub fn open

(path: P) -> Result 14 | where 15 | P: AsRef, 16 | { 17 | if !path.as_ref().exists() { 18 | return Err(anyhow!( 19 | "file not exists: {}", 20 | path.as_ref().to_string_lossy() 21 | )); 22 | } 23 | 24 | let db_conn = SqliteConnection::open(path)?; 25 | 26 | // Verify the table schema. 27 | let mut stmt = db_conn.prepare("PRAGMA table_info('files')")?; 28 | let rows = stmt.query([])?; 29 | let mut cols_to_check = HashMap::from([ 30 | ("fileID".to_owned(), "TEXT"), 31 | ("domain".to_owned(), "TEXT"), 32 | ("relativePath".to_owned(), "TEXT"), 33 | ("flags".to_owned(), "INTEGER"), 34 | ("file".to_owned(), "BLOB"), 35 | ]); 36 | rows.map(|r| { 37 | let name: String = r.get(1)?; 38 | let typ: String = r.get(2)?; 39 | Ok((name, typ)) 40 | }) 41 | .map_err(AnyhowError::from) 42 | .for_each(|r| { 43 | let Some(expected_type) = cols_to_check.get(&r.0) else { 44 | return Ok(()); 45 | }; 46 | if *expected_type != r.1 { 47 | return Err(anyhow!( 48 | "column type is not matched, expected `{}` but got `{}`", 49 | expected_type, 50 | r.1 51 | )); 52 | } 53 | cols_to_check.remove(&r.0); 54 | 55 | Ok(()) 56 | })?; 57 | drop(stmt); 58 | 59 | if !cols_to_check.is_empty() { 60 | return Err(anyhow!("table schema is not compatible")); 61 | } 62 | 63 | Ok(Self { db_conn }) 64 | } 65 | 66 | pub fn query_domains(&self) -> Result> { 67 | let mut stmt = self 68 | .db_conn 69 | .prepare("SELECT domain FROM files GROUP BY domain")?; 70 | let rows = stmt.query([])?; 71 | Ok(rows.map(|r| r.get(0)).collect()?) 72 | } 73 | 74 | pub fn query_files(&self, domain: &str) -> Result> { 75 | let mut stmt = self 76 | .db_conn 77 | .prepare("SELECT fileID, relativePath, flags, file FROM files WHERE domain = ?")?; 78 | let rows = stmt.query([domain])?; 79 | rows.map(|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?))) 80 | .map_err(AnyhowError::from) 81 | .map(|(file_id, relative_path, flags, file)| { 82 | let file_buf: Vec = file; 83 | // TODO: parse metadata from the plist. 84 | let _file_plist: plist::Value = plist::from_bytes(&file_buf)?; 85 | 86 | let flags: u64 = flags; 87 | Ok(ManifestFile { 88 | file_id, 89 | relative_path, 90 | file_type: TryFrom::try_from(flags) 91 | .map_err(|_| anyhow!("unknown file type: {flags}"))?, 92 | }) 93 | }) 94 | .collect() 95 | } 96 | } 97 | 98 | #[readonly::make] 99 | #[derive(Debug)] 100 | pub struct ManifestFile { 101 | pub file_id: String, 102 | pub relative_path: String, 103 | pub file_type: ManifestFileType, 104 | } 105 | 106 | #[derive(PartialEq, Eq, Clone, Copy, Debug)] 107 | pub enum ManifestFileType { 108 | File, 109 | Directory, 110 | SymbolicLink, 111 | } 112 | 113 | impl TryFrom for ManifestFileType { 114 | type Error = &'static str; 115 | 116 | fn try_from(value: u64) -> std::result::Result { 117 | Ok(match value { 118 | 1 => Self::File, 119 | 2 => Self::Directory, 120 | 4 => Self::SymbolicLink, 121 | _ => return Err("unknown type"), 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/fs_index.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{hash_map, HashMap}; 2 | use std::path::{Component as PathComponent, Path}; 3 | use std::result::Result as StdResult; 4 | 5 | use anyhow::Result; 6 | 7 | use crate::utils::string_pool::*; 8 | 9 | #[derive(Debug)] 10 | pub struct FileSystemIndex<'p> { 11 | entries: HashMap>, 12 | root_entry: Entry<'p>, 13 | next_id: u64, 14 | file_count: usize, 15 | 16 | string_pool: &'p StringPool, 17 | } 18 | 19 | impl<'p> FileSystemIndex<'p> { 20 | pub fn new(string_pool: &'p StringPool) -> Self { 21 | Self { 22 | entries: Default::default(), 23 | root_entry: Entry { 24 | name: string_pool.intern("/"), 25 | entry_type: EntryType::new_dir(), 26 | }, 27 | next_id: 1, 28 | file_count: 0, 29 | string_pool, 30 | } 31 | } 32 | 33 | pub fn file_count(&self) -> usize { 34 | self.file_count 35 | } 36 | 37 | pub fn walk_files(&self, f: F) -> StdResult<(), E> 38 | where 39 | F: FnMut(&str, &str) -> StdResult<(), E>, 40 | { 41 | fn recursively_walk<'p, F, E>( 42 | entries: &HashMap>, 43 | current_entry: &Entry<'p>, 44 | current_path: &str, 45 | f: &mut F, 46 | ) -> StdResult<(), E> 47 | where 48 | F: FnMut(&str, &str) -> StdResult<(), E>, 49 | { 50 | match ¤t_entry.entry_type { 51 | EntryType::File { file_id } => f(current_path, file_id), 52 | EntryType::Dir { children } => { 53 | for child_id in children.values() { 54 | let child_entry = entries 55 | .get(child_id) 56 | .expect("internal state is inconsistent"); 57 | let child_path = if current_path.is_empty() { 58 | child_entry.name.to_string() 59 | } else { 60 | format!("{current_path}/{}", child_entry.name) 61 | }; 62 | 63 | recursively_walk(entries, child_entry, &child_path, f)?; 64 | } 65 | 66 | Ok(()) 67 | } 68 | } 69 | } 70 | 71 | let mut f = f; 72 | recursively_walk(&self.entries, &self.root_entry, "", &mut f) 73 | } 74 | 75 | pub fn add_file

(&mut self, path: P, file_id: String) -> Result<()> 76 | where 77 | P: AsRef, 78 | { 79 | let mut current_entry = &mut self.root_entry; 80 | if let Some(parent) = path.as_ref().parent() { 81 | // Get the parent path and create all intermediate paths if needed. 82 | for component in parent.components() { 83 | let PathComponent::Normal(component) = component else { 84 | return Err(anyhow!( 85 | "invalid path, unexpected path component: `{component:?}`" 86 | )); 87 | }; 88 | let Some(component_str) = component.to_str().map(|s| self.string_pool.intern(s)) 89 | else { 90 | return Err(anyhow!("unsupported path component, not UTF-8 compatible")); 91 | }; 92 | 93 | let EntryType::Dir { children } = &mut current_entry.entry_type else { 94 | return Err(anyhow!( 95 | "intermediate parent path`{}` is not a directory", 96 | ¤t_entry.name 97 | )); 98 | }; 99 | let (entry_id, existed) = match children.entry(component_str.clone()) { 100 | hash_map::Entry::Occupied(entry_id) => (*entry_id.get(), true), 101 | hash_map::Entry::Vacant(vacant_entry_id) => { 102 | let entry_id = self.next_id; 103 | self.next_id += 1; 104 | vacant_entry_id.insert(entry_id); 105 | (entry_id, false) 106 | } 107 | }; 108 | 109 | if !existed { 110 | let entry = Entry { 111 | name: component_str, 112 | entry_type: EntryType::new_dir(), 113 | }; 114 | self.entries.insert(entry_id, entry); 115 | } 116 | current_entry = self 117 | .entries 118 | .get_mut(&entry_id) 119 | .expect("internal state is inconsistent") 120 | } 121 | } 122 | 123 | let Some(file_name_str) = path 124 | .as_ref() 125 | .file_name() 126 | .and_then(|p| p.to_str()) 127 | .map(|s| self.string_pool.intern(s)) 128 | else { 129 | return Err(anyhow!("unsupported file name, not UTF-8 compatible")); 130 | }; 131 | 132 | let EntryType::Dir { children } = &mut current_entry.entry_type else { 133 | return Err(anyhow!( 134 | "parent path `{}` is not a directory", 135 | ¤t_entry.name 136 | )); 137 | }; 138 | 139 | let entry_id = self.next_id; 140 | self.next_id += 1; 141 | 142 | children.insert(file_name_str.clone(), entry_id); 143 | 144 | let entry = Entry { 145 | name: file_name_str, 146 | entry_type: EntryType::new_file(file_id), 147 | }; 148 | self.entries.insert(entry_id, entry); 149 | 150 | self.file_count += 1; 151 | 152 | Ok(()) 153 | } 154 | } 155 | 156 | #[derive(Debug)] 157 | struct Entry<'p> { 158 | name: StringId<'p>, 159 | entry_type: EntryType<'p>, 160 | } 161 | 162 | #[derive(Debug)] 163 | enum EntryType<'p> { 164 | File { 165 | file_id: String, 166 | }, 167 | Dir { 168 | children: HashMap, u64>, 169 | }, 170 | } 171 | 172 | impl<'p> EntryType<'p> { 173 | fn new_file(file_id: String) -> Self { 174 | EntryType::File { file_id } 175 | } 176 | 177 | fn new_dir() -> Self { 178 | EntryType::Dir { 179 | children: Default::default(), 180 | } 181 | } 182 | } 183 | 184 | #[cfg(test)] 185 | mod tests { 186 | use std::assert_matches::assert_matches; 187 | use std::collections::HashMap; 188 | 189 | use super::FileSystemIndex; 190 | use crate::utils::string_pool::StringPool; 191 | 192 | #[test] 193 | fn it_works() { 194 | let string_pool = StringPool::new(); 195 | let mut index = FileSystemIndex::new(&string_pool); 196 | 197 | let mut added_files: HashMap = HashMap::new(); 198 | 199 | let mut assert_add_file = |path: &str, file_id: &str| { 200 | let res = index.add_file(path, file_id.to_owned()); 201 | added_files.insert(path.to_owned(), file_id.to_owned()); 202 | assert_matches!(res, Ok(())); 203 | }; 204 | 205 | assert_add_file("Library/Cookies/a", "a"); 206 | assert_add_file("Library/Cookies/b", "b"); 207 | assert_add_file("Library/Preferences/com.example.test.plist", "c"); 208 | 209 | let res = index.walk_files(|path, file_id| { 210 | if let Some(expected_file_id) = added_files.remove(path) { 211 | assert_eq!(file_id, expected_file_id); 212 | } else { 213 | return Err(format!("unexpected file: {path}")); 214 | } 215 | Ok(()) 216 | }); 217 | assert_matches!(res, Ok(())); 218 | assert_eq!(added_files.len(), 0); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(assert_matches)] 2 | 3 | #[macro_use] 4 | extern crate anyhow; 5 | 6 | mod app; 7 | mod cli; 8 | mod ctx; 9 | mod db; 10 | mod fs_index; 11 | mod utils; 12 | 13 | fn main() { 14 | let args = cli::parse_args(); 15 | if let Err(err) = app::run(args) { 16 | let prefix = console::style("error: ").red().bold().to_string(); 17 | println!("{prefix}{err:?}"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod perf_timer; 2 | pub mod string_pool; 3 | 4 | pub use perf_timer::PerfTimer; 5 | -------------------------------------------------------------------------------- /src/utils/perf_timer.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | pub struct PerfTimer(Instant); 4 | 5 | impl PerfTimer { 6 | pub fn new() -> Self { 7 | Self(Instant::now()) 8 | } 9 | 10 | pub fn finish(self) { 11 | let msg = format!("finished in {}ms", self.0.elapsed().as_millis()); 12 | println!("\n{}", console::style(msg).dim()); 13 | } 14 | } 15 | 16 | impl Default for PerfTimer { 17 | fn default() -> Self { 18 | Self::new() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/string_pool.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | use std::fmt::{Debug, Display}; 4 | use std::hash::{Hash, Hasher}; 5 | 6 | #[derive(Default, Debug)] 7 | pub struct StringPool { 8 | inner: RefCell, 9 | } 10 | 11 | #[derive(Default, Debug)] 12 | struct Inner { 13 | pool: Vec, 14 | idx_map: HashMap, 15 | } 16 | 17 | impl StringPool { 18 | pub fn new() -> Self { 19 | Self::default() 20 | } 21 | 22 | pub fn intern(&self, s: &str) -> StringId { 23 | let mut inner_mut = self.inner.borrow_mut(); 24 | if let Some(idx) = inner_mut.idx_map.get(s).cloned() { 25 | return StringId { pool: self, idx }; 26 | } 27 | 28 | inner_mut.pool.push(s.to_owned()); 29 | let idx = inner_mut.pool.len() - 1; 30 | 31 | inner_mut.idx_map.insert(s.to_owned(), idx); 32 | 33 | StringId { pool: self, idx } 34 | } 35 | 36 | fn unchecked_get(&self, idx: usize) -> String { 37 | self.inner.borrow().pool[idx].clone() 38 | } 39 | } 40 | 41 | pub struct StringId<'p> { 42 | pool: &'p StringPool, 43 | idx: usize, 44 | } 45 | 46 | impl<'p> Clone for StringId<'p> { 47 | fn clone(&self) -> Self { 48 | Self { 49 | pool: self.pool, 50 | idx: self.idx, 51 | } 52 | } 53 | } 54 | 55 | impl<'p> Hash for StringId<'p> { 56 | fn hash(&self, state: &mut H) { 57 | self.to_string().hash(state) 58 | } 59 | } 60 | 61 | impl<'p> PartialEq for StringId<'p> { 62 | fn eq(&self, other: &Self) -> bool { 63 | self.to_string() == other.to_string() 64 | } 65 | } 66 | 67 | impl<'p> Eq for StringId<'p> {} 68 | 69 | impl<'p> Debug for StringId<'p> { 70 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 71 | f.debug_struct("StringId") 72 | .field("value", &self.to_string()) 73 | .finish() 74 | } 75 | } 76 | 77 | impl<'p> Display for StringId<'p> { 78 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 79 | ::fmt(&self.pool.unchecked_get(self.idx), f) 80 | } 81 | } 82 | --------------------------------------------------------------------------------