diff --git a/Cargo.lock b/Cargo.lock index b41e608c..a1c5af25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,15 +112,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arc-swap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" -dependencies = [ - "rustversion", -] - [[package]] name = "arrayref" version = "0.3.9" @@ -222,12 +213,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "beef" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" - [[package]] name = "bincode" version = "1.3.3" @@ -283,15 +268,6 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "blake3" version = "1.8.5" @@ -303,16 +279,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "cpufeatures 0.3.0", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", + "cpufeatures", ] [[package]] @@ -331,7 +298,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", - "regex-automata", "serde", ] @@ -412,7 +378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures", "rand_core 0.10.1", ] @@ -470,15 +436,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "clru" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" -dependencies = [ - "hashbrown 0.16.1", -] - [[package]] name = "colorchoice" version = "1.0.5" @@ -520,15 +477,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -556,41 +504,12 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.1" @@ -654,26 +573,15 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", - "subtle", -] - [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer", "const-oid", - "crypto-common 0.2.1", + "crypto-common", ] [[package]] @@ -708,27 +616,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "either" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "env_filter" version = "2.0.0" @@ -768,26 +655,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "etcetera" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" -dependencies = [ - "cfg-if", - "windows-sys 0.61.2", -] - -[[package]] -name = "faster-hex" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" -dependencies = [ - "heapless 0.8.0", - "serde", -] - [[package]] name = "fastrand" version = "2.4.1" @@ -832,12 +699,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -960,16 +821,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1012,923 +863,176 @@ dependencies = [ ] [[package]] -name = "gix" -version = "0.85.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8b2e38ebfc4484dfef8580ddcaf8abb7285e6f3eb6413ff6775d104ae96ca6" -dependencies = [ - "gix-actor", - "gix-attributes", - "gix-command", - "gix-commitgraph", - "gix-config", - "gix-date", - "gix-diff", - "gix-discover", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-hashtable", - "gix-ignore", - "gix-index", - "gix-lock", - "gix-object", - "gix-odb", - "gix-pack", - "gix-path", - "gix-pathspec", - "gix-protocol", - "gix-ref", - "gix-refspec", - "gix-revision", - "gix-revwalk", - "gix-sec", - "gix-shallow", - "gix-submodule", - "gix-tempfile", - "gix-trace", - "gix-traverse", - "gix-url", - "gix-utils", - "gix-validate", - "gix-worktree", - "gix-worktree-stream", - "nonempty", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-actor" -version = "0.41.2" +name = "glob" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f9308ad6fd35b2a865cbe4117ac61b2be59e4a9ef1621c7a9794f7c8e52c5b" -dependencies = [ - "bstr", - "gix-date", - "gix-error", -] +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] -name = "gix-attributes" -version = "0.33.2" +name = "globset" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b40888d0ed415c0744a6cdc61eebf0304c9d26ab726725b718443c322e5ba4" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ + "aho-corasick", "bstr", - "gix-glob", - "gix-path", - "gix-quote", - "gix-trace", - "kstring", - "smallvec", - "thiserror 2.0.18", - "unicode-bom", + "log", + "regex-automata", + "regex-syntax", ] [[package]] -name = "gix-bitmap" -version = "0.3.3" +name = "h2" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd1d118d0f5d88b96e6f6e13b566475fef4797ead4a02c26fed36c1375066f7" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ - "gix-error", + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] -name = "gix-chunk" -version = "0.7.3" +name = "hash32" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a871e5cab12ba568845714473505deefffb3c04eb47f4708ce344cd459c1cc" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ - "gix-error", + "byteorder", ] [[package]] -name = "gix-command" -version = "0.9.1" +name = "hashbrown" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00706d4fef135ef4b01680d5218c6ee40cda8baf697b864296cbc887d19118f6" -dependencies = [ - "bstr", - "gix-path", - "gix-quote", - "gix-trace", - "shell-words", -] +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] -name = "gix-commitgraph" -version = "0.37.1" +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f675d0df484a7f6a47e64bd6f311af489d947c0323b0564f36d14f3d7762abb" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "bstr", - "gix-chunk", - "gix-error", - "gix-hash", - "memmap2", - "nonempty", + "allocator-api2", + "equivalent", + "foldhash", ] [[package]] -name = "gix-config" -version = "0.58.0" +name = "hashbrown" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a29bf266c4cdaf759e535c24ad4ce655b987aeb6911075643403cc7cc5ade583" -dependencies = [ - "bstr", - "gix-config-value", - "gix-features", - "gix-glob", - "gix-path", - "gix-ref", - "gix-sec", - "smallvec", - "thiserror 2.0.18", - "unicode-bom", -] +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] -name = "gix-config-value" -version = "0.18.1" +name = "heapless" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed42168329552f6c2e5df09665c104199d45d84bedb53683738a49b57fe1baab" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" dependencies = [ - "bitflags 2.11.1", - "bstr", - "gix-path", - "libc", - "thiserror 2.0.18", + "hash32", + "stable_deref_trait", ] [[package]] -name = "gix-date" -version = "0.15.6" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e47b9e8cdc688296609b706428de570f88b1e0eed7156dde7b4a89d26fa4567" -dependencies = [ - "bstr", - "gix-error", - "itoa", - "jiff", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "gix-diff" -version = "0.65.0" +name = "hostname" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92c6d56c94edf92d78203a1cd416f770e35e10b6955ede6b9d7d0c22ff88a5f3" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ - "bstr", - "gix-command", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-imara-diff", - "gix-object", - "gix-path", - "gix-tempfile", - "gix-trace", - "gix-traverse", - "gix-worktree", - "thiserror 2.0.18", + "cfg-if", + "libc", + "windows-link", ] [[package]] -name = "gix-discover" -version = "0.53.0" +name = "http" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d624d5b23b10c1d85337645227abe353ac95ab8ff66a7bdd5ce689b2db33a722" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ - "bstr", - "dunce", - "gix-fs", - "gix-path", - "gix-ref", - "gix-sec", - "thiserror 2.0.18", + "bytes", + "itoa", ] [[package]] -name = "gix-error" -version = "0.2.5" +name = "http-body" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9292309fd944e71b2a3c96d3c03a6feb8852db646febdde7cbb9f79cb5f329" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bstr", + "bytes", + "http", ] [[package]] -name = "gix-features" -version = "0.48.1" +name = "http-body-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1849ae154d38bc403185be14fa871e38e3c93ee606875d94e207fdb9fba52dbc" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "crc32fast", - "crossbeam-channel", - "gix-path", - "gix-trace", - "gix-utils", - "libc", - "once_cell", - "parking_lot", - "prodash", - "thiserror 2.0.18", - "walkdir", - "zlib-rs", + "futures-core", + "http", + "http-body", + "pin-project-lite", ] [[package]] -name = "gix-filter" -version = "0.32.0" +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6644fb2ef97928c278675b239f366b457103d7e436f811d27331a8daf212759c" -dependencies = [ - "bstr", - "encoding_rs", - "gix-attributes", - "gix-command", - "gix-hash", - "gix-object", - "gix-packetline", - "gix-path", - "gix-quote", - "gix-trace", - "gix-utils", - "smallvec", - "thiserror 2.0.18", -] +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "gix-fs" -version = "0.21.2" +name = "httpdate" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cdff46db8798e47e2f727d84b9379aac5add3dd3d9d0b07bb4d7d5d640771fe" -dependencies = [ - "bstr", - "fastrand", - "gix-features", - "gix-path", - "gix-utils", - "thiserror 2.0.18", -] +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] -name = "gix-glob" -version = "0.26.1" +name = "hybrid-array" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1fcb8ef5b16bcf874abe9b68d8abb3c0493c876d367ab824151f30a0f3f3756" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ - "bitflags 2.11.1", - "bstr", - "gix-features", - "gix-path", + "typenum", ] [[package]] -name = "gix-hash" -version = "0.25.1" +name = "hyper" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0926d3819c837750b4e03c7754901e73f68b8c9b690753a6372a1bed4eedce" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ - "faster-hex", - "gix-features", - "sha1-checked", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-hashtable" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e261d54091f0d1c729bc83f54548c071bdec60a697de1e58e88bdfd7a99d24e" -dependencies = [ - "gix-hash", - "hashbrown 0.17.1", - "parking_lot", -] - -[[package]] -name = "gix-ignore" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d491bab9bf2c9f341dc754f425c31d5d3f63aca615312167b82e1deeaca97d8d" -dependencies = [ - "bstr", - "gix-glob", - "gix-path", - "gix-trace", - "unicode-bom", -] - -[[package]] -name = "gix-imara-diff" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1" -dependencies = [ - "bstr", - "hashbrown 0.17.1", -] - -[[package]] -name = "gix-index" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d45f82ec5a4d7542ea595e9ad16e03e26c8cb4f221e5bc9fcdcf469f63a681" -dependencies = [ - "bitflags 2.11.1", - "bstr", - "filetime", - "fnv", - "gix-bitmap", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-traverse", - "gix-utils", - "gix-validate", - "hashbrown 0.17.1", - "itoa", - "libc", - "memmap2", - "rustix", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-lock" -version = "23.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c9dedd9e90b0d47624d2ed241d394e09294118364e87b9b7e5f1fe755f3c2c" -dependencies = [ - "gix-tempfile", - "gix-utils", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-object" -version = "0.62.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "019b38afc3eac1e41f9fe09a327664b313ba4a120fa5f40e3678795d0e42783e" -dependencies = [ - "bstr", - "gix-actor", - "gix-date", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-utils", - "gix-validate", - "itoa", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-odb" -version = "0.82.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fadc59f6fa0f9dd445eceee61060a2b59ca557f48da9fc677f567db535b782a" -dependencies = [ - "arc-swap", - "gix-features", - "gix-fs", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-pack", - "gix-path", - "gix-quote", - "memmap2", - "parking_lot", - "tempfile", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-pack" -version = "0.72.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3e7f1726cd2c0cd1cf1fc20be8a8e623f0b163f1f8d6fc836cfb9bc8cd758b" -dependencies = [ - "clru", - "gix-chunk", - "gix-error", - "gix-features", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-path", - "memmap2", - "smallvec", - "thiserror 2.0.18", - "uluru", -] - -[[package]] -name = "gix-packetline" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b217dd0ee0c4021ecf169a4a519b1b4f80d15e3f3765f3dc466223dc0ac891d7" -dependencies = [ - "bstr", - "faster-hex", - "gix-trace", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed3e8d7a82e886e17a72e03d4ba0c13db6f2219b6cd4e2900b4cae426ec20c9" -dependencies = [ - "bstr", - "gix-trace", - "gix-validate", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-pathspec" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3050783b41ee11511e1e8fb35623df81806194f4030395f14f48ea37c2798c9f" -dependencies = [ - "bitflags 2.11.1", - "bstr", - "gix-attributes", - "gix-config-value", - "gix-glob", - "gix-path", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-protocol" -version = "0.63.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978468bae4ea2df20c72db3b20d0bdb548a0c1090b85a83643b553e6e0e041f2" -dependencies = [ - "bstr", - "gix-date", - "gix-features", - "gix-hash", - "gix-ref", - "gix-shallow", - "gix-transport", - "gix-utils", - "maybe-async", - "nonempty", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-quote" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e541fc33cc2b783b7979040d445a0c86a2eca747c8faea4ca84230d06ae6ef" -dependencies = [ - "bstr", - "gix-error", - "gix-utils", -] - -[[package]] -name = "gix-ref" -version = "0.65.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bbfbce1dfd7d7f8469ddef6d3518376aff664348f153cbe0fc3e58ef993d24e" -dependencies = [ - "gix-actor", - "gix-features", - "gix-fs", - "gix-hash", - "gix-lock", - "gix-object", - "gix-path", - "gix-tempfile", - "gix-utils", - "gix-validate", - "memmap2", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-refspec" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc36a4fb1a1540b59cf2da498783080743fa274b02a3f19ca444fc4015a9d4f" -dependencies = [ - "bstr", - "gix-error", - "gix-glob", - "gix-hash", - "gix-revision", - "gix-validate", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-revision" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885075c3c21eb9c06e0be3b3728ba5932c04e1c1011dcee7c81801980e3e986f" -dependencies = [ - "bstr", - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-object", - "gix-revwalk", - "nonempty", -] - -[[package]] -name = "gix-revwalk" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f11fe7ca2585193d3d70bbe0be175a2008d883a704cc7a55e454e113e689455" -dependencies = [ - "gix-commitgraph", - "gix-date", - "gix-error", - "gix-hash", - "gix-hashtable", - "gix-object", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-sec" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4fe6c152c1d50aea36f299825702cd37e303307832fec1d0fdd5844e47ce2f" -dependencies = [ - "bitflags 2.11.1", - "gix-path", - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "gix-shallow" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a292fc2fe548c5dfa575479d16b445b0ddf1dd2f56f1fec6aed386f82553cd97" -dependencies = [ - "bstr", - "gix-hash", - "gix-lock", - "nonempty", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-submodule" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f9f594f7cbda0b38ba6b633b3e9a7b7901acdc5d27bc186a16633800cd1ac8" -dependencies = [ - "bstr", - "gix-config", - "gix-path", - "gix-pathspec", - "gix-refspec", - "gix-url", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-tempfile" -version = "23.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27850097e1ff9515f46a0dad0f5f9c9d020e972727772dabab9450690c4adb22" -dependencies = [ - "dashmap", - "gix-fs", - "libc", - "parking_lot", - "tempfile", -] - -[[package]] -name = "gix-trace" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be3eb81d9dc914335923e50d52829c551feefd6a72d176c4130c546b67a60814" - -[[package]] -name = "gix-transport" -version = "0.57.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186874f7ad1fb2f9a2f2aa9c2dabc7f9dd087bef74c1a0eee2b4a9cf0248fcb3" -dependencies = [ - "bstr", - "gix-command", - "gix-features", - "gix-packetline", - "gix-quote", - "gix-sec", - "gix-url", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-traverse" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5062cca8f2977565bbaf666ec31dbdb9bc9d9293beb65f9bec52e6c1121b62a1" -dependencies = [ - "bitflags 2.11.1", - "gix-commitgraph", - "gix-date", - "gix-hash", - "gix-hashtable", - "gix-object", - "gix-revwalk", - "smallvec", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-url" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d68e70e96da0e5f9c871f1566349e0fd0e1a20bb483c7f54af1dd0b85b4b29" -dependencies = [ - "bstr", - "gix-path", - "percent-encoding", - "thiserror 2.0.18", -] - -[[package]] -name = "gix-utils" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1795bd2a970ca8b2185318c2abb97d955c71992f1cf28de73ad3b593a9f3ce8" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "unicode-normalization", -] - -[[package]] -name = "gix-validate" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a034e84d1e04e1b1f20f51f12491da230b6ac8b925d0c8e1b89bcd87a7c5ccc" -dependencies = [ - "bstr", -] - -[[package]] -name = "gix-worktree" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92399ed66f259592050c6ed9dc80105e095a2f8e87e6b83d98aa2e21d8e27036" -dependencies = [ - "bstr", - "gix-attributes", - "gix-fs", - "gix-glob", - "gix-hash", - "gix-ignore", - "gix-index", - "gix-object", - "gix-path", - "gix-validate", -] - -[[package]] -name = "gix-worktree-stream" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f3a878c89a05470ad98c644b0015777c530da24854dd29e41fe4f41176840f" -dependencies = [ - "gix-attributes", - "gix-error", - "gix-features", - "gix-filter", - "gix-fs", - "gix-hash", - "gix-object", - "gix-path", - "gix-traverse", - "parking_lot", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "globset" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heapless" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hostname" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" -dependencies = [ - "cfg-if", - "libc", - "windows-link", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hybrid-array" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" -dependencies = [ - "typenum", -] - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", ] [[package]] @@ -2136,16 +1240,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "interim" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ce9099a85f468663d3225bf87e85d0548968441e1db12248b996b24f0f5b5a" -dependencies = [ - "chrono", - "logos", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -2168,24 +1262,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" @@ -2200,104 +1276,29 @@ checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ "defmt 1.1.1", "jiff-core", - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-link", -] - -[[package]] -name = "jiff-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" -dependencies = [ - "defmt 1.1.1", -] - -[[package]] -name = "jiff-static" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - -[[package]] -name = "jj-lib" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22e2ced34a1a51f0f449bfff2cbacda1c338b2611268b5684a80251e4c8bf71" -dependencies = [ - "async-trait", - "blake2", - "bstr", - "chrono", - "clru", - "digest 0.10.7", - "dunce", - "either", - "etcetera", - "futures", - "gix", - "gix-ignore", - "globset", - "hashbrown 0.17.1", - "indexmap", - "interim", - "itertools 0.15.0", - "jj-lib-proc-macros", - "maplit", - "once_cell", - "pest", - "pest_derive", - "pollster", - "prost", - "rand 0.10.1", - "rand_chacha 0.10.0", - "rayon", - "ref-cast", - "regex", - "rustix", - "same-file", - "serde", - "smallvec", - "strsim", - "tempfile", - "thiserror 2.0.18", - "toml_edit 0.25.13+spec-1.1.0", - "tracing", - "winreg", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt 1.1.1", ] [[package]] -name = "jj-lib-proc-macros" -version = "0.43.0" +name = "jiff-static" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c21554e3755e5ecfec934d84ac64d0295d9c2f2b96f6a740a1ddb66addf4142e" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ + "jiff-core", "proc-macro2", "quote", "syn", @@ -2470,15 +1471,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "kstring" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b609e7ca5ea38f093c20a4a102335b247221c9643b7a6bc3510f196f99499a9e" -dependencies = [ - "static_assertions", -] - [[package]] name = "kvm-bindings" version = "0.12.1" @@ -2595,40 +1587,6 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" -[[package]] -name = "logos" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" -dependencies = [ - "logos-derive", -] - -[[package]] -name = "logos-codegen" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" -dependencies = [ - "beef", - "fnv", - "lazy_static", - "proc-macro2", - "quote", - "regex-syntax", - "rustc_version", - "syn", -] - -[[package]] -name = "logos-derive" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" -dependencies = [ - "logos-codegen", -] - [[package]] name = "lru" version = "0.12.5" @@ -2650,12 +1608,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "matchers" version = "0.2.0" @@ -2758,12 +1710,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nonempty" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2845,16 +1791,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - [[package]] name = "parking_lot_core" version = "0.9.12" @@ -2940,12 +1876,10 @@ dependencies = [ "clap", "env_logger", "fuser", - "jj-lib", "libc", "log", "persisting-control", "persisting-overlay-core", - "pollster", "tempfile", ] @@ -3006,7 +1940,7 @@ dependencies = [ "tokio", "tokio-util", "toml", - "toml_edit 0.22.27", + "toml_edit", "tracing", "unicode-width", "uuid", @@ -3033,48 +1967,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "pest" -version = "2.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" -dependencies = [ - "pest", -] - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -3087,12 +1979,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - [[package]] name = "portable-atomic" version = "1.13.1" @@ -3156,15 +2042,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prodash" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" -dependencies = [ - "parking_lot", -] - [[package]] name = "proptest" version = "1.11.0" @@ -3184,29 +2061,6 @@ dependencies = [ "unarray", ] -[[package]] -name = "prost" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" -dependencies = [ - "anyhow", - "itertools 0.13.0", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pulsing-actor" version = "0.1.2" @@ -3373,16 +2227,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rand_chacha" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" -dependencies = [ - "ppv-lite86", - "rand_core 0.10.1", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -3416,26 +2260,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -3456,26 +2280,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "regex" version = "1.12.3" @@ -3648,15 +2452,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -3732,15 +2527,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3766,27 +2552,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha1-checked" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" -dependencies = [ - "digest 0.10.7", - "sha1", -] - [[package]] name = "sha2" version = "0.11.0" @@ -3794,8 +2559,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -3807,12 +2572,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-words" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" - [[package]] name = "shlex" version = "1.3.0" @@ -3846,9 +2605,6 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] [[package]] name = "smoltcp" @@ -3860,7 +2616,7 @@ dependencies = [ "byteorder", "cfg-if", "defmt 0.3.100", - "heapless 0.9.3", + "heapless", "log", "managed", ] @@ -3881,12 +2637,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" @@ -4097,9 +2847,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", + "serde_spanned", + "toml_datetime", + "toml_edit", ] [[package]] @@ -4111,15 +2861,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_edit" version = "0.22.27" @@ -4128,34 +2869,10 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", + "serde_spanned", + "toml_datetime", "toml_write", - "winnow 0.7.15", -] - -[[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 1.0.2", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.2", + "winnow", ] [[package]] @@ -4164,12 +2881,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" - [[package]] name = "tower" version = "0.5.3" @@ -4324,48 +3035,18 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uluru" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" -dependencies = [ - "arrayvec", -] - [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" -[[package]] -name = "unicode-bom" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" - [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-width" version = "0.2.2" @@ -4438,12 +3119,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "virtio-bindings" version = "0.2.7" @@ -4523,16 +3198,6 @@ dependencies = [ "libc", ] -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -4713,15 +3378,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -4878,25 +3534,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" -dependencies = [ - "cfg-if", - "windows-sys 0.61.2", -] - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -5116,12 +3753,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zlib-rs" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" - [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index e1fb80a8..dcd8353b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ http-body-util = "0.1" hyper = "1" hyper-util = { version = "0.1", default-features = false } ipnet = "2" -jj-lib = "0.43.0" libc = "0.2" libloading = "0.8" libkrun = "=1.19.3" @@ -55,7 +54,6 @@ persisting-overlayfs = { path = "crates/persisting-overlayfs", default-features persisting-overlay-core = { path = "crates/persisting-overlay-core" } persisting-overlaynet = { path = "crates/persisting-overlaynet" } persisting-replay = { path = "crates/persisting-replay" } -pollster = "0.4" pulsing-actor = { version = "0.1.2", default-features = false } proptest = "1" regex = "1" diff --git a/crates/persisting-control/src/overlay.rs b/crates/persisting-control/src/overlay.rs index 39ba1b88..5547f2d7 100644 --- a/crates/persisting-control/src/overlay.rs +++ b/crates/persisting-control/src/overlay.rs @@ -63,25 +63,14 @@ pub struct OverlayRecord { } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum OverlayUpper { - Directory { - upper_dir: PathBuf, - work_dir: PathBuf, - }, - Jujutsu { - store_path: PathBuf, - workspace: String, - upper_dir: PathBuf, - }, +pub struct OverlayUpper { + pub upper_dir: PathBuf, + pub work_dir: PathBuf, } impl OverlayUpper { pub fn path(&self) -> &Path { - match self { - Self::Directory { upper_dir, .. } => upper_dir, - Self::Jujutsu { upper_dir, .. } => upper_dir, - } + &self.upper_dir } } diff --git a/crates/persisting-control/tests/overlay_contracts.rs b/crates/persisting-control/tests/overlay_contracts.rs index 57901ff5..c8c44054 100644 --- a/crates/persisting-control/tests/overlay_contracts.rs +++ b/crates/persisting-control/tests/overlay_contracts.rs @@ -49,11 +49,15 @@ fn inspection_observations_are_only_emitted_when_present() { fn legacy_overlay_and_apply_records_keep_defaults() { let overlay: OverlayRecord = serde_json::from_value(json!({ "id": "overlay-1", "target": "/target", - "upper": {"kind": "directory", "upper_dir": "/stage/upper", "work_dir": "/stage/work"}, + "upper": { "upper_dir": "/stage/upper", "work_dir": "/stage/work"}, "merged_dir": "/stage/merged", "stage_dir": "/stage", "auto_apply": false, "state": "staged" })) .unwrap(); + assert_eq!( + serde_json::to_value(&overlay.upper).unwrap(), + json!({"upper_dir": "/stage/upper", "work_dir": "/stage/work"}) + ); assert_eq!(overlay.generation, 0); assert_eq!(overlay.state, OverlayState::Staged); assert!(overlay.excluded_paths.is_empty()); diff --git a/crates/persisting-gateway/src/config.rs b/crates/persisting-gateway/src/config.rs index 1c18bf0b..8be60c02 100644 --- a/crates/persisting-gateway/src/config.rs +++ b/crates/persisting-gateway/src/config.rs @@ -64,17 +64,6 @@ pub struct OverlayConfig { /// `{capture_storage}/.overlay/{session_id}/`. #[serde(default)] pub stage_dir: Option, - /// Writable upper backend. `directory` is the default; `jujutsu` adds - /// named persistent forks in a shared repository. - #[serde(default)] - pub backend: OverlayBackend, - /// Shared Jujutsu store. All named workspaces use the same object store and - /// operation log (overrides `{storage}/.overlay/jujutsu`). - #[serde(default)] - pub jujutsu_store_path: Option, - /// Jujutsu workspace/fork name (defaults to the pVisor session id). - #[serde(default)] - pub jujutsu_workspace: Option, /// Writable upper directory (overrides `{stage_dir}/upper` when set). #[serde(default)] pub upper_dir: Option, @@ -93,14 +82,6 @@ pub struct OverlayConfig { pub auto_discard: bool, } -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum OverlayBackend { - #[default] - Directory, - Jujutsu, -} - fn default_admin_listen() -> String { "127.0.0.1:9876".to_string() } @@ -198,20 +179,6 @@ impl ProxyConfig { if self.overlay.auto_apply && self.overlay.auto_discard { anyhow::bail!("overlay auto_apply and auto_discard are mutually exclusive"); } - match self.overlay.backend { - OverlayBackend::Directory => { - if self.overlay.jujutsu_store_path.is_some() - || self.overlay.jujutsu_workspace.is_some() - { - anyhow::bail!("overlay backend `directory` cannot use Jujutsu options"); - } - } - OverlayBackend::Jujutsu => { - if self.overlay.upper_dir.is_some() || self.overlay.work_dir.is_some() { - anyhow::bail!("overlay backend `jujutsu` cannot use directory upper options"); - } - } - } let mut seen = HashSet::new(); for route in &self.models { if !seen.insert(route.name.clone()) { diff --git a/crates/persisting-overlayfs/Cargo.toml b/crates/persisting-overlayfs/Cargo.toml index 27e10d12..99439b80 100644 --- a/crates/persisting-overlayfs/Cargo.toml +++ b/crates/persisting-overlayfs/Cargo.toml @@ -7,10 +7,6 @@ license.workspace = true description = "Cross-platform FUSE overlay (macFUSE / libfuse) for pVisor staging" readme = "README.md" -[features] -default = ["jujutsu"] -jujutsu = ["dep:jj-lib", "dep:pollster"] - [[bin]] name = "persisting-overlayfs" path = "src/main.rs" @@ -23,8 +19,6 @@ persisting-control.workspace = true log.workspace = true env_logger.workspace = true clap = { workspace = true, features = ["derive"] } -jj-lib = { workspace = true, optional = true } -pollster = { workspace = true, optional = true } [target.'cfg(target_os = "macos")'.dependencies] fuser = { workspace = true, features = ["abi-7-19", "libfuse", "macfuse-5"] } diff --git a/crates/persisting-overlayfs/README.md b/crates/persisting-overlayfs/README.md index 30f9c2d1..6a24fdd3 100644 --- a/crates/persisting-overlayfs/README.md +++ b/crates/persisting-overlayfs/README.md @@ -3,7 +3,7 @@ **Cross-platform FUSE overlay for pVisor staging (macFUSE / libfuse).** Owns the unprivileged, in-process FUSE overlay: ordered multi-`lowerdir` merge, -directory or Jujutsu upper, portable `.wh.*` whiteouts, and the optional +directory upper, portable `.wh.*` whiteouts, and the optional standalone `persisting-overlayfs` diagnostic CLI. Does not own review, apply, drop, or Run lifecycle. diff --git a/crates/persisting-overlayfs/src/jj_backend.rs b/crates/persisting-overlayfs/src/jj_backend.rs deleted file mode 100644 index 758eddfb..00000000 --- a/crates/persisting-overlayfs/src/jj_backend.rs +++ /dev/null @@ -1,300 +0,0 @@ -//! Jujutsu-managed directory uppers. -//! -//! Each overlay fork is a Jujutsu workspace. All workspaces point at the same -//! repository, so their working-copy commits, operation log, and Git objects -//! live in one store while the writable POSIX directories remain independent. - -use jj_lib::config::StackedConfig; -use jj_lib::gitignore::GitIgnoreFile; -use jj_lib::lock::FileLock; -use jj_lib::matchers::EverythingMatcher; -use jj_lib::object_id::ObjectId as _; -use jj_lib::ref_name::{WorkspaceName, WorkspaceNameBuf}; -use jj_lib::repo::{Repo as _, StoreFactories}; -use jj_lib::settings::UserSettings; -use jj_lib::working_copy::SnapshotOptions; -use jj_lib::workspace::{Workspace, default_working_copy_factories, default_working_copy_factory}; -use pollster::FutureExt as _; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; - -const CONTROL_DIR: &str = "control"; -const WORKSPACES_DIR: &str = "workspaces"; -const UPPER_DIR: &str = "upper"; - -fn io_other(error: impl std::fmt::Display) -> io::Error { - io::Error::other(error.to_string()) -} - -fn settings() -> io::Result { - UserSettings::from_config(StackedConfig::with_defaults()).map_err(io_other) -} - -fn validate_fork(fork: &str) -> io::Result<()> { - if fork.is_empty() - || fork == "." - || fork == ".." - || fork.contains('/') - || fork.contains('\\') - || fork.as_bytes().contains(&0) - { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("invalid Jujutsu overlay workspace name: {fork:?}"), - )); - } - Ok(()) -} - -fn load_workspace(settings: &UserSettings, root: &Path) -> io::Result { - Workspace::load( - settings, - root, - &StoreFactories::default(), - &default_working_copy_factories(), - ) - .map_err(io_other) -} - -/// One writable OverlayFS fork backed by a workspace in a shared Jujutsu repo. -pub(crate) struct JujutsuWorkspace { - store_path: PathBuf, - workspace_root: PathBuf, - upper_dir: PathBuf, - fork: WorkspaceNameBuf, - // Held for the lifetime of a writable mount. Jujutsu's normal working-copy - // lock only covers an individual snapshot, while FUSE writes happen between - // snapshots and must not have two writers for the same workspace. - _mount_lock: Option, -} - -impl std::fmt::Debug for JujutsuWorkspace { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("JujutsuWorkspace") - .field("store_path", &self.store_path) - .field("workspace_root", &self.workspace_root) - .field("upper_dir", &self.upper_dir) - .field("fork", &self.fork.as_str()) - .finish_non_exhaustive() - } -} - -impl JujutsuWorkspace { - pub(crate) fn open(store_path: PathBuf, fork: String, read_only: bool) -> io::Result { - validate_fork(&fork)?; - fs::create_dir_all(&store_path)?; - let store_path = fs::canonicalize(store_path)?; - let control_root = store_path.join(CONTROL_DIR); - let workspace_root = store_path.join(WORKSPACES_DIR).join(&fork); - let upper_dir = workspace_root.join(UPPER_DIR); - let settings = settings()?; - - // Serialize repository/workspace creation across processes. The lock is - // dropped before the mount begins; Jujutsu handles later op-log writes. - let _init_lock = FileLock::lock(store_path.join("init.lock")).map_err(io_other)?; - if !control_root.join(".jj").is_dir() { - fs::create_dir_all(&control_root)?; - Workspace::init_internal_git(&settings, &control_root) - .block_on() - .map_err(io_other)?; - } - if !workspace_root.join(".jj").is_dir() { - fs::create_dir_all(&workspace_root)?; - let control = load_workspace(&settings, &control_root)?; - let repo = control - .repo_loader() - .load_at_head() - .block_on() - .map_err(io_other)?; - Workspace::init_workspace_with_existing_repo( - &workspace_root, - control.repo_path(), - &repo, - &*default_working_copy_factory(), - WorkspaceNameBuf::from(fork.clone()), - ) - .block_on() - .map_err(io_other)?; - } - fs::create_dir_all(&upper_dir)?; - let _mount_lock = if read_only { - None - } else { - Some( - FileLock::try_lock(workspace_root.join(".jj").join("persisting-overlay.lock")) - .map_err(io_other)? - .ok_or_else(|| { - io::Error::new( - io::ErrorKind::WouldBlock, - format!( - "Jujutsu overlay workspace {fork:?} is already mounted writable" - ), - ) - })?, - ) - }; - - Ok(Self { - store_path, - workspace_root, - upper_dir, - fork: WorkspaceNameBuf::from(fork), - _mount_lock, - }) - } - - pub(crate) fn upper_dir(&self) -> &Path { - &self.upper_dir - } - - /// Snapshot the upper directory into this workspace's working-copy commit. - pub(crate) fn snapshot(&self) -> io::Result> { - snapshot_workspace(&self.workspace_root, &self.fork) - } -} - -fn snapshot_workspace( - workspace_root: &Path, - expected_name: &WorkspaceName, -) -> io::Result> { - let settings = settings()?; - let mut workspace = load_workspace(&settings, workspace_root)?; - if workspace.workspace_name() != expected_name { - return Err(io::Error::other(format!( - "Jujutsu workspace name mismatch: expected {:?}, found {:?}", - expected_name.as_str(), - workspace.workspace_name().as_str() - ))); - } - let repo = workspace - .repo_loader() - .load_at_head() - .block_on() - .map_err(io_other)?; - let wc_commit_id = repo - .view() - .get_wc_commit_id(expected_name) - .ok_or_else(|| io::Error::other("Jujutsu workspace has no working-copy commit"))? - .clone(); - let old_commit = repo.store().get_commit(&wc_commit_id).map_err(io_other)?; - - let everything = EverythingMatcher; - let options = SnapshotOptions { - base_ignores: GitIgnoreFile::empty(), - progress: None, - start_tracking_matcher: &everything, - // Overlay snapshots are exact filesystem state, not source-control - // intent. A file hidden by an upper-layer .gitignore must still be - // recoverable from this workspace head. - force_tracking_matcher: &everything, - max_new_file_size: u64::MAX, - }; - let mut locked = workspace - .start_working_copy_mutation() - .block_on() - .map_err(io_other)?; - let (new_tree, _stats) = locked - .locked_wc() - .snapshot(&options) - .block_on() - .map_err(io_other)?; - - if new_tree.tree_ids() == old_commit.tree().tree_ids() { - locked - .finish(repo.operation().id().clone()) - .block_on() - .map_err(io_other)?; - return Ok(None); - } - - let mut transaction = repo.start_transaction(); - transaction.set_workspace_name(expected_name); - transaction.set_is_snapshot(true); - let new_commit = transaction - .repo_mut() - .rewrite_commit(&old_commit) - .set_tree(new_tree) - .write() - .block_on() - .map_err(io_other)?; - transaction - .repo_mut() - .rebase_descendants() - .block_on() - .map_err(io_other)?; - let new_repo = transaction - .commit(format!( - "snapshot persisting OverlayFS workspace {}", - expected_name.as_str() - )) - .block_on() - .map_err(io_other)?; - locked - .finish(new_repo.operation().id().clone()) - .block_on() - .map_err(io_other)?; - Ok(Some(new_commit.id().hex())) -} - -/// Snapshot a named fork after an out-of-band apply or discard operation. -pub fn snapshot_jujutsu_upper(store_path: &Path, fork: &str) -> io::Result> { - let workspace = JujutsuWorkspace::open(store_path.to_owned(), fork.to_owned(), false)?; - workspace.snapshot() -} - -/// Deterministic directory used as the live upper for a named fork. -pub fn jujutsu_upper_dir(store_path: &Path, fork: &str) -> io::Result { - validate_fork(fork)?; - Ok(store_path.join(WORKSPACES_DIR).join(fork).join(UPPER_DIR)) -} - -/// Initialize a Jujutsu-backed upper for a mountless virtio-fs consumer. -pub fn prepare_jujutsu_upper(store_path: &Path, fork: &str) -> io::Result { - let workspace = JujutsuWorkspace::open(store_path.to_owned(), fork.to_owned(), false)?; - Ok(workspace.upper_dir().to_path_buf()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn two_forks_share_one_repository_and_keep_independent_heads() { - let temp = tempfile::tempdir().unwrap(); - let store = temp.path().join("overlay.jj"); - let first = JujutsuWorkspace::open(store.clone(), "first".into(), false).unwrap(); - fs::write(first.upper_dir().join("value"), b"first").unwrap(); - let first_commit = first.snapshot().unwrap().unwrap(); - drop(first); - - let second = JujutsuWorkspace::open(store.clone(), "second".into(), false).unwrap(); - fs::write(second.upper_dir().join("value"), b"second").unwrap(); - let second_commit = second.snapshot().unwrap().unwrap(); - assert_ne!(first_commit, second_commit); - assert_eq!( - fs::read(store.join("workspaces/first/upper/value")).unwrap(), - b"first" - ); - assert_eq!( - fs::read(store.join("workspaces/second/upper/value")).unwrap(), - b"second" - ); - - let settings = settings().unwrap(); - let control = load_workspace(&settings, &store.join(CONTROL_DIR)).unwrap(); - let repo = control.repo_loader().load_at_head().block_on().unwrap(); - let workspaces = repo.view().wc_commit_ids(); - assert!(workspaces.contains_key(&WorkspaceNameBuf::from("first"))); - assert!(workspaces.contains_key(&WorkspaceNameBuf::from("second"))); - assert_eq!(control.repo_path(), store.join("control/.jj/repo")); - } - - #[test] - fn fork_name_cannot_escape_store() { - let temp = tempfile::tempdir().unwrap(); - assert!(JujutsuWorkspace::open(temp.path().into(), "../escape".into(), false).is_err()); - assert!(jujutsu_upper_dir(temp.path(), "nested/name").is_err()); - } -} diff --git a/crates/persisting-overlayfs/src/jj_disabled.rs b/crates/persisting-overlayfs/src/jj_disabled.rs deleted file mode 100644 index c8fec721..00000000 --- a/crates/persisting-overlayfs/src/jj_disabled.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Lightweight build stub for the optional Jujutsu upper backend. - -use std::io; -use std::path::{Path, PathBuf}; - -const WORKSPACES_DIR: &str = "workspaces"; -const UPPER_DIR: &str = "upper"; - -fn unsupported() -> io::Error { - io::Error::new( - io::ErrorKind::Unsupported, - "the Jujutsu OverlayFS backend is not compiled in; enable the `jujutsu` feature", - ) -} - -fn validate_fork(fork: &str) -> io::Result<()> { - if fork.is_empty() - || fork == "." - || fork == ".." - || fork.contains('/') - || fork.contains('\\') - || fork.as_bytes().contains(&0) - { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("invalid Jujutsu overlay workspace name: {fork:?}"), - )); - } - Ok(()) -} - -#[derive(Debug)] -pub(crate) struct JujutsuWorkspace { - upper_dir: PathBuf, -} - -impl JujutsuWorkspace { - pub(crate) fn open(_store_path: PathBuf, _fork: String, _read_only: bool) -> io::Result { - Err(unsupported()) - } - - pub(crate) fn upper_dir(&self) -> &Path { - &self.upper_dir - } - - pub(crate) fn snapshot(&self) -> io::Result> { - Err(unsupported()) - } -} - -pub fn snapshot_jujutsu_upper(_store_path: &Path, _fork: &str) -> io::Result> { - Err(unsupported()) -} - -/// Return the deterministic upper path without initializing a Jujutsu store. -/// Actual mounting still fails with an explicit feature error in this build. -pub fn jujutsu_upper_dir(store_path: &Path, fork: &str) -> io::Result { - validate_fork(fork)?; - Ok(store_path.join(WORKSPACES_DIR).join(fork).join(UPPER_DIR)) -} - -pub fn prepare_jujutsu_upper(_store_path: &Path, _fork: &str) -> io::Result { - Err(unsupported()) -} diff --git a/crates/persisting-overlayfs/src/lib.rs b/crates/persisting-overlayfs/src/lib.rs index 8cf502b6..1814117e 100644 --- a/crates/persisting-overlayfs/src/lib.rs +++ b/crates/persisting-overlayfs/src/lib.rs @@ -6,38 +6,19 @@ mod fs; mod observation; -pub use observation::FsMetrics; -#[cfg(feature = "jujutsu")] -mod jj_backend; -#[cfg(not(feature = "jujutsu"))] -#[path = "jj_disabled.rs"] -mod jj_backend; - use anyhow::{Context, Result, bail}; use fs::OverlayFs; use fuser::{BackgroundSession, MountOption, Session}; -use jj_backend::JujutsuWorkspace; -pub use jj_backend::{jujutsu_upper_dir, prepare_jujutsu_upper, snapshot_jujutsu_upper}; +pub use observation::FsMetrics; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::time::Duration; -#[derive(Clone, Debug)] -pub enum UpperBackend { - Directory { - upper_dir: PathBuf, - work_dir: Option, - }, - Jujutsu { - store_path: PathBuf, - workspace: String, - }, -} - #[derive(Clone, Debug)] pub struct OverlayMountConfig { pub lower_dirs: Vec, - pub upper: UpperBackend, + pub upper_dir: PathBuf, + pub work_dir: Option, pub mountpoint: PathBuf, pub allow_other: bool, pub allow_root: bool, @@ -67,10 +48,8 @@ impl OverlayMountConfig { ) -> Self { Self { lower_dirs, - upper: UpperBackend::Directory { - upper_dir, - work_dir, - }, + upper_dir, + work_dir, mountpoint, allow_other: false, allow_root: false, @@ -85,26 +64,11 @@ impl OverlayMountConfig { observation: None, } } - - pub fn new_jujutsu( - lower_dirs: Vec, - store_path: PathBuf, - workspace: String, - mountpoint: PathBuf, - ) -> Self { - let mut config = Self::new(lower_dirs, PathBuf::new(), None, mountpoint); - config.upper = UpperBackend::Jujutsu { - store_path, - workspace, - }; - config - } } #[derive(Debug)] pub struct OverlaySession { background: Option, - jujutsu: Option, mountpoint: PathBuf, } @@ -139,11 +103,6 @@ impl OverlaySession { bail!("FUSE mount did not detach: {}", self.mountpoint.display()); } } - if let Some(workspace) = self.jujutsu.take() { - workspace - .snapshot() - .context("snapshot Jujutsu overlay workspace")?; - } Ok(()) } } @@ -157,14 +116,13 @@ impl Drop for OverlaySession { pub fn mount(config: OverlayMountConfig) -> Result { #[cfg(target_os = "macos")] check_fskit_version(&config)?; - let (filesystem, mountpoint, options, jujutsu) = prepare(config)?; + let (filesystem, mountpoint, options) = prepare(config)?; let session = Session::new(filesystem, &mountpoint, &options) .with_context(|| format!("mount {}", mountpoint.display()))?; let background = BackgroundSession::new(session).context("start FUSE request loop")?; log::info!("persisting-overlayfs mounted at {}", mountpoint.display()); Ok(OverlaySession { background: Some(background), - jujutsu, mountpoint, }) } @@ -172,16 +130,11 @@ pub fn mount(config: OverlayMountConfig) -> Result { pub fn run_foreground(config: OverlayMountConfig) -> Result<()> { #[cfg(target_os = "macos")] check_fskit_version(&config)?; - let (filesystem, mountpoint, options, jujutsu) = prepare(config)?; + let (filesystem, mountpoint, options) = prepare(config)?; log::info!("persisting-overlayfs mounted at {}", mountpoint.display()); let mut session = Session::new(filesystem, &mountpoint, &options) .with_context(|| format!("mount {}", mountpoint.display()))?; session.run().context("FUSE session")?; - if let Some(workspace) = jujutsu { - workspace - .snapshot() - .context("snapshot Jujutsu overlay workspace")?; - } Ok(()) } @@ -218,33 +171,15 @@ fn require_fskit_version(version: &str) -> Result<()> { Ok(()) } -fn prepare( - mut config: OverlayMountConfig, -) -> Result<( - OverlayFs, - PathBuf, - Vec, - Option, -)> { +fn prepare(mut config: OverlayMountConfig) -> Result<(OverlayFs, PathBuf, Vec)> { if config.lower_dirs.is_empty() { bail!("lowerdir must list at least one path"); } - match &config.upper { - UpperBackend::Directory { - upper_dir, - work_dir, - } => { - std::fs::create_dir_all(upper_dir) - .with_context(|| format!("create upperdir {}", upper_dir.display()))?; - if let Some(work) = work_dir { - std::fs::create_dir_all(work) - .with_context(|| format!("create workdir {}", work.display()))?; - } - } - UpperBackend::Jujutsu { store_path, .. } => { - std::fs::create_dir_all(store_path) - .with_context(|| format!("create Jujutsu store {}", store_path.display()))?; - } + std::fs::create_dir_all(&config.upper_dir) + .with_context(|| format!("create upperdir {}", config.upper_dir.display()))?; + if let Some(work) = &config.work_dir { + std::fs::create_dir_all(work) + .with_context(|| format!("create workdir {}", work.display()))?; } let fskit = config.backend.as_deref() == Some("fskit"); if let Some(backend) = &config.backend @@ -262,25 +197,12 @@ fn prepare( .with_context(|| format!("create mountpoint {}", config.mountpoint.display()))?; } - config.upper = match config.upper { - UpperBackend::Directory { - upper_dir, - work_dir, - } => UpperBackend::Directory { - upper_dir: std::fs::canonicalize(upper_dir)?, - work_dir: work_dir - .map(std::fs::canonicalize) - .transpose() - .context("canonicalize workdir")?, - }, - UpperBackend::Jujutsu { - store_path, - workspace, - } => UpperBackend::Jujutsu { - store_path: std::fs::canonicalize(store_path)?, - workspace, - }, - }; + config.upper_dir = std::fs::canonicalize(config.upper_dir)?; + config.work_dir = config + .work_dir + .map(std::fs::canonicalize) + .transpose() + .context("canonicalize workdir")?; config.lower_dirs = config .lower_dirs .into_iter() @@ -317,16 +239,9 @@ fn prepare( if !lower.is_dir() { bail!("lowerdir is not a directory: {}", lower.display()); } - let upper_overlaps = match &config.upper { - UpperBackend::Directory { upper_dir, .. } => { - (upper_dir.starts_with(lower) && !hidden_from_lower(lower, upper_dir)) - || lower.starts_with(upper_dir) - } - UpperBackend::Jujutsu { store_path, .. } => { - (store_path.starts_with(lower) && !hidden_from_lower(lower, store_path)) - || lower.starts_with(store_path) - } - }; + let upper_dir = &config.upper_dir; + let upper_overlaps = (upper_dir.starts_with(lower) && !hidden_from_lower(lower, upper_dir)) + || lower.starts_with(upper_dir); let mount_overlaps = (mountpoint.starts_with(lower) && !hidden_from_lower(lower, &mountpoint)) || lower.starts_with(&mountpoint); @@ -337,11 +252,9 @@ fn prepare( ); } } - if let UpperBackend::Directory { - upper_dir, - work_dir, - } = &config.upper { + let upper_dir = &config.upper_dir; + let work_dir = &config.work_dir; if mountpoint.starts_with(upper_dir) || upper_dir.starts_with(&mountpoint) { bail!("upperdir and mountpoint must not overlap"); } @@ -368,47 +281,16 @@ fn prepare( } } - let mut jujutsu = None; - let preimage_dir = config.preimage_dir; - let filesystem = match config.upper { - UpperBackend::Directory { - upper_dir, - work_dir, - } => { - if config.excluded_paths.is_empty() && preimage_dir.is_none() { - OverlayFs::new(config.lower_dirs, upper_dir, work_dir)? - } else { - OverlayFs::new_with_exclusions_and_preimages( - config.lower_dirs, - upper_dir, - work_dir, - config.excluded_paths, - preimage_dir, - )? - } - } - UpperBackend::Jujutsu { - store_path, - workspace, - } => { - let workspace = JujutsuWorkspace::open(store_path, workspace, config.read_only)?; - let upper_dir = workspace.upper_dir().to_path_buf(); - let filesystem = if config.excluded_paths.is_empty() && preimage_dir.is_none() { - OverlayFs::new(config.lower_dirs, upper_dir, None)? - } else { - OverlayFs::new_with_exclusions_and_preimages( - config.lower_dirs, - upper_dir, - None, - config.excluded_paths, - preimage_dir, - )? - }; - if !config.read_only { - jujutsu = Some(workspace); - } - filesystem - } + let filesystem = if config.excluded_paths.is_empty() && config.preimage_dir.is_none() { + OverlayFs::new(config.lower_dirs, config.upper_dir, config.work_dir)? + } else { + OverlayFs::new_with_exclusions_and_preimages( + config.lower_dirs, + config.upper_dir, + config.work_dir, + config.excluded_paths, + config.preimage_dir, + )? } .with_private_root(fskit && !config.allow_other) .with_read_only(config.read_only) @@ -436,7 +318,7 @@ fn prepare( if config.read_only { options.push(MountOption::RO); } - Ok((filesystem, mountpoint, options, jujutsu)) + Ok((filesystem, mountpoint, options)) } /// Check the mount table without issuing requests to an unresponsive FSKit server. @@ -504,16 +386,13 @@ mod mount_config_tests { } #[test] - fn platform_default_backend_applies_to_directory_and_jujutsu_mounts() { + fn platform_default_backend_applies_to_mounts() { let expected = if cfg!(target_os = "macos") { Some("fskit") } else { None }; let directory = OverlayMountConfig::new(vec![], "upper".into(), None, "merged".into()); - let jujutsu = - OverlayMountConfig::new_jujutsu(vec![], "store".into(), "test".into(), "merged".into()); assert_eq!(directory.backend.as_deref(), expected); - assert_eq!(jujutsu.backend.as_deref(), expected); } } diff --git a/crates/persisting-overlayfs/src/main.rs b/crates/persisting-overlayfs/src/main.rs index 27b6504f..71339eaa 100644 --- a/crates/persisting-overlayfs/src/main.rs +++ b/crates/persisting-overlayfs/src/main.rs @@ -19,7 +19,7 @@ use std::path::PathBuf; about = "Cross-platform FUSE overlay for pVisor (macFUSE / libfuse)" )] struct Args { - /// Mount options: lowerdir=a:b plus upperdir=u or jjstore=s,jjworkspace=w. + /// Mount options: lowerdir=a:b plus upperdir=u. #[arg(short = 'o', long = "options", value_name = "OPTS")] #[arg(required = true)] options: Vec, @@ -34,9 +34,7 @@ struct Args { #[derive(Debug)] struct MountOpts { lowerdir: Vec, - upperdir: Option, - jjstore: Option, - jjworkspace: Option, + upperdir: PathBuf, workdir: Option, allow_other: bool, allow_root: bool, @@ -72,8 +70,6 @@ fn split_escaped(raw: &str, separator: char) -> Vec { fn parse_options(raw: &str) -> Result { let mut lowerdir = None; let mut upperdir = None; - let mut jjstore = None; - let mut jjworkspace = None; let mut workdir = None; let mut allow_other = false; let mut allow_root = false; @@ -109,8 +105,6 @@ fn parse_options(raw: &str) -> Result { ); } "upperdir" => upperdir = Some(PathBuf::from(v)), - "jjstore" => jjstore = Some(PathBuf::from(v)), - "jjworkspace" => jjworkspace = Some(v.to_owned()), "workdir" => workdir = Some(PathBuf::from(v)), "fsname" => fsname = v.to_string(), "backend" if matches!(v, "kernel" | "fskit") => backend = Some(v.to_string()), @@ -122,27 +116,10 @@ fn parse_options(raw: &str) -> Result { if lowerdir.is_empty() { bail!("lowerdir must list at least one path"); } - let backend_count = usize::from(upperdir.is_some()) + usize::from(jjstore.is_some()); - if backend_count == 0 { - bail!("missing upper backend: specify upperdir= or jjstore="); - } - if backend_count != 1 { - bail!("upperdir= and jjstore= are mutually exclusive"); - } - if upperdir.is_none() && workdir.is_some() { - bail!("workdir= is only valid with the directory upper backend"); - } - if jjstore.is_some() && jjworkspace.as_deref().is_none_or(str::is_empty) { - bail!("jjworkspace= is required with jjstore="); - } - if jjstore.is_none() && jjworkspace.is_some() { - bail!("jjworkspace= is only valid with jjstore="); - } + let upperdir = upperdir.context("missing upperdir=")?; Ok(MountOpts { lowerdir, upperdir, - jjstore, - jjworkspace, workdir, allow_other, allow_root, @@ -167,19 +144,8 @@ fn main() -> Result<()> { .init(); let opts = parse_options(&args.options.join(","))?; - let mut config = match (opts.upperdir, opts.jjstore) { - (Some(upperdir), None) => { - OverlayMountConfig::new(opts.lowerdir, upperdir, opts.workdir, args.mountpoint) - } - (None, Some(store)) => OverlayMountConfig::new_jujutsu( - opts.lowerdir, - store, - opts.jjworkspace - .expect("parse_options requires jjworkspace"), - args.mountpoint, - ), - _ => unreachable!("parse_options validates the upper backend"), - }; + let mut config = + OverlayMountConfig::new(opts.lowerdir, opts.upperdir, opts.workdir, args.mountpoint); config.allow_other = opts.allow_other; config.allow_root = opts.allow_root; config.default_permissions = opts.default_permissions; @@ -223,20 +189,4 @@ mod tests { assert_eq!(options.workdir, Some(PathBuf::from("/work"))); assert_eq!(args.mountpoint, PathBuf::from("/merged")); } - - #[test] - fn jujutsu_upper_requires_store_and_workspace() { - let options = - parse_options("lowerdir=/lower,jjstore=/shared/overlay.jj,jjworkspace=attempt-1") - .expect("Jujutsu options"); - assert_eq!(options.jjstore, Some(PathBuf::from("/shared/overlay.jj"))); - assert_eq!(options.jjworkspace.as_deref(), Some("attempt-1")); - assert!(parse_options("lowerdir=/lower,jjstore=/shared/overlay.jj").is_err()); - assert!( - parse_options( - "lowerdir=/lower,jjstore=/shared/overlay.jj,jjworkspace=x,upperdir=/upper" - ) - .is_err() - ); - } } diff --git a/crates/persisting-pvisor/Cargo.toml b/crates/persisting-pvisor/Cargo.toml index 497ac67c..1b64df59 100644 --- a/crates/persisting-pvisor/Cargo.toml +++ b/crates/persisting-pvisor/Cargo.toml @@ -10,7 +10,6 @@ build = "build.rs" [features] default = [] -jujutsu-overlay = ["persisting-overlayfs/jujutsu"] [dependencies] anyhow.workspace = true diff --git a/crates/persisting-pvisor/README.md b/crates/persisting-pvisor/README.md index aa3622fe..63981af4 100644 --- a/crates/persisting-pvisor/README.md +++ b/crates/persisting-pvisor/README.md @@ -33,9 +33,6 @@ temporary stage that is removed at Job exit unless `--stage PATH` retains it; removing that stage also removes its Run Bundle. Capture is a Gateway capability, not a second product. -The default build excludes Jujutsu. Use `jujutsu-overlay` for the Jujutsu upper -backend. Default features are empty. - ## Develop ```bash diff --git a/crates/persisting-pvisor/src/cli/env.rs b/crates/persisting-pvisor/src/cli/env.rs index 6dea30f6..57fcc257 100644 --- a/crates/persisting-pvisor/src/cli/env.rs +++ b/crates/persisting-pvisor/src/cli/env.rs @@ -5,8 +5,7 @@ use std::path::PathBuf; use std::process::Command; use anyhow::{Context, Result, bail}; -use clap::{Args, Subcommand, ValueEnum}; -use persisting_overlayfs::jujutsu_upper_dir; +use clap::{Args, Subcommand}; use crate::runtime::{ ApplySelection, OverlayRecord, OverlayState, OverlayUpper, RunLease, RunRecord, all_runs, @@ -46,13 +45,6 @@ enum EnvCommand { Delete(DeleteArgs), } -#[derive(Debug, Clone, Copy, Default, ValueEnum)] -enum EnvBackend { - #[default] - Directory, - Jujutsu, -} - #[derive(Debug, Args)] struct CreateArgs { /// Stable environment name. @@ -63,11 +55,6 @@ struct CreateArgs { /// Root containing all pVisor environments. #[arg(long, value_name = "DIR", env = "PERSISTING_ENV_HOME")] root: Option, - #[arg(long, value_enum, default_value_t = EnvBackend::Directory)] - backend: EnvBackend, - /// Shared Jujutsu store (defaults to `/.jujutsu`). - #[arg(long, value_name = "DIR")] - jujutsu_store: Option, #[arg(long, default_value = "agent")] agent: String, } @@ -184,21 +171,13 @@ fn create(args: CreateArgs) -> Result { target.display() ); fs::create_dir_all(&stage)?; - let jujutsu_store = args.jujutsu_store.unwrap_or_else(|| root.join(".jujutsu")); let overlay = OverlayRecord { id: args.name.clone(), generation: 0, target: target.clone(), - upper: match args.backend { - EnvBackend::Directory => OverlayUpper::Directory { - upper_dir: stage.join("upper"), - work_dir: stage.join("work"), - }, - EnvBackend::Jujutsu => OverlayUpper::Jujutsu { - upper_dir: jujutsu_upper_dir(&jujutsu_store, &args.name)?, - store_path: jujutsu_store, - workspace: args.name.clone(), - }, + upper: OverlayUpper { + upper_dir: stage.join("upper"), + work_dir: stage.join("work"), }, merged_dir: stage.join("merged"), stage_dir: stage.clone(), @@ -561,8 +540,6 @@ mod tests { name: "demo".into(), target, root: Some(root.clone()), - backend: EnvBackend::Directory, - jujutsu_store: None, agent: "test".into(), })?; let select = SelectArgs { @@ -597,8 +574,6 @@ mod tests { name: "demo".into(), target: target.clone(), root: Some(root.clone()), - backend: EnvBackend::Directory, - jujutsu_store: None, agent: "test".into(), })?; let select = SelectArgs { @@ -607,9 +582,7 @@ mod tests { }; let record = selected(&select)?; let overlay = record.overlay.context("overlay")?; - let OverlayUpper::Directory { upper_dir, .. } = &overlay.upper else { - unreachable!("directory fixture") - }; + let OverlayUpper { upper_dir, .. } = &overlay.upper; fs::create_dir_all(upper_dir)?; fs::write(upper_dir.join("committed.txt"), b"value")?; fs::write(upper_dir.join("later.txt"), b"later")?; @@ -643,9 +616,7 @@ mod tests { assert_eq!(after_apply.state, OverlayState::Staged); assert_eq!(after_apply.generation, 1); - let OverlayUpper::Directory { upper_dir, .. } = &after_apply.upper else { - unreachable!("directory fixture") - }; + let OverlayUpper { upper_dir, .. } = &after_apply.upper; fs::create_dir_all(upper_dir)?; fs::write(upper_dir.join("discarded.txt"), b"value")?; drop_changes(select.clone())?; @@ -673,8 +644,6 @@ mod tests { name: "recover".into(), target, root: Some(root.clone()), - backend: EnvBackend::Directory, - jujutsu_store: None, agent: "test".into(), })?; let select = SelectArgs { diff --git a/crates/persisting-pvisor/src/cli/replay.rs b/crates/persisting-pvisor/src/cli/replay.rs index d777ecea..a91f4ce2 100644 --- a/crates/persisting-pvisor/src/cli/replay.rs +++ b/crates/persisting-pvisor/src/cli/replay.rs @@ -13,10 +13,9 @@ use persisting_replay::{ use serde_json::json; use crate::config::{ - OverlayFsBackend as PVisorOverlayFsBackend, OverlayFsCommit as PVisorOverlayFsCommit, - OverlayFsSettings, OverlayNetMode as PVisorOverlayNetMode, - OverlayNetPolicy as PVisorOverlayNetPolicy, RunConfig as PVisorRunConfig, RunExecutorKind, - RunPolicy, + OverlayFsCommit as PVisorOverlayFsCommit, OverlayFsSettings, + OverlayNetMode as PVisorOverlayNetMode, OverlayNetPolicy as PVisorOverlayNetPolicy, + RunConfig as PVisorRunConfig, RunExecutorKind, RunPolicy, }; #[derive(Debug, Clone, Args)] @@ -133,10 +132,6 @@ pub struct ReplayArgs { #[arg(long, value_name = "DIR")] overlayfs_compose: Vec, - /// Outer OverlayFS backend: directory or jujutsu. - #[arg(long, value_name = "BACKEND")] - overlayfs_backend: Option, - /// Outer OverlayFS commit behavior: manual, apply, or drop. #[arg(long, value_name = "MODE")] overlayfs_commit: Option, @@ -217,7 +212,6 @@ fn needs_managed_run(config: &ReplayToml) -> bool { || !config.run.pass_env.is_empty() || config.overlayfs.path.is_some() || !config.overlayfs.compose.is_empty() - || config.overlayfs.backend.is_some() || config.overlayfs.commit.is_some() || config.overlaynet.mode.is_some() || config.overlaynet.policy.is_some() @@ -232,7 +226,6 @@ fn direct_managed_requested(args: &ReplayArgs) -> bool { || !args.pass_env.is_empty() || args.overlayfs_path.is_some() || !args.overlayfs_compose.is_empty() - || args.overlayfs_backend.is_some() || args.overlayfs_commit.is_some() || args.overlaynet.is_some() || args.overlaynet_policy.is_some() @@ -282,7 +275,6 @@ fn direct_managed_config(args: &ReplayArgs) -> Result { overlayfs: ReplayOverlayFsConfig { path: args.overlayfs_path.clone(), compose: args.overlayfs_compose.clone(), - backend: args.overlayfs_backend.clone(), commit: args.overlayfs_commit.clone(), }, overlaynet: ReplayOverlayNetConfig { @@ -324,7 +316,6 @@ fn run_managed(config: &ReplayToml) -> Result { if config.overlayfs.path.is_some() || !config.overlayfs.compose.is_empty() - || config.overlayfs.backend.is_some() || config.overlayfs.commit.is_some() { let mut overlay = OverlayFsSettings { @@ -332,15 +323,6 @@ fn run_managed(config: &ReplayToml) -> Result { compose: config.overlayfs.compose.clone(), ..OverlayFsSettings::default() }; - overlay.backend = match config.overlayfs.backend.as_deref().unwrap_or("directory") { - "directory" => PVisorOverlayFsBackend::Directory, - "jujutsu" => PVisorOverlayFsBackend::Jujutsu, - other => { - return Err(ReplayError::configuration(format!( - "unsupported overlayfs.backend {other:?}" - ))); - } - }; overlay.commit = match config.overlayfs.commit.as_deref().unwrap_or("manual") { "manual" => PVisorOverlayFsCommit::Manual, "apply" => PVisorOverlayFsCommit::Apply, @@ -568,7 +550,6 @@ fn reject_direct(args: &ReplayArgs) -> Result<(), ReplayError> { || !args.pass_env.is_empty() || args.overlayfs_path.is_some() || !args.overlayfs_compose.is_empty() - || args.overlayfs_backend.is_some() || args.overlayfs_commit.is_some() || args.overlaynet.is_some() || args.overlaynet_policy.is_some(); @@ -639,7 +620,6 @@ pass_env = ["OPENAI_BASE_URL", "OPENAI_API_KEY", "MODEL_NAME"] [overlayfs] path = "/workspace" compose = ["/workspace"] -backend = "directory" commit = "manual" [overlaynet] diff --git a/crates/persisting-pvisor/src/cli/run.rs b/crates/persisting-pvisor/src/cli/run.rs index 59d7099d..5684054f 100644 --- a/crates/persisting-pvisor/src/cli/run.rs +++ b/crates/persisting-pvisor/src/cli/run.rs @@ -73,17 +73,15 @@ use persisting_control::{ FilesystemAccess, FilesystemCapability, PolicyMode, RunInvocation, RunSpec, RunState, StdioMode, }; use persisting_gateway::config::{ - CaptureLevel, ModelRoute, NetworkConfig, NetworkMode, OverlayBackend, OverlayConfig, - ProxyConfig, + CaptureLevel, ModelRoute, NetworkConfig, NetworkMode, OverlayConfig, ProxyConfig, }; use persisting_overlaynet::{NetworkAccessRule, NetworkBandwidthLimit}; use serde::Deserialize; use crate::config::{ ContainerMount, ContainerNetwork, ContainerPlatform, FilesystemAccessLevel, FilesystemMode, - GatewayMode, GatewayProfile, OverlayFsBackend, OverlayFsCommit, OverlayFsSettings, - OverlayNetMode, OverlayNetPolicy, OverlayNetSettings, RunConfig, RunExecutorKind, RunPolicy, - RunStdio, + GatewayMode, GatewayProfile, OverlayFsCommit, OverlayFsSettings, OverlayNetMode, + OverlayNetPolicy, OverlayNetSettings, RunConfig, RunExecutorKind, RunPolicy, RunStdio, }; use crate::runtime::{RunLineage, default_run_home, resolve_run}; use crate::{ @@ -458,10 +456,6 @@ struct OverlayFsOverrides { /// Explicitly remove default/config file rules before adding --access rules. #[arg(long)] clear_access: bool, - /// Changeset upper-layer backend: `directory` writes plain files, `jujutsu` - /// stores content-addressed snapshots in a shared repository. - #[arg(long = "overlayfs-backend", value_enum)] - backend: Option, /// Aggregate byte budget for the staged filesystem; the Job fails once the /// stage exceeds it. #[arg(long = "overlayfs-max-size", value_name = "SIZE")] @@ -901,7 +895,6 @@ async fn run_prepared_spec(args: RunArgs) -> anyhow::Result { && overlay.merged_dir.is_none() && overlay.compose.is_empty() && overlay.access_policy == Default::default() - && overlay.backend == OverlayFsBackend::Directory && overlay.commit == OverlayFsCommit::Manual && overlay.stage.as_deref() == Some(stage_path.as_path()) }), @@ -1063,14 +1056,7 @@ pub async fn fork(args: ForkArgs) -> anyhow::Result { let source = resolve_run(Some(&args.source), &storage)?; let checkpoint = match args.checkpoint.as_deref() { Some(id) => { - anyhow::ensure!( - !id.trim().is_empty() - && id != "." - && id != ".." - && !id.contains('/') - && !id.contains('\\'), - "checkpoint id must be one non-empty path-safe segment" - ); + crate::runtime::checkpoint::validate_checkpoint_id(id)?; LogicalCheckpoint::read(&source.stage_dir().join(crate::CHECKPOINTS_DIR).join(id))? } None => create_logical_checkpoint(&source, None)?, @@ -1115,13 +1101,12 @@ pub async fn fork(args: ForkArgs) -> anyhow::Result { stage_size_bytes: None, mount: Vec::new(), access: Vec::new(), - backend: OverlayFsBackend::Directory, commit: OverlayFsCommit::Manual, }); let stage = select_run_storage(&config, &fork_workspace, &run_id)?; std::fs::create_dir_all(&stage)?; let upper = stage.join("upper"); - if let Err(error) = restore_logical_checkpoint(&checkpoint, &upper) { + if let Err(error) = restore_logical_checkpoint(&checkpoint, &upper, &stage.join("preimages")) { let _ = std::fs::remove_dir_all(&stage); return Err(error); } @@ -1301,7 +1286,7 @@ async fn execute_config( .unwrap_or(std::env::current_dir()?); let workspace = resolve_workspace(&workspace)?; let storage = resolve_run_storage(&select_run_storage(&config, &workspace, &run_id)?)?; - let mut overlay = resolve_overlay(&config, &workspace, &storage, &run_id)?; + let mut overlay = resolve_overlay(&config, &workspace, &storage)?; #[cfg(unix)] super::tui::announce_stage( overlay @@ -2173,7 +2158,6 @@ fn apply_cli(config: &mut RunConfig, args: RunArgs) -> anyhow::Result<()> { let enables_overlayfs = !args.overlayfs.mounts.is_empty() || !args.overlayfs.access.is_empty() || args.overlayfs.clear_access - || args.overlayfs.backend.is_some() || args.overlayfs.max_size.is_some() || args.stage.is_some(); if enables_overlayfs { @@ -2221,9 +2205,6 @@ fn apply_cli(config: &mut RunConfig, args: RunArgs) -> anyhow::Result<()> { }) .collect(); } - if let Some(value) = args.overlayfs.backend { - overlayfs.backend = value; - } if let Some(value) = args.overlayfs.max_size { overlayfs.stage_size_bytes = Some(value.0); } @@ -2635,7 +2616,6 @@ fn resolve_overlay( config: &RunConfig, workspace: &Path, storage: &Path, - run_id: &str, ) -> anyhow::Result> { let Some(overlayfs) = &config.overlayfs else { return Ok(None); @@ -2686,14 +2666,6 @@ fn resolve_overlay( lower_dirs: compose, stage_dir: Some(stage.clone()), merged_dir, - backend: match overlayfs.backend { - OverlayFsBackend::Directory => OverlayBackend::Directory, - OverlayFsBackend::Jujutsu => OverlayBackend::Jujutsu, - }, - jujutsu_store_path: (overlayfs.backend == OverlayFsBackend::Jujutsu) - .then(|| stage.join("jujutsu")), - jujutsu_workspace: (overlayfs.backend == OverlayFsBackend::Jujutsu) - .then(|| run_id.to_owned()), auto_apply: overlayfs.commit == OverlayFsCommit::Apply, auto_discard: overlayfs.commit == OverlayFsCommit::Drop, ..OverlayHint::default() @@ -2853,7 +2825,7 @@ mod tests { let mut config = RunConfig::default(); apply_run_options(&mut config, preset_args(&[flag, "--", "bash"])).unwrap(); normalize_filesystem_config(&mut config).unwrap(); - let overlay = resolve_overlay(&config, &workspace, &storage, "job") + let overlay = resolve_overlay(&config, &workspace, &storage) .unwrap() .unwrap(); assert_eq!(overlay.stage_dir, Some(storage.canonicalize().unwrap())); @@ -3170,7 +3142,6 @@ level = "warn" &config, Path::new("."), &tempfile::tempdir().unwrap().path().join("stage"), - "test", ) .unwrap() .unwrap(); @@ -3915,13 +3886,7 @@ sandbox = "required""# fn help_exposes_compositional_overlayfs_without_a_mode_switch() { let error = Cli::try_parse_from(["pvisor", "run", "--help"]).unwrap_err(); let help = error.to_string(); - for option in [ - "--mount", - "--access", - "--stage", - "--overlayfs-backend", - "--overlayfs-max-size", - ] { + for option in ["--mount", "--access", "--stage", "--overlayfs-max-size"] { assert!(help.contains(option), "missing {option}"); } for obsolete in ["--overlayfs-mode", "--overlayfs-lower"] { @@ -4127,8 +4092,6 @@ sandbox = "required""# "run", "--stage", "/tmp/pvisor-stage", - "--overlayfs-backend", - "jujutsu", "--", "true", ]) @@ -4140,7 +4103,6 @@ sandbox = "required""# let mut config = RunConfig::default(); apply_cli(&mut config, *args).unwrap(); let overlayfs = config.overlayfs.expect("OverlayFS should be enabled"); - assert_eq!(overlayfs.backend, OverlayFsBackend::Jujutsu); assert_eq!( overlayfs.stage.as_deref(), Some(Path::new("/tmp/pvisor-stage")) @@ -4164,7 +4126,7 @@ sandbox = "required""# ..RunConfig::default() }; - let hint = resolve_overlay(&config, &workspace, &storage, "run-test") + let hint = resolve_overlay(&config, &workspace, &storage) .unwrap() .unwrap(); assert_eq!( @@ -4197,7 +4159,7 @@ sandbox = "required""# }), ..RunConfig::default() }; - let hint = resolve_overlay(&config, &workspace, &storage, "run-test") + let hint = resolve_overlay(&config, &workspace, &storage) .unwrap() .unwrap(); assert_eq!( @@ -4237,7 +4199,7 @@ sandbox = "required""# }), ..RunConfig::default() }; - assert!(resolve_overlay(&config, &workspace, &storage, "run-test").is_ok()); + assert!(resolve_overlay(&config, &workspace, &storage).is_ok()); } let config = RunConfig { @@ -4248,7 +4210,7 @@ sandbox = "required""# }), ..RunConfig::default() }; - assert!(resolve_overlay(&config, &workspace, &storage, "run-test").is_err()); + assert!(resolve_overlay(&config, &workspace, &storage).is_err()); } #[test] diff --git a/crates/persisting-pvisor/src/config.rs b/crates/persisting-pvisor/src/config.rs index 6540973b..21a6cbb2 100644 --- a/crates/persisting-pvisor/src/config.rs +++ b/crates/persisting-pvisor/src/config.rs @@ -266,7 +266,6 @@ pub struct OverlayFsSettings { /// Aggregate byte budget for the whole staged filesystem. #[serde(rename = "max_size")] pub stage_size_bytes: Option, - pub backend: OverlayFsBackend, #[serde(skip)] pub commit: OverlayFsCommit, } @@ -283,7 +282,6 @@ impl Default for OverlayFsSettings { compose: Vec::new(), stage: None, stage_size_bytes: None, - backend: OverlayFsBackend::Directory, commit: OverlayFsCommit::Manual, } } @@ -316,14 +314,6 @@ pub enum FilesystemAccessLevel { Write, } -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum OverlayFsBackend { - #[default] - Directory, - Jujutsu, -} - #[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, clap::ValueEnum)] #[serde(rename_all = "kebab-case")] pub enum OverlayFsCommit { diff --git a/crates/persisting-pvisor/src/executor/container.rs b/crates/persisting-pvisor/src/executor/container.rs index ba58a5ea..ac95368f 100644 --- a/crates/persisting-pvisor/src/executor/container.rs +++ b/crates/persisting-pvisor/src/executor/container.rs @@ -5,17 +5,17 @@ use crate::config::{ContainerMount, ContainerPlatform, ContainerSettings}; use crate::executor::artifact::resolve_pvisor_binary; use crate::executor::delegated::{DelegatedRunFiles, RESULT_FILENAME, SPEC_FILENAME}; use crate::executor::{AttemptContext, RunExecutor}; +use crate::executor::{join_capture, read_limited, stdio}; use async_trait::async_trait; use persisting_control::{ ExecutorDescriptor, ExecutorKind, IsolationKind, ProcessOutput, RunFailure, RunFailureKind, - RunInvocation, RunResult, RunSpec, RunState, StdioMode, + RunInvocation, RunResult, RunSpec, RunState, }; use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::time::Duration; -use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::process::{Child, Command}; const CAPTURE_CONFIG_ENV: &str = "PERSISTING_CAPTURE_CONFIG"; @@ -27,12 +27,6 @@ pub struct ContainerExecutor { settings: ContainerSettings, } -#[derive(Debug)] -struct Captured { - text: String, - truncated: bool, -} - #[derive(Debug, Clone, PartialEq, Eq)] struct BindMount { source: PathBuf, @@ -656,45 +650,6 @@ fn container_name(run_id: &str, attempt_id: &str) -> String { format!("pvisor-{run}-{suffix}") } -fn stdio(mode: StdioMode) -> Stdio { - match mode { - StdioMode::Inherit => Stdio::inherit(), - StdioMode::Capture => Stdio::piped(), - StdioMode::Null => Stdio::null(), - } -} - -async fn read_limited( - mut reader: R, - limit: usize, -) -> std::io::Result { - let mut retained = Vec::with_capacity(limit.min(8192)); - let mut buffer = [0_u8; 8192]; - let mut truncated = false; - loop { - let read = reader.read(&mut buffer).await?; - if read == 0 { - break; - } - let keep = limit.saturating_sub(retained.len()).min(read); - retained.extend_from_slice(&buffer[..keep]); - truncated |= keep < read; - } - Ok(Captured { - text: String::from_utf8_lossy(&retained).into_owned(), - truncated, - }) -} - -async fn join_capture( - task: Option>>, -) -> Option { - match task { - Some(task) => task.await.ok().and_then(Result::ok), - None => None, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/persisting-pvisor/src/executor/delegated.rs b/crates/persisting-pvisor/src/executor/delegated.rs index 4d8f5359..36088072 100644 --- a/crates/persisting-pvisor/src/executor/delegated.rs +++ b/crates/persisting-pvisor/src/executor/delegated.rs @@ -1,4 +1,4 @@ -//! Files and result hand-off for a pVisor delegated through Docker or KVM. +//! Files and result hand-off for a pVisor delegated through an OCI container or libkrun VM. use crate::util::write_private_json; use persisting_control::{AttemptId, RunInvocation, RunResult, RunSpec}; diff --git a/crates/persisting-pvisor/src/executor/mod.rs b/crates/persisting-pvisor/src/executor/mod.rs index 1253f202..9ec19a2c 100644 --- a/crates/persisting-pvisor/src/executor/mod.rs +++ b/crates/persisting-pvisor/src/executor/mod.rs @@ -9,11 +9,14 @@ pub(crate) mod vm; use crate::runtime::event::RunEventPublisher; use async_trait::async_trait; +use persisting_control::StdioMode; use persisting_control::{ AttemptId, ExecutorDescriptor, RunInvocation, RunResult, RunSpec, RunState, RunStatus, }; use serde_json::json; +use std::process::Stdio; use std::sync::Arc; +use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::sync::watch; use tokio_util::sync::CancellationToken; @@ -141,3 +144,71 @@ pub trait RunExecutor: Send + Sync { } async fn execute(&self, context: AttemptContext) -> RunResult; } + +#[derive(Debug)] +pub(crate) struct Captured { + pub text: String, + pub truncated: bool, +} + +pub(crate) fn stdio(mode: StdioMode) -> Stdio { + match mode { + StdioMode::Inherit => Stdio::inherit(), + StdioMode::Capture => Stdio::piped(), + StdioMode::Null => Stdio::null(), + } +} + +pub(crate) async fn read_limited( + mut reader: R, + limit: usize, +) -> std::io::Result { + let mut retained = Vec::with_capacity(limit.min(8192)); + let mut buffer = [0_u8; 8192]; + let mut truncated = false; + loop { + let read = reader.read(&mut buffer).await?; + if read == 0 { + break; + } + let keep = limit.saturating_sub(retained.len()).min(read); + retained.extend_from_slice(&buffer[..keep]); + truncated |= keep < read; + } + Ok(Captured { + text: String::from_utf8_lossy(&retained).into_owned(), + truncated, + }) +} + +pub(crate) async fn join_capture( + task: Option>>, +) -> Option { + match task { + Some(task) => task.await.ok().and_then(Result::ok), + None => None, + } +} + +#[cfg(test)] +mod output_tests { + use super::*; + use tokio::io::AsyncWriteExt; + + #[tokio::test] + async fn bounded_capture_drains_output_after_the_limit() { + let (reader, mut writer) = tokio::io::duplex(16); + let writer = tokio::spawn(async move { writer.write_all(&[b'x'; 32768]).await.unwrap() }); + let captured = + tokio::time::timeout(std::time::Duration::from_secs(2), read_limited(reader, 8)) + .await + .unwrap() + .unwrap(); + writer.await.unwrap(); + assert_eq!(captured.text, "xxxxxxxx"); + assert!(captured.truncated); + let captured = read_limited(&b"ok"[..], 8).await.unwrap(); + assert_eq!(captured.text, "ok"); + assert!(!captured.truncated); + } +} diff --git a/crates/persisting-pvisor/src/executor/process.rs b/crates/persisting-pvisor/src/executor/process.rs index 53570c8c..e8f45ebf 100644 --- a/crates/persisting-pvisor/src/executor/process.rs +++ b/crates/persisting-pvisor/src/executor/process.rs @@ -8,7 +8,7 @@ use crate::executor::sandbox::{ #[cfg(target_os = "linux")] use crate::executor::sandbox::{ROOTLESS_ATTESTATION, SandboxPlan, landlock_runtime_available}; use crate::executor::sandbox::{SANDBOX_ARG0_ENV, SANDBOX_PLAN_ENV, SANDBOX_SETUP_FAILED_WARNING}; -use crate::executor::{AttemptContext, RunExecutor}; +use crate::executor::{AttemptContext, Captured, RunExecutor, stdio}; use async_trait::async_trait; use persisting_control::{ CapabilityDimension, CapabilityEnforcementEvidence, ExecutorDescriptor, ExecutorKind, @@ -21,8 +21,7 @@ use persisting_control::{FilesystemAccess, NetworkCapability}; use std::path::Path; use std::path::PathBuf; #[cfg(target_os = "linux")] -use std::process::Command as StdCommand; -use std::process::Stdio; +use std::process::{Command as StdCommand, Stdio}; use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::process::{Child, Command}; @@ -360,12 +359,6 @@ impl Drop for SandboxResources { } } -#[derive(Debug)] -struct Captured { - text: String, - truncated: bool, -} - async fn read_limited( mut reader: R, limit: usize, @@ -397,14 +390,6 @@ async fn read_limited( }) } -fn stdio(mode: StdioMode) -> Stdio { - match mode { - StdioMode::Inherit => Stdio::inherit(), - StdioMode::Capture => Stdio::piped(), - StdioMode::Null => Stdio::null(), - } -} - #[cfg(any(target_os = "linux", target_os = "macos"))] fn network_isolation(spec: &RunSpec) -> std::io::Result { if crate::executor::sandbox::sandbox_required(spec) { @@ -1639,6 +1624,7 @@ impl RunExecutor for ProcessExecutor { #[cfg(test)] mod tests { use super::*; + use std::process::Stdio; #[cfg(unix)] #[tokio::test] diff --git a/crates/persisting-pvisor/src/executor/vm/supported.rs b/crates/persisting-pvisor/src/executor/vm/supported.rs index 2f35ff1f..418d84e3 100644 --- a/crates/persisting-pvisor/src/executor/vm/supported.rs +++ b/crates/persisting-pvisor/src/executor/vm/supported.rs @@ -2,22 +2,21 @@ use crate::config::VmSettings; use crate::executor::{AttemptContext, RunExecutor}; +use crate::executor::{join_capture, read_limited, stdio}; use crate::util::write_private_json; use anyhow::Context as _; use async_trait::async_trait; use persisting_control::{ CapabilityDimension, CapabilityEnforcementEvidence, ExecutorDescriptor, ExecutorKind, IsolationKind, ProcessOutput, ResourceLimits, RunFailure, RunFailureKind, RunInvocation, - RunResult, RunState, StdioMode, + RunResult, RunState, }; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::ffi::CString; use std::os::fd::{AsRawFd, RawFd}; use std::path::{Path, PathBuf}; -use std::process::Stdio; use std::time::Duration; -use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::process::Command; const RUNNER_SPEC_ENV: &str = "PERSISTING_KRUN_RUNNER_SPEC"; @@ -60,12 +59,6 @@ pub struct VmExecutor { settings: VmSettings, } -#[derive(Debug)] -struct Captured { - text: String, - truncated: bool, -} - #[derive(Debug, Serialize, Deserialize)] struct RunnerSpec { root: OverlayDeviceSpec, @@ -1064,45 +1057,6 @@ fn shell_quote(value: &str) -> anyhow::Result { Ok(format!("'{}'", value.replace('\'', "'\"'\"'"))) } -fn stdio(mode: StdioMode) -> Stdio { - match mode { - StdioMode::Inherit => Stdio::inherit(), - StdioMode::Capture => Stdio::piped(), - StdioMode::Null => Stdio::null(), - } -} - -async fn read_limited( - mut reader: R, - limit: usize, -) -> std::io::Result { - let mut retained = Vec::with_capacity(limit.min(8192)); - let mut buffer = [0_u8; 8192]; - let mut truncated = false; - loop { - let read = reader.read(&mut buffer).await?; - if read == 0 { - break; - } - let keep = limit.saturating_sub(retained.len()).min(read); - retained.extend_from_slice(&buffer[..keep]); - truncated |= keep < read; - } - Ok(Captured { - text: String::from_utf8_lossy(&retained).into_owned(), - truncated, - }) -} - -async fn join_capture( - task: Option>>, -) -> Option { - match task { - Some(task) => task.await.ok().and_then(Result::ok), - None => None, - } -} - fn failed_to_start( spec: &persisting_control::RunSpec, attempt_id: &persisting_control::AttemptId, diff --git a/crates/persisting-pvisor/src/lib.rs b/crates/persisting-pvisor/src/lib.rs index 47338eac..b57e3876 100644 --- a/crates/persisting-pvisor/src/lib.rs +++ b/crates/persisting-pvisor/src/lib.rs @@ -24,10 +24,9 @@ mod util; pub use config::{ ContainerMount, ContainerNetwork, ContainerPlatform, ContainerSettings, FilesystemMode, - GatewayDriverConfig, GatewayMode, GatewaySettings, NetworkDriverConfig, OverlayFsBackend, - OverlayFsCommit, OverlayFsSettings, OverlayNetMode, OverlayNetPolicy, OverlayNetSettings, - PVisorConfig, RecordSettings, RunConfig, RunExecutorKind, RunPolicy, RunSettings, RunStdio, - VmSettings, + GatewayDriverConfig, GatewayMode, GatewaySettings, NetworkDriverConfig, OverlayFsCommit, + OverlayFsSettings, OverlayNetMode, OverlayNetPolicy, OverlayNetSettings, PVisorConfig, + RecordSettings, RunConfig, RunExecutorKind, RunPolicy, RunSettings, RunStdio, VmSettings, }; pub use executor::container::ContainerExecutor; pub use executor::process::ProcessExecutor; diff --git a/crates/persisting-pvisor/src/runtime/attempt.rs b/crates/persisting-pvisor/src/runtime/attempt.rs index ff8c8cbd..16443cf0 100644 --- a/crates/persisting-pvisor/src/runtime/attempt.rs +++ b/crates/persisting-pvisor/src/runtime/attempt.rs @@ -632,32 +632,10 @@ pub(crate) fn prepare_overlay_attempt( if vm_network.is_some() { mark_vm_network(&mut plan); } - match &overlay_record.upper { - super::overlay::OverlayUpper::Directory { upper_dir, .. } => { - plan.env.insert( - "PERSISTING_OVERLAY_UPPER".into(), - upper_dir.display().to_string(), - ); - } - super::overlay::OverlayUpper::Jujutsu { - store_path, - workspace, - upper_dir, - } => { - plan.env.insert( - "PERSISTING_OVERLAY_UPPER".into(), - upper_dir.display().to_string(), - ); - plan.env.insert( - "PERSISTING_OVERLAY_JUJUTSU_STORE".into(), - store_path.display().to_string(), - ); - plan.env.insert( - "PERSISTING_OVERLAY_JUJUTSU_WORKSPACE".into(), - workspace.clone(), - ); - } - } + plan.env.insert( + "PERSISTING_OVERLAY_UPPER".into(), + overlay_record.upper.path().display().to_string(), + ); let RunInvocation::Process(ref mut process) = spec.invocation; apply_implant(process, &plan); run_record.environment.runtime_injected_keys = plan.env.keys().cloned().collect(); @@ -986,7 +964,6 @@ fn apply_overlay_override( overlay_cfg: &mut persisting_gateway::config::OverlayConfig, overlay_override: &OverlayHint, ) { - overlay_cfg.backend = overlay_override.backend; if overlay_override != &OverlayHint::default() { overlay_cfg.access_policy = overlay_override.access_policy.clone(); } @@ -1003,24 +980,9 @@ fn apply_overlay_override( } if let Some(upper) = &overlay_override.upper_dir { overlay_cfg.upper_dir = Some(upper.display().to_string()); - overlay_cfg.backend = persisting_gateway::config::OverlayBackend::Directory; - overlay_cfg.jujutsu_store_path = None; - overlay_cfg.jujutsu_workspace = None; } if let Some(work) = &overlay_override.work_dir { overlay_cfg.work_dir = Some(work.display().to_string()); - overlay_cfg.backend = persisting_gateway::config::OverlayBackend::Directory; - overlay_cfg.jujutsu_store_path = None; - overlay_cfg.jujutsu_workspace = None; - } - if let Some(store) = &overlay_override.jujutsu_store_path { - overlay_cfg.jujutsu_store_path = Some(store.display().to_string()); - overlay_cfg.backend = persisting_gateway::config::OverlayBackend::Jujutsu; - overlay_cfg.upper_dir = None; - overlay_cfg.work_dir = None; - } - if let Some(workspace) = &overlay_override.jujutsu_workspace { - overlay_cfg.jujutsu_workspace = Some(workspace.clone()); } if !overlay_override.lower_dirs.is_empty() { // The final lower is the base/apply target; preceding entries are @@ -1117,13 +1079,8 @@ fn inject_krun_overlay_metadata( let Some(record) = record else { return; }; - let (upper, work) = match &record.upper { - super::overlay::OverlayUpper::Directory { - upper_dir, - work_dir, - } => (upper_dir.clone(), Some(work_dir.clone())), - super::overlay::OverlayUpper::Jujutsu { upper_dir, .. } => (upper_dir.clone(), None), - }; + let upper = &record.upper.upper_dir; + let work = &record.upper.work_dir; spec.metadata.insert( "pvisor.vm.workspace_overlay".into(), serde_json::json!({ @@ -1305,32 +1262,10 @@ fn enrich_with_session( "PERSISTING_OVERLAY_TARGET".into(), rec.target.display().to_string(), ); - match &rec.upper { - super::overlay::OverlayUpper::Directory { upper_dir, .. } => { - plan.env.insert( - "PERSISTING_OVERLAY_UPPER".into(), - upper_dir.display().to_string(), - ); - } - super::overlay::OverlayUpper::Jujutsu { - store_path, - workspace, - upper_dir, - } => { - plan.env.insert( - "PERSISTING_OVERLAY_UPPER".into(), - upper_dir.display().to_string(), - ); - plan.env.insert( - "PERSISTING_OVERLAY_JUJUTSU_STORE".into(), - store_path.display().to_string(), - ); - plan.env.insert( - "PERSISTING_OVERLAY_JUJUTSU_WORKSPACE".into(), - workspace.clone(), - ); - } - } + plan.env.insert( + "PERSISTING_OVERLAY_UPPER".into(), + rec.upper.path().display().to_string(), + ); plan.env.insert( "PERSISTING_OVERLAY_STAGE".into(), rec.stage_dir.display().to_string(), diff --git a/crates/persisting-pvisor/src/runtime/bundle.rs b/crates/persisting-pvisor/src/runtime/bundle.rs index 54e6748e..4c690717 100644 --- a/crates/persisting-pvisor/src/runtime/bundle.rs +++ b/crates/persisting-pvisor/src/runtime/bundle.rs @@ -4,7 +4,7 @@ use crate::executor::sandbox::SANDBOX_SETUP_FAILED_WARNING; use crate::runtime::{ ChangeEntry, OverlayState, RunLineage, RunRecord, overlay_changes, overlay_status, }; -use crate::util::{atomic_write, sync_directory}; +use crate::util::sync_directory; use crate::{AgentCtlSnapshot, unix_now_ms}; use persisting_control::{ ArtifactRef, CapabilityDimension, ExecutorDescriptor, IsolationKind, ProcessOutput, @@ -367,7 +367,7 @@ impl RunBundle { pub fn write(&self, stage_dir: &Path) -> anyhow::Result { let path = Self::path(stage_dir); - atomic_write(&path, &serde_json::to_vec_pretty(self)?, 0o600)?; + crate::util::write_private_json(&path, self)?; Ok(path) } @@ -589,7 +589,7 @@ mod tests { id: "run-1".into(), generation: 0, target: temp.path().join("target"), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper, work_dir: temp.path().join("work"), }, diff --git a/crates/persisting-pvisor/src/runtime/checkpoint.rs b/crates/persisting-pvisor/src/runtime/checkpoint.rs index 9fd3244d..606ae152 100644 --- a/crates/persisting-pvisor/src/runtime/checkpoint.rs +++ b/crates/persisting-pvisor/src/runtime/checkpoint.rs @@ -4,22 +4,22 @@ use crate::runtime::{ OverlayState, RunRecord, is_live, restore_overlay_upper, snapshot_overlay_upper, }; use crate::unix_now_ms; -use crate::util::{atomic_write, create_dir_all_durable}; +use crate::util::{create_dir_all_durable, sync_directory, write_private_json}; use serde::{Deserialize, Serialize}; use std::fs; -use std::os::unix::fs::PermissionsExt; +use std::os::unix::fs::DirBuilderExt; use std::path::{Path, PathBuf}; pub const CHECKPOINTS_DIR: &str = "checkpoints"; const CHECKPOINT_FILENAME: &str = "checkpoint.json"; -const CHECKPOINT_SCHEMA_VERSION: u32 = 1; +const CHECKPOINT_SCHEMA_VERSION: u32 = 2; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum CheckpointConsistency { /// The process tree was no longer running when the filesystem was copied. Stopped, - /// Reserved for a live AgentCtl cooperative quiescence barrier. + /// Captured at a live AgentCtl cooperative quiescence barrier. AgentQuiesced, } @@ -32,6 +32,7 @@ pub struct LogicalCheckpoint { pub consistency: CheckpointConsistency, pub source_stage: PathBuf, pub upper_snapshot: PathBuf, + pub preimages_snapshot: PathBuf, pub target: PathBuf, #[serde(default)] pub lower_dirs: Vec, @@ -118,79 +119,89 @@ fn create_checkpoint( .stage_dir() .join(CHECKPOINTS_DIR) .join(&checkpoint_id); - anyhow::ensure!( - !root.exists(), - "logical checkpoint already exists: {}", - root.display() - ); - create_dir_all_durable(&root)?; - fs::set_permissions(&root, fs::Permissions::from_mode(0o700))?; - let upper_snapshot = root.join("upper"); - if let Err(error) = snapshot_overlay_upper(overlay, &upper_snapshot) { + let parent = root.parent().expect("checkpoint has a parent"); + create_dir_all_durable(parent)?; + fs::DirBuilder::new().mode(0o700).create(&root)?; + let result = (|| -> anyhow::Result { + sync_directory(parent)?; + let upper_snapshot = root.join("upper"); + snapshot_overlay_upper(overlay, &upper_snapshot)?; + let preimages_snapshot = root.join("preimages"); + let journal = record.stage_dir().join("preimages"); + if journal.is_dir() { + restore_overlay_upper(&journal, &preimages_snapshot)?; + } else { + create_dir_all_durable(&preimages_snapshot)?; + } + let checkpoint = LogicalCheckpoint { + schema_version: CHECKPOINT_SCHEMA_VERSION, + checkpoint_id, + run_id: record.run_id.clone(), + created_at_unix_ms: unix_now_ms(), + consistency, + source_stage: record.stage_dir(), + upper_snapshot, + preimages_snapshot, + target: overlay.target.clone(), + lower_dirs: if record.overlay_lowers.is_empty() { + vec![overlay.target.clone()] + } else { + record.overlay_lowers.clone() + }, + protect_target: overlay.protect_target, + access_policy: overlay.access_policy.clone(), + }; + write_private_json(&root.join(CHECKPOINT_FILENAME), &checkpoint)?; + Ok(checkpoint) + })(); + if result.is_err() { let _ = fs::remove_dir_all(&root); - return Err(error.into()); } - let checkpoint = LogicalCheckpoint { - schema_version: CHECKPOINT_SCHEMA_VERSION, - checkpoint_id, - run_id: record.run_id.clone(), - created_at_unix_ms: unix_now_ms(), - consistency, - source_stage: record.stage_dir(), - upper_snapshot, - target: overlay.target.clone(), - lower_dirs: if record.overlay_lowers.is_empty() { - vec![overlay.target.clone()] - } else { - record.overlay_lowers.clone() - }, - protect_target: overlay.protect_target, - access_policy: overlay.access_policy.clone(), - }; - atomic_write( - &root.join(CHECKPOINT_FILENAME), - &serde_json::to_vec_pretty(&checkpoint)?, - 0o600, - )?; - Ok(checkpoint) + result } pub fn latest_logical_checkpoint(record: &RunRecord) -> anyhow::Result { let root = record.stage_dir().join(CHECKPOINTS_DIR); - let mut checkpoints = if root.is_dir() { + let checkpoints = if root.is_dir() { fs::read_dir(root)? .filter_map(Result::ok) .filter_map(|entry| LogicalCheckpoint::read(&entry.path()).ok()) .filter(|checkpoint| checkpoint.run_id == record.run_id) - .collect::>() + .max_by_key(|checkpoint| checkpoint.created_at_unix_ms) } else { - Vec::new() + None }; - checkpoints.sort_by_key(|checkpoint| std::cmp::Reverse(checkpoint.created_at_unix_ms)); - checkpoints - .into_iter() - .next() - .ok_or_else(|| anyhow::anyhow!("Run {} has no logical checkpoints", record.run_id)) + checkpoints.ok_or_else(|| anyhow::anyhow!("Run {} has no logical checkpoints", record.run_id)) } pub fn restore_logical_checkpoint( checkpoint: &LogicalCheckpoint, destination_upper: &Path, + destination_preimages: &Path, ) -> anyhow::Result<()> { + let sources = [&checkpoint.upper_snapshot, &checkpoint.preimages_snapshot]; + let sources = sources + .map(|source| source.canonicalize()) + .into_iter() + .collect::>>()?; anyhow::ensure!( - checkpoint.upper_snapshot.is_dir(), - "logical checkpoint upper is missing: {}", - checkpoint.upper_snapshot.display() + sources.iter().all(|source| source.is_dir()), + "checkpoint snapshots must be directories" ); - let source = checkpoint.upper_snapshot.canonicalize()?; - let destination = absolute_candidate(destination_upper)?; + let destinations = [ + absolute_candidate(destination_upper)?, + absolute_candidate(destination_preimages)?, + ]; + let overlaps = |left: &Path, right: &Path| left.starts_with(right) || right.starts_with(left); anyhow::ensure!( - !source.starts_with(&destination) && !destination.starts_with(&source), - "checkpoint source and fork upper must not overlap: source={}, destination={}", - source.display(), - destination.display() + !overlaps(&destinations[0], &destinations[1]) + && sources.iter().all(|source| destinations + .iter() + .all(|destination| !overlaps(source, destination))), + "checkpoint sources and restore destinations must not overlap" ); - restore_overlay_upper(&checkpoint.upper_snapshot, destination_upper)?; + restore_overlay_upper(&sources[0], &destinations[0])?; + restore_overlay_upper(&sources[1], &destinations[1])?; Ok(()) } @@ -213,11 +224,15 @@ fn absolute_candidate(path: &Path) -> anyhow::Result { Ok(parent.join(name)) } -fn validate_checkpoint_id(id: &str) -> anyhow::Result<()> { +pub(crate) fn validate_checkpoint_id(id: &str) -> anyhow::Result<()> { let trimmed = id.trim(); anyhow::ensure!(!trimmed.is_empty(), "checkpoint id cannot be empty"); anyhow::ensure!( - trimmed != "." && trimmed != ".." && !trimmed.contains('/') && !trimmed.contains('\\'), + trimmed != "." + && trimmed != ".." + && !trimmed.contains('/') + && !trimmed.contains('\\') + && !id.contains('\0'), "checkpoint id must be one path-safe segment" ); Ok(()) @@ -262,7 +277,7 @@ mod tests { id: "run-source".into(), generation: 0, target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper, work_dir: root.join("work"), }, @@ -307,7 +322,12 @@ mod tests { checkpoint.access_policy ); let restored = temp.path().join("restored"); - restore_logical_checkpoint(&checkpoint, &restored).unwrap(); + restore_logical_checkpoint( + &checkpoint, + &restored, + &temp.path().join("restored-preimages"), + ) + .unwrap(); assert_eq!(fs::read(restored.join("one")).unwrap(), b"value"); assert_eq!( @@ -330,4 +350,53 @@ mod tests { let record = stopped_record(temp.path()); assert!(create_logical_checkpoint(&record, Some("../escape")).is_err()); } + + #[test] + fn fork_preserves_conflict_baselines_and_validates_before_replacing() { + use persisting_overlay_core::{OverlayCore, load_preimages, preimage_journal_is_complete}; + let temp = tempfile::tempdir().unwrap(); + let mut parent = stopped_record(temp.path()); + let overlay = parent.overlay.as_ref().unwrap(); + let target = overlay.target.clone(); + let upper = overlay.upper.path().to_path_buf(); + fs::write(target.join("value"), b"original").unwrap(); + let core = OverlayCore::new_with_exclusions_and_preimages( + vec![target.clone()], + upper.clone(), + Some(temp.path().join("work")), + vec![], + Some(temp.path().join("preimages")), + ) + .unwrap(); + core.copy_up(Path::new("value")).unwrap(); + fs::write(upper.join("value"), b"staged").unwrap(); + let checkpoint = create_logical_checkpoint(&parent, Some("baseline")).unwrap(); + assert!(create_logical_checkpoint(&parent, Some("baseline")).is_err()); + fs::write(target.join("value"), b"external-edit").unwrap(); + assert!(crate::runtime::overlay::apply_overlay(parent.overlay.as_mut().unwrap()).is_err()); + let stage = temp.path().join("child"); + fs::create_dir(&stage).unwrap(); + let upper = stage.join("upper"); + let journal = stage.join("preimages"); + restore_logical_checkpoint(&checkpoint, &upper, &journal).unwrap(); + assert!(preimage_journal_is_complete(&journal)); + assert_eq!( + load_preimages(&journal).unwrap(), + load_preimages(&checkpoint.preimages_snapshot).unwrap() + ); + let mut child = parent.overlay.unwrap(); + child.id = "child".into(); + child.stage_dir = stage.clone(); + child.upper = OverlayUpper { + upper_dir: upper.clone(), + work_dir: stage.join("work"), + }; + assert!(crate::runtime::overlay::apply_overlay(&mut child).is_err()); + assert_eq!(fs::read(target.join("value")).unwrap(), b"external-edit"); + assert!(restore_logical_checkpoint(&checkpoint, &upper, &upper.join("nested")).is_err()); + assert_eq!(fs::read(upper.join("value")).unwrap(), b"staged"); + fs::remove_dir_all(&checkpoint.preimages_snapshot).unwrap(); + assert!(restore_logical_checkpoint(&checkpoint, &upper, &journal).is_err()); + assert_eq!(fs::read(upper.join("value")).unwrap(), b"staged"); + } } diff --git a/crates/persisting-pvisor/src/runtime/implant.rs b/crates/persisting-pvisor/src/runtime/implant.rs index 82f7808e..7fcf55bf 100644 --- a/crates/persisting-pvisor/src/runtime/implant.rs +++ b/crates/persisting-pvisor/src/runtime/implant.rs @@ -2,8 +2,6 @@ use serde_json::json; use std::collections::BTreeMap; use std::path::PathBuf; -use persisting_gateway::config::OverlayBackend; - /// Optional in-process FUSE overlay root for one Attempt. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct OverlayHint { @@ -16,14 +14,8 @@ pub struct OverlayHint { pub upper_dir: Option, /// Work directory required by overlay implementations. pub work_dir: Option, - /// Shared Jujutsu repository root for all OverlayFS forks. - pub jujutsu_store_path: Option, - /// Jujutsu workspace/fork name within the shared repository. - pub jujutsu_workspace: Option, /// Merged mount point visible to the Agent as cwd/root when set. pub merged_dir: Option, - /// Writable staging representation. - pub backend: OverlayBackend, /// Apply staged changes when the Run exits successfully or unsuccessfully. pub auto_apply: bool, /// Discard staged changes when the Run exits. diff --git a/crates/persisting-pvisor/src/runtime/overlay.rs b/crates/persisting-pvisor/src/runtime/overlay.rs index f7d4cee9..415afc08 100644 --- a/crates/persisting-pvisor/src/runtime/overlay.rs +++ b/crates/persisting-pvisor/src/runtime/overlay.rs @@ -19,14 +19,11 @@ pub use persisting_control::overlay::{ ChangeKind, OverlayRecord, OverlayState, OverlayStatus, OverlayUpper, }; use persisting_control::overlay::{PathFingerprint, PathPreimage}; -use persisting_gateway::config::{OverlayBackend, OverlayConfig}; +use persisting_gateway::config::OverlayConfig; use persisting_overlay_core::{ fingerprint_at, load_preimages, preimage_journal_is_complete, remove_preimages, }; -use persisting_overlayfs::{ - OverlayMountConfig, OverlaySession, jujutsu_upper_dir, mount as mount_embedded_overlay, - snapshot_jujutsu_upper, -}; +use persisting_overlayfs::{OverlayMountConfig, OverlaySession, mount as mount_embedded_overlay}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::{BTreeSet, HashMap}; @@ -69,8 +66,6 @@ pub enum OverlayError { InvalidState(String), #[error("overlay metadata update failed: {0}")] Persist(String), - #[error("overlay finalization failed: {0}")] - Finalize(String), #[error("io: {0}")] Io(#[from] std::io::Error), } @@ -238,21 +233,6 @@ pub fn resolve_overlay_workspace( if !cfg.enabled && cfg.target.is_none() && cfg.lower_dirs.is_empty() { return Ok(None); } - match cfg.backend { - OverlayBackend::Directory - if cfg.jujutsu_store_path.is_some() || cfg.jujutsu_workspace.is_some() => - { - return Err(OverlayError::InvalidConfig( - "directory cannot be combined with Jujutsu options".into(), - )); - } - OverlayBackend::Jujutsu if cfg.upper_dir.is_some() || cfg.work_dir.is_some() => { - return Err(OverlayError::InvalidConfig( - "jujutsu cannot be combined with upper_dir or work_dir".into(), - )); - } - _ => {} - } let resolve = |p: &str| -> PathBuf { let path = PathBuf::from(p); @@ -277,37 +257,17 @@ pub fn resolve_overlay_workspace( .map(resolve) .unwrap_or_else(|| storage.join(".overlay").join(session_id)); - let upper = match cfg.backend { - OverlayBackend::Directory => OverlayUpper::Directory { - upper_dir: cfg - .upper_dir - .as_deref() - .map(resolve) - .unwrap_or_else(|| stage_dir.join("upper")), - work_dir: cfg - .work_dir - .as_deref() - .map(resolve) - .unwrap_or_else(|| stage_dir.join("work")), - }, - OverlayBackend::Jujutsu => { - let store_path = cfg - .jujutsu_store_path - .as_deref() - .map(resolve) - .unwrap_or_else(|| storage.join(".overlay").join("jujutsu")); - let workspace = cfg - .jujutsu_workspace - .clone() - .unwrap_or_else(|| session_id.to_owned()); - let upper_dir = jujutsu_upper_dir(&store_path, &workspace) - .map_err(|error| OverlayError::InvalidConfig(error.to_string()))?; - OverlayUpper::Jujutsu { - store_path, - workspace, - upper_dir, - } - } + let upper = OverlayUpper { + upper_dir: cfg + .upper_dir + .as_deref() + .map(resolve) + .unwrap_or_else(|| stage_dir.join("upper")), + work_dir: cfg + .work_dir + .as_deref() + .map(resolve) + .unwrap_or_else(|| stage_dir.join("work")), }; let resolved_lowers = cfg .lower_dirs @@ -330,16 +290,8 @@ pub fn resolve_overlay_workspace( }); let mut backing_paths = vec![stage_dir.clone(), merged.clone()]; - match &upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => { - backing_paths.push(upper_dir.clone()); - backing_paths.push(work_dir.clone()); - } - OverlayUpper::Jujutsu { store_path, .. } => backing_paths.push(store_path.clone()), - } + backing_paths.push(upper.upper_dir.clone()); + backing_paths.push(upper.work_dir.clone()); backing_paths.extend(resolved_lowers); let mut excluded_paths = backing_paths .into_iter() @@ -380,35 +332,13 @@ pub fn resolve_overlay_workspace( /// Build an [`OverlayHint`] from a resolved record + full lower stack. pub fn hint_from_record(record: &OverlayRecord, lower_dirs: Vec) -> OverlayHint { - let (upper_dir, work_dir, jujutsu_store_path, jujutsu_workspace) = match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => (Some(upper_dir.clone()), Some(work_dir.clone()), None, None), - OverlayUpper::Jujutsu { - store_path, - workspace, - .. - } => ( - None, - None, - Some(store_path.clone()), - Some(workspace.clone()), - ), - }; OverlayHint { access_policy: record.access_policy.clone(), lower_dirs, stage_dir: Some(record.stage_dir.clone()), - upper_dir, - work_dir, - jujutsu_store_path, - jujutsu_workspace, + upper_dir: Some(record.upper.upper_dir.clone()), + work_dir: Some(record.upper.work_dir.clone()), merged_dir: Some(record.merged_dir.clone()), - backend: match &record.upper { - OverlayUpper::Directory { .. } => OverlayBackend::Directory, - OverlayUpper::Jujutsu { .. } => OverlayBackend::Jujutsu, - }, auto_apply: record.auto_apply, auto_discard: record.auto_discard, protect_target: record.protect_target, @@ -524,43 +454,17 @@ pub(crate) fn mount_overlay_record_observed( create_dir_all_durable(dir) .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; } - match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => { - create_dir_all_durable(upper_dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - create_dir_all_durable(work_dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - } - OverlayUpper::Jujutsu { store_path, .. } => { - create_dir_all_durable(store_path) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - } - } - - let mut config = match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => OverlayMountConfig::new( - lower_dirs.to_vec(), - upper_dir.clone(), - Some(work_dir.clone()), - record.merged_dir.clone(), - ), - OverlayUpper::Jujutsu { - store_path, - workspace, - .. - } => OverlayMountConfig::new_jujutsu( - lower_dirs.to_vec(), - store_path.clone(), - workspace.clone(), - record.merged_dir.clone(), - ), - }; + create_dir_all_durable(&record.upper.upper_dir) + .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; + create_dir_all_durable(&record.upper.work_dir) + .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; + + let mut config = OverlayMountConfig::new( + lower_dirs.to_vec(), + record.upper.upper_dir.clone(), + Some(record.upper.work_dir.clone()), + record.merged_dir.clone(), + ); config.fsname = format!("pvisor-{}", record.id); config.excluded_paths = record.excluded_paths.clone(); config.access_policy = record.access_policy.clone(); @@ -594,29 +498,11 @@ pub(crate) fn prepare_overlay_record_mountless( } create_dir_all_durable(&record.stage_dir.join("preimages/entries")) .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => { - create_dir_all_durable(upper_dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - create_dir_all_durable(work_dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - } - OverlayUpper::Jujutsu { - store_path, - workspace, - upper_dir, - } => { - create_dir_all_durable(store_path) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - persisting_overlayfs::prepare_jujutsu_upper(store_path, workspace) - .map_err(OverlayError::Prepare)?; - create_dir_all_durable(upper_dir) - .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; - } - } + create_dir_all_durable(&record.upper.upper_dir) + .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; + create_dir_all_durable(&record.upper.work_dir) + .map_err(|error| OverlayError::Prepare(io::Error::other(error)))?; + let mut record = record.clone(); record.state = OverlayState::Active; write_overlay_record(&record)?; @@ -626,14 +512,6 @@ pub(crate) fn prepare_overlay_record_mountless( pub(crate) fn stage_overlay_record(record: &mut OverlayRecord) -> anyhow::Result<()> { if record.state == OverlayState::Active { record.state = OverlayState::Staged; - if let OverlayUpper::Jujutsu { - store_path, - workspace, - .. - } = &record.upper - { - snapshot_jujutsu_upper(store_path, workspace)?; - } write_overlay_record(record)?; } Ok(()) @@ -651,27 +529,12 @@ pub fn mount_overlay_record_read_only( return Err(OverlayError::MissingTarget); } let mountpoint = host_mountpoint(mountpoint); - let mut config = match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => OverlayMountConfig::new( - lower_dirs.to_vec(), - upper_dir.clone(), - Some(work_dir.clone()), - mountpoint.to_path_buf(), - ), - OverlayUpper::Jujutsu { - store_path, - workspace, - .. - } => OverlayMountConfig::new_jujutsu( - lower_dirs.to_vec(), - store_path.clone(), - workspace.clone(), - mountpoint.to_path_buf(), - ), - }; + let mut config = OverlayMountConfig::new( + lower_dirs.to_vec(), + record.upper.upper_dir.clone(), + Some(record.upper.work_dir.clone()), + mountpoint.to_path_buf(), + ); config.fsname = format!("pvisor-inspect-{}", record.id); config.excluded_paths = record.excluded_paths.clone(); for lower in lower_dirs { @@ -725,11 +588,7 @@ pub fn load_overlay_record(stage_dir: &Path) -> Result Result { - let upper_dir = match &record.upper { - OverlayUpper::Directory { upper_dir, .. } | OverlayUpper::Jujutsu { upper_dir, .. } => { - upper_dir - } - }; + let upper_dir = record.upper.path(); let mut changed = 0usize; let mut whiteouts = 0usize; let mut sample = Vec::new(); @@ -846,15 +705,21 @@ pub fn snapshot_overlay_upper( /// Restore a raw upper snapshot into a directory upper. pub fn restore_overlay_upper(source: &Path, destination: &Path) -> Result<(), OverlayError> { + if !source.is_dir() { + return Err(OverlayError::InvalidConfig(format!( + "snapshot source is not a directory: {}", + source.display() + ))); + } if path_exists(destination) { remove_path(destination)?; } fs::create_dir_all(destination)?; - if !source.is_dir() { - return Ok(()); - } let mut hard_links = HashMap::new(); - snapshot_directory_raw(source, destination, &mut hard_links, &|| false)?; + snapshot_directory_raw(source, destination, &mut hard_links)?; + if let Some(parent) = destination.parent() { + File::open(parent)?.sync_all()?; + } Ok(()) } @@ -1404,16 +1269,6 @@ fn complete_target_applied( prune_selected_upper(&upper_dir, selected_paths)?; } - if let OverlayUpper::Jujutsu { - store_path, - workspace, - .. - } = &record.upper - { - snapshot_jujutsu_upper(store_path, workspace) - .map_err(|error| OverlayError::Finalize(error.to_string()))?; - } - let remaining = overlay_changes(record, lower_dirs)?; if remaining.is_empty() { cleanup_terminal_overlay_data(record)?; @@ -1443,19 +1298,6 @@ pub fn discard_overlay(record: &mut OverlayRecord) -> Result<(), OverlayError> { } OverlayState::Active | OverlayState::Staged => {} } - match &record.upper { - OverlayUpper::Directory { .. } => {} - OverlayUpper::Jujutsu { - store_path, - workspace, - upper_dir, - } => { - clear_path(upper_dir)?; - snapshot_jujutsu_upper(store_path, workspace) - .map_err(|error| OverlayError::Finalize(error.to_string()))?; - clear_path(upper_dir)?; - } - } cleanup_terminal_overlay_data(record)?; record.state = OverlayState::Discarded; write_overlay_record(record)?; @@ -1463,16 +1305,8 @@ pub fn discard_overlay(record: &mut OverlayRecord) -> Result<(), OverlayError> { } fn cleanup_terminal_overlay_data(record: &OverlayRecord) -> Result<(), OverlayError> { - match &record.upper { - OverlayUpper::Directory { - upper_dir, - work_dir, - } => { - clear_path(upper_dir)?; - clear_path(work_dir)?; - } - OverlayUpper::Jujutsu { upper_dir, .. } => clear_path(upper_dir)?, - } + clear_path(&record.upper.upper_dir)?; + clear_path(&record.upper.work_dir)?; // Never recursively remove a mountpoint: after a clean teardown this is // either absent or an empty placeholder. A non-empty directory is retained @@ -1855,27 +1689,18 @@ fn snapshot_directory_raw( source: &Path, destination: &Path, hard_links: &mut HashMap<(u64, u64), PathBuf>, - cancelled: &dyn Fn() -> bool, ) -> Result<(), OverlayError> { - if cancelled() { - return Err(io::Error::new(io::ErrorKind::Interrupted, "materialization cancelled").into()); - } ensure_directory(destination)?; for entry in fs::read_dir(source)? { - if cancelled() { - return Err( - io::Error::new(io::ErrorKind::Interrupted, "materialization cancelled").into(), - ); - } let entry = entry?; snapshot_entry_raw( &entry.path(), &destination.join(entry.file_name()), hard_links, - cancelled, )?; } copy_snapshot_metadata(source, destination)?; + File::open(destination)?.sync_all()?; Ok(()) } @@ -1883,15 +1708,11 @@ fn snapshot_entry_raw( source: &Path, destination: &Path, hard_links: &mut HashMap<(u64, u64), PathBuf>, - cancelled: &dyn Fn() -> bool, ) -> Result<(), OverlayError> { - if cancelled() { - return Err(io::Error::new(io::ErrorKind::Interrupted, "materialization cancelled").into()); - } let metadata = fs::symlink_metadata(source)?; let kind = metadata.file_type(); if kind.is_dir() { - return snapshot_directory_raw(source, destination, hard_links, cancelled); + return snapshot_directory_raw(source, destination, hard_links); } if let Some(parent) = destination.parent() { fs::create_dir_all(parent)?; @@ -1926,6 +1747,9 @@ fn snapshot_entry_raw( } } copy_snapshot_metadata(source, destination)?; + if kind.is_file() { + File::open(destination)?.sync_all()?; + } Ok(()) } @@ -2318,27 +2142,13 @@ mod tests { assert_eq!(rec.stage_dir, PathBuf::from("/tmp/store/.overlay/run-1")); assert_eq!( rec.upper, - OverlayUpper::Directory { + OverlayUpper { upper_dir: PathBuf::from("/tmp/store/.overlay/run-1/upper"), work_dir: PathBuf::from("/tmp/store/.overlay/run-1/work") } ); } - #[test] - fn resolve_rejects_parallel_upper_backends() { - let cfg = OverlayConfig { - enabled: true, - target: Some("/proj".into()), - jujutsu_store_path: Some("/shared/jj".into()), - ..OverlayConfig::default() - }; - assert!(matches!( - resolve_overlay_workspace(&cfg, Path::new("/tmp/store"), "run-1"), - Err(OverlayError::InvalidConfig(_)) - )); - } - #[test] fn root_overlay_hides_its_stage_and_compose_backing_paths() { let cfg = OverlayConfig { @@ -2390,7 +2200,7 @@ mod tests { id: "mountless".into(), generation: 0, target: lower.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: stage.join("upper"), work_dir: stage.join("work"), }, @@ -2413,44 +2223,6 @@ mod tests { ); } - #[test] - fn jujutsu_sessions_share_store_but_get_distinct_workspaces() { - let storage = Path::new("/tmp/store"); - let cfg = OverlayConfig { - enabled: true, - target: Some("/proj".into()), - backend: OverlayBackend::Jujutsu, - ..OverlayConfig::default() - }; - let first = resolve_overlay_workspace(&cfg, storage, "fork-a") - .unwrap() - .unwrap(); - let second = resolve_overlay_workspace(&cfg, storage, "fork-b") - .unwrap() - .unwrap(); - let OverlayUpper::Jujutsu { - store_path: first_store, - workspace: first_workspace, - upper_dir: first_upper, - } = first.upper - else { - panic!("expected Jujutsu upper") - }; - let OverlayUpper::Jujutsu { - store_path: second_store, - workspace: second_workspace, - upper_dir: second_upper, - } = second.upper - else { - panic!("expected Jujutsu upper") - }; - assert_eq!(first_store, second_store); - assert_eq!(first_store, PathBuf::from("/tmp/store/.overlay/jujutsu")); - assert_eq!(first_workspace, "fork-a"); - assert_eq!(second_workspace, "fork-b"); - assert_ne!(first_upper, second_upper); - } - #[test] fn lower_stack_keeps_target_as_bottom_base_layer() { let cfg = OverlayConfig { @@ -2540,7 +2312,7 @@ mod tests { id: "embedded-e2e".into(), generation: 0, target: lower.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper.clone(), work_dir: work, }, @@ -2617,7 +2389,7 @@ mod tests { id: "t".into(), generation: 0, target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper.clone(), work_dir: work.clone(), }, @@ -2671,7 +2443,7 @@ mod tests { generation: 0, id: "selective".into(), target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper.clone(), work_dir: stage.join("work"), }, @@ -2801,7 +2573,7 @@ mod tests { id: operation.into(), generation: 0, target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper.clone(), work_dir: stage.join("work"), }, @@ -2882,7 +2654,7 @@ mod tests { generation: 0, id: format!("recover-{target_already_mutated}"), target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper.clone(), work_dir: stage.join("work"), }, @@ -2973,7 +2745,7 @@ mod tests { generation: 0, id: "conflicting-apply".into(), target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper.clone(), work_dir: work, }, @@ -3009,7 +2781,7 @@ mod tests { generation: 0, id: "opaque-recovery".into(), target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper.clone(), work_dir: stage.join("work"), }, @@ -3086,7 +2858,7 @@ mod tests { generation: 0, id: "glob".into(), target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper.clone(), work_dir: stage.join("work"), }, @@ -3137,7 +2909,7 @@ mod tests { generation: 0, id: "opaque-select".into(), target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper, work_dir: stage.join("work"), }, @@ -3189,7 +2961,7 @@ mod tests { generation: 0, id: "hard-links".into(), target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper, work_dir: stage.join("work"), }, @@ -3233,7 +3005,7 @@ mod tests { generation: 0, id: "invalid-selection".into(), target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper, work_dir: tmp.path().join("work"), }, @@ -3276,7 +3048,7 @@ mod tests { generation: 0, id: "changes".into(), target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper, work_dir: tmp.path().join("work"), }, @@ -3324,7 +3096,7 @@ mod tests { generation: 0, id: "immutable".into(), target: target.clone(), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper, work_dir: tmp.path().join("work"), }, @@ -3385,7 +3157,7 @@ mod tests { generation: 0, id: "t".into(), target: tmp.path().join("target"), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper, work_dir: tmp.path().join("work"), }, @@ -3412,7 +3184,7 @@ mod tests { id: "t".into(), generation: 0, target: tmp.path().join("target"), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper.clone(), work_dir: tmp.path().join("work"), }, @@ -3442,7 +3214,7 @@ mod tests { id: "applied-run".into(), generation: 0, target: applied_target, - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: applied_upper, work_dir: applied_root.path().join("work"), }, @@ -3472,7 +3244,7 @@ mod tests { id: "dropped-run".into(), generation: 0, target: dropped_root.path().join("target"), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: dropped_upper, work_dir: dropped_root.path().join("work"), }, diff --git a/crates/persisting-pvisor/src/runtime/registry.rs b/crates/persisting-pvisor/src/runtime/registry.rs index 32bfd399..0d5d875f 100644 --- a/crates/persisting-pvisor/src/runtime/registry.rs +++ b/crates/persisting-pvisor/src/runtime/registry.rs @@ -1,8 +1,8 @@ //! Durable Run identity, project association, and liveness metadata. use super::overlay::{ - OverlayRecord, OverlayUpper, ReadOnlyOverlayMount, load_overlay_record, - mount_overlay_record_read_only, overlay_status, + OverlayRecord, ReadOnlyOverlayMount, load_overlay_record, mount_overlay_record_read_only, + overlay_status, }; use crate::util::{atomic_write, create_dir_all_durable}; use anyhow::Context; @@ -111,11 +111,14 @@ impl RunRecord { pub fn write(&self) -> anyhow::Result<()> { let stage = self.stage_dir(); let path = stage.join(RUN_META_FILENAME); - atomic_write(&path, &serde_json::to_vec_pretty(self)?, 0o600)?; + crate::util::write_private_json(&path, self)?; let index_dir = self.storage.join(".pvisor").join("runs"); atomic_write( - &index_dir.join(format!("{}.json", encode_id(&self.run_id))), + &index_dir.join(format!( + "{}.json", + crate::util::encode_hex(self.run_id.as_bytes()) + )), &serde_json::to_vec_pretty(&RunIndex { run_id: self.run_id.clone(), stage_dir: stage, @@ -131,11 +134,10 @@ impl RunRecord { } pub fn remove_index(&self) -> anyhow::Result<()> { - let path = self - .storage - .join(".pvisor") - .join("runs") - .join(format!("{}.json", encode_id(&self.run_id))); + let path = self.storage.join(".pvisor").join("runs").join(format!( + "{}.json", + crate::util::encode_hex(self.run_id.as_bytes()) + )); match fs::remove_file(path) { Ok(()) => Ok(()), Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), @@ -458,7 +460,7 @@ pub fn resolve_run(selector: Option<&Path>, storage: &Path) -> anyhow::Result anyhow::Result { if record.overlay.as_ref().is_some_and(|overlay| { path_within(&absolute, &overlay.target) || path_within(&absolute, &overlay.merged_dir) - || match &overlay.upper { - OverlayUpper::Directory { upper_dir, .. } => { - path_within(&absolute, upper_dir) - } - OverlayUpper::Jujutsu { - store_path, - upper_dir, - .. - } => { - path_within(&absolute, upper_dir) - || path_within(&absolute, store_path) - } - } + || path_within(&absolute, overlay.upper.path()) }) { return Ok(record); } @@ -654,18 +644,10 @@ fn latest_run(storage: &Path) -> anyhow::Result { .ok_or_else(|| anyhow::anyhow!("no pVisor Runs found under {}", storage.display())) } -fn encode_id(id: &str) -> String { - let mut encoded = String::with_capacity(id.len() * 2); - for byte in id.as_bytes() { - use std::fmt::Write; - let _ = write!(encoded, "{byte:02x}"); - } - encoded -} - #[cfg(test)] mod tests { use super::*; + use crate::runtime::OverlayUpper; fn record(storage: &Path, stage: &Path, upper: &Path) -> RunRecord { RunRecord { @@ -696,7 +678,7 @@ mod tests { id: "session-test".into(), generation: 0, target: storage.join("target"), - upper: OverlayUpper::Directory { + upper: OverlayUpper { upper_dir: upper.to_path_buf(), work_dir: stage.join("work"), }, diff --git a/crates/persisting-pvisor/src/runtime/run.rs b/crates/persisting-pvisor/src/runtime/run.rs index ca259758..04864856 100644 --- a/crates/persisting-pvisor/src/runtime/run.rs +++ b/crates/persisting-pvisor/src/runtime/run.rs @@ -113,14 +113,7 @@ impl RunHandle { checkpoint_id: &str, timeout: std::time::Duration, ) -> anyhow::Result { - anyhow::ensure!( - !checkpoint_id.trim().is_empty() - && checkpoint_id != "." - && checkpoint_id != ".." - && !checkpoint_id.contains('/') - && !checkpoint_id.contains('\\'), - "checkpoint id must be one non-empty path-safe segment" - ); + super::checkpoint::validate_checkpoint_id(checkpoint_id)?; let record = self .checkpoint_record .as_ref() diff --git a/crates/persisting-pvisor/tests/run_config_cli.rs b/crates/persisting-pvisor/tests/run_config_cli.rs index f6500447..fa077359 100644 --- a/crates/persisting-pvisor/tests/run_config_cli.rs +++ b/crates/persisting-pvisor/tests/run_config_cli.rs @@ -466,7 +466,6 @@ fn every_public_run_option_is_accepted_by_the_real_cli_parser() { &["--container-mount", "source=\"/tmp\",target=\"/workspace\""], &["--mount", "/tmp/lower:read"], &["--access", "**/.ssh:deny"], - &["--overlayfs-backend", "directory"], &["--overlaynet", "proxy"], &["--overlaynet", "auto"], &["--overlaynet"], diff --git a/crates/persisting-replay/src/config.rs b/crates/persisting-replay/src/config.rs index 78352e6a..ba065076 100644 --- a/crates/persisting-replay/src/config.rs +++ b/crates/persisting-replay/src/config.rs @@ -69,7 +69,6 @@ pub struct OverlayFsConfig { /// Host layers composed in declaration order. #[serde(default)] pub compose: Vec, - pub backend: Option, pub commit: Option, } diff --git a/crates/persisting-replay/src/io.rs b/crates/persisting-replay/src/io.rs index f1915632..1dd2c9e3 100644 --- a/crates/persisting-replay/src/io.rs +++ b/crates/persisting-replay/src/io.rs @@ -7,11 +7,30 @@ use sha2::{Digest, Sha256}; use crate::error::{ReplayError, ReplayErrorKind, ResultExt}; pub fn read_regular_file(path: &Path) -> Result, ReplayError> { - let metadata = fs::symlink_metadata(path).replay_context( + let mut options = OpenOptions::new(); + options.read(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + // Check the opened inode, without following a final symlink or blocking on a FIFO. + options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK); + } + #[cfg(not(unix))] + if fs::symlink_metadata(path) + .replay_context(ReplayErrorKind::Configuration, "inspect input")? + .is_symlink() + { + return Err(ReplayError::configuration("input must not be a symlink")); + } + let file = options.open(path).replay_context( + ReplayErrorKind::Configuration, + format!("open {}", path.display()), + )?; + let metadata = file.metadata().replay_context( ReplayErrorKind::Configuration, format!("inspect {}", path.display()), )?; - if !metadata.file_type().is_file() || metadata.file_type().is_symlink() { + if !metadata.is_file() { return Err(ReplayError::configuration(format!( "input must be a regular file: {}", path.display() @@ -24,15 +43,13 @@ pub fn read_regular_file(path: &Path) -> Result, ReplayError> { path.display() ))); } - let mut file = File::open(path).replay_context( - ReplayErrorKind::Configuration, - format!("open {}", path.display()), - )?; - let mut bytes = Vec::with_capacity(metadata.len() as usize); - file.read_to_end(&mut bytes).replay_context( - ReplayErrorKind::Configuration, - format!("read {}", path.display()), - )?; + let mut bytes = Vec::with_capacity(metadata.len().min(8192) as usize); + file.take(metadata.len() + 1) + .read_to_end(&mut bytes) + .replay_context( + ReplayErrorKind::Configuration, + format!("read {}", path.display()), + )?; if bytes.len() as u64 != metadata.len() { return Err(ReplayError::configuration(format!( "input changed while being read: {}", @@ -105,3 +122,33 @@ pub fn canonicalize( ) -> Result { fs::canonicalize(path).replay_context(kind, format!("resolve {label} {}", path.display())) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn replay_input_accepts_only_bounded_regular_files() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("trajectory"); + fs::write(&path, b"trajectory").unwrap(); + assert_eq!(read_regular_file(&path).unwrap(), b"trajectory"); + assert!(read_regular_file(temp.path()).is_err()); + #[cfg(unix)] + { + let link = temp.path().join("link"); + std::os::unix::fs::symlink(&path, &link).unwrap(); + assert!(read_regular_file(&link).is_err()); + let fifo = temp.path().join("fifo"); + use std::os::unix::ffi::OsStrExt; + let name = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap(); + assert_eq!(unsafe { libc::mkfifo(name.as_ptr(), 0o600) }, 0); + assert!(read_regular_file(&fifo).is_err()); + } + File::create(&path) + .unwrap() + .set_len(256 * 1024 * 1024 + 1) + .unwrap(); + assert!(read_regular_file(&path).is_err()); + } +} diff --git a/docs/src/en/concepts/run-model.md b/docs/src/en/concepts/run-model.md index b4babe92..7c1fbd28 100644 --- a/docs/src/en/concepts/run-model.md +++ b/docs/src/en/concepts/run-model.md @@ -30,6 +30,6 @@ Network requests and external service mutations are also consequences, but files ## Checkpoint: a filesystem snapshot -The `fork` command snapshots the upper layer of a stopped staged Run. The embedded API also supports cooperative AgentCtl quiescence. A checkpoint preserves staged files and lineage, not process memory, external services, or an immutable copy of every lower layer. +The `fork` command snapshots the upper layer of a stopped staged Run. The embedded API also supports cooperative AgentCtl quiescence. A checkpoint preserves staged files, first-touch conflict baselines, and lineage, not process memory, external services, or an immutable copy of every lower layer. Snapshot contents are synced before the manifest is published. Embedded callers restore both directories with `restore_logical_checkpoint(checkpoint, destination_upper, destination_preimages)`. Read [review and apply](../guides/review-apply.md) for the operational workflow and [capabilities and evidence](capabilities-and-evidence.md) for execution guarantees. diff --git a/docs/src/en/design/architecture.md b/docs/src/en/design/architecture.md index 2348df16..84af130d 100644 --- a/docs/src/en/design/architecture.md +++ b/docs/src/en/design/architecture.md @@ -9,7 +9,7 @@ PolicyVisor (pVisor) combines capability admission, executors, runtime controls, | `persisting-pvisor` | CLI, admission, Attempt lifecycle, executors, Run Bundle, review/apply/checkpoint | | `persisting-control` | Run/Overlay contracts, capability policy, control messages/client, and shared event records | | `persisting-overlay-core` | Shared copy-on-write semantics and first-touch file fingerprints | -| `persisting-overlayfs` | Host FUSE adapter and optional Jujutsu backend | +| `persisting-overlayfs` | Host FUSE adapter | | `persisting-overlaynet` | Network authorization, resolution, proxy forwarding and VM network attachment | | `persisting-gateway` | Model routing, protocol conversion and capture | | `persisting-replay` | Agent-native trajectory replay and continuation adapters | diff --git a/docs/src/en/development/engineering.md b/docs/src/en/development/engineering.md index 47648f07..b8ab021f 100644 --- a/docs/src/en/development/engineering.md +++ b/docs/src/en/development/engineering.md @@ -14,7 +14,7 @@ package launches the packaged Rust binary; it is not a second runtime. | `crates/persisting-control/` | Shared contracts, policies, AgentCtl messages, IR and event schemas | | `crates/persisting-gateway/` | Agent protocol forwarding, conversion, capture and projection | | `crates/persisting-overlay-core/` | FUSE-independent overlay operations and file access enforcement | -| `crates/persisting-overlayfs/` | FUSE adapter, mounts and optional Jujutsu upper storage | +| `crates/persisting-overlayfs/` | FUSE adapter and mounts | | `crates/persisting-overlaynet/` | Egress policy, HTTP proxy and VM virtio-net data plane | | `crates/persisting-replay/` | Replay planning, native agent adapters and continuation bridges | | `pvisor/`, `setup.py`, `scripts/packaging/` | Python launcher and wheel assembly | diff --git a/docs/src/en/reference/cli.md b/docs/src/en/reference/cli.md index c869fd5c..64e90452 100644 --- a/docs/src/en/reference/cli.md +++ b/docs/src/en/reference/cli.md @@ -332,7 +332,6 @@ cannot identify every private key; add rules for custom names. ```toml [filesystem] stage = "../stage-001" -backend = "directory" mount = [{ source = "/opt/tool", access = "stage" }] access = [ { path = "**/.ssh", level = "deny" }, @@ -463,7 +462,6 @@ pvisor run \ --stage ../stage-001 \ --mount /opt/tool:stage \ --access '**/.ssh:deny' \ - --overlayfs-backend directory \ --overlaynet-allow api.openai.com:443 \ --overlaynet-deny 169.254.0.0/16 \ --overlaynet-limit 10mbps \ @@ -499,7 +497,6 @@ command = ["my-agent"] stage = "../stage-001" mount = [{ source = "/opt/tool", access = "stage" }] access = [{ path = "**/.ssh", level = "deny" }] -backend = "directory" [overlaynet] mode = "proxy" @@ -533,8 +530,8 @@ Run it with `pvisor run --config run.toml`. Explicit CLI scalars replace TOML scalars. Network and Gateway list options replace their complete configured lists; filesystem `--mount` and `--access` entries are appended to configured entries. Every serialized `[filesystem]` field has a CLI form: `stage`, `mount`, -`access`, `backend`, and `max_size` map to `--stage`, `--mount`, `--access`, -`--overlayfs-backend`, and `--overlayfs-max-size`. +`access`, and `max_size` map to `--stage`, `--mount`, `--access`, +and `--overlayfs-max-size`. The size limit is checked after execution, so it does not bound peak space used while the Agent is running. The command after `--` replaces `run.command`. @@ -635,7 +632,7 @@ project/ # reusable workspace / default base ├── run.json ├── run-bundle.json # mode 0600; outcome + safety + changes + effects ├── overlay.json # when OverlayFS is enabled - ├── upper/ # or a Run-named Jujutsu workspace upper + ├── upper/ ├── merged/ ├── checkpoints/ ├── lease.lock diff --git a/docs/src/zh/concepts/run-model.md b/docs/src/zh/concepts/run-model.md index a75eec30..573297d7 100644 --- a/docs/src/zh/concepts/run-model.md +++ b/docs/src/zh/concepts/run-model.md @@ -29,6 +29,6 @@ Fork 根据逻辑检查点创建带有来源关系的新 Run,不会恢复原 ## Checkpoint:文件系统快照 -`fork` 命令对已停止的暂存 Run 的 upper 层创建快照。嵌入式 API 还支持 AgentCtl 协作静默点。检查点保留暂存文件和来源关系,不保存进程内存、外部服务状态,也不冻结所有 lower 层。 +`fork` 命令对已停止的暂存 Run 的 upper 层创建快照。嵌入式 API 还支持 AgentCtl 协作静默点。检查点保留暂存文件、首次修改时的冲突基线和来源关系,不保存进程内存、外部服务状态,也不冻结所有 lower 层。快照内容同步落盘后才发布 manifest。嵌入式调用方通过 `restore_logical_checkpoint(checkpoint, destination_upper, destination_preimages)` 同时恢复文件和冲突基线。 操作步骤见[审查与应用](../guides/review-apply.md),执行保证见[能力与证据](capabilities-and-evidence.md)。 diff --git a/docs/src/zh/design/architecture.md b/docs/src/zh/design/architecture.md index aecc72cf..4730201c 100644 --- a/docs/src/zh/design/architecture.md +++ b/docs/src/zh/design/architecture.md @@ -9,7 +9,7 @@ PolicyVisor(pVisor)通过能力准入、执行器、运行时控制和执行 | `persisting-pvisor` | CLI、准入、Attempt 生命周期、执行器、Run Bundle、审查/应用/检查点 | | `persisting-control` | 运行与 Overlay 契约、能力策略、控制消息与客户端、共享事件记录 | | `persisting-overlay-core` | 共享写时复制语义与首次修改时的文件指纹 | -| `persisting-overlayfs` | 宿主 FUSE 适配器与可选 Jujutsu 后端 | +| `persisting-overlayfs` | 宿主 FUSE 适配器 | | `persisting-overlaynet` | 网络授权、解析、代理转发与 VM 网络接入 | | `persisting-gateway` | 模型路由、协议转换与捕获 | | `persisting-replay` | Agent 原生轨迹的回放与续跑适配 | diff --git a/docs/src/zh/development/engineering.md b/docs/src/zh/development/engineering.md index fefdf8d2..376643e7 100644 --- a/docs/src/zh/development/engineering.md +++ b/docs/src/zh/development/engineering.md @@ -13,7 +13,7 @@ Cargo workspace 按产品职责划分。Python `pvisor/` 只负责启动随包 | `crates/persisting-control/` | 共享契约、策略、AgentCtl 消息、IR 和事件 schema | | `crates/persisting-gateway/` | Agent 协议转发、转换、采集与投影 | | `crates/persisting-overlay-core/` | 不依赖 FUSE 的 OverlayFS 操作和文件访问控制 | -| `crates/persisting-overlayfs/` | FUSE 适配、挂载及可选的 Jujutsu 上层存储 | +| `crates/persisting-overlayfs/` | FUSE 适配及挂载 | | `crates/persisting-overlaynet/` | 出站策略、HTTP 代理和 VM virtio-net 数据通路 | | `crates/persisting-replay/` | 回放规划、原生 Agent 适配器和续跑协议桥 | | `pvisor/`、`setup.py`、`scripts/packaging/` | Python 启动器和 wheel 打包 | diff --git a/docs/src/zh/reference/cases.md b/docs/src/zh/reference/cases.md index 76e298ce..096f772b 100644 --- a/docs/src/zh/reference/cases.md +++ b/docs/src/zh/reference/cases.md @@ -582,7 +582,6 @@ workspace 和 user 规则写入 `~/.config/pvisor/config.toml` 的 `permissions` --stage /tmp/pvisor-cases/composed-stage \ --mount "/tmp/pvisor-cases/base:$PWD/view:stage" \ --mount "/tmp/pvisor-cases/layer:$PWD/view:stage" \ - --overlayfs-backend directory \ -- /bin/true ``` diff --git a/docs/src/zh/reference/cli.md b/docs/src/zh/reference/cli.md index c3dee68c..d10f6398 100644 --- a/docs/src/zh/reference/cli.md +++ b/docs/src/zh/reference/cli.md @@ -295,7 +295,6 @@ TOML 中对应: ```toml [filesystem] stage = "../stage-001" -backend = "directory" mount = [{ source = "/opt/tool", access = "stage" }] access = [ { path = "**/.ssh", level = "deny" }, @@ -410,7 +409,6 @@ pvisor run \ --stage ../stage-001 \ --mount /opt/tool:stage \ --access '**/.ssh:deny' \ - --overlayfs-backend directory \ --overlaynet-allow api.openai.com:443 \ --overlaynet-deny 169.254.0.0/16 \ --overlaynet-limit 10mbps \ @@ -444,7 +442,6 @@ command = ["my-agent"] stage = "../stage-001" mount = [{ source = "/opt/tool", access = "stage" }] access = [{ path = "**/.ssh", level = "deny" }] -backend = "directory" [overlaynet] mode = "proxy" @@ -476,8 +473,8 @@ destination = "./capture" 用 `pvisor run --config run.toml` 运行。显式 CLI 标量替换 TOML 标量。网络和 Gateway 列表选项替换配置中的完整列表;文件系统的 `--mount` 和 `--access` 追加到配置条目。 -`[filesystem]` 的序列化字段 `stage`、`mount`、`access`、`backend`、`max_size` -分别对应 `--stage`、`--mount`、`--access`、`--overlayfs-backend`、 +`[filesystem]` 的序列化字段 `stage`、`mount`、`access`、`max_size` +分别对应 `--stage`、`--mount`、`--access`、 `--overlayfs-max-size`。`--` 之后的命令替换 `run.command`。 大小限制在运行结束后检查,因此不限制 Agent 运行期间的峰值占用。 @@ -566,7 +563,7 @@ project/ # reusable workspace / default base ├── run.json ├── run-bundle.json # mode 0600; outcome + safety + changes + effects ├── overlay.json # when OverlayFS is enabled - ├── upper/ # or a Run-named Jujutsu workspace upper + ├── upper/ ├── merged/ ├── checkpoints/ ├── lease.lock