diff options
| author | Nikolay Govorov <me@govorov.online> | 2026-07-12 04:11:34 +0100 |
|---|---|---|
| committer | Nikolay Govorov <me@govorov.online> | 2026-07-12 04:11:34 +0100 |
| commit | 2a56ee9d2c402c55d3efb8491d1dab755c0505a4 (patch) | |
| tree | 87988837bc155baa0e8c036bfee66548e1cf25dc | |
| parent | 666ba6b7058c7fbecf4501b3468d9896e3c30d94 (diff) | |
| download | tar tar.gz tar.bz2 tar.lz tar.xz tar.zst zip | |
Basic OCI workflow: image import, pull/push, and run.
Diffstat
| -rw-r--r-- | .gitignore | 1 | +1 −0 |
| -rw-r--r-- | Cargo.lock | 2216 | +2170 −46 |
| -rw-r--r-- | Cargo.toml | 1 | +1 −0 |
| -rw-r--r-- | crates/hule-image/Cargo.toml | 17 | +17 −0 |
| -rw-r--r-- | crates/hule-image/src/lib.rs | 793 | +793 −0 |
| -rw-r--r-- | crates/hule/Cargo.toml | 7 | +6 −1 |
| -rw-r--r-- | crates/hule/src/main.rs | 719 | +570 −149 |
| -rwxr-xr-x | images/alpine/genimg | 65 | +33 −32 |
| -rwxr-xr-x | images/debian/genimg | 66 | +33 −33 |
| -rwxr-xr-x | images/fedora/genimg | 58 | +26 −32 |
| -rwxr-xr-x | images/freebsd/genimg | 46 | +21 −25 |
| -rwxr-xr-x | images/ubuntu/genimg | 55 | +26 −29 |
12 files changed, 3697 insertions, 347 deletions
diff --git a/.gitignore b/.gitignore index 9e0e89b..4af8e00 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ __pycache__ # Built machine images and manifests *.hmi *.hmm +/images/*/*/*/config.json diff --git a/Cargo.lock b/Cargo.lock index 2c164ec..ec39167 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,115 +3,2239 @@ version = 4 [[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[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", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "getset" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-auth" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" +dependencies = [ + "memchr", +] + +[[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 = "hule" version = "0.1.0" dependencies = [ - "hule-oci", - "hule-vmm", + "futures-util", + "hule-image", + "hule-oci", + "hule-vmm", + "oci-client", + "serde_json", + "sha2 0.10.9", + "tokio", + "zstd", +] + +[[package]] +name = "hule-image" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "hule-oci" +version = "0.1.0" + +[[package]] +name = "hule-vmm" +version = "0.1.0" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "10.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" +dependencies = [ + "aws-lc-rs", + "base64", + "getrandom 0.2.17", + "js-sys", + "serde", + "serde_json", + "signature", + "zeroize", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oci-client" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5261a7fb43d9c53b8e63e6d5e86860719dad253d015d022066c72d585125aed8" +dependencies = [ + "bytes", + "chrono", + "futures-util", + "hex", + "http", + "http-auth", + "jsonwebtoken", + "lazy_static", + "oci-spec", + "olpc-cjson", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2 0.11.0", + "thiserror", + "tokio", + "tracing", + "unicase", +] + +[[package]] +name = "oci-spec" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8445a2631507cec628a15fdd6154b54a3ab3f20ed4fe9d73a3b8b7a4e1ba03a" +dependencies = [ + "const_format", + "derive_builder", + "getset", + "regex", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", +] + +[[package]] +name = "olpc-cjson" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" +dependencies = [ + "serde", + "serde_json", + "unicode-normalization", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +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 = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[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-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", "serde", - "serde_json", ] [[package]] -name = "hule-oci" -version = "0.1.0" +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "hule-vmm" -version = "0.1.0" +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "itoa" -version = "1.0.18" +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] [[package]] -name = "memchr" -version = "2.8.3" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] -name = "quote" -version = "1.0.46" +name = "wasm-streams" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[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 = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", + "quote", + "syn", ] [[package]] -name = "serde" -version = "1.0.228" +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ - "serde_core", - "serde_derive", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "serde_derive", + "windows-link", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", "syn", + "synstructure", ] [[package]] -name = "serde_json" -version = "1.0.150" +name = "zerofrom" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "zerofrom-derive", ] [[package]] -name = "syn" -version = "2.0.118" +name = "zerofrom-derive" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", + "synstructure", ] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "zeroize" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 251b05f..ef31bac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ repository = "https://github.com/dimidiumlabs/hule" # local dependencies hule-oci = { path = "crates/hule-oci" } hule-vmm = { path = "crates/hule-vmm" } +hule-image = { path = "crates/hule-image" } # external dependencies serde = { version = "1", features = ["derive"] } diff --git a/crates/hule-image/Cargo.toml b/crates/hule-image/Cargo.toml new file mode 100644 --- /dev/null +++ b/crates/hule-image/Cargo.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "hule-image" +description = "Types and validation for Hule machine images" + +publish.workspace = true +edition.workspace = true +version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true diff --git a/crates/hule-image/src/lib.rs b/crates/hule-image/src/lib.rs new file mode 100644 --- /dev/null +++ b/crates/hule-image/src/lib.rs @@ -0,0 +1,793 @@ +// SPDX-FileCopyrightText: 2026 Nikolay Govorov +// SPDX-License-Identifier: Apache-2.0 + +//! Types and validation for a Hule machine image `config.json`. +//! +//! This crate deliberately has no filesystem, registry, or VMM concerns. It +//! validates the declared image contract; callers decide how referenced files +//! are stored and executed. +//! +//! # Schema evolution +//! +//! Bump [`SCHEMA_VERSION`] only for breaking changes. Within a version: +//! [`Boot`]/[`Access`] are menus, so they tolerate unrecognized entries via +//! `Unknown`; [`Architecture`]/[`DiskFormat`] are single mandatory values +//! with no fallback, so they stay closed enums; `system.os` and digest +//! algorithms stay plain strings. No `deny_unknown_fields`; `rename_all = +//! "camelCase"` on every struct. + +use std::collections::HashSet; +use std::fmt; +use std::path::{Component, Path}; + +use serde::{Deserialize, Serialize}; + +pub const SCHEMA_VERSION: u8 = 1; +pub const KIND: &str = "MachineImage"; + +pub const BOOT_LINUX_DIRECT: &str = "linux/direct"; +pub const BOOT_FIRMWARE_DISK_BIOS: &str = "firmware-disk/bios"; +pub const BOOT_FIRMWARE_DISK_UEFI: &str = "firmware-disk/uefi"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum DiskFormat { + Qcow2, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Digest(pub String); + +impl fmt::Display for Digest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl Digest { + fn validate_into(&self, field: &str, validation: &mut Validation) { + let Some(hex) = self.0.strip_prefix("sha256:") else { + validation.error(format!("{field} must use sha256")); + return; + }; + if hex.len() != 64 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) { + validation.error(format!("{field} must contain 64 hexadecimal digits")); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResourceRange { + pub minimum: u64, + pub default: u64, +} + +impl ResourceRange { + fn validate_into(self, field: &str, validation: &mut Validation) { + if self.minimum == 0 { + validation.error(format!("{field}.minimum must be greater than zero")); + } + if self.default < self.minimum { + validation.error(format!("{field}.default must be at least minimum")); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileRef { + pub path: String, + pub digest: Digest, +} + +impl FileRef { + fn validate_into(&self, field: &str, validation: &mut Validation) { + validation.path(&self.path, &format!("{field}.path")); + self.digest + .validate_into(&format!("{field}.digest"), validation); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BootBios { + pub protocol: String, + pub disk: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BootUefi { + pub protocol: String, + pub disk: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BootLinux { + pub protocol: String, + pub disk: String, + pub kernel: FileRef, + pub initrd: FileRef, + pub cmdline: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(untagged)] +pub enum Boot { + Bios(BootBios), + Uefi(BootUefi), + Linux(BootLinux), + Unknown(serde_json::Value), +} + +impl<'de> Deserialize<'de> for Boot { + fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + use serde::de::Error; + + let value = serde_json::Value::deserialize(deserializer)?; + let protocol = value.get("protocol").and_then(|p| p.as_str()).unwrap_or(""); + + #[derive(Deserialize)] + struct Raw { + disk: String, + #[serde(default)] + kernel: Option<FileRef>, + #[serde(default)] + initrd: Option<FileRef>, + #[serde(default)] + cmdline: Option<String>, + } + + match protocol { + BOOT_FIRMWARE_DISK_BIOS => { + let raw: Raw = serde_json::from_value(value).map_err(Error::custom)?; + if raw.kernel.is_some() || raw.initrd.is_some() || raw.cmdline.is_some() { + return Err(Error::custom( + "firmware-disk boot must not contain kernel, initrd, or cmdline", + )); + } + Ok(Self::Bios(BootBios { + protocol: BOOT_FIRMWARE_DISK_BIOS.into(), + disk: raw.disk, + })) + } + BOOT_FIRMWARE_DISK_UEFI => { + let raw: Raw = serde_json::from_value(value).map_err(Error::custom)?; + if raw.kernel.is_some() || raw.initrd.is_some() || raw.cmdline.is_some() { + return Err(Error::custom( + "firmware-disk boot must not contain kernel, initrd, or cmdline", + )); + } + Ok(Self::Uefi(BootUefi { + protocol: BOOT_FIRMWARE_DISK_UEFI.into(), + disk: raw.disk, + })) + } + BOOT_LINUX_DIRECT => { + let raw: Raw = serde_json::from_value(value).map_err(Error::custom)?; + Ok(Self::Linux(BootLinux { + protocol: BOOT_LINUX_DIRECT.into(), + disk: raw.disk, + kernel: raw.kernel.ok_or_else(|| Error::missing_field("kernel"))?, + initrd: raw.initrd.ok_or_else(|| Error::missing_field("initrd"))?, + cmdline: raw.cmdline.ok_or_else(|| Error::missing_field("cmdline"))?, + })) + } + _ => Ok(Self::Unknown(value)), + } + } +} + +impl Boot { + pub fn protocol(&self) -> &str { + match self { + Self::Bios(v) => &v.protocol, + Self::Uefi(v) => &v.protocol, + Self::Linux(v) => &v.protocol, + Self::Unknown(v) => v.get("protocol").and_then(|p| p.as_str()).unwrap_or(""), + } + } + + pub fn disk(&self) -> &str { + match self { + Self::Bios(v) => &v.disk, + Self::Uefi(v) => &v.disk, + Self::Linux(v) => &v.disk, + Self::Unknown(_) => "", + } + } + + /// False only for `Unknown`. + pub fn is_recognized(&self) -> bool { + !matches!(self, Self::Unknown(_)) + } + + fn validate_into(&self, field: &str, validation: &mut Validation) { + match self { + Self::Bios(value) => { + if value.protocol != BOOT_FIRMWARE_DISK_BIOS { + validation.error(format!( + "{field} BIOS protocol must be '{BOOT_FIRMWARE_DISK_BIOS}'" + )); + } + validation.required(&value.disk, &format!("{field}.disk")); + } + Self::Uefi(value) => { + if value.protocol != BOOT_FIRMWARE_DISK_UEFI { + validation.error(format!( + "{field} UEFI protocol must be '{BOOT_FIRMWARE_DISK_UEFI}'" + )); + } + validation.required(&value.disk, &format!("{field}.disk")); + } + Self::Linux(value) => { + if value.protocol != BOOT_LINUX_DIRECT { + validation.error(format!( + "{field} direct Linux protocol must be '{BOOT_LINUX_DIRECT}'" + )); + } + validation.required(&value.disk, &format!("{field}.disk")); + value + .kernel + .validate_into(&format!("{field}.kernel"), validation); + value + .initrd + .validate_into(&format!("{field}.initrd"), validation); + validation.required(&value.cmdline, &format!("{field}.cmdline")); + } + // Nothing to check: unrecognized entries are tolerated, not inspected. + Self::Unknown(_) => {} + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Architecture { + Amd64, + Arm64, + Loong64, + Ppc64le, + Riscv64, + S390x, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct System { + pub os: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option<String>, + pub version: String, + pub architecture: Architecture, +} + +impl System { + fn validate_into(&self, field: &str, validation: &mut Validation) { + validation.required(&self.os, &format!("{field}.os")); + if let Some(name) = &self.name { + validation.required(name, &format!("{field}.name")); + } + validation.required(&self.version, &format!("{field}.version")); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Disk { + pub id: String, + pub format: DiskFormat, + pub path: String, + pub digest: Digest, + pub virt_size: u64, + pub disk_size: u64, +} + +impl Disk { + fn validate_into(&self, field: &str, validation: &mut Validation) { + validation.required(&self.id, &format!("{field}.id")); + validation.path(&self.path, &format!("{field}.path")); + self.digest + .validate_into(&format!("{field}.digest"), validation); + if self.virt_size == 0 { + validation.error(format!("{field}.virtSize must be greater than zero")); + } + if self.disk_size == 0 { + validation.error(format!("{field}.diskSize must be greater than zero")); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(untagged, rename_all = "camelCase")] +pub enum Access { + Ssh { + #[serde(rename = "type")] + kind: String, + port: u16, + user: String, + auth: String, + }, + /// Unrecognized access surface, preserved verbatim. + Unknown(serde_json::Value), +} + +impl<'de> Deserialize<'de> for Access { + fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + use serde::de::Error; + + let value = serde_json::Value::deserialize(deserializer)?; + let kind = value.get("type").and_then(|t| t.as_str()).unwrap_or(""); + + match kind { + "ssh" => { + #[derive(Deserialize)] + struct Raw { + port: u16, + user: String, + auth: String, + } + let raw: Raw = serde_json::from_value(value).map_err(Error::custom)?; + Ok(Self::Ssh { + kind: "ssh".into(), + port: raw.port, + user: raw.user, + auth: raw.auth, + }) + } + _ => Ok(Self::Unknown(value)), + } + } +} + +impl Access { + /// False only for `Unknown`. + pub fn is_recognized(&self) -> bool { + !matches!(self, Self::Unknown(_)) + } + + fn validate_into(&self, field: &str, validation: &mut Validation) { + match self { + Self::Ssh { port, user, auth, .. } => { + if *port == 0 { + validation.error(format!("{field}.port must be greater than zero")); + } + validation.required(user, &format!("{field}.user")); + validation.required(auth, &format!("{field}.auth")); + } + Self::Unknown(_) => {} + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkDhcp {} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkStatic { + pub address: String, + pub gateway: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "mode", rename_all = "lowercase")] +pub enum Network { + Dhcp(NetworkDhcp), + Static(NetworkStatic), +} + +impl Network { + fn validate_into(&self, field: &str, validation: &mut Validation) { + if let Self::Static(network) = self { + validation.required(&network.address, &format!("{field}.address")); + validation.required(&network.gateway, &format!("{field}.gateway")); + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Machine { + pub cpu: ResourceRange, + pub ram: ResourceRange, + pub boot: Vec<Boot>, + pub access: Vec<Access>, + pub network: Network, +} + +impl Machine { + fn validate_into(&self, field: &str, validation: &mut Validation) { + self.cpu.validate_into(&format!("{field}.cpu"), validation); + self.ram.validate_into(&format!("{field}.ram"), validation); + if !self.boot.iter().any(Boot::is_recognized) { + validation.error(format!( + "{field}.boot must contain at least one supported boot protocol" + )); + } + for (index, boot) in self.boot.iter().enumerate() { + boot.validate_into(&format!("{field}.boot[{index}]"), validation); + } + if !self.access.iter().any(Access::is_recognized) { + validation.error(format!( + "{field}.access must contain at least one supported access method \ + (a running machine must be reachable by something, if only to shut it down)" + )); + } + for (index, access) in self.access.iter().enumerate() { + access.validate_into(&format!("{field}.access[{index}]"), validation); + } + self.network + .validate_into(&format!("{field}.network"), validation); + } +} + +#[derive(Default)] +struct Validation { + issues: Vec<String>, +} + +impl Validation { + fn error(&mut self, message: impl Into<String>) { + self.issues.push(message.into()); + } + + fn required(&mut self, value: &str, field: &str) { + if value.trim().is_empty() { + self.error(format!("{field} must not be empty")); + } + } + + fn path(&mut self, value: &str, field: &str) { + let path = Path::new(value); + if value.is_empty() + || path.is_absolute() + || path + .components() + .any(|component| !matches!(component, Component::Normal(_))) + { + self.error(format!("{field} must be a relative normalized path")); + } + } + + fn finish(self) -> Result<(), ValidationError> { + if self.issues.is_empty() { + Ok(()) + } else { + Err(ValidationError { + issues: self.issues, + }) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ValidationError { + issues: Vec<String>, +} + +impl ValidationError { + fn single(message: impl Into<String>) -> Self { + Self { + issues: vec![message.into()], + } + } + + pub fn issues(&self) -> &[String] { + &self.issues + } +} + +impl fmt::Display for ValidationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "invalid Hule image configuration: {}", + self.issues.join("; ") + ) + } +} + +impl std::error::Error for ValidationError {} + +#[derive(Debug)] +pub enum ParseError { + Json(serde_json::Error), + Validation(ValidationError), +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Json(error) => write!(f, "invalid JSON: {error}"), + Self::Validation(error) => error.fmt(f), + } + } +} + +impl std::error::Error for ParseError {} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MachineImage { + pub schema_version: u8, + pub kind: String, + pub system: System, + pub machine: Machine, + pub disks: Vec<Disk>, +} + +impl MachineImage { + pub fn from_json(data: &[u8]) -> Result<Self, ParseError> { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Header { + schema_version: u8, + #[serde(default)] + kind: Option<String>, + } + + let header: Header = serde_json::from_slice(data).map_err(ParseError::Json)?; + if header.schema_version != SCHEMA_VERSION { + return Err(ParseError::Validation(ValidationError::single(format!( + "unsupported schemaVersion {}", + header.schema_version + )))); + } + if header.kind.as_deref() != Some(KIND) { + return Err(ParseError::Validation(ValidationError::single(format!( + "kind must be '{KIND}'" + )))); + } + + let image: Self = serde_json::from_slice(data).map_err(ParseError::Json)?; + image.validate().map_err(ParseError::Validation)?; + Ok(image) + } + + pub fn validate(&self) -> Result<(), ValidationError> { + if self.schema_version != SCHEMA_VERSION { + return Err(ValidationError::single(format!( + "unsupported schemaVersion {}", + self.schema_version + ))); + } + if self.kind != KIND { + return Err(ValidationError::single(format!("kind must be '{KIND}'"))); + } + + let mut validation = Validation::default(); + self.system.validate_into("system", &mut validation); + self.machine.validate_into("machine", &mut validation); + + if self.disks.is_empty() { + validation.error("disks must not be empty"); + } + let mut disk_ids = HashSet::new(); + let mut paths = HashSet::new(); + for (index, disk) in self.disks.iter().enumerate() { + let at = format!("disks[{index}]"); + disk.validate_into(&at, &mut validation); + if !disk_ids.insert(disk.id.as_str()) { + validation.error(format!("duplicate disk id '{}'", disk.id)); + } + if !paths.insert(disk.path.as_str()) { + validation.error(format!("duplicate image path '{}'", disk.path)); + } + } + + let mut boot_protocols = HashSet::new(); + for (index, boot) in self.machine.boot.iter().enumerate() { + if !boot.is_recognized() { + // Tolerated, not cross-referenced: we don't know what its + // fields (e.g. "disk") even mean. + continue; + } + if !boot_protocols.insert(boot.protocol()) { + validation.error(format!("duplicate boot protocol '{}'", boot.protocol())); + } + if !disk_ids.contains(boot.disk()) { + validation.error(format!( + "machine.boot[{index}] references unknown disk '{}'", + boot.disk() + )); + } + if let Boot::Linux(linux) = boot { + if self.system.os != "linux" { + validation.error(format!( + "machine.boot[{index}] protocol '{BOOT_LINUX_DIRECT}' requires system.os 'linux'" + )); + } + for path in [&linux.kernel.path, &linux.initrd.path] { + if !paths.insert(path) { + validation.error(format!("duplicate image path '{path}'")); + } + } + } + } + + validation.finish() + } + + pub fn referenced_paths(&self) -> impl Iterator<Item = &str> { + self.disks.iter().map(|d| d.path.as_str()).chain( + self.machine + .boot + .iter() + .flat_map(|boot| match boot { + Boot::Bios(_) | Boot::Uefi(_) | Boot::Unknown(_) => [None, None], + Boot::Linux(v) => [Some(v.kernel.path.as_str()), Some(v.initrd.path.as_str())], + }) + .flatten(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn valid_json() -> Vec<u8> { + br#"{"schemaVersion":1,"kind":"MachineImage","system":{"os":"linux","name":"alpine","version":"3.24","architecture":"amd64"},"machine":{"cpu":{"minimum":1,"default":2},"ram":{"minimum":268435456,"default":1073741824},"boot":[{"protocol":"firmware-disk/bios","disk":"root"}],"access":[{"type":"ssh","port":22,"user":"build","auth":"empty-password"}],"network":{"mode":"static","address":"10.0.2.15/24","gateway":"10.0.2.2"}},"disks":[{"id":"root","format":"qcow2","path":"root.hmi","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","virtSize":1024,"diskSize":512}]}"#.to_vec() + } + + #[test] + fn parses_and_round_trips_valid_config() { + let image = MachineImage::from_json(&valid_json()).unwrap(); + let encoded = serde_json::to_vec(&image).unwrap(); + assert_eq!(MachineImage::from_json(&encoded).unwrap(), image); + } + + #[test] + fn rejects_unknown_disk_and_unsafe_path() { + let mut image = MachineImage::from_json(&valid_json()).unwrap(); + image.disks[0].path = "../root.hmi".into(); + match &mut image.machine.boot[0] { + Boot::Bios(v) => v.disk = "missing".into(), + Boot::Uefi(_) | Boot::Linux(_) | Boot::Unknown(_) => unreachable!(), + } + let error = image.validate().unwrap_err(); + assert!( + error + .issues() + .iter() + .any(|v| v.contains("relative normalized path")) + ); + assert!(error.issues().iter().any(|v| v.contains("unknown disk"))); + } + + #[test] + fn rejects_invalid_digest_and_resource_range() { + let mut image = MachineImage::from_json(&valid_json()).unwrap(); + image.disks[0].digest = Digest("md5:no".into()); + image.machine.cpu.default = 0; + let error = image.validate().unwrap_err(); + assert_eq!(error.issues().len(), 2); + } + + #[test] + fn system_name_is_optional_but_not_empty() { + let mut image = MachineImage::from_json(&valid_json()).unwrap(); + image.system.name = None; + let encoded = serde_json::to_value(&image).unwrap(); + assert!(encoded["system"].get("name").is_none()); + image.validate().unwrap(); + + image.system.name = Some(String::new()); + assert!(image.validate().is_err()); + } + + #[test] + fn parses_uefi_firmware_disk_boot() { + let mut image = MachineImage::from_json(&valid_json()).unwrap(); + image.machine.boot[0] = Boot::Uefi(BootUefi { + protocol: BOOT_FIRMWARE_DISK_UEFI.into(), + disk: "root".into(), + }); + + let encoded = serde_json::to_vec(&image).unwrap(); + let decoded = MachineImage::from_json(&encoded).unwrap(); + assert_eq!(decoded.machine.boot[0].protocol(), BOOT_FIRMWARE_DISK_UEFI); + } + + #[test] + fn rejects_config_whose_only_boot_entry_is_unrecognized() { + // An unrecognized boot protocol must not fail *parsing* -- see + // `mixed_boot_list_tolerates_unrecognized_entries` below for why -- + // but a config with *no* recognized entry at all is unusable and + // must fail *validation*. + let json = String::from_utf8(valid_json()) + .unwrap() + .replace(BOOT_FIRMWARE_DISK_BIOS, "firmware-disk/unknown"); + let error = MachineImage::from_json(json.as_bytes()).unwrap_err(); + assert!( + error + .to_string() + .contains("must contain at least one supported boot protocol") + ); + } + + #[test] + fn mixed_boot_list_tolerates_unrecognized_entries() { + // A newer image may list a protocol this version doesn't know about + // alongside one it does; parsing and validation must both succeed, + // and the unrecognized entry must round-trip losslessly. + let json = String::from_utf8(valid_json()).unwrap().replace( + r#"[{"protocol":"firmware-disk/bios","disk":"root"}]"#, + r#"[{"protocol":"firmware-disk/bios","disk":"root"},{"protocol":"vsock/direct","disk":"root","futureField":42}]"#, + ); + let image = MachineImage::from_json(json.as_bytes()).unwrap(); + assert_eq!(image.machine.boot.len(), 2); + assert!(image.machine.boot[0].is_recognized()); + assert!(!image.machine.boot[1].is_recognized()); + + let encoded = serde_json::to_vec(&image).unwrap(); + assert_eq!(MachineImage::from_json(&encoded).unwrap(), image); + } + + #[test] + fn rejects_config_whose_only_access_entry_is_unrecognized() { + let json = String::from_utf8(valid_json()).unwrap().replace( + r#"{"type":"ssh","port":22,"user":"build","auth":"empty-password"}"#, + r#"{"type":"vsock-agent","channel":9000}"#, + ); + let error = MachineImage::from_json(json.as_bytes()).unwrap_err(); + assert!( + error + .to_string() + .contains("must contain at least one supported access method") + ); + } + + #[test] + fn parses_dhcp_network_without_static_parameters() { + let json = String::from_utf8(valid_json()).unwrap().replace( + r#"{"mode":"static","address":"10.0.2.15/24","gateway":"10.0.2.2"}"#, + r#"{"mode":"dhcp"}"#, + ); + let image = MachineImage::from_json(json.as_bytes()).unwrap(); + assert!(matches!(image.machine.network, Network::Dhcp(_))); + } + + #[test] + fn rejects_unknown_schema_before_parsing_schema_fields() { + let error = MachineImage::from_json(br#"{"schemaVersion":2}"#).unwrap_err(); + let ParseError::Validation(error) = error else { + panic!("expected validation error") + }; + assert_eq!(error.issues(), ["unsupported schemaVersion 2"]); + } + + #[test] + fn rejects_unknown_kind_before_parsing_kind_fields() { + let error = MachineImage::from_json(br#"{"schemaVersion":1,"kind":"Other"}"#).unwrap_err(); + let ParseError::Validation(error) = error else { + panic!("expected validation error") + }; + assert_eq!(error.issues(), ["kind must be 'MachineImage'"]); + } + + #[test] + fn rejects_unknown_architecture() { + let json = String::from_utf8(valid_json()) + .unwrap() + .replace("\"architecture\":\"amd64\"", "\"architecture\":\"mips\""); + let error = MachineImage::from_json(json.as_bytes()).unwrap_err(); + assert!(error.to_string().contains("unknown variant `mips`")); + } + + #[test] + fn parses_additional_oci_architectures() { + for architecture in ["loong64", "riscv64", "s390x"] { + let json = String::from_utf8(valid_json()).unwrap().replace( + "\"architecture\":\"amd64\"", + &format!("\"architecture\":\"{architecture}\""), + ); + MachineImage::from_json(json.as_bytes()).unwrap(); + } + } +} diff --git a/crates/hule/Cargo.toml b/crates/hule/Cargo.toml index 8a5dfa7..41a87ef 100644 --- a/crates/hule/Cargo.toml +++ b/crates/hule/Cargo.toml @@ -15,5 +15,10 @@ repository.workspace = true [dependencies] hule-oci.workspace = true hule-vmm.workspace = true -serde.workspace = true +hule-image.workspace = true serde_json.workspace = true +oci-client = "0.17" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] } +futures-util = "0.3" +zstd = "0.13" +sha2 = "0.10" diff --git a/crates/hule/src/main.rs b/crates/hule/src/main.rs index ba3a72e..fdff3a8 100644 --- a/crates/hule/src/main.rs +++ b/crates/hule/src/main.rs @@ -1,198 +1,549 @@ // SPDX-FileCopyrightText: 2026 Nikolay Govorov // SPDX-License-Identifier: Apache-2.0 -//! Manifest-driven VM harness +//! Manifest-driven VM harness and OCI image lifecycle. //! -//! Reads a Hule Machine Manifest (`image.hmm`) from an image directory and +//! Reads a Hule machine configuration (`config.json`) from an image directory and //! boots the guest with qemu according to the boot protocol it declares. No //! per-distro logic lives here -- everything comes from the manifest. See //! docs/boot-protocol.md. +//! +//! Images live in a local content-addressed store (`$HOME/.hule/store`, a +//! plain OCI Image Layout) and move in and out of OCI registries via +//! `import`/`push`/`pull`; `run` resolves a reference against that store +//! instead of taking a raw directory. +use std::collections::{BTreeMap, BTreeSet}; +use std::io::Read; use std::path::{Path, PathBuf}; use std::process::{Command, exit}; -use serde::Deserialize; +use hule_image::{Access, Architecture, BOOT_FIRMWARE_DISK_BIOS, Boot, MachineImage}; +use oci_client::annotations::{ORG_OPENCONTAINERS_IMAGE_REF_NAME, ORG_OPENCONTAINERS_IMAGE_TITLE}; +use oci_client::client::{ClientConfig, ClientProtocol, Config, ImageLayer}; +use oci_client::manifest::{ + ImageIndexEntry, OCI_IMAGE_MEDIA_TYPE, OciDescriptor, OciImageIndex, OciImageManifest, +}; +use oci_client::secrets::RegistryAuth; +use oci_client::{Client, Reference}; + +type R<T> = std::result::Result<T, String>; -#[derive(Deserialize)] -struct MinDefault { - minimum: u64, - default: u64, +fn die(msg: impl AsRef<str>) -> ! { + eprintln!("hule: {}", msg.as_ref()); + exit(1); } -#[derive(Deserialize)] -struct System { - family: String, - distro: String, - release: String, +// ---- local OCI-layout store ------------------------------------------- + +const CHUNK_SIZE: usize = 256 * 1024 * 1024; +const HULE_CHUNK_MEDIA_TYPE: &str = "application/vnd.hule.disk.chunk.v1"; +const HULE_CONFIG_MEDIA_TYPE: &str = "application/vnd.hule.machine.config.v1+json"; +const ANNOTATION_CHUNK_OFFSET: &str = "io.hule.chunk.offset"; +const ANNOTATION_CHUNK_LENGTH: &str = "io.hule.chunk.length"; + +fn store_root() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| die("HOME is not set")); + PathBuf::from(home).join(".hule").join("store") } -#[derive(Deserialize)] -struct Machine { - arch: String, - cpu: MinDefault, - ram: MinDefault, +fn blobs_dir() -> PathBuf { + store_root().join("blobs").join("sha256") } -#[derive(Deserialize)] -struct Disk { - id: String, - path: String, +fn materialized_dir(manifest_digest: &str) -> PathBuf { + store_root() + .join("materialized") + .join(strip_sha256(manifest_digest)) } -#[derive(Deserialize)] -struct FileRef { - path: String, - checksum: String, +fn index_path() -> PathBuf { + store_root().join("index.json") } -/// A boot protocol entry, discriminated by its `id`. `firmware-disk/*` boots a -/// self-contained disk; `linux/*` is direct-kernel and therefore carries the -/// kernel, initrd and cmdline (validated at parse time, not left optional). -enum Boot { - FirmwareDisk { - id: String, - disk: String, - }, - Linux { - id: String, - disk: String, - kernel: FileRef, - initrd: FileRef, - cmdline: String, - }, +fn strip_sha256(digest: &str) -> &str { + digest.strip_prefix("sha256:").unwrap_or(digest) } -impl Boot { - fn id(&self) -> &str { - match self { - Boot::FirmwareDisk { id, .. } | Boot::Linux { id, .. } => id, - } +fn sha256_hex(data: &[u8]) -> String { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(data); + format!("sha256:{:x}", hasher.finalize()) +} + +fn ensure_store() -> R<()> { + std::fs::create_dir_all(blobs_dir()).map_err(|e| e.to_string())?; + std::fs::create_dir_all(store_root().join("materialized")).map_err(|e| e.to_string())?; + let layout = store_root().join("oci-layout"); + if !layout.exists() { + std::fs::write(&layout, br#"{"imageLayoutVersion":"1.0.0"}"#).map_err(|e| e.to_string())?; + } + Ok(()) +} + +/// Writes raw bytes as a content-addressed blob, no-op if already present. +fn write_blob(data: &[u8]) -> R<String> { + let digest = sha256_hex(data); + let path = blobs_dir().join(strip_sha256(&digest)); + if !path.exists() { + let tmp = path.with_extension("tmp"); + std::fs::write(&tmp, data).map_err(|e| e.to_string())?; + std::fs::rename(&tmp, &path).map_err(|e| e.to_string())?; + } + Ok(digest) +} + +fn read_blob(digest: &str) -> R<Vec<u8>> { + std::fs::read(blobs_dir().join(strip_sha256(digest))).map_err(|e| e.to_string()) +} + +fn read_index() -> R<OciImageIndex> { + let path = index_path(); + if !path.exists() { + return Ok(OciImageIndex { + schema_version: 2, + media_type: None, + manifests: vec![], + artifact_type: None, + annotations: None, + }); + } + let data = std::fs::read(&path).map_err(|e| e.to_string())?; + serde_json::from_slice(&data).map_err(|e| e.to_string()) +} + +fn write_index(index: &OciImageIndex) -> R<()> { + let data = serde_json::to_vec_pretty(index).map_err(|e| e.to_string())?; + std::fs::write(index_path(), data).map_err(|e| e.to_string()) +} + +fn index_lookup(index: &OciImageIndex, reference: &str) -> Option<String> { + index + .manifests + .iter() + .find(|m| { + m.annotations + .as_ref() + .and_then(|a| a.get(ORG_OPENCONTAINERS_IMAGE_REF_NAME)) + .map(String::as_str) + == Some(reference) + }) + .map(|m| m.digest.clone()) +} + +fn index_set(index: &mut OciImageIndex, reference: &str, digest: &str, size: u64) { + index.manifests.retain(|m| { + m.annotations + .as_ref() + .and_then(|a| a.get(ORG_OPENCONTAINERS_IMAGE_REF_NAME)) + .map(String::as_str) + != Some(reference) + }); + let mut annotations = BTreeMap::new(); + annotations.insert( + ORG_OPENCONTAINERS_IMAGE_REF_NAME.to_string(), + reference.to_string(), + ); + index.manifests.push(ImageIndexEntry { + media_type: OCI_IMAGE_MEDIA_TYPE.to_string(), + digest: digest.to_string(), + size: size as i64, + platform: None, + annotations: Some(annotations), + artifact_type: None, + }); +} + +fn hardlink_or_copy(src: &Path, dest: &Path) -> R<()> { + if std::fs::hard_link(src, dest).is_err() { + std::fs::copy(src, dest).map_err(|e| e.to_string())?; } + Ok(()) +} - fn disk(&self) -> &str { - match self { - Boot::FirmwareDisk { disk, .. } | Boot::Linux { disk, .. } => disk, +fn write_at(file: &std::fs::File, offset: u64, data: &[u8]) -> R<()> { + use std::os::unix::fs::FileExt; + let mut written = 0usize; + while written < data.len() { + let n = file + .write_at(&data[written..], offset + written as u64) + .map_err(|e| e.to_string())?; + if n == 0 { + return Err("write_at wrote 0 bytes".to_string()); } + written += n; } + Ok(()) } -impl<'de> Deserialize<'de> for Boot { - fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> { - use serde::de::Error; - - #[derive(Deserialize)] - struct Raw { - id: String, - disk: String, - #[serde(default)] - kernel: Option<FileRef>, - #[serde(default)] - initrd: Option<FileRef>, - #[serde(default)] - cmdline: Option<String>, +/// Splits `path` into fixed-size chunks, compresses each independently +/// (own zstd frame, no state shared between chunks -- see the design notes +/// in the project plan on why this must not be done the other way around), +/// and writes each as its own blob. Returns one descriptor per chunk, all +/// sharing a title annotation plus a chunk offset/length in terms of the +/// *uncompressed* file so pull can reassemble it. +fn write_file_chunks(path: &Path) -> R<Vec<OciDescriptor>> { + let title = path + .file_name() + .ok_or_else(|| format!("{} has no file name", path.display()))? + .to_string_lossy() + .to_string(); + let mut file = std::fs::File::open(path).map_err(|e| e.to_string())?; + let mut descriptors = Vec::new(); + let mut offset: u64 = 0; + loop { + let mut buf = Vec::with_capacity(CHUNK_SIZE); + (&mut file) + .take(CHUNK_SIZE as u64) + .read_to_end(&mut buf) + .map_err(|e| e.to_string())?; + if buf.is_empty() { + break; } + let length = buf.len() as u64; + let compressed = zstd::stream::encode_all(&buf[..], 0).map_err(|e| e.to_string())?; + let digest = write_blob(&compressed)?; - let r = Raw::deserialize(d)?; - if r.id.starts_with("linux/") { - Ok(Boot::Linux { - kernel: r.kernel.ok_or_else(|| Error::missing_field("kernel"))?, - initrd: r.initrd.ok_or_else(|| Error::missing_field("initrd"))?, - cmdline: r.cmdline.ok_or_else(|| Error::missing_field("cmdline"))?, - id: r.id, - disk: r.disk, - }) - } else if r.id.starts_with("firmware-disk/") { - Ok(Boot::FirmwareDisk { - id: r.id, - disk: r.disk, - }) - } else { - Err(Error::custom(format!("unknown boot protocol id '{}'", r.id))) + let mut annotations = BTreeMap::new(); + annotations.insert(ORG_OPENCONTAINERS_IMAGE_TITLE.to_string(), title.clone()); + annotations.insert(ANNOTATION_CHUNK_OFFSET.to_string(), offset.to_string()); + annotations.insert(ANNOTATION_CHUNK_LENGTH.to_string(), length.to_string()); + + descriptors.push(OciDescriptor { + media_type: HULE_CHUNK_MEDIA_TYPE.to_string(), + digest, + size: compressed.len() as i64, + urls: None, + annotations: Some(annotations), + artifact_type: None, + }); + + offset += length; + if length < CHUNK_SIZE as u64 { + break; } } + if descriptors.is_empty() { + return Err(format!("{} is empty", path.display())); + } + Ok(descriptors) } -/// A guest access surface, discriminated by `type`. -#[derive(Deserialize)] -#[serde(tag = "type", rename_all = "lowercase")] -enum Access { - Ssh { port: u16 }, +fn make_client(reference: &Reference) -> Client { + let registry = reference.resolve_registry(); + let host = registry.split(':').next().unwrap_or(registry); + let protocol = if host == "localhost" || host == "127.0.0.1" { + ClientProtocol::Http + } else { + ClientProtocol::Https + }; + Client::new(ClientConfig { + protocol, + ..Default::default() + }) } -#[derive(Deserialize)] -struct Network { - mode: String, - address: String, - gateway: String, +fn parse_reference(s: &str) -> R<Reference> { + s.parse() + .map_err(|e| format!("invalid reference '{s}': {e}")) } -#[derive(Deserialize)] -struct Manifest { - schemaVersion: u8, - kind: String, +// ---- import / push / pull / run --------------------------------------- + +async fn cmd_import(path: &Path, reference: Option<&str>) -> R<()> { + ensure_store()?; + let config_path = path.join("config.json"); + if !config_path.is_file() { + return Err(format!("{} does not contain config.json", path.display())); + } + let config_bytes = std::fs::read(&config_path).map_err(|e| e.to_string())?; + MachineImage::from_json(&config_bytes).map_err(|e| e.to_string())?; + + let mut entries: Vec<_> = std::fs::read_dir(path) + .map_err(|e| e.to_string())? + .collect::<std::result::Result<Vec<_>, _>>() + .map_err(|e| e.to_string())?; + entries.sort_by_key(|e| e.file_name()); - system: System, - machine: Machine, + let mut layers = Vec::new(); + let mut sources: Vec<(String, PathBuf)> = Vec::new(); + for entry in entries { + if !entry.file_type().map_err(|e| e.to_string())?.is_file() { + continue; + } + let name = entry.file_name().to_string_lossy().to_string(); + if name == "config.json" { + continue; + } + let file_path = entry.path(); + layers.extend(write_file_chunks(&file_path)?); + sources.push((name, file_path)); + } - boot: Vec<Boot>, - disks: Vec<Disk>, - access: Vec<Access>, - network: Network, + let config_digest = write_blob(&config_bytes)?; + let config = OciDescriptor { + media_type: HULE_CONFIG_MEDIA_TYPE.to_string(), + digest: config_digest, + size: config_bytes.len() as i64, + urls: None, + annotations: None, + artifact_type: None, + }; + + let manifest = OciImageManifest { + schema_version: 2, + media_type: Some(OCI_IMAGE_MEDIA_TYPE.to_string()), + config, + layers, + subject: None, + artifact_type: None, + annotations: None, + }; + let manifest_bytes = serde_json::to_vec(&manifest).map_err(|e| e.to_string())?; + let manifest_digest = write_blob(&manifest_bytes)?; + + let materialized = materialized_dir(&manifest_digest); + std::fs::create_dir_all(&materialized).map_err(|e| e.to_string())?; + for (title, source) in &sources { + let dest = materialized.join(title); + if !dest.exists() { + hardlink_or_copy(source, &dest)?; + } + } + + if let Some(reference) = reference { + let mut index = read_index()?; + index_set( + &mut index, + reference, + &manifest_digest, + manifest_bytes.len() as u64, + ); + write_index(&index)?; + eprintln!( + "hule: imported {} as {reference} ({manifest_digest})", + path.display() + ); + } else { + eprintln!( + "hule: imported {} ({manifest_digest}, untagged)", + path.display() + ); + } + Ok(()) } -fn die(msg: impl AsRef<str>) -> ! { - eprintln!("hule: {}", msg.as_ref()); - exit(1); +async fn cmd_push(reference_str: &str) -> R<()> { + ensure_store()?; + let index = read_index()?; + let manifest_digest = index_lookup(&index, reference_str) + .ok_or_else(|| format!("no local image tagged '{reference_str}'"))?; + let manifest_bytes = read_blob(&manifest_digest)?; + let manifest: OciImageManifest = + serde_json::from_slice(&manifest_bytes).map_err(|e| e.to_string())?; + + let reference = parse_reference(reference_str)?; + let client = make_client(&reference); + let auth = RegistryAuth::Anonymous; + + let mut layers = Vec::new(); + for descriptor in &manifest.layers { + let data = read_blob(&descriptor.digest)?; + layers.push(ImageLayer::new( + data, + descriptor.media_type.clone(), + descriptor.annotations.clone(), + )); + } + let config_bytes = read_blob(&manifest.config.digest)?; + let config = Config::new( + config_bytes, + manifest.config.media_type.clone(), + manifest.config.annotations.clone(), + ); + + client + .push(&reference, &layers, config, &auth, Some(manifest)) + .await + .map_err(|e| e.to_string())?; + + eprintln!("hule: pushed {reference_str}"); + Ok(()) } -fn main() { - let args: Vec<String> = std::env::args().collect(); - // hule boot <image-dir> [port] - if args.len() < 3 || args[1] != "boot" { - eprintln!("usage: {} boot <image-dir> [port]", args[0]); - exit(2); +async fn cmd_pull(reference_str: &str) -> R<()> { + ensure_store()?; + let reference = parse_reference(reference_str)?; + let client = make_client(&reference); + let auth = RegistryAuth::Anonymous; + + let image_data = client + .pull(&reference, &auth, vec![HULE_CHUNK_MEDIA_TYPE]) + .await + .map_err(|e| e.to_string())?; + let manifest = image_data + .manifest + .ok_or("registry returned no image manifest")?; + + // `client.pull()` fetches layers via `buffer_unordered`, so `image_data.layers` is in + // completion order, NOT `manifest.layers` order -- do not zip the two. Each `ImageLayer` + // already carries its own annotations, so it's self-describing without cross-referencing. + for layer in &image_data.layers { + write_blob(&layer.data)?; } - let dir = PathBuf::from(&args[2]); - let port: u16 = args.get(3).map_or(8022, |s| { - s.parse().unwrap_or_else(|_| die(format!("invalid port '{s}'"))) - }); + write_blob(&image_data.config.data)?; + + let manifest_bytes = serde_json::to_vec(&manifest).map_err(|e| e.to_string())?; + let manifest_digest = write_blob(&manifest_bytes)?; + + let materialized = materialized_dir(&manifest_digest); + std::fs::create_dir_all(&materialized).map_err(|e| e.to_string())?; + + let mut by_title: BTreeMap<String, Vec<(u64, &[u8])>> = BTreeMap::new(); + for layer in &image_data.layers { + let annotations = layer + .annotations + .as_ref() + .ok_or("chunk layer missing annotations")?; + let title = annotations + .get(ORG_OPENCONTAINERS_IMAGE_TITLE) + .ok_or("chunk layer missing title annotation")?; + let offset: u64 = annotations + .get(ANNOTATION_CHUNK_OFFSET) + .ok_or("chunk layer missing offset annotation")? + .parse() + .map_err(|_| "invalid chunk offset annotation".to_string())?; + by_title + .entry(title.clone()) + .or_default() + .push((offset, &layer.data[..])); + } + + for (title, mut chunks) in by_title { + chunks.sort_by_key(|(offset, _)| *offset); + let file = std::fs::File::create(materialized.join(&title)).map_err(|e| e.to_string())?; + for (offset, compressed) in chunks { + let decompressed = zstd::stream::decode_all(compressed).map_err(|e| e.to_string())?; + write_at(&file, offset, &decompressed)?; + } + } + + let mut index = read_index()?; + index_set( + &mut index, + reference_str, + &manifest_digest, + manifest_bytes.len() as u64, + ); + write_index(&index)?; + + eprintln!("hule: pulled {reference_str} ({manifest_digest})"); + Ok(()) +} + +async fn cmd_run(reference_str: &str, port: u16) -> R<()> { + ensure_store()?; + let mut index = read_index()?; + let manifest_digest = match index_lookup(&index, reference_str) { + Some(d) => d, + None => { + eprintln!("hule: {reference_str} not found locally, pulling..."); + cmd_pull(reference_str).await?; + index = read_index()?; + index_lookup(&index, reference_str) + .ok_or_else(|| format!("pull of '{reference_str}' did not produce a local tag"))? + } + }; + + let manifest_bytes = read_blob(&manifest_digest)?; + let manifest: OciImageManifest = + serde_json::from_slice(&manifest_bytes).map_err(|e| e.to_string())?; + let config_bytes = read_blob(&manifest.config.digest)?; - let manifest_path = dir.join("image.hmm"); + let scratch = std::env::temp_dir().join(format!("hule-run-{}", std::process::id())); + std::fs::create_dir_all(&scratch).map_err(|e| e.to_string())?; + std::fs::write(scratch.join("config.json"), &config_bytes).map_err(|e| e.to_string())?; + + let materialized = materialized_dir(&manifest_digest); + let mut titles: BTreeSet<String> = BTreeSet::new(); + for descriptor in &manifest.layers { + if let Some(title) = descriptor + .annotations + .as_ref() + .and_then(|a| a.get(ORG_OPENCONTAINERS_IMAGE_TITLE)) + { + titles.insert(title.clone()); + } + } + for title in titles { + let dest = scratch.join(&title); + if !dest.exists() { + hardlink_or_copy(&materialized.join(&title), &dest)?; + } + } + + launch_qemu(&scratch, port); +} + +// ---- qemu launch (unchanged logic, factored out of the old boot-only main) -- + +/// Reads `dir/config.json` and execs qemu according to its boot protocol. +/// Never returns. +fn launch_qemu(dir: &Path, port: u16) -> ! { + let manifest_path = dir.join("config.json"); let data = std::fs::read_to_string(&manifest_path) .unwrap_or_else(|e| die(format!("cannot read {}: {e}", manifest_path.display()))); - let m: Manifest = serde_json::from_str(&data) - .unwrap_or_else(|e| die(format!("invalid manifest {}: {e}", manifest_path.display()))); + let m = MachineImage::from_json(data.as_bytes()).unwrap_or_else(|e| { + die(format!( + "invalid machine configuration {}: {e}", + manifest_path.display() + )) + }); // Negotiation: qemu handles every protocol we emit. Prefer direct-kernel // (skips firmware+bootloader), else firmware-disk. Manifest order is not // significant. let proto = m + .machine .boot .iter() - .find(|b| matches!(b, Boot::Linux { .. })) - .or_else(|| m.boot.iter().find(|b| matches!(b, Boot::FirmwareDisk { .. }))) + .find(|b| matches!(b, Boot::Linux(_))) + .or_else(|| { + m.machine + .boot + .iter() + .find(|b| matches!(b, Boot::Bios(_)) && b.protocol() == BOOT_FIRMWARE_DISK_BIOS) + }) .unwrap_or_else(|| die("no control-supported boot protocol in manifest")); let disk = m .disks .iter() .find(|d| d.id == proto.disk()) - .unwrap_or_else(|| die(format!("boot entry references unknown disk '{}'", proto.disk()))); + .unwrap_or_else(|| { + die(format!( + "boot entry references unknown disk '{}'", + proto.disk() + )) + }); let disk_path = dir.join(&disk.path); let ssh_port = m + .machine .access .iter() .find_map(|a| match a { - Access::Ssh { port } => Some(*port), + Access::Ssh { port, .. } => Some(*port), + Access::Unknown(_) => None, }) .unwrap_or(22); - let qa = qemu_arch(&m.machine.arch); + let qa = qemu_arch(&m.system.architecture); let mem_mib = m.machine.ram.default / (1024 * 1024); let mut cmd = Command::new(format!("qemu-system-{qa}")); - cmd.args(cpu_opts(&m.machine.arch)); + cmd.args(cpu_opts(&m.system.architecture)); cmd.args([ "-pidfile".into(), format!("/tmp/qemu-{port}.id"), @@ -214,39 +565,42 @@ fn main() { // Ephemeral: snapshot=on discards guest writes. match proto { - Boot::Linux { - kernel, - initrd, - cmdline, - .. - } => { + Boot::Linux(boot) => { // Direct kernel boot: sidecar kernel/initrd + canonical cmdline. cmd.args([ "-drive".into(), - format!("file={},media=disk,snapshot=on,id=root,if=none", disk_path.display()), + format!( + "file={},media=disk,snapshot=on,id=root,if=none", + disk_path.display() + ), "-device".into(), "virtio-blk-pci,drive=root".into(), "-kernel".into(), - dir.join(&kernel.path).display().to_string(), + dir.join(&boot.kernel.path).display().to_string(), "-initrd".into(), - dir.join(&initrd.path).display().to_string(), + dir.join(&boot.initrd.path).display().to_string(), "-append".into(), - cmdline.clone(), + boot.cmdline.clone(), ]); } - Boot::FirmwareDisk { .. } => { + Boot::Bios(_) => { // Self-bootable disk, qemu's firmware boots it. cmd.args([ "-drive".into(), - format!("file={},media=disk,snapshot=on,if=virtio", disk_path.display()), + format!( + "file={},media=disk,snapshot=on,if=virtio", + disk_path.display() + ), ]); } + Boot::Uefi(_) => die("UEFI boot is not supported by the QEMU backend yet"), + Boot::Unknown(_) => die("unreachable: negotiation never selects an unrecognized boot protocol"), } eprintln!( "hule: booting {} via {} (ssh: localhost:{port})", dir.display(), - proto.id() + proto.protocol() ); let status = cmd .status() @@ -255,34 +609,101 @@ fn main() { } /// Hule machine arch -> qemu-system-<arch> suffix. -fn qemu_arch(arch: &str) -> &'static str { +fn qemu_arch(arch: &Architecture) -> &'static str { match arch { - "amd64" => "x86_64", - "arm64" => "aarch64", - "ppc64el" => "ppc64le", - other => die(format!("unsupported arch '{other}'")), + Architecture::Amd64 => "x86_64", + Architecture::Arm64 => "aarch64", + Architecture::Loong64 => "loongarch64", + Architecture::Ppc64le => "ppc64le", + Architecture::Riscv64 => "riscv64", + Architecture::S390x => "s390x", } } /// Machine/accel flags for an arch (KVM when the host matches, else TCG). -fn cpu_opts(arch: &str) -> Vec<String> { - let qa = qemu_arch(arch); - let kvm = std::env::consts::ARCH == qa && Path::new("/dev/kvm").exists(); +fn cpu_opts(arch: &Architecture) -> Vec<String> { + let host_matches = match arch { + Architecture::Amd64 => std::env::consts::ARCH == "x86_64", + Architecture::Arm64 => std::env::consts::ARCH == "aarch64", + Architecture::Loong64 => std::env::consts::ARCH == "loongarch64", + Architecture::Ppc64le => { + std::env::consts::ARCH == "powerpc64" && cfg!(target_endian = "little") + } + Architecture::Riscv64 => std::env::consts::ARCH == "riscv64", + Architecture::S390x => std::env::consts::ARCH == "s390x", + }; + let kvm = host_matches && Path::new("/dev/kvm").exists(); let mut v: Vec<String> = Vec::new(); - match qa { - "aarch64" => v.extend(["-M".into(), "virt".into()]), - "ppc64le" => v.extend(["-machine".into(), "pseries".into()]), + match arch { + Architecture::Arm64 | Architecture::Riscv64 => v.extend(["-M".into(), "virt".into()]), + Architecture::Ppc64le => v.extend(["-machine".into(), "pseries".into()]), _ => {} } if kvm { v.extend(["-cpu".into(), "host".into(), "-enable-kvm".into()]); } else { - match qa { - "aarch64" => v.extend(["-cpu".into(), "cortex-a53".into()]), - "x86_64" => v.extend(["-cpu".into(), "qemu64".into()]), - "ppc64le" => v.extend(["-cpu".into(), "power9".into()]), - _ => {} + match arch { + Architecture::Amd64 => v.extend(["-cpu".into(), "qemu64".into()]), + Architecture::Arm64 => v.extend(["-cpu".into(), "cortex-a53".into()]), + Architecture::Loong64 => v.extend(["-cpu".into(), "la464".into()]), + Architecture::Ppc64le => v.extend(["-cpu".into(), "power9".into()]), + Architecture::Riscv64 => v.extend(["-cpu".into(), "rv64".into()]), + Architecture::S390x => v.extend(["-cpu".into(), "max".into()]), } } v } + +// ---- CLI dispatch ------------------------------------------------------- + +#[tokio::main] +async fn main() { + let args: Vec<String> = std::env::args().collect(); + let usage = |prog: &str| -> ! { + eprintln!("usage:"); + eprintln!(" {prog} import <image-dir> [ref]"); + eprintln!(" {prog} push <ref>"); + eprintln!(" {prog} pull <ref>"); + eprintln!(" {prog} run <ref> [port]"); + exit(2); + }; + + if args.len() < 2 { + usage(&args[0]); + } + + match args[1].as_str() { + "import" => { + if args.len() < 3 { + usage(&args[0]); + } + let reference = args.get(3).map(String::as_str); + cmd_import(Path::new(&args[2]), reference) + .await + .unwrap_or_else(|e| die(e)); + } + "push" => { + if args.len() < 3 { + usage(&args[0]); + } + cmd_push(&args[2]).await.unwrap_or_else(|e| die(e)); + } + "pull" => { + if args.len() < 3 { + usage(&args[0]); + } + cmd_pull(&args[2]).await.unwrap_or_else(|e| die(e)); + } + "run" => { + if args.len() < 3 { + usage(&args[0]); + } + let port: u16 = args.get(3).map_or(8022, |s| { + s.parse() + .unwrap_or_else(|_| die(format!("invalid port '{s}'"))) + }); + cmd_run(&args[2], port).await.unwrap_or_else(|e| die(e)); + } + _ => usage(&args[0]), + } +} diff --git a/images/alpine/genimg b/images/alpine/genimg index ef2aaf1..3bf3e2d 100755 --- a/images/alpine/genimg +++ b/images/alpine/genimg @@ -7,7 +7,7 @@ self=$(dirname "$(readlink -f "$0")") cd "$self" version=${1:-} -arch="${2:-x86_64}" +arch="${2:-amd64}" linux="${linux:-linux-lts}" case $version in @@ -23,6 +23,15 @@ edge) ;; esac +case $arch in +amd64) apk_arch=x86_64 ;; +arm64) apk_arch=aarch64 ;; +*) + echo "unsupported architecture $arch" >&2 + exit 1 + ;; +esac + out="$version/$arch" cleanup() { @@ -52,7 +61,7 @@ for i in $(seq 1 5); do done trap cleanup EXIT -if [ "$arch" == "x86_64" ] || [ "$arch" == "i686" ]; then +if [ "$arch" = "amd64" ]; then dd if=/usr/share/syslinux/mbr.bin of=/dev/nbd0 bs=1 count=440 fi sfdisk --no-reread /dev/nbd0 <<EOF @@ -76,7 +85,7 @@ apk add -U \ -X http://dl-cdn.alpinelinux.org/alpine/$release/main/ \ -X http://dl-cdn.alpinelinux.org/alpine/$release/community/ \ --allow-untrusted \ - --arch="$arch" \ + --arch="$apk_arch" \ --root=/mnt \ --initdb \ acct alpine-base alpine-conf alpine-sdk linux-firmware-none $linux \ @@ -190,33 +199,38 @@ sync cleanup trap : EXIT -# --- Hule Machine Manifest (.hmm) -- docs/boot-protocol.md 10b --------------- -case "$arch" in -x86_64) march=amd64 ;; -aarch64) march=arm64 ;; -*) march=$arch ;; -esac +# --- Hule machine configuration (`config.json`) ------------------------------ disk_sha=$(sha256sum "$out/root.hmi" | cut -d' ' -f1) disk_bytes=$(stat -c%s "$out/root.hmi") virtual_bytes=$((size_gib * 1024 * 1024 * 1024)) mem_min=$((256 * 1024 * 1024)) mem_def=$((1024 * 1024 * 1024)) -cat >"$out/image.hmm" <<EOF +cat >"$out/config.json" <<EOF { "schemaVersion": 1, "kind": "MachineImage", "system": { - "family": "linux", - "distro": "alpine", - "release": "$version" + "os": "linux", + "name": "alpine", + "version": "$version", + "architecture": "$arch" }, "machine": { - "arch": "$march", "cpu": { "minimum": 1, "default": 2 }, - "ram": { "minimum": $mem_min, "default": $mem_def } + "ram": { "minimum": $mem_min, "default": $mem_def }, + "boot": [ + { + "protocol": "firmware-disk/bios", + "disk": "root" + } + ], + "access": [ + { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" } + ], + "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" } }, "disks": [ @@ -224,23 +238,10 @@ cat >"$out/image.hmm" <<EOF "id": "root", "format": "qcow2", "path": "root.hmi", - "checksum": "sha256:$disk_sha", - "virtualSize": $virtual_bytes, - "physicalSize": $disk_bytes + "digest": "sha256:$disk_sha", + "virtSize": $virtual_bytes, + "diskSize": $disk_bytes } - ], - - "boot": [ - { - "id": "firmware-disk/bios", - "disk": "root" - } - ], - - "access": [ - { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" } - ], - - "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" } + ] } EOF diff --git a/images/debian/genimg b/images/debian/genimg index eafe118..116fab2 100755 --- a/images/debian/genimg +++ b/images/debian/genimg @@ -19,16 +19,19 @@ esac case $arch in amd64) + darch=amd64 iface=ens3 qarch=x86_64 kpkg=linux-image-amd64 ;; arm64) + darch=arm64 iface=enp0s1 qarch=aarch64 kpkg=linux-image-arm64 ;; -ppc64el) +ppc64le) + darch=ppc64el iface=enp0s0 qarch=ppc64le kpkg=linux-image-powerpc64le @@ -88,9 +91,9 @@ mkdir /mnt/boot mount /dev/nbd0p1 /mnt/boot if [ "$arch" = "amd64" ]; then - debootstrap --include=gnupg2 --arch=$arch $release /mnt + debootstrap --include=gnupg2 --arch=$darch $release /mnt else - ./qemu-debootstrap --include=gnupg2 --arch=$arch $release /mnt + ./qemu-debootstrap --include=gnupg2 --arch=$darch $release /mnt fi mount --bind /dev /mnt/dev @@ -190,7 +193,7 @@ arm64) cp /mnt/boot/vmlinuz-* "$out/vmlinuz" cp /mnt/boot/initrd.img-* "$out/initrd" ;; -ppc64el) +ppc64le) cp /mnt/boot/vmlinux-* "$out/vmlinux" cp /mnt/boot/initrd.img-* "$out/initrd" ;; @@ -202,7 +205,7 @@ sync cleanup trap : EXIT -# --- Hule Machine Manifest (.hmm) -- docs/boot-protocol.md 10b --------------- +# --- Hule machine configuration (`config.json`) ------------------------------ disk_sha=$(sha256sum "$out/root.hmi" | cut -d' ' -f1) disk_bytes=$(stat -c%s "$out/root.hmi") virtual_bytes=$((size_gib * 1024 * 1024 * 1024)) @@ -215,7 +218,7 @@ amd64) protocols=$( cat <<JSON { - "id": "firmware-disk/bios", + "protocol": "firmware-disk/bios", "disk": "root" } JSON @@ -227,25 +230,25 @@ arm64) protocols=$( cat <<JSON { - "id": "linux/arm64-image", + "protocol": "linux/direct", "disk": "root", - "kernel": { "path": "vmlinuz", "checksum": "sha256:$kernel_sha" }, - "initrd": { "path": "initrd", "checksum": "sha256:$initrd_sha" }, + "kernel": { "path": "vmlinuz", "digest": "sha256:$kernel_sha" }, + "initrd": { "path": "initrd", "digest": "sha256:$initrd_sha" }, "cmdline": "$cmdline" } JSON ) ;; -ppc64el) +ppc64le) kernel_sha=$(sha256sum "$out/vmlinux" | cut -d' ' -f1) initrd_sha=$(sha256sum "$out/initrd" | cut -d' ' -f1) protocols=$( cat <<JSON { - "id": "linux/ppc64le-elf", + "protocol": "linux/direct", "disk": "root", - "kernel": { "path": "vmlinux", "checksum": "sha256:$kernel_sha" }, - "initrd": { "path": "initrd", "checksum": "sha256:$initrd_sha" }, + "kernel": { "path": "vmlinux", "digest": "sha256:$kernel_sha" }, + "initrd": { "path": "initrd", "digest": "sha256:$initrd_sha" }, "cmdline": "$cmdline" } JSON @@ -253,21 +256,28 @@ JSON ;; esac -cat >"$out/image.hmm" <<EOF +cat >"$out/config.json" <<EOF { "schemaVersion": 1, "kind": "MachineImage", "system": { - "family": "linux", - "distro": "debian", - "release": "$release" + "os": "linux", + "name": "debian", + "version": "$release", + "architecture": "$arch" }, "machine": { - "arch": "$arch", "cpu": { "minimum": 1, "default": 2 }, - "ram": { "minimum": $mem_min, "default": $mem_def } + "ram": { "minimum": $mem_min, "default": $mem_def }, + "boot": [ +$protocols + ], + "access": [ + { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" } + ], + "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" } }, "disks": [ @@ -275,20 +285,10 @@ cat >"$out/image.hmm" <<EOF "id": "root", "format": "qcow2", "path": "root.hmi", - "checksum": "sha256:$disk_sha", - "virtualSize": $virtual_bytes, - "physicalSize": $disk_bytes + "digest": "sha256:$disk_sha", + "virtSize": $virtual_bytes, + "diskSize": $disk_bytes } - ], - - "boot": [ -$protocols - ], - - "access": [ - { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" } - ], - - "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" } + ] } EOF diff --git a/images/fedora/genimg b/images/fedora/genimg index 74f1e33..35c2695 100755 --- a/images/fedora/genimg +++ b/images/fedora/genimg @@ -10,7 +10,7 @@ self=$(dirname "$(readlink -f "$0")") cd "$self" release=${1:-} -arch=${2:-x86_64} +arch=${2:-amd64} case $release in 42 | 43 | 44) ;; @@ -21,7 +21,8 @@ case $release in esac case $arch in -x86_64) +amd64) + fedora_arch=x86_64 iface=ens3 ;; *) @@ -148,7 +149,7 @@ run_root dnf clean all cat >/mnt/etc/dracut.conf.d/virtio-blk.conf <<EOF add_drivers="virtio-blk" EOF -kernel_version=$(ls /mnt/boot | grep "vmlinuz.*.$arch" | cut -d- -f2-) +kernel_version=$(ls /mnt/boot | grep "vmlinuz.*.$fedora_arch" | cut -d- -f2-) run_root dracut --force --kver "$kernel_version" run_root grub2-install --target=i386-pc $NBD_DEVICE @@ -181,34 +182,40 @@ sync cleanup trap : EXIT -# --- Hule Machine Manifest (.hmm) -- docs/boot-protocol.md 10b --------------- -# x86_64 only, self-booting via grub2 (BIOS). No boot.cmdline: grub owns it and +# --- Hule machine configuration (`config.json`) ------------------------------ +# amd64 only, self-booting via grub2 (BIOS). No boot.cmdline: grub owns it and # firmware-disk boot takes no cmdline from the VMM. -case "$arch" in -x86_64) march=amd64 ;; -*) march=$arch ;; -esac disk_sha=$(sha256sum "$out/root.hmi" | cut -d' ' -f1) disk_bytes=$(stat -c%s "$out/root.hmi") virtual_bytes=$((size_gib * 1024 * 1024 * 1024)) mem_min=$((256 * 1024 * 1024)) mem_def=$((1024 * 1024 * 1024)) -cat >"$out/image.hmm" <<EOF +cat >"$out/config.json" <<EOF { "schemaVersion": 1, "kind": "MachineImage", "system": { - "family": "linux", - "distro": "fedora", - "release": "$release" + "os": "linux", + "name": "fedora", + "version": "$release", + "architecture": "$arch" }, "machine": { - "arch": "$march", "cpu": { "minimum": 1, "default": 2 }, - "ram": { "minimum": $mem_min, "default": $mem_def } + "ram": { "minimum": $mem_min, "default": $mem_def }, + "boot": [ + { + "protocol": "firmware-disk/bios", + "disk": "root" + } + ], + "access": [ + { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" } + ], + "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" } }, "disks": [ @@ -216,23 +223,10 @@ cat >"$out/image.hmm" <<EOF "id": "root", "format": "qcow2", "path": "root.hmi", - "checksum": "sha256:$disk_sha", - "virtualSize": $virtual_bytes, - "physicalSize": $disk_bytes - } - ], - - "boot": [ - { - "id": "firmware-disk/bios", - "disk": "root" + "digest": "sha256:$disk_sha", + "virtSize": $virtual_bytes, + "diskSize": $disk_bytes } - ], - - "access": [ - { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" } - ], - - "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" } + ] } EOF diff --git a/images/freebsd/genimg b/images/freebsd/genimg index 8b23ed0..e360436 100755 --- a/images/freebsd/genimg +++ b/images/freebsd/genimg @@ -198,7 +198,7 @@ mkimg -s gpt \ cleanup trap : EXIT -# --- Hule Machine Manifest (.hmm) -- docs/boot-protocol.md 10b --------------- +# --- Hule machine configuration (`config.json`) ------------------------------ # GPT + freebsd-boot (gptboot/pmbr), booted by the VMM's BIOS firmware. No # boot.cmdline (the FreeBSD loader owns boot; firmware-disk takes none from the # VMM). FreeBSD host tools: sha256(1) and stat -f, not GNU coreutils. @@ -208,21 +208,30 @@ virtual_bytes=$((size_gib * 1024 * 1024 * 1024)) mem_min=$((256 * 1024 * 1024)) mem_def=$((1024 * 1024 * 1024)) -cat >"$out/image.hmm" <<HMM +cat >"$out/config.json" <<CONFIG { "schemaVersion": 1, "kind": "MachineImage", "system": { - "family": "freebsd", - "distro": "freebsd", - "release": "$version" + "os": "freebsd", + "version": "$version", + "architecture": "$arch" }, "machine": { - "arch": "$arch", "cpu": { "minimum": 1, "default": 2 }, - "ram": { "minimum": $mem_min, "default": $mem_def } + "ram": { "minimum": $mem_min, "default": $mem_def }, + "boot": [ + { + "protocol": "firmware-disk/bios", + "disk": "root" + } + ], + "access": [ + { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" } + ], + "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" } }, "disks": [ @@ -230,23 +239,10 @@ cat >"$out/image.hmm" <<HMM "id": "root", "format": "qcow2", "path": "root.hmi", - "checksum": "sha256:$disk_sha", - "virtualSize": $virtual_bytes, - "physicalSize": $disk_bytes + "digest": "sha256:$disk_sha", + "virtSize": $virtual_bytes, + "diskSize": $disk_bytes } - ], - - "boot": [ - { - "id": "firmware-disk/bios", - "disk": "root" - } - ], - - "access": [ - { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" } - ], - - "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" } + ] } -HMM +CONFIG diff --git a/images/ubuntu/genimg b/images/ubuntu/genimg index c503b05..114438c 100755 --- a/images/ubuntu/genimg +++ b/images/ubuntu/genimg @@ -209,7 +209,7 @@ sync cleanup trap : EXIT -# --- Hule Machine Manifest (.hmm) ------------------------------------------- +# --- Hule machine configuration (`config.json`) ------------------------------ # JSON description of the image: the .hmi disk(s), the boot protocols this image # can honestly be booted with, and the surfaces it exposes. The declaration is a # promise (see docs/boot-protocol.md); only list what this build produces. @@ -228,7 +228,7 @@ amd64) protocols=$( cat <<JSON { - "id": "firmware-disk/bios", + "protocol": "firmware-disk/bios", "disk": "root" } JSON @@ -242,10 +242,10 @@ JSON protocols=$( cat <<JSON { - "id": "linux/arm64-image", + "protocol": "linux/direct", "disk": "root", - "kernel": { "path": "vmlinuz", "checksum": "sha256:$kernel_sha" }, - "initrd": { "path": "initrd", "checksum": "sha256:$initrd_sha" }, + "kernel": { "path": "vmlinuz", "digest": "sha256:$kernel_sha" }, + "initrd": { "path": "initrd", "digest": "sha256:$initrd_sha" }, "cmdline": "$cmdline" } JSON @@ -253,21 +253,32 @@ JSON ;; esac -cat >"$out/image.hmm" <<EOF +cat >"$out/config.json" <<EOF { "schemaVersion": 1, "kind": "MachineImage", "system": { - "family": "linux", - "distro": "ubuntu", - "release": "$release" + "os": "linux", + "name": "ubuntu", + "version": "$release", + "architecture": "$arch" }, "machine": { - "arch": "$arch", "cpu": { "minimum": 1, "default": 2 }, - "ram": { "minimum": $mem_min, "default": $mem_def } + "ram": { "minimum": $mem_min, "default": $mem_def }, + "boot": [ +$protocols + ], + "access": [ + { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" } + ], + "network": { + "mode": "$net_mode", + "address": "$net_address", + "gateway": "$net_gateway" + } }, "disks": [ @@ -275,24 +286,10 @@ cat >"$out/image.hmm" <<EOF "id": "root", "format": "qcow2", "path": "root.hmi", - "checksum": "sha256:$disk_sha", - "virtualSize": $virtual_bytes, - "physicalSize": $disk_bytes + "digest": "sha256:$disk_sha", + "virtSize": $virtual_bytes, + "diskSize": $disk_bytes } - ], - - "boot": [ -$protocols - ], - - "access": [ - { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" } - ], - - "network": { - "mode": "$net_mode", - "address": "$net_address", - "gateway": "$net_gateway" - } + ] } EOF |
