├── .github ├── FUNDING.yml └── workflows │ └── cid.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── api.golden ├── command-reference-short.golden ├── command-reference.golden ├── rust-toolchain ├── src ├── lib.rs ├── main.rs ├── run.rs └── sessions.rs └── tests └── api.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: SUPERCILEX 2 | -------------------------------------------------------------------------------- /.github/workflows/cid.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | - name: Cargo Cache 12 | uses: actions/cache@v3 13 | with: 14 | path: | 15 | ~/.cargo/bin/ 16 | ~/.cargo/registry/index/ 17 | ~/.cargo/registry/cache/ 18 | ~/.cargo/git/db/ 19 | target/ 20 | key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} 21 | restore-keys: ${{ runner.os }}-cargo- 22 | - name: Build project 23 | run: cargo build --workspace --release 24 | 25 | test: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: Install Rust 31 | run: rustup component add rustfmt clippy 32 | - name: Cargo Cache 33 | uses: actions/cache@v3 34 | with: 35 | path: | 36 | ~/.cargo/bin/ 37 | ~/.cargo/registry/index/ 38 | ~/.cargo/registry/cache/ 39 | ~/.cargo/git/db/ 40 | target/ 41 | key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} 42 | restore-keys: ${{ runner.os }}-cargo- 43 | - name: Run tests 44 | run: cargo test --workspace 45 | 46 | deploy_release: 47 | needs: [build, test] 48 | runs-on: ubuntu-latest 49 | if: startsWith(github.ref, 'refs/tags/') 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v3 53 | - name: Publish release 54 | run: cargo publish 55 | env: 56 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 57 | 58 | attach_binaries: 59 | strategy: 60 | fail-fast: false 61 | matrix: 62 | include: 63 | - target: x86_64-unknown-linux-gnu 64 | os: ubuntu-latest 65 | tool: cargo 66 | - target: x86_64-unknown-linux-musl 67 | os: ubuntu-latest 68 | tool: cargo 69 | - target: aarch64-unknown-linux-gnu 70 | os: ubuntu-latest 71 | tool: RUSTFLAGS="-Ctarget-feature=-outline-atomics" cross 72 | - target: riscv64gc-unknown-linux-gnu 73 | os: ubuntu-latest 74 | tool: cross 75 | needs: [build, test] 76 | runs-on: ${{ matrix.os }} 77 | if: startsWith(github.ref, 'refs/tags/') 78 | steps: 79 | - name: Checkout 80 | uses: actions/checkout@v3 81 | - name: Install Rust 82 | run: | 83 | rustup target add ${{ matrix.target }} 84 | rustup component add rust-src 85 | - name: Install cross 86 | if: contains(matrix.tool, 'cross') 87 | run: cargo install cross 88 | - name: Build binary 89 | run: ${{ matrix.tool }} build --workspace --release --locked --target=${{ matrix.target }} -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort 90 | - name: Upload binary 91 | uses: svenstaro/upload-release-action@v2 92 | with: 93 | repo_token: ${{ secrets.GITHUB_TOKEN }} 94 | file: target/${{ matrix.target }}/release/forkfs 95 | asset_name: ${{ matrix.target }}-forkfs 96 | tag: ${{ github.ref }} 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aligned" 7 | version = "0.4.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923" 10 | dependencies = [ 11 | "as-slice", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys 0.59.0", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell", 61 | "windows-sys 0.59.0", 62 | ] 63 | 64 | [[package]] 65 | name = "anyhow" 66 | version = "1.0.97" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 69 | 70 | [[package]] 71 | name = "as-slice" 72 | version = "0.2.1" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" 75 | dependencies = [ 76 | "stable_deref_trait", 77 | ] 78 | 79 | [[package]] 80 | name = "automod" 81 | version = "1.0.15" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "ebb4bd301db2e2ca1f5be131c24eb8ebf2d9559bc3744419e93baf8ddea7e670" 84 | dependencies = [ 85 | "proc-macro2", 86 | "quote", 87 | "syn", 88 | ] 89 | 90 | [[package]] 91 | name = "bitflags" 92 | version = "2.9.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 95 | 96 | [[package]] 97 | name = "bon" 98 | version = "3.4.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "8a8a41e51fda5f7d87152d00f50d08ce24bf5cee8a962facf7f2526a66f8a5fa" 101 | dependencies = [ 102 | "bon-macros", 103 | "rustversion", 104 | ] 105 | 106 | [[package]] 107 | name = "bon-macros" 108 | version = "3.4.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "6b592add4016ac26ca340298fed5cc2524abe8bacae78ebca3780286da588304" 111 | dependencies = [ 112 | "darling", 113 | "ident_case", 114 | "prettyplease", 115 | "proc-macro2", 116 | "quote", 117 | "rustversion", 118 | "syn", 119 | ] 120 | 121 | [[package]] 122 | name = "camino" 123 | version = "1.1.9" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" 126 | dependencies = [ 127 | "serde", 128 | ] 129 | 130 | [[package]] 131 | name = "cargo-manifest" 132 | version = "0.17.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "8b2ce2075c35e4b492b93e3d5dd1dd3670de553f15045595daef8164ed9a3751" 135 | dependencies = [ 136 | "serde", 137 | "thiserror 1.0.69", 138 | "toml", 139 | ] 140 | 141 | [[package]] 142 | name = "cargo-platform" 143 | version = "0.1.9" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" 146 | dependencies = [ 147 | "serde", 148 | ] 149 | 150 | [[package]] 151 | name = "cargo_metadata" 152 | version = "0.18.1" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" 155 | dependencies = [ 156 | "camino", 157 | "cargo-platform", 158 | "semver", 159 | "serde", 160 | "serde_json", 161 | "thiserror 1.0.69", 162 | ] 163 | 164 | [[package]] 165 | name = "cfg-if" 166 | version = "1.0.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 169 | 170 | [[package]] 171 | name = "cfg_aliases" 172 | version = "0.2.1" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 175 | 176 | [[package]] 177 | name = "clap" 178 | version = "4.5.32" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" 181 | dependencies = [ 182 | "clap_builder", 183 | "clap_derive", 184 | ] 185 | 186 | [[package]] 187 | name = "clap_builder" 188 | version = "4.5.32" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" 191 | dependencies = [ 192 | "anstream", 193 | "anstyle", 194 | "clap_lex", 195 | "strsim", 196 | "terminal_size", 197 | ] 198 | 199 | [[package]] 200 | name = "clap_derive" 201 | version = "4.5.32" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 204 | dependencies = [ 205 | "heck", 206 | "proc-macro2", 207 | "quote", 208 | "syn", 209 | ] 210 | 211 | [[package]] 212 | name = "clap_lex" 213 | version = "0.7.4" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 216 | 217 | [[package]] 218 | name = "colorchoice" 219 | version = "1.0.3" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 222 | 223 | [[package]] 224 | name = "content_inspector" 225 | version = "0.2.4" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" 228 | dependencies = [ 229 | "memchr", 230 | ] 231 | 232 | [[package]] 233 | name = "crossbeam-channel" 234 | version = "0.5.14" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" 237 | dependencies = [ 238 | "crossbeam-utils", 239 | ] 240 | 241 | [[package]] 242 | name = "crossbeam-deque" 243 | version = "0.8.6" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 246 | dependencies = [ 247 | "crossbeam-epoch", 248 | "crossbeam-utils", 249 | ] 250 | 251 | [[package]] 252 | name = "crossbeam-epoch" 253 | version = "0.9.18" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 256 | dependencies = [ 257 | "crossbeam-utils", 258 | ] 259 | 260 | [[package]] 261 | name = "crossbeam-utils" 262 | version = "0.8.21" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 265 | 266 | [[package]] 267 | name = "cvt" 268 | version = "0.1.2" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "d2ae9bf77fbf2d39ef573205d554d87e86c12f1994e9ea335b0651b9b278bcf1" 271 | dependencies = [ 272 | "cfg-if", 273 | ] 274 | 275 | [[package]] 276 | name = "darling" 277 | version = "0.20.10" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 280 | dependencies = [ 281 | "darling_core", 282 | "darling_macro", 283 | ] 284 | 285 | [[package]] 286 | name = "darling_core" 287 | version = "0.20.10" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 290 | dependencies = [ 291 | "fnv", 292 | "ident_case", 293 | "proc-macro2", 294 | "quote", 295 | "strsim", 296 | "syn", 297 | ] 298 | 299 | [[package]] 300 | name = "darling_macro" 301 | version = "0.20.10" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 304 | dependencies = [ 305 | "darling_core", 306 | "quote", 307 | "syn", 308 | ] 309 | 310 | [[package]] 311 | name = "dirs" 312 | version = "6.0.0" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 315 | dependencies = [ 316 | "dirs-sys", 317 | ] 318 | 319 | [[package]] 320 | name = "dirs-sys" 321 | version = "0.5.0" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 324 | dependencies = [ 325 | "libc", 326 | "option-ext", 327 | "redox_users", 328 | "windows-sys 0.59.0", 329 | ] 330 | 331 | [[package]] 332 | name = "dissimilar" 333 | version = "1.0.10" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921" 336 | 337 | [[package]] 338 | name = "dunce" 339 | version = "1.0.5" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 342 | 343 | [[package]] 344 | name = "either" 345 | version = "1.15.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 348 | 349 | [[package]] 350 | name = "equivalent" 351 | version = "1.0.2" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 354 | 355 | [[package]] 356 | name = "errno" 357 | version = "0.3.10" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 360 | dependencies = [ 361 | "libc", 362 | "windows-sys 0.59.0", 363 | ] 364 | 365 | [[package]] 366 | name = "error-stack" 367 | version = "0.5.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "fe413319145d1063f080f27556fd30b1d70b01e2ba10c2a6e40d4be982ffc5d1" 370 | dependencies = [ 371 | "anyhow", 372 | "rustc_version", 373 | ] 374 | 375 | [[package]] 376 | name = "expect-test" 377 | version = "1.5.1" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "63af43ff4431e848fb47472a920f14fa71c24de13255a5692e93d4e90302acb0" 380 | dependencies = [ 381 | "dissimilar", 382 | "once_cell", 383 | ] 384 | 385 | [[package]] 386 | name = "fastrand" 387 | version = "2.3.0" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 390 | 391 | [[package]] 392 | name = "filetime" 393 | version = "0.2.25" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 396 | dependencies = [ 397 | "cfg-if", 398 | "libc", 399 | "libredox", 400 | "windows-sys 0.59.0", 401 | ] 402 | 403 | [[package]] 404 | name = "fnv" 405 | version = "1.0.7" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 408 | 409 | [[package]] 410 | name = "forkfs" 411 | version = "0.2.8" 412 | dependencies = [ 413 | "clap", 414 | "dirs", 415 | "error-stack", 416 | "fuc_engine", 417 | "rustix", 418 | "supercilex-tests", 419 | "thiserror 2.0.12", 420 | "trycmd", 421 | ] 422 | 423 | [[package]] 424 | name = "fs_at" 425 | version = "0.2.1" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "14af6c9694ea25db25baa2a1788703b9e7c6648dcaeeebeb98f7561b5384c036" 428 | dependencies = [ 429 | "aligned", 430 | "cfg-if", 431 | "cvt", 432 | "libc", 433 | "nix", 434 | "windows-sys 0.52.0", 435 | ] 436 | 437 | [[package]] 438 | name = "fuc_engine" 439 | version = "3.0.1" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "0be51ebe743a4d4337cc171cf933e15874c40c7a29d6f4d6e7b33ccae69e15fe" 442 | dependencies = [ 443 | "bon", 444 | "crossbeam-channel", 445 | "once_cell", 446 | "rayon", 447 | "remove_dir_all", 448 | "rustix", 449 | "thiserror 2.0.12", 450 | ] 451 | 452 | [[package]] 453 | name = "getrandom" 454 | version = "0.2.15" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 457 | dependencies = [ 458 | "cfg-if", 459 | "libc", 460 | "wasi 0.11.0+wasi-snapshot-preview1", 461 | ] 462 | 463 | [[package]] 464 | name = "getrandom" 465 | version = "0.3.1" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 468 | dependencies = [ 469 | "cfg-if", 470 | "libc", 471 | "wasi 0.13.3+wasi-0.2.2", 472 | "windows-targets", 473 | ] 474 | 475 | [[package]] 476 | name = "glob" 477 | version = "0.3.2" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 480 | 481 | [[package]] 482 | name = "hashbag" 483 | version = "0.1.12" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "98f494b2060b2a8f5e63379e1e487258e014cee1b1725a735816c0107a2e9d93" 486 | 487 | [[package]] 488 | name = "hashbrown" 489 | version = "0.15.2" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 492 | 493 | [[package]] 494 | name = "heck" 495 | version = "0.5.0" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 498 | 499 | [[package]] 500 | name = "humantime" 501 | version = "2.1.0" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 504 | 505 | [[package]] 506 | name = "humantime-serde" 507 | version = "1.1.1" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" 510 | dependencies = [ 511 | "humantime", 512 | "serde", 513 | ] 514 | 515 | [[package]] 516 | name = "ident_case" 517 | version = "1.0.1" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 520 | 521 | [[package]] 522 | name = "indexmap" 523 | version = "2.8.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" 526 | dependencies = [ 527 | "equivalent", 528 | "hashbrown", 529 | ] 530 | 531 | [[package]] 532 | name = "is_terminal_polyfill" 533 | version = "1.70.1" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 536 | 537 | [[package]] 538 | name = "itoa" 539 | version = "1.0.15" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 542 | 543 | [[package]] 544 | name = "libc" 545 | version = "0.2.171" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 548 | 549 | [[package]] 550 | name = "libredox" 551 | version = "0.1.3" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 554 | dependencies = [ 555 | "bitflags", 556 | "libc", 557 | "redox_syscall", 558 | ] 559 | 560 | [[package]] 561 | name = "linux-raw-sys" 562 | version = "0.9.2" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" 565 | 566 | [[package]] 567 | name = "memchr" 568 | version = "2.7.4" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 571 | 572 | [[package]] 573 | name = "nix" 574 | version = "0.29.0" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 577 | dependencies = [ 578 | "bitflags", 579 | "cfg-if", 580 | "cfg_aliases", 581 | "libc", 582 | ] 583 | 584 | [[package]] 585 | name = "normalize-line-endings" 586 | version = "0.3.0" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 589 | 590 | [[package]] 591 | name = "normpath" 592 | version = "1.3.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" 595 | dependencies = [ 596 | "windows-sys 0.59.0", 597 | ] 598 | 599 | [[package]] 600 | name = "once_cell" 601 | version = "1.21.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" 604 | 605 | [[package]] 606 | name = "option-ext" 607 | version = "0.2.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 610 | 611 | [[package]] 612 | name = "os_pipe" 613 | version = "1.2.1" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" 616 | dependencies = [ 617 | "libc", 618 | "windows-sys 0.59.0", 619 | ] 620 | 621 | [[package]] 622 | name = "pin-project-lite" 623 | version = "0.2.16" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 626 | 627 | [[package]] 628 | name = "prettyplease" 629 | version = "0.2.30" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a" 632 | dependencies = [ 633 | "proc-macro2", 634 | "syn", 635 | ] 636 | 637 | [[package]] 638 | name = "proc-macro2" 639 | version = "1.0.94" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 642 | dependencies = [ 643 | "unicode-ident", 644 | ] 645 | 646 | [[package]] 647 | name = "public-api" 648 | version = "0.44.2" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "89b782050f709c24580467137c5908b1474756b8543c45e464332dc561ffbe65" 651 | dependencies = [ 652 | "hashbag", 653 | "rustdoc-types", 654 | "serde", 655 | "serde_json", 656 | "thiserror 2.0.12", 657 | ] 658 | 659 | [[package]] 660 | name = "quote" 661 | version = "1.0.40" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 664 | dependencies = [ 665 | "proc-macro2", 666 | ] 667 | 668 | [[package]] 669 | name = "rayon" 670 | version = "1.10.0" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 673 | dependencies = [ 674 | "either", 675 | "rayon-core", 676 | ] 677 | 678 | [[package]] 679 | name = "rayon-core" 680 | version = "1.12.1" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 683 | dependencies = [ 684 | "crossbeam-deque", 685 | "crossbeam-utils", 686 | ] 687 | 688 | [[package]] 689 | name = "redox_syscall" 690 | version = "0.5.10" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" 693 | dependencies = [ 694 | "bitflags", 695 | ] 696 | 697 | [[package]] 698 | name = "redox_users" 699 | version = "0.5.0" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 702 | dependencies = [ 703 | "getrandom 0.2.15", 704 | "libredox", 705 | "thiserror 2.0.12", 706 | ] 707 | 708 | [[package]] 709 | name = "remove_dir_all" 710 | version = "1.0.0" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "808cc0b475acf76adf36f08ca49429b12aad9f678cb56143d5b3cb49b9a1dd08" 713 | dependencies = [ 714 | "cfg-if", 715 | "cvt", 716 | "fs_at", 717 | "libc", 718 | "normpath", 719 | "rayon", 720 | "windows-sys 0.59.0", 721 | ] 722 | 723 | [[package]] 724 | name = "rustc_version" 725 | version = "0.4.1" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 728 | dependencies = [ 729 | "semver", 730 | ] 731 | 732 | [[package]] 733 | name = "rustdoc-json" 734 | version = "0.9.5" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "9e27b4d503f72ab0feb20d2b4968930f21ebf1fc904f8792329f4176f02a65d6" 737 | dependencies = [ 738 | "cargo-manifest", 739 | "cargo_metadata", 740 | "serde", 741 | "thiserror 2.0.12", 742 | "toml", 743 | "tracing", 744 | ] 745 | 746 | [[package]] 747 | name = "rustdoc-types" 748 | version = "0.35.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "bf583db9958b3161d7980a56a8ee3c25e1a40708b81259be72584b7e0ea07c95" 751 | dependencies = [ 752 | "serde", 753 | ] 754 | 755 | [[package]] 756 | name = "rustix" 757 | version = "1.0.2" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" 760 | dependencies = [ 761 | "bitflags", 762 | "errno", 763 | "libc", 764 | "linux-raw-sys", 765 | "windows-sys 0.59.0", 766 | ] 767 | 768 | [[package]] 769 | name = "rustversion" 770 | version = "1.0.20" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 773 | 774 | [[package]] 775 | name = "ryu" 776 | version = "1.0.20" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 779 | 780 | [[package]] 781 | name = "same-file" 782 | version = "1.0.6" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 785 | dependencies = [ 786 | "winapi-util", 787 | ] 788 | 789 | [[package]] 790 | name = "semver" 791 | version = "1.0.26" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 794 | dependencies = [ 795 | "serde", 796 | ] 797 | 798 | [[package]] 799 | name = "serde" 800 | version = "1.0.219" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 803 | dependencies = [ 804 | "serde_derive", 805 | ] 806 | 807 | [[package]] 808 | name = "serde_derive" 809 | version = "1.0.219" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 812 | dependencies = [ 813 | "proc-macro2", 814 | "quote", 815 | "syn", 816 | ] 817 | 818 | [[package]] 819 | name = "serde_json" 820 | version = "1.0.140" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 823 | dependencies = [ 824 | "itoa", 825 | "memchr", 826 | "ryu", 827 | "serde", 828 | ] 829 | 830 | [[package]] 831 | name = "serde_spanned" 832 | version = "0.6.8" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 835 | dependencies = [ 836 | "serde", 837 | ] 838 | 839 | [[package]] 840 | name = "shlex" 841 | version = "1.3.0" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 844 | 845 | [[package]] 846 | name = "similar" 847 | version = "2.7.0" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" 850 | 851 | [[package]] 852 | name = "snapbox" 853 | version = "0.6.21" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "96dcfc4581e3355d70ac2ee14cfdf81dce3d85c85f1ed9e2c1d3013f53b3436b" 856 | dependencies = [ 857 | "anstream", 858 | "anstyle", 859 | "content_inspector", 860 | "dunce", 861 | "filetime", 862 | "libc", 863 | "normalize-line-endings", 864 | "os_pipe", 865 | "similar", 866 | "snapbox-macros", 867 | "tempfile", 868 | "wait-timeout", 869 | "walkdir", 870 | "windows-sys 0.59.0", 871 | ] 872 | 873 | [[package]] 874 | name = "snapbox-macros" 875 | version = "0.3.10" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af" 878 | dependencies = [ 879 | "anstream", 880 | ] 881 | 882 | [[package]] 883 | name = "stable_deref_trait" 884 | version = "1.2.0" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 887 | 888 | [[package]] 889 | name = "strsim" 890 | version = "0.11.1" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 893 | 894 | [[package]] 895 | name = "supercilex-tests" 896 | version = "0.4.16" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "74c9de70c7caa8bcdf8007c90e764c3bc7269da5b7598c0037084f4339eccf1c" 899 | dependencies = [ 900 | "clap_builder", 901 | "expect-test", 902 | "public-api", 903 | "rustdoc-json", 904 | ] 905 | 906 | [[package]] 907 | name = "syn" 908 | version = "2.0.100" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 911 | dependencies = [ 912 | "proc-macro2", 913 | "quote", 914 | "unicode-ident", 915 | ] 916 | 917 | [[package]] 918 | name = "tempfile" 919 | version = "3.18.0" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" 922 | dependencies = [ 923 | "cfg-if", 924 | "fastrand", 925 | "getrandom 0.3.1", 926 | "once_cell", 927 | "rustix", 928 | "windows-sys 0.59.0", 929 | ] 930 | 931 | [[package]] 932 | name = "terminal_size" 933 | version = "0.4.2" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" 936 | dependencies = [ 937 | "rustix", 938 | "windows-sys 0.59.0", 939 | ] 940 | 941 | [[package]] 942 | name = "thiserror" 943 | version = "1.0.69" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 946 | dependencies = [ 947 | "thiserror-impl 1.0.69", 948 | ] 949 | 950 | [[package]] 951 | name = "thiserror" 952 | version = "2.0.12" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 955 | dependencies = [ 956 | "thiserror-impl 2.0.12", 957 | ] 958 | 959 | [[package]] 960 | name = "thiserror-impl" 961 | version = "1.0.69" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 964 | dependencies = [ 965 | "proc-macro2", 966 | "quote", 967 | "syn", 968 | ] 969 | 970 | [[package]] 971 | name = "thiserror-impl" 972 | version = "2.0.12" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 975 | dependencies = [ 976 | "proc-macro2", 977 | "quote", 978 | "syn", 979 | ] 980 | 981 | [[package]] 982 | name = "toml" 983 | version = "0.8.20" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 986 | dependencies = [ 987 | "indexmap", 988 | "serde", 989 | "serde_spanned", 990 | "toml_datetime", 991 | "toml_edit", 992 | ] 993 | 994 | [[package]] 995 | name = "toml_datetime" 996 | version = "0.6.8" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 999 | dependencies = [ 1000 | "serde", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "toml_edit" 1005 | version = "0.22.24" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 1008 | dependencies = [ 1009 | "indexmap", 1010 | "serde", 1011 | "serde_spanned", 1012 | "toml_datetime", 1013 | "winnow", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "tracing" 1018 | version = "0.1.41" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1021 | dependencies = [ 1022 | "pin-project-lite", 1023 | "tracing-attributes", 1024 | "tracing-core", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "tracing-attributes" 1029 | version = "0.1.28" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 1032 | dependencies = [ 1033 | "proc-macro2", 1034 | "quote", 1035 | "syn", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "tracing-core" 1040 | version = "0.1.33" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1043 | dependencies = [ 1044 | "once_cell", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "trycmd" 1049 | version = "0.15.9" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "a8b5cf29388862aac065d6597ac9c8e842d1cc827cb50f7c32f11d29442eaae4" 1052 | dependencies = [ 1053 | "anstream", 1054 | "automod", 1055 | "glob", 1056 | "humantime", 1057 | "humantime-serde", 1058 | "rayon", 1059 | "serde", 1060 | "shlex", 1061 | "snapbox", 1062 | "toml_edit", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "unicode-ident" 1067 | version = "1.0.18" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1070 | 1071 | [[package]] 1072 | name = "utf8parse" 1073 | version = "0.2.2" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1076 | 1077 | [[package]] 1078 | name = "wait-timeout" 1079 | version = "0.2.1" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 1082 | dependencies = [ 1083 | "libc", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "walkdir" 1088 | version = "2.5.0" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1091 | dependencies = [ 1092 | "same-file", 1093 | "winapi-util", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "wasi" 1098 | version = "0.11.0+wasi-snapshot-preview1" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1101 | 1102 | [[package]] 1103 | name = "wasi" 1104 | version = "0.13.3+wasi-0.2.2" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 1107 | dependencies = [ 1108 | "wit-bindgen-rt", 1109 | ] 1110 | 1111 | [[package]] 1112 | name = "winapi-util" 1113 | version = "0.1.9" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1116 | dependencies = [ 1117 | "windows-sys 0.59.0", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "windows-sys" 1122 | version = "0.52.0" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1125 | dependencies = [ 1126 | "windows-targets", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "windows-sys" 1131 | version = "0.59.0" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1134 | dependencies = [ 1135 | "windows-targets", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "windows-targets" 1140 | version = "0.52.6" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1143 | dependencies = [ 1144 | "windows_aarch64_gnullvm", 1145 | "windows_aarch64_msvc", 1146 | "windows_i686_gnu", 1147 | "windows_i686_gnullvm", 1148 | "windows_i686_msvc", 1149 | "windows_x86_64_gnu", 1150 | "windows_x86_64_gnullvm", 1151 | "windows_x86_64_msvc", 1152 | ] 1153 | 1154 | [[package]] 1155 | name = "windows_aarch64_gnullvm" 1156 | version = "0.52.6" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1159 | 1160 | [[package]] 1161 | name = "windows_aarch64_msvc" 1162 | version = "0.52.6" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1165 | 1166 | [[package]] 1167 | name = "windows_i686_gnu" 1168 | version = "0.52.6" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1171 | 1172 | [[package]] 1173 | name = "windows_i686_gnullvm" 1174 | version = "0.52.6" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1177 | 1178 | [[package]] 1179 | name = "windows_i686_msvc" 1180 | version = "0.52.6" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1183 | 1184 | [[package]] 1185 | name = "windows_x86_64_gnu" 1186 | version = "0.52.6" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1189 | 1190 | [[package]] 1191 | name = "windows_x86_64_gnullvm" 1192 | version = "0.52.6" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1195 | 1196 | [[package]] 1197 | name = "windows_x86_64_msvc" 1198 | version = "0.52.6" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1201 | 1202 | [[package]] 1203 | name = "winnow" 1204 | version = "0.7.3" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" 1207 | dependencies = [ 1208 | "memchr", 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "wit-bindgen-rt" 1213 | version = "0.33.0" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 1216 | dependencies = [ 1217 | "bitflags", 1218 | ] 1219 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "forkfs" 3 | version = "0.2.8" 4 | authors = ["Alex Saveau "] 5 | edition = "2024" 6 | description = "ForkFS allows you to sandbox a process's changes to your file system." 7 | repository = "https://github.com/SUPERCILEX/forkfs" 8 | keywords = ["tools", "isolate", "files"] 9 | categories = ["command-line-utilities", "development-tools", "development-tools::debugging", "filesystem"] 10 | license = "Apache-2.0" 11 | 12 | [dependencies] 13 | clap = { version = "4.5.32", features = ["derive", "wrap_help"] } 14 | dirs = "6.0.0" 15 | error-stack = { version = "0.5.0", default-features = false, features = ["std"] } 16 | fuc_engine = "3.0.1" 17 | rustix = { version = "1.0.2", features = ["fs", "process", "thread", "mount", "linux_latest"] } 18 | thiserror = "2.0.12" 19 | 20 | [dev-dependencies] 21 | supercilex-tests = "0.4.16" 22 | trycmd = "0.15.9" 23 | 24 | [profile.release] 25 | lto = true 26 | codegen-units = 1 27 | strip = true 28 | panic = "abort" 29 | 30 | [profile.dr] 31 | inherits = "release" 32 | debug = true 33 | strip = false 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ForkFS 2 | 3 | ForkFS allows you to sandbox a process's changes to your file system. 4 | 5 | You can think of it as a lightweight container: programs still have access to your real system 6 | (and can therefore jump out of the sandbox), but their disk changes are re-routed to special 7 | directories without changing the real file system. 8 | 9 | A brief technical overview of the project is available at https://alexsaveau.dev/blog/forkfs. 10 | 11 | ## Installation 12 | 13 | > Note: ForkFS is Linux-only. 14 | 15 | ### Use prebuilt binaries 16 | 17 | Binaries for a number of platforms are available on the 18 | [release page](https://github.com/SUPERCILEX/forkfs/releases/latest). 19 | 20 | ### Build from source 21 | 22 | ```console,ignore 23 | $ cargo +nightly install forkfs 24 | ``` 25 | 26 | > To install cargo, follow 27 | > [these instructions](https://doc.rust-lang.org/cargo/getting-started/installation.html). 28 | 29 | ## Usage 30 | 31 | Run a command in the sandbox: 32 | 33 | ```sh 34 | $ forkfs run -- 35 | ``` 36 | 37 | All file system changes the command makes will only exist within the sandbox and will not modify 38 | your real file system. 39 | 40 | You can also start a bash shell wherein any command you execute has its file operations sandboxed: 41 | 42 | ```sh 43 | $ forkfs run bash 44 | ``` 45 | 46 | More details: 47 | 48 | ```console 49 | $ forkfs --help 50 | A sandboxing file system emulator 51 | 52 | You can think of ForkFS as a lightweight container: programs still have access to your real system 53 | (and can therefore jump out of the sandbox), but their disk changes are re-routed to special 54 | directories without changing the real file system. Under the hood, ForkFS is implemented as a 55 | wrapper around OverlayFS. 56 | 57 | Warning: we make no security claims. Do NOT use this tool with potentially malicious software. 58 | 59 | PS: you might also be interested in Firejail: . 60 | 61 | Usage: forkfs 62 | 63 | Commands: 64 | run Run commands inside the sandbox 65 | sessions Manage sessions 66 | help Print this message or the help of the given subcommand(s) 67 | 68 | Options: 69 | -h, --help 70 | Print help (use `-h` for a summary) 71 | 72 | -V, --version 73 | Print version 74 | 75 | $ forkfs sessions --help 76 | Manage sessions 77 | 78 | Each session has its own separate view of the file system that is persistent. That is, individual 79 | command invocations build upon each other. 80 | 81 | Actives sessions are those that are mounted, while inactive sessions remember the changes that were 82 | made within them, but are not ready to be used. 83 | 84 | Note: weird things may happen if the real file system changes after establishing a session. You may 85 | want to delete all sessions to restore clean behavior in such cases. 86 | 87 | Usage: forkfs sessions 88 | 89 | Commands: 90 | list List sessions 91 | stop Unmount active sessions 92 | delete Delete sessions 93 | help Print this message or the help of the given subcommand(s) 94 | 95 | Options: 96 | -h, --help 97 | Print help (use `-h` for a summary) 98 | 99 | ``` 100 | -------------------------------------------------------------------------------- /api.golden: -------------------------------------------------------------------------------- 1 | pub mod forkfs 2 | pub enum forkfs::Error 3 | pub forkfs::Error::InvalidArgument 4 | pub forkfs::Error::Io 5 | pub forkfs::Error::NotRoot 6 | pub forkfs::Error::SessionNotFound 7 | pub forkfs::Error::SetupRequired 8 | impl core::error::Error for forkfs::Error 9 | impl core::fmt::Debug for forkfs::Error 10 | pub fn forkfs::Error::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result 11 | impl core::fmt::Display for forkfs::Error 12 | pub fn forkfs::Error::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result 13 | impl core::marker::Freeze for forkfs::Error 14 | impl core::marker::Send for forkfs::Error 15 | impl core::marker::Sync for forkfs::Error 16 | impl core::marker::Unpin for forkfs::Error 17 | impl core::panic::unwind_safe::RefUnwindSafe for forkfs::Error 18 | impl core::panic::unwind_safe::UnwindSafe for forkfs::Error 19 | impl error_stack::context::Context for forkfs::Error where C: core::error::Error + core::marker::Send + core::marker::Sync + 'static 20 | pub fn forkfs::Error::provide<'a>(&'a self, request: &mut core::error::Request<'a>) 21 | impl core::convert::Into for forkfs::Error where U: core::convert::From 22 | pub fn forkfs::Error::into(self) -> U 23 | impl core::convert::TryFrom for forkfs::Error where U: core::convert::Into 24 | pub type forkfs::Error::Error = core::convert::Infallible 25 | pub fn forkfs::Error::try_from(value: U) -> core::result::Result>::Error> 26 | impl core::convert::TryInto for forkfs::Error where U: core::convert::TryFrom 27 | pub type forkfs::Error::Error = >::Error 28 | pub fn forkfs::Error::try_into(self) -> core::result::Result>::Error> 29 | impl alloc::string::ToString for forkfs::Error where T: core::fmt::Display + ?core::marker::Sized 30 | pub fn forkfs::Error::to_string(&self) -> alloc::string::String 31 | impl core::any::Any for forkfs::Error where T: 'static + ?core::marker::Sized 32 | pub fn forkfs::Error::type_id(&self) -> core::any::TypeId 33 | impl core::borrow::Borrow for forkfs::Error where T: ?core::marker::Sized 34 | pub fn forkfs::Error::borrow(&self) -> &T 35 | impl core::borrow::BorrowMut for forkfs::Error where T: ?core::marker::Sized 36 | pub fn forkfs::Error::borrow_mut(&mut self) -> &mut T 37 | impl core::convert::From for forkfs::Error 38 | pub fn forkfs::Error::from(t: T) -> T 39 | pub enum forkfs::SessionOperand<'a, S> 40 | pub forkfs::SessionOperand::All 41 | pub forkfs::SessionOperand::List(&'a [S]) 42 | impl<'a, S: core::clone::Clone> core::clone::Clone for forkfs::SessionOperand<'a, S> 43 | pub fn forkfs::SessionOperand<'a, S>::clone(&self) -> forkfs::SessionOperand<'a, S> 44 | impl<'a, S: core::marker::Copy> core::marker::Copy for forkfs::SessionOperand<'a, S> 45 | impl<'a, S> core::marker::Freeze for forkfs::SessionOperand<'a, S> 46 | impl<'a, S> core::marker::Send for forkfs::SessionOperand<'a, S> where S: core::marker::Sync 47 | impl<'a, S> core::marker::Sync for forkfs::SessionOperand<'a, S> where S: core::marker::Sync 48 | impl<'a, S> core::marker::Unpin for forkfs::SessionOperand<'a, S> 49 | impl<'a, S> core::panic::unwind_safe::RefUnwindSafe for forkfs::SessionOperand<'a, S> where S: core::panic::unwind_safe::RefUnwindSafe 50 | impl<'a, S> core::panic::unwind_safe::UnwindSafe for forkfs::SessionOperand<'a, S> where S: core::panic::unwind_safe::RefUnwindSafe 51 | impl core::convert::Into for forkfs::SessionOperand<'a, S> where U: core::convert::From 52 | pub fn forkfs::SessionOperand<'a, S>::into(self) -> U 53 | impl core::convert::TryFrom for forkfs::SessionOperand<'a, S> where U: core::convert::Into 54 | pub type forkfs::SessionOperand<'a, S>::Error = core::convert::Infallible 55 | pub fn forkfs::SessionOperand<'a, S>::try_from(value: U) -> core::result::Result>::Error> 56 | impl core::convert::TryInto for forkfs::SessionOperand<'a, S> where U: core::convert::TryFrom 57 | pub type forkfs::SessionOperand<'a, S>::Error = >::Error 58 | pub fn forkfs::SessionOperand<'a, S>::try_into(self) -> core::result::Result>::Error> 59 | impl alloc::borrow::ToOwned for forkfs::SessionOperand<'a, S> where T: core::clone::Clone 60 | pub type forkfs::SessionOperand<'a, S>::Owned = T 61 | pub fn forkfs::SessionOperand<'a, S>::clone_into(&self, target: &mut T) 62 | pub fn forkfs::SessionOperand<'a, S>::to_owned(&self) -> T 63 | impl core::any::Any for forkfs::SessionOperand<'a, S> where T: 'static + ?core::marker::Sized 64 | pub fn forkfs::SessionOperand<'a, S>::type_id(&self) -> core::any::TypeId 65 | impl core::borrow::Borrow for forkfs::SessionOperand<'a, S> where T: ?core::marker::Sized 66 | pub fn forkfs::SessionOperand<'a, S>::borrow(&self) -> &T 67 | impl core::borrow::BorrowMut for forkfs::SessionOperand<'a, S> where T: ?core::marker::Sized 68 | pub fn forkfs::SessionOperand<'a, S>::borrow_mut(&mut self) -> &mut T 69 | impl core::clone::CloneToUninit for forkfs::SessionOperand<'a, S> where T: core::clone::Clone 70 | pub unsafe fn forkfs::SessionOperand<'a, S>::clone_to_uninit(&self, dst: *mut u8) 71 | impl core::convert::From for forkfs::SessionOperand<'a, S> 72 | pub fn forkfs::SessionOperand<'a, S>::from(t: T) -> T 73 | pub fn forkfs::delete_sessions>(sessions: forkfs::SessionOperand<'_, S>) -> error_stack::result::Result<(), forkfs::Error> 74 | pub fn forkfs::list_sessions() -> error_stack::result::Result<(), forkfs::Error> 75 | pub fn forkfs::run>(session: &str, command: &[T]) -> error_stack::result::Result<(), forkfs::Error> 76 | pub fn forkfs::stop_sessions>(sessions: forkfs::SessionOperand<'_, S>) -> error_stack::result::Result<(), forkfs::Error> 77 | -------------------------------------------------------------------------------- /command-reference-short.golden: -------------------------------------------------------------------------------- 1 | A sandboxing file system emulator 2 | 3 | Usage: forkfs 4 | 5 | Commands: 6 | run Run commands inside the sandbox 7 | sessions Manage sessions 8 | help Print this message or the help of the given subcommand(s) 9 | 10 | Options: 11 | -h, --help Print help (use `--help` for more detail) 12 | -V, --version Print version 13 | 14 | --- 15 | 16 | Run commands inside the sandbox 17 | 18 | Usage: forkfs run [OPTIONS] ... 19 | 20 | Arguments: 21 | ... The command to run in isolation 22 | 23 | Options: 24 | -s, --session The fork/sandbox to use [default: default] 25 | -h, --help Print help (use `--help` for more detail) 26 | 27 | --- 28 | 29 | Manage sessions 30 | 31 | Usage: forkfs sessions 32 | 33 | Commands: 34 | list List sessions 35 | stop Unmount active sessions 36 | delete Delete sessions 37 | help Print this message or the help of the given subcommand(s) 38 | 39 | Options: 40 | -h, --help Print help (use `--help` for more detail) 41 | 42 | --- 43 | 44 | List sessions 45 | 46 | Usage: forkfs sessions list 47 | 48 | Options: 49 | -h, --help Print help (use `--help` for more detail) 50 | 51 | --- 52 | 53 | Unmount active sessions 54 | 55 | Usage: forkfs sessions stop [OPTIONS] ... 56 | 57 | Arguments: 58 | ... The session(s) to operate on 59 | 60 | Options: 61 | -a, --all Operate on all sessions 62 | -h, --help Print help (use `--help` for more detail) 63 | 64 | --- 65 | 66 | Delete sessions 67 | 68 | Usage: forkfs sessions delete [OPTIONS] ... 69 | 70 | Arguments: 71 | ... The session(s) to operate on 72 | 73 | Options: 74 | -a, --all Operate on all sessions 75 | -h, --help Print help (use `--help` for more detail) 76 | 77 | --- 78 | 79 | Print this message or the help of the given subcommand(s) 80 | 81 | Usage: forkfs sessions help [COMMAND] 82 | 83 | Commands: 84 | list List sessions 85 | stop Unmount active sessions 86 | delete Delete sessions 87 | help Print this message or the help of the given subcommand(s) 88 | 89 | --- 90 | 91 | List sessions 92 | 93 | Usage: forkfs sessions help list 94 | 95 | --- 96 | 97 | Unmount active sessions 98 | 99 | Usage: forkfs sessions help stop 100 | 101 | --- 102 | 103 | Delete sessions 104 | 105 | Usage: forkfs sessions help delete 106 | 107 | --- 108 | 109 | Print this message or the help of the given subcommand(s) 110 | 111 | Usage: forkfs sessions help help 112 | 113 | --- 114 | 115 | Print this message or the help of the given subcommand(s) 116 | 117 | Usage: forkfs help [COMMAND] 118 | 119 | Commands: 120 | run Run commands inside the sandbox 121 | sessions Manage sessions 122 | help Print this message or the help of the given subcommand(s) 123 | 124 | --- 125 | 126 | Run commands inside the sandbox 127 | 128 | Usage: forkfs help run 129 | 130 | --- 131 | 132 | Manage sessions 133 | 134 | Usage: forkfs help sessions [COMMAND] 135 | 136 | Commands: 137 | list List sessions 138 | stop Unmount active sessions 139 | delete Delete sessions 140 | 141 | --- 142 | 143 | List sessions 144 | 145 | Usage: forkfs help sessions list 146 | 147 | --- 148 | 149 | Unmount active sessions 150 | 151 | Usage: forkfs help sessions stop 152 | 153 | --- 154 | 155 | Delete sessions 156 | 157 | Usage: forkfs help sessions delete 158 | 159 | --- 160 | 161 | Print this message or the help of the given subcommand(s) 162 | 163 | Usage: forkfs help help 164 | -------------------------------------------------------------------------------- /command-reference.golden: -------------------------------------------------------------------------------- 1 | A sandboxing file system emulator 2 | 3 | You can think of ForkFS as a lightweight container: programs still have access to your real system 4 | (and can therefore jump out of the sandbox), but their disk changes are re-routed to special 5 | directories without changing the real file system. Under the hood, ForkFS is implemented as a 6 | wrapper around OverlayFS. 7 | 8 | Warning: we make no security claims. Do NOT use this tool with potentially malicious software. 9 | 10 | PS: you might also be interested in Firejail: . 11 | 12 | Usage: forkfs 13 | 14 | Commands: 15 | run Run commands inside the sandbox 16 | sessions Manage sessions 17 | help Print this message or the help of the given subcommand(s) 18 | 19 | Options: 20 | -h, --help 21 | Print help (use `-h` for a summary) 22 | 23 | -V, --version 24 | Print version 25 | 26 | --- 27 | 28 | Run commands inside the sandbox 29 | 30 | Usage: forkfs run [OPTIONS] ... 31 | 32 | Arguments: 33 | ... 34 | The command to run in isolation 35 | 36 | Options: 37 | -s, --session 38 | The fork/sandbox to use 39 | 40 | If it does not exist or is inactive, it will be created and activated. 41 | 42 | [default: default] 43 | 44 | -h, --help 45 | Print help (use `-h` for a summary) 46 | 47 | --- 48 | 49 | Manage sessions 50 | 51 | Each session has its own separate view of the file system that is persistent. That is, individual 52 | command invocations build upon each other. 53 | 54 | Actives sessions are those that are mounted, while inactive sessions remember the changes that were 55 | made within them, but are not ready to be used. 56 | 57 | Note: weird things may happen if the real file system changes after establishing a session. You may 58 | want to delete all sessions to restore clean behavior in such cases. 59 | 60 | Usage: forkfs sessions 61 | 62 | Commands: 63 | list List sessions 64 | stop Unmount active sessions 65 | delete Delete sessions 66 | help Print this message or the help of the given subcommand(s) 67 | 68 | Options: 69 | -h, --help 70 | Print help (use `-h` for a summary) 71 | 72 | --- 73 | 74 | List sessions 75 | 76 | `[active]` sessions are denoted with brackets while `inactive` sessions are bare. 77 | 78 | Usage: forkfs sessions list 79 | 80 | Options: 81 | -h, --help 82 | Print help (use `-h` for a summary) 83 | 84 | --- 85 | 86 | Unmount active sessions 87 | 88 | Usage: forkfs sessions stop [OPTIONS] ... 89 | 90 | Arguments: 91 | ... 92 | The session(s) to operate on 93 | 94 | Options: 95 | -a, --all 96 | Operate on all sessions 97 | 98 | -h, --help 99 | Print help (use `-h` for a summary) 100 | 101 | --- 102 | 103 | Delete sessions 104 | 105 | Usage: forkfs sessions delete [OPTIONS] ... 106 | 107 | Arguments: 108 | ... 109 | The session(s) to operate on 110 | 111 | Options: 112 | -a, --all 113 | Operate on all sessions 114 | 115 | -h, --help 116 | Print help (use `-h` for a summary) 117 | 118 | --- 119 | 120 | Print this message or the help of the given subcommand(s) 121 | 122 | Usage: forkfs sessions help [COMMAND] 123 | 124 | Commands: 125 | list List sessions 126 | stop Unmount active sessions 127 | delete Delete sessions 128 | help Print this message or the help of the given subcommand(s) 129 | 130 | --- 131 | 132 | List sessions 133 | 134 | Usage: forkfs sessions help list 135 | 136 | --- 137 | 138 | Unmount active sessions 139 | 140 | Usage: forkfs sessions help stop 141 | 142 | --- 143 | 144 | Delete sessions 145 | 146 | Usage: forkfs sessions help delete 147 | 148 | --- 149 | 150 | Print this message or the help of the given subcommand(s) 151 | 152 | Usage: forkfs sessions help help 153 | 154 | --- 155 | 156 | Print this message or the help of the given subcommand(s) 157 | 158 | Usage: forkfs help [COMMAND] 159 | 160 | Commands: 161 | run Run commands inside the sandbox 162 | sessions Manage sessions 163 | help Print this message or the help of the given subcommand(s) 164 | 165 | --- 166 | 167 | Run commands inside the sandbox 168 | 169 | Usage: forkfs help run 170 | 171 | --- 172 | 173 | Manage sessions 174 | 175 | Usage: forkfs help sessions [COMMAND] 176 | 177 | Commands: 178 | list List sessions 179 | stop Unmount active sessions 180 | delete Delete sessions 181 | 182 | --- 183 | 184 | List sessions 185 | 186 | Usage: forkfs help sessions list 187 | 188 | --- 189 | 190 | Unmount active sessions 191 | 192 | Usage: forkfs help sessions stop 193 | 194 | --- 195 | 196 | Delete sessions 197 | 198 | Usage: forkfs help sessions delete 199 | 200 | --- 201 | 202 | Print this message or the help of the given subcommand(s) 203 | 204 | Usage: forkfs help help 205 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(let_chains)] 2 | #![feature(dir_entry_ext2)] 3 | 4 | use std::{ 5 | fmt::{Debug, Display}, 6 | io, 7 | path::PathBuf, 8 | }; 9 | 10 | use error_stack::{Result, ResultExt}; 11 | pub use run::run; 12 | pub use sessions::{ 13 | Op as SessionOperand, delete as delete_sessions, list as list_sessions, stop as stop_sessions, 14 | }; 15 | 16 | mod run; 17 | mod sessions; 18 | 19 | #[derive(thiserror::Error, Debug)] 20 | pub enum Error { 21 | #[error("An IO error occurred.")] 22 | Io, 23 | #[error("Invalid argument.")] 24 | InvalidArgument, 25 | #[error("ForkFS must be run as root.")] 26 | NotRoot, 27 | #[error("Session not found.")] 28 | SessionNotFound, 29 | #[error("Setup required.")] 30 | SetupRequired, 31 | } 32 | 33 | fn get_sessions_dir() -> PathBuf { 34 | let mut sessions_dir = dirs::cache_dir().unwrap_or_else(|| PathBuf::from("/tmp")); 35 | sessions_dir.push("forkfs"); 36 | sessions_dir 37 | } 38 | 39 | trait IoErr { 40 | fn map_io_err_lazy( 41 | self, 42 | f: impl FnOnce() -> P, 43 | ) -> Out; 44 | 45 | fn map_io_err(self, p: P) -> Out; 46 | } 47 | 48 | impl IoErr> for io::Result { 49 | fn map_io_err_lazy( 50 | self, 51 | f: impl FnOnce() -> P, 52 | ) -> Result { 53 | self.attach_printable_lazy(f).change_context(Error::Io) 54 | } 55 | 56 | fn map_io_err(self, p: P) -> Result { 57 | self.attach_printable(p).change_context(Error::Io) 58 | } 59 | } 60 | 61 | impl IoErr> for std::result::Result { 62 | fn map_io_err_lazy( 63 | self, 64 | f: impl FnOnce() -> P, 65 | ) -> Result { 66 | self.map_err(io::Error::from).map_io_err_lazy(f) 67 | } 68 | 69 | fn map_io_err(self, p: P) -> Result { 70 | self.map_err(io::Error::from).map_io_err(p) 71 | } 72 | } 73 | 74 | mod path_undo { 75 | use std::{ 76 | fmt::{Debug, Formatter}, 77 | ops::{Deref, DerefMut}, 78 | path::{Path, PathBuf}, 79 | }; 80 | 81 | pub struct TmpPath<'a>(&'a mut PathBuf); 82 | 83 | impl<'a> TmpPath<'a> { 84 | pub fn new(path: &'a mut PathBuf, child: impl AsRef) -> Self { 85 | path.push(child); 86 | Self(path) 87 | } 88 | } 89 | 90 | impl Deref for TmpPath<'_> { 91 | type Target = PathBuf; 92 | 93 | fn deref(&self) -> &Self::Target { 94 | self.0 95 | } 96 | } 97 | 98 | impl DerefMut for TmpPath<'_> { 99 | fn deref_mut(&mut self) -> &mut Self::Target { 100 | self.0 101 | } 102 | } 103 | 104 | impl AsRef for TmpPath<'_> { 105 | fn as_ref(&self) -> &Path { 106 | self.0 107 | } 108 | } 109 | 110 | impl Debug for TmpPath<'_> { 111 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 112 | Debug::fmt(&**self, f) 113 | } 114 | } 115 | 116 | impl Drop for TmpPath<'_> { 117 | fn drop(&mut self) { 118 | self.pop(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::OsString, 3 | io, 4 | io::Write, 5 | process::{ExitCode, Termination}, 6 | }; 7 | 8 | use clap::{ArgAction, Args, Parser, Subcommand}; 9 | use error_stack::Result; 10 | use forkfs::SessionOperand; 11 | 12 | #[allow(clippy::doc_markdown)] 13 | /// A sandboxing file system emulator 14 | /// 15 | /// You can think of ForkFS as a lightweight container: programs still have 16 | /// access to your real system (and can therefore jump out of the sandbox), but 17 | /// their disk changes are re-routed to special directories without changing the 18 | /// real file system. Under the hood, ForkFS is implemented as a wrapper around 19 | /// OverlayFS. 20 | /// 21 | /// Warning: we make no security claims. Do NOT use this tool with potentially 22 | /// malicious software. 23 | /// 24 | /// PS: you might also be interested in Firejail: . 25 | #[derive(Parser, Debug)] 26 | #[command(version, author = "Alex Saveau (@SUPERCILEX)")] 27 | #[command(infer_subcommands = true, infer_long_args = true)] 28 | #[command(disable_help_flag = true)] 29 | #[command(max_term_width = 100)] 30 | #[cfg_attr(test, command(help_expected = true))] 31 | struct ForkFs { 32 | #[command(subcommand)] 33 | cmd: Cmd, 34 | 35 | #[arg(short, long, short_alias = '?', global = true)] 36 | #[arg(action = ArgAction::Help, help = "Print help (use `--help` for more detail)")] 37 | #[arg(long_help = "Print help (use `-h` for a summary)")] 38 | help: Option, 39 | } 40 | 41 | #[derive(Subcommand, Debug)] 42 | enum Cmd { 43 | /// Run commands inside the sandbox 44 | #[command(alias = "execute")] 45 | Run(Run), 46 | 47 | /// Manage sessions 48 | /// 49 | /// Each session has its own separate view of the file system that is 50 | /// persistent. That is, individual command invocations build upon each 51 | /// other. 52 | /// 53 | /// Actives sessions are those that are mounted, while inactive sessions 54 | /// remember the changes that were made within them, but are not ready to be 55 | /// used. 56 | /// 57 | /// Note: weird things may happen if the real file system changes after 58 | /// establishing a session. You may want to delete all sessions to 59 | /// restore clean behavior in such cases. 60 | #[command(subcommand)] 61 | Sessions(Sessions), 62 | } 63 | 64 | #[derive(Args, Debug)] 65 | #[command(arg_required_else_help = true)] 66 | struct Run { 67 | /// The command to run in isolation 68 | #[arg(required = true)] 69 | command: Vec, 70 | 71 | /// The fork/sandbox to use 72 | /// 73 | /// If it does not exist or is inactive, it will be created and activated. 74 | #[arg(short = 's', long = "session", short_alias = 'n', aliases = & ["name", "id"])] 75 | #[arg(default_value = "default")] 76 | session: String, 77 | } 78 | 79 | #[derive(Subcommand, Debug)] 80 | enum Sessions { 81 | /// List sessions 82 | /// 83 | /// `[active]` sessions are denoted with brackets while `inactive` sessions 84 | /// are bare. 85 | #[command(alias = "ls")] 86 | List, 87 | 88 | /// Unmount active sessions 89 | #[command(alias = "close")] 90 | Stop(SessionCmd), 91 | 92 | /// Delete sessions 93 | #[command(alias = "destroy")] 94 | Delete(SessionCmd), 95 | } 96 | 97 | #[derive(Args, Debug)] 98 | #[command(arg_required_else_help = true)] 99 | struct SessionCmd { 100 | /// The session(s) to operate on 101 | #[arg(required = true, group = "names")] 102 | sessions: Vec, 103 | 104 | /// Operate on all sessions 105 | #[arg(short = 'a', long = "all", group = "names")] 106 | all: bool, 107 | } 108 | 109 | fn main() -> ExitCode { 110 | #[cfg(not(debug_assertions))] 111 | error_stack::Report::install_debug_hook::(|_, _| {}); 112 | 113 | let args = ForkFs::parse(); 114 | 115 | match forkfs(args) { 116 | Ok(o) => o.report(), 117 | Err(err) => { 118 | drop(writeln!(io::stderr(), "Error: {err:?}")); 119 | err.report() 120 | } 121 | } 122 | } 123 | 124 | fn forkfs(ForkFs { cmd, help: _ }: ForkFs) -> Result<(), forkfs::Error> { 125 | match cmd { 126 | Cmd::Run(r) => run(r), 127 | Cmd::Sessions(s) => sessions(s), 128 | } 129 | } 130 | 131 | fn run(Run { command, session }: Run) -> Result<(), forkfs::Error> { 132 | forkfs::run(&session, command.as_slice()) 133 | } 134 | 135 | fn sessions(sessions: Sessions) -> Result<(), forkfs::Error> { 136 | match sessions { 137 | Sessions::List => forkfs::list_sessions(), 138 | Sessions::Stop(SessionCmd { sessions, all }) => forkfs::stop_sessions(if all { 139 | SessionOperand::All 140 | } else { 141 | SessionOperand::List(sessions.as_slice()) 142 | }), 143 | Sessions::Delete(SessionCmd { sessions, all }) => forkfs::delete_sessions(if all { 144 | SessionOperand::All 145 | } else { 146 | SessionOperand::List(sessions.as_slice()) 147 | }), 148 | } 149 | } 150 | 151 | #[cfg(test)] 152 | mod cli_tests { 153 | use clap::CommandFactory; 154 | 155 | use super::*; 156 | 157 | #[test] 158 | fn verify_app() { 159 | ForkFs::command().debug_assert(); 160 | } 161 | 162 | #[test] 163 | fn help_for_review() { 164 | supercilex_tests::help_for_review(ForkFs::command()); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/run.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | env::{current_dir, set_current_dir}, 4 | ffi::{CStr, OsStr}, 5 | os::unix::{fs::chroot, process::CommandExt}, 6 | path::Path, 7 | process::Command, 8 | }; 9 | 10 | use error_stack::{Result, ResultExt}; 11 | use rustix::{ 12 | fs::{CWD, readlinkat}, 13 | io::Errno, 14 | process::{Uid, getuid}, 15 | thread::{CapabilityFlags, capabilities, set_thread_uid}, 16 | }; 17 | 18 | use crate::{Error, IoErr, get_sessions_dir, sessions::maybe_create_session}; 19 | 20 | pub fn run>(session: &str, command: &[T]) -> Result<(), Error> { 21 | let uid = getuid(); 22 | validate_permissions(uid)?; 23 | 24 | let mut session_dir = get_sessions_dir(); 25 | session_dir.push(session); 26 | 27 | maybe_create_session(&mut session_dir)?; 28 | 29 | session_dir.push("merged"); 30 | enter_session(&session_dir)?; 31 | 32 | run_command(command, uid) 33 | } 34 | 35 | fn enter_session(target: &Path) -> Result<(), Error> { 36 | // Must be retrieved before chroot-ing 37 | let current_dir = current_dir().map_io_err("Failed to get current directory")?; 38 | 39 | chroot(target).map_io_err_lazy(|| format!("Failed to change root {target:?}"))?; 40 | set_current_dir(current_dir) 41 | .map_io_err_lazy(|| format!("Failed to change current directory {target:?}")) 42 | } 43 | 44 | fn run_command(args: &[impl AsRef], prev_uid: Uid) -> Result<(), Error> { 45 | let mut command = Command::new(args[0].as_ref()); 46 | 47 | // Downgrade privilege level to pre-sudo if possible 48 | if !prev_uid.is_root() { 49 | command.uid(prev_uid.as_raw()); 50 | } else if let Some(uid) = env::var_os("SUDO_UID").as_ref().and_then(|s| s.to_str()) 51 | && let Ok(uid) = uid.parse() 52 | { 53 | command.uid(uid); 54 | } 55 | 56 | Err(command.args(&args[1..]).exec()).map_io_err_lazy(|| { 57 | format!( 58 | "Failed to exec {:?}", 59 | args.iter().map(AsRef::as_ref).collect::>() 60 | ) 61 | }) 62 | } 63 | 64 | fn validate_permissions(uid: Uid) -> Result<(), Error> { 65 | if uid.is_root() { 66 | return Ok(()); 67 | } 68 | 69 | match set_thread_uid(Uid::ROOT) { 70 | Err(Errno::PERM) => { 71 | // Continue to capability check 72 | } 73 | r => { 74 | return r.map_io_err("Failed to become root"); 75 | } 76 | } 77 | 78 | { 79 | let effective_capabilities = capabilities(None) 80 | .map_io_err("Failed to retrieve capabilities")? 81 | .effective; 82 | if effective_capabilities.contains( 83 | CapabilityFlags::CHOWN | CapabilityFlags::SYS_CHROOT | CapabilityFlags::SYS_ADMIN, 84 | ) { 85 | return Ok(()); 86 | } 87 | } 88 | 89 | let path = readlinkat(CWD, "/proc/self/exe", Vec::new()); 90 | let path = path.as_deref().map(CStr::to_string_lossy); 91 | let path = path.as_deref().ok().unwrap_or(""); 92 | 93 | Err(Error::SetupRequired).attach_printable(format!( 94 | "Welcome to ForkFS! 95 | 96 | Under the hood, ForkFS is implemented as a wrapper around OverlayFS. As a 97 | consequence, elevated privileges are required and can be granted in one of 98 | three ways (ordered by recommendation): 99 | 100 | - $ sudo setcap \ 101 | cap_chown,cap_sys_chroot,cap_sys_admin,cap_dac_override,cap_fowner,cap_setpcap,cap_mknod,\ 102 | cap_lease,cap_setfcap+ep {path} 103 | 104 | This grants `forkfs` precisely the capabilities it needs. 105 | 106 | cap_dac_override onwards are capabilities that are required for OverlayFS to 107 | be able to perform those actions. 108 | 109 | - $ sudo chown root {path}; sudo chmod u+s {path} 110 | 111 | This transfers ownership of the `forkfs` binary to root and specifies that 112 | the binary should be executed as its owner (i.e. root). 113 | 114 | - $ sudo -E forkfs ... 115 | 116 | This simply invokes `forkfs` as root. This option is problematic because 117 | sudo alters the environment, causing PATH lookups to fail and changing 118 | your home directory. 119 | 120 | If you do go down this route, be consistent with your usage of `-E`. Bare 121 | `sudo` vs `sudo -E` will change the forkfs environment, meaning sessions 122 | that appear in `sudo` will not appear in `sudo -E` and vice versa. 123 | 124 | PS: if you've already seen this message, then you probably upgraded to a new 125 | version of ForkFS and will therefore need to rerun this setup.", 126 | )) 127 | } 128 | -------------------------------------------------------------------------------- /src/sessions.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::CString, 3 | fmt::Write as FmtWrite, 4 | fs, 5 | fs::DirEntry, 6 | io, 7 | io::{ErrorKind, Write}, 8 | os::unix::fs::DirEntryExt2, 9 | path::{Path, PathBuf}, 10 | }; 11 | 12 | use error_stack::{Result, ResultExt}; 13 | use rustix::{ 14 | fs::{AtFlags, CWD, StatxFlags, statx}, 15 | mount::{ 16 | MountFlags, MountPropagationFlags, UnmountFlags, mount, mount_bind_recursive, mount_change, 17 | unmount, 18 | }, 19 | }; 20 | 21 | use crate::{Error, IoErr, get_sessions_dir, path_undo::TmpPath}; 22 | 23 | #[derive(Copy, Clone)] 24 | pub enum Op<'a, S> { 25 | All, 26 | List(&'a [S]), 27 | } 28 | 29 | pub fn list() -> Result<(), Error> { 30 | let mut stdout = io::stdout().lock(); 31 | let mut is_first = true; 32 | iter_all_sessions(|entry, session| { 33 | let name = entry.file_name_ref().to_string_lossy(); 34 | let session_active = is_active_session(session, true)?; 35 | 36 | let mut print = || { 37 | if !is_first { 38 | write!(stdout, ", ")?; 39 | } 40 | if session_active { 41 | write!(stdout, "[{name}]") 42 | } else { 43 | write!(stdout, "{name}") 44 | } 45 | }; 46 | 47 | print().map_io_err("Failed to write to stdout")?; 48 | is_first = false; 49 | 50 | Ok(()) 51 | }) 52 | } 53 | 54 | pub fn stop>(sessions: Op) -> Result<(), Error> { 55 | iter_op(sessions, stop_session) 56 | } 57 | 58 | pub fn delete>(sessions: Op) -> Result<(), Error> { 59 | iter_op(sessions, |session| { 60 | stop_session(session)?; 61 | delete_session(session) 62 | }) 63 | } 64 | 65 | pub fn maybe_create_session(dir: &mut PathBuf) -> Result<(), Error> { 66 | if is_active_session(dir, false)? { 67 | return Ok(()); 68 | } 69 | 70 | for path in ["diff", "work", "merged"] { 71 | let dir = TmpPath::new(dir, path); 72 | fs::create_dir_all(&dir) 73 | .map_io_err_lazy(|| format!("Failed to create directory {dir:?}"))?; 74 | } 75 | start_session(dir) 76 | } 77 | 78 | fn start_session(dir: &mut PathBuf) -> Result<(), Error> { 79 | let command = { 80 | let mut command = String::from("lowerdir=/,"); 81 | { 82 | let diff = TmpPath::new(dir, "diff"); 83 | write!(command, "upperdir={},", diff.display()).unwrap(); 84 | } 85 | { 86 | let work = TmpPath::new(dir, "work"); 87 | write!(command, "workdir={}", work.display()).unwrap(); 88 | } 89 | 90 | CString::new(command.into_bytes()) 91 | .attach_printable("Invalid path bytes") 92 | .change_context(Error::InvalidArgument)? 93 | }; 94 | 95 | let mut merged = TmpPath::new(dir, "merged"); 96 | mount( 97 | c"overlay", 98 | &*merged, 99 | c"overlay", 100 | MountFlags::empty(), 101 | command.as_c_str(), 102 | ) 103 | .map_io_err_lazy(|| format!("Failed to mount directory {merged:?}"))?; 104 | 105 | for (source, target) in [ 106 | (c"/proc", "proc"), 107 | (c"/dev", "dev"), 108 | (c"/run", "run"), 109 | (c"/tmp", "tmp"), 110 | ] { 111 | let target = TmpPath::new(&mut merged, target); 112 | mount_bind_recursive(source, &*target) 113 | .map_io_err_lazy(|| format!("Failed to bind mount directory {target:?}"))?; 114 | mount_change( 115 | &*target, 116 | MountPropagationFlags::DOWNSTREAM | MountPropagationFlags::REC, 117 | ) 118 | .map_io_err_lazy(|| format!("Failed to enslave mount {target:?}"))?; 119 | } 120 | 121 | Ok(()) 122 | } 123 | 124 | fn stop_session(session: &mut PathBuf) -> Result<(), Error> { 125 | if !is_active_session(session, true)? { 126 | return Ok(()); 127 | } 128 | 129 | let mut merged = TmpPath::new(session, "merged"); 130 | 131 | for target in ["proc", "dev", "run", "tmp"] { 132 | let target = TmpPath::new(&mut merged, target); 133 | unmount(&*target, UnmountFlags::DETACH) 134 | .map_io_err_lazy(|| format!("Failed to unmount directory {target:?}"))?; 135 | } 136 | 137 | unmount(&*merged, UnmountFlags::empty()) 138 | .map_io_err_lazy(|| format!("Failed to unmount directory {merged:?}")) 139 | } 140 | 141 | fn delete_session(session: &Path) -> Result<(), Error> { 142 | fuc_engine::remove_dir_all(session) 143 | .attach_printable_lazy(|| format!("Failed to delete directory {session:?}")) 144 | .change_context(Error::Io) 145 | } 146 | 147 | fn iter_all_sessions( 148 | mut f: impl FnMut(DirEntry, &mut PathBuf) -> Result<(), Error>, 149 | ) -> Result<(), Error> { 150 | let mut sessions_dir = get_sessions_dir(); 151 | for entry in match fs::read_dir(&sessions_dir) { 152 | Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()), 153 | r => r.map_io_err_lazy(|| format!("Failed to open directory {sessions_dir:?}"))?, 154 | } { 155 | let entry = 156 | entry.map_io_err_lazy(|| format!("Failed to read directory {sessions_dir:?}"))?; 157 | let mut session = TmpPath::new(&mut sessions_dir, entry.file_name_ref()); 158 | 159 | f(entry, &mut session)?; 160 | } 161 | Ok(()) 162 | } 163 | 164 | #[allow(clippy::needless_pass_by_value)] 165 | fn iter_op>( 166 | sessions: Op, 167 | mut f: impl FnMut(&mut PathBuf) -> Result<(), Error>, 168 | ) -> Result<(), Error> { 169 | match sessions { 170 | Op::All => iter_all_sessions(|_, session| f(session)), 171 | Op::List(sessions) => { 172 | let mut sessions_dir = get_sessions_dir(); 173 | for session in sessions { 174 | let mut session = TmpPath::new(&mut sessions_dir, session.as_ref()); 175 | f(&mut session)?; 176 | } 177 | Ok(()) 178 | } 179 | } 180 | } 181 | 182 | fn is_active_session(session: &mut PathBuf, must_exist: bool) -> Result { 183 | let mount = { 184 | let merged = TmpPath::new(session, "merged"); 185 | match statx(CWD, &*merged, AtFlags::empty(), StatxFlags::MNT_ID) { 186 | Err(e) if !must_exist && e.kind() == ErrorKind::NotFound => { 187 | return Ok(false); 188 | } 189 | r => r, 190 | } 191 | .map_io_err_lazy(|| format!("Failed to stat {merged:?}")) 192 | .change_context(Error::SessionNotFound)? 193 | .stx_mnt_id 194 | }; 195 | 196 | let parent_mount = statx(CWD, &*session, AtFlags::empty(), StatxFlags::MNT_ID) 197 | .map_io_err_lazy(|| format!("Failed to stat {session:?}"))? 198 | .stx_mnt_id; 199 | 200 | Ok(parent_mount != mount) 201 | } 202 | -------------------------------------------------------------------------------- /tests/api.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn fmt() { 3 | supercilex_tests::fmt(); 4 | } 5 | 6 | #[test] 7 | fn clippy() { 8 | supercilex_tests::clippy(); 9 | } 10 | 11 | #[test] 12 | fn api() { 13 | supercilex_tests::api(); 14 | } 15 | 16 | #[test] 17 | fn readme() { 18 | trycmd::TestCases::new().case("README.md"); 19 | } 20 | --------------------------------------------------------------------------------