diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5b7c18c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4071 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[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 = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" +dependencies = [ + "backtrace", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "array-init" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23589ecb866b460d3a0f1278834750268c607e8e28a1b982c907219f3178cd72" +dependencies = [ + "nodrop", +] + +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "async-trait" +version = "0.1.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "aws-lc-rs" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd755adf9707cf671e31d944a189be3deaaeee11c8bc1d669bb8022ac90fbd0" +dependencies = [ + "aws-lc-sys", + "paste", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9dd2e03ee80ca2822dd6ea431163d2ef259f2066a4d6ccaca6d9dcb386aa43" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "paste", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object 0.36.5", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.96", + "which", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[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 = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "buf-list" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56bd1685d994a3e2a3ed802eb1ecee8cb500b0ad4df48cb4d5d1a2f04749c3a" +dependencies = [ + "bytes", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "camino-tempfile" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb905055fa81e4d427f919b2cd0d76a998267de7d225ea767a1894743a5263c2" +dependencies = [ + "camino", + "tempfile", +] + +[[package]] +name = "cc" +version = "1.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-any" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73" + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "datatest-stable" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a560b3fd20463b56397bd457aa71243ccfdcffe696050b66e3b1e0ec0457e7f1" +dependencies = [ + "camino", + "fancy-regex", + "libtest-mimic", + "walkdir", +] + +[[package]] +name = "debug-ignore" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe7ed1d93f4553003e20b629abe9085e1e81b1429520f897f8f8860bc6dfc21" + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "dropshot" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84e9c34a06ac21fefe60cf9e5cc321eac9f3d3e2d693e030da3709cf4275479" +dependencies = [ + "async-stream", + "async-trait", + "base64 0.22.1", + "bytes", + "camino", + "chrono", + "debug-ignore", + "dropshot_endpoint", + "form_urlencoded", + "futures", + "hostname 0.4.0", + "http", + "http-body-util", + "hyper", + "hyper-util", + "indexmap 2.7.1", + "multer", + "openapiv3", + "paste", + "percent-encoding", + "rustls 0.22.4", + "rustls-pemfile", + "schemars", + "scopeguard", + "semver", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "slog", + "slog-async", + "slog-bunyan", + "slog-json", + "slog-term", + "thiserror 2.0.11", + "tokio", + "tokio-rustls", + "toml 0.8.20", + "uuid", + "version_check", + "waitgroup", +] + +[[package]] +name = "dropshot_endpoint" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4c7e4e96bfedd670ecbaffc1848ab28dd5892b214003517d9667e7a5b465ce" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_tokenstream", + "syn 2.0.96", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" + +[[package]] +name = "either" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "is-terminal", + "log", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "flagset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.7.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hubtools" +version = "0.4.6" +source = "git+https://github.com/oxidecomputer/hubtools.git?branch=main#f48e2da029ba6552cff5c07ff8a2fc21cc56aa32" +dependencies = [ + "hex", + "lpc55_areas", + "lpc55_sign", + "object 0.30.4", + "path-slash", + "rsa", + "thiserror 1.0.69", + "tlvc", + "tlvc-text", + "toml 0.7.8", + "x509-cert", + "zerocopy 0.6.6", + "zip 0.6.6", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec 1.14.0", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "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 = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec 1.14.0", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec 1.14.0", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown 0.15.1", + "serde", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.170" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libtest-mimic" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc0bda45ed5b3a2904262c1bb91e526127aa70e7ef3758aba2ef93cf896b9b58" +dependencies = [ + "clap", + "escape8259", + "termcolor", + "threadpool", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lpc55_areas" +version = "0.2.5" +source = "git+https://github.com/oxidecomputer/lpc55_support#131520fc913ecce9b80557e854751953f743a7d2" +dependencies = [ + "bitfield", + "clap", + "packed_struct", + "serde", +] + +[[package]] +name = "lpc55_sign" +version = "0.3.4" +source = "git+https://github.com/oxidecomputer/lpc55_support#131520fc913ecce9b80557e854751953f743a7d2" +dependencies = [ + "byteorder", + "const-oid", + "crc-any", + "der", + "env_logger", + "hex", + "log", + "lpc55_areas", + "num-traits", + "packed_struct", + "pem-rfc7468", + "rsa", + "serde", + "serde-hex", + "sha2", + "thiserror 1.0.69", + "x509-cert", + "zerocopy 0.6.6", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "serde", + "smallvec 1.14.0", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +dependencies = [ + "crc32fast", + "hashbrown 0.13.2", + "indexmap 1.9.3", + "memchr", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[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.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "openapiv3" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc02deea53ffe807708244e5914f6b099ad7015a207ee24317c22112e17d9c5c" +dependencies = [ + "indexmap 2.7.1", + "serde", + "serde_json", +] + +[[package]] +name = "packed_struct" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36b29691432cc9eff8b282278473b63df73bea49bc3ec5e67f31a3ae9c3ec190" +dependencies = [ + "bitvec", + "packed_struct_codegen", + "serde", +] + +[[package]] +name = "packed_struct_codegen" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd6706dfe50d53e0f6aa09e12c034c44faacd23e966ae5a209e8bdb8f179f98" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec 1.14.0", + "windows-targets", +] + +[[package]] +name = "parse-display" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287d8d3ebdce117b8539f59411e4ed9ec226e0a4153c7f55495c6070d68e6f72" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc048687be30d79502dea2f623d052f3a074012c6eac41726b7ab17213616b1" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.96", +] + +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "path-slash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498a099351efa4becc6a19c72aa9270598e8fd274ca47052e37455241c88b696" + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +dependencies = [ + "proc-macro2", + "syn 2.0.96", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "serde", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[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 = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "semver", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.96", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-hex" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca37e3e4d1b39afd7ff11ee4e947efae85adfddf4841787bfa47c470e96dc26d" +dependencies = [ + "array-init", + "serde", + "smallvec 0.6.14", +] + +[[package]] +name = "serde_derive" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "serde_human_bytes" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/serde_human_bytes?branch=main#0a09794501b6208120528c3b457d5f3a8cb17424" +dependencies = [ + "hex", + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_tokenstream" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.96", +] + +[[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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "slog-async" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84" +dependencies = [ + "crossbeam-channel", + "slog", + "take_mut", + "thread_local", +] + +[[package]] +name = "slog-bunyan" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaaf6e68789d3f0411f1e72bc443214ef252a1038b6e344836e50442541f190" +dependencies = [ + "hostname 0.3.1", + "slog", + "slog-json", + "time", +] + +[[package]] +name = "slog-envlogger" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "906a1a0bc43fed692df4b82a5e2fbfc3733db8dad8bb514ab27a4f23ad04f5c0" +dependencies = [ + "log", + "regex", + "slog", + "slog-async", + "slog-scope", + "slog-stdlog", + "slog-term", +] + +[[package]] +name = "slog-json" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1e53f61af1e3c8b852eef0a9dee29008f55d6dd63794f3f12cef786cf0f219" +dependencies = [ + "serde", + "serde_json", + "slog", + "time", +] + +[[package]] +name = "slog-scope" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f95a4b4c3274cd2869549da82b57ccc930859bdbf5bcea0424bc5f140b3c786" +dependencies = [ + "arc-swap", + "lazy_static", + "slog", +] + +[[package]] +name = "slog-stdlog" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6706b2ace5bbae7291d3f8d2473e2bfab073ccd7d03670946197aec98471fa3e" +dependencies = [ + "log", + "slog", + "slog-scope", +] + +[[package]] +name = "slog-term" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" +dependencies = [ + "is-terminal", + "slog", + "term", + "thread_local", + "time", +] + +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "futures-core", + "pin-project", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.96", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.96", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "test-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf41af45e3f54cc184831d629d41d5b2bda8297e29c81add7ae4f362ed5e01b" +dependencies = [ + "proc-macro2", + "quote", + "structmeta", + "syn 2.0.96", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +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 = "tls_codec" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e78c9c330f8c85b2bae7c8368f2739157db9991235123aa1b15ef9502bfb6a" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "tlvc" +version = "0.3.1" +source = "git+https://github.com/oxidecomputer/tlvc#e644a21a7ca973ed31499106ea926bd63ebccc6f" +dependencies = [ + "byteorder", + "crc", + "zerocopy 0.6.6", +] + +[[package]] +name = "tlvc-text" +version = "0.3.0" +source = "git+https://github.com/oxidecomputer/tlvc#e644a21a7ca973ed31499106ea926bd63ebccc6f" +dependencies = [ + "ron", + "serde", + "tlvc", + "zerocopy 0.6.6", +] + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.24", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.7.1", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap 2.7.1", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.7.3", +] + +[[package]] +name = "tough" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4f60327896014cd6f78d6a15ef07de21d21b1046efc86e8046ecd48e688fc12" +dependencies = [ + "async-recursion", + "async-trait", + "aws-lc-rs", + "bytes", + "chrono", + "dyn-clone", + "futures", + "futures-core", + "globset", + "hex", + "log", + "olpc-cjson", + "pem", + "percent-encoding", + "reqwest", + "rustls 0.23.19", + "serde", + "serde_json", + "serde_plain", + "snafu", + "tempfile", + "tokio", + "tokio-util", + "typed-path", + "untrusted 0.7.1", + "url", + "walkdir", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +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 = "tufaceous" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_cmd", + "camino", + "chrono", + "clap", + "console", + "datatest-stable", + "dropshot", + "fs-err", + "humantime", + "predicates", + "semver", + "slog", + "slog-async", + "slog-envlogger", + "slog-term", + "tempfile", + "tokio", + "tufaceous-artifact", + "tufaceous-lib", +] + +[[package]] +name = "tufaceous-artifact" +version = "0.1.0" +dependencies = [ + "parse-display", + "proptest", + "schemars", + "semver", + "serde", + "serde_human_bytes", + "serde_json", + "strum", + "test-strategy", +] + +[[package]] +name = "tufaceous-brand-metadata" +version = "0.1.0" +dependencies = [ + "semver", + "serde", + "serde_json", + "tar", +] + +[[package]] +name = "tufaceous-lib" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "aws-lc-rs", + "base64 0.22.1", + "buf-list", + "bytes", + "camino", + "camino-tempfile", + "chrono", + "debug-ignore", + "dropshot", + "flate2", + "fs-err", + "futures", + "hex", + "hubtools", + "itertools 0.13.0", + "parse-size", + "rand", + "semver", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "slog", + "tar", + "tokio", + "toml 0.8.20", + "tough", + "tufaceous-artifact", + "tufaceous-brand-metadata", + "url", + "zip 2.1.3", +] + +[[package]] +name = "typed-path" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82205ffd44a9697e34fc145491aa47310f9871540bb7909eaa9365e0a9a46607" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "wasm-streams" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[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 = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +dependencies = [ + "memchr", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", + "tls_codec", +] + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +dependencies = [ + "byteorder", + "zerocopy-derive 0.6.6", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy-derive" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "crossbeam-utils", +] + +[[package]] +name = "zip" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "indexmap 2.7.1", + "memchr", + "thiserror 1.0.69", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..46d00b9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,69 @@ +[workspace] +resolver = "2" +members = [ + "artifact", + "bin", + "brand-metadata", + "lib", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" +publish = false + +[workspace.dependencies] +anyhow = "1.0.96" +assert_cmd = "2.0.16" +async-trait = "0.1.86" +aws-lc-rs = "1.12.4" +base64 = "0.22.1" +buf-list = "1.0.3" +bytes = "1.10.0" +camino = { version = "1.1.9", features = ["serde1"] } +camino-tempfile = "1.1.1" +chrono = { version = "0.4.40", default-features = false, features = ["std"] } +clap = { version = "4.5.31", features = ["cargo", "derive", "env", "wrap_help"] } +console = { version = "0.15.10", default-features = false } +datatest-stable = "0.2.9" +debug-ignore = "1.0.5" +dropshot = "0.15.1" +flate2 = "1.0.35" +fs-err = "2.11.0" +futures = "0.3.31" +hex = "0.4.3" +hubtools = { git = "https://github.com/oxidecomputer/hubtools.git", branch = "main" } +humantime = "2.1.0" +itertools = "0.13.0" +parse-display = "0.10.0" +parse-size = "1.1.0" +predicates = "3.1.3" +proptest = "1.5.0" +rand = "0.8.5" +schemars = { version = "0.8.21", features = ["semver"] } +semver = { version = "1.0.25", features = ["serde"] } +serde = { version = "1.0.218", features = ["derive"] } +serde_human_bytes = { git = "https://github.com/oxidecomputer/serde_human_bytes", branch = "main" } +serde_json = "1.0.139" +serde_path_to_error = "0.1.16" +sha2 = "0.10.8" +slog = "2.7.0" +slog-async = "2.8.0" +slog-envlogger = "2.2.0" +slog-term = "2.9.1" +strum = { version = "0.26.3", features = ["derive"] } +tar = "0.4.44" +tempfile = "3.13.0" +test-strategy = "0.4.0" +tokio = "1.43.0" +toml = "0.8.20" +tough = { version = "0.19.0", features = [ "http" ] } +tufaceous-artifact = { path = "artifact", default-features = false } +tufaceous-brand-metadata = { path = "brand-metadata" } +tufaceous-lib = { path = "lib" } +url = "2.5.3" +# NOTE: Avoid upgrading zip until https://github.com/zip-rs/zip2/issues/231 is resolved +zip = { version = "=2.1.3", default-features = false } + +[workspace.lints.clippy] diff --git a/artifact/Cargo.toml b/artifact/Cargo.toml new file mode 100644 index 0000000..c3ea252 --- /dev/null +++ b/artifact/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tufaceous-artifact" +version.workspace = true +edition.workspace = true +license.workspace = true +publish.workspace = true + +[features] +proptest = ["dep:proptest", "dep:test-strategy"] +schemars = ["dep:schemars"] + +[dependencies] +parse-display.workspace = true +proptest = { workspace = true, optional = true } +schemars = { workspace = true, optional = true } +semver.workspace = true +serde.workspace = true +serde_human_bytes.workspace = true +strum.workspace = true +test-strategy = { workspace = true, optional = true } + +[dev-dependencies] +serde_json.workspace = true + +[lints] +workspace = true diff --git a/artifact/src/lib.rs b/artifact/src/lib.rs new file mode 100644 index 0000000..018fe63 --- /dev/null +++ b/artifact/src/lib.rs @@ -0,0 +1,288 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::borrow::Cow; +use std::convert::Infallible; +use std::fmt; +use std::str::FromStr; + +use parse_display::{Display, FromStr}; +use semver::Version; +use serde::{Deserialize, Serialize}; +use strum::{EnumIter, IntoEnumIterator}; + +/// Description of the `artifacts.json` target found in rack update +/// repositories. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ArtifactsDocument { + pub system_version: Version, + pub artifacts: Vec, +} + +impl ArtifactsDocument { + /// Creates an artifacts document with the provided system version and an + /// empty list of artifacts. + pub fn empty(system_version: Version) -> Self { + Self { system_version, artifacts: Vec::new() } + } +} + +/// Describes an artifact available in the repository. +/// +/// See also [`crate::api::internal::nexus::UpdateArtifactId`], which is used +/// internally in Nexus and Sled Agent. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct Artifact { + /// Used to differentiate between different series of artifacts of the same + /// kind. This is used by the control plane to select the correct artifact. + /// + /// For SP and ROT images ([`KnownArtifactKind::GimletSp`], + /// [`KnownArtifactKind::GimletRot`], [`KnownArtifactKind::PscSp`], + /// [`KnownArtifactKind::PscRot`], [`KnownArtifactKind::SwitchSp`], + /// [`KnownArtifactKind::SwitchRot`]), `name` is the value of the board + /// (`BORD`) tag in the image caboose. + /// + /// In the future when [`KnownArtifactKind::ControlPlane`] is split up into + /// separate zones, `name` will be the zone name. + pub name: String, + pub version: Version, + pub kind: ArtifactKind, + pub target: String, +} + +/// The kind of artifact we are dealing with. +/// +/// To ensure older versions of Nexus can work with update repositories that +/// describe artifact kinds it is not yet aware of, this is a newtype wrapper +/// around a string. The set of known artifact kinds is described in +/// [`KnownArtifactKind`], and this type has conversions to and from it. +#[derive( + Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Deserialize, Serialize, +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(transparent)] +pub struct ArtifactKind(Cow<'static, str>); + +impl ArtifactKind { + /// Creates a new `ArtifactKind` from a string. + pub fn new(kind: String) -> Self { + Self(kind.into()) + } + + /// Creates a new `ArtifactKind` from a static string. + pub const fn from_static(kind: &'static str) -> Self { + Self(Cow::Borrowed(kind)) + } + + /// Creates a new `ArtifactKind` from a known kind. + pub fn from_known(kind: KnownArtifactKind) -> Self { + Self::new(kind.to_string()) + } + + /// Returns the kind as a string. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Converts self to a `KnownArtifactKind`, if it is known. + pub fn to_known(&self) -> Option { + self.0.parse().ok() + } +} + +/// These artifact kinds are not stored anywhere, but are derived from stored +/// kinds and used as internal identifiers. +impl ArtifactKind { + /// Gimlet root of trust bootloader slot image identifier. + /// + /// Derived from [`KnownArtifactKind::GimletRotBootloader`]. + pub const GIMLET_ROT_STAGE0: Self = + Self::from_static("gimlet_rot_bootloader"); + + /// Gimlet root of trust A slot image identifier. + /// + /// Derived from [`KnownArtifactKind::GimletRot`]. + pub const GIMLET_ROT_IMAGE_A: Self = + Self::from_static("gimlet_rot_image_a"); + + /// Gimlet root of trust B slot image identifier. + /// + /// Derived from [`KnownArtifactKind::GimletRot`]. + pub const GIMLET_ROT_IMAGE_B: Self = + Self::from_static("gimlet_rot_image_b"); + + /// PSC root of trust stage0 image identifier. + /// + /// Derived from [`KnownArtifactKind::PscRotBootloader`]. + pub const PSC_ROT_STAGE0: Self = Self::from_static("psc_rot_bootloader"); + + /// PSC root of trust A slot image identifier. + /// + /// Derived from [`KnownArtifactKind::PscRot`]. + pub const PSC_ROT_IMAGE_A: Self = Self::from_static("psc_rot_image_a"); + + /// PSC root of trust B slot image identifier. + /// + /// Derived from [`KnownArtifactKind::PscRot`]. + pub const PSC_ROT_IMAGE_B: Self = Self::from_static("psc_rot_image_b"); + + /// Switch root of trust A slot image identifier. + /// + /// Derived from [`KnownArtifactKind::SwitchRotBootloader`]. + pub const SWITCH_ROT_STAGE0: Self = + Self::from_static("switch_rot_bootloader"); + + /// Switch root of trust A slot image identifier. + /// + /// Derived from [`KnownArtifactKind::SwitchRot`]. + pub const SWITCH_ROT_IMAGE_A: Self = + Self::from_static("switch_rot_image_a"); + + /// Switch root of trust B slot image identifier. + /// + /// Derived from [`KnownArtifactKind::SwitchRot`]. + pub const SWITCH_ROT_IMAGE_B: Self = + Self::from_static("switch_rot_image_b"); + + /// Host phase 1 identifier. + /// + /// Derived from [`KnownArtifactKind::Host`]. + pub const HOST_PHASE_1: Self = Self::from_static("host_phase_1"); + + /// Host phase 2 identifier. + /// + /// Derived from [`KnownArtifactKind::Host`]. + pub const HOST_PHASE_2: Self = Self::from_static("host_phase_2"); + + /// Trampoline phase 1 identifier. + /// + /// Derived from [`KnownArtifactKind::Trampoline`]. + pub const TRAMPOLINE_PHASE_1: Self = + Self::from_static("trampoline_phase_1"); + + /// Trampoline phase 2 identifier. + /// + /// Derived from [`KnownArtifactKind::Trampoline`]. + pub const TRAMPOLINE_PHASE_2: Self = + Self::from_static("trampoline_phase_2"); +} + +impl From for ArtifactKind { + fn from(kind: KnownArtifactKind) -> Self { + Self::from_known(kind) + } +} + +impl fmt::Display for ArtifactKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl FromStr for ArtifactKind { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self::new(s.to_owned())) + } +} + +/// Kinds of update artifacts, as used by Nexus to determine what updates are available and by +/// sled-agent to determine how to apply an update when asked. +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + Ord, + PartialOrd, + Display, + FromStr, + Deserialize, + Serialize, + EnumIter, +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[display(style = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum KnownArtifactKind { + // Sled Artifacts + GimletSp, + GimletRot, + GimletRotBootloader, + Host, + Trampoline, + /// Composite artifact of all control plane zones + ControlPlane, + /// Individual control plane zone + Zone, + + // PSC Artifacts + PscSp, + PscRot, + PscRotBootloader, + + // Switch Artifacts + SwitchSp, + SwitchRot, + SwitchRotBootloader, +} + +impl KnownArtifactKind { + /// Returns an iterator over all the variants in this struct. + /// + /// This is provided as a helper so dependent packages don't have to pull in + /// strum explicitly. + pub fn iter() -> KnownArtifactKindIter { + ::iter() + } +} + +#[cfg(test)] +mod tests { + use super::{ArtifactKind, KnownArtifactKind}; + + #[test] + fn serde_artifact_kind() { + assert_eq!( + serde_json::from_str::("\"gimlet_sp\"") + .unwrap() + .to_known(), + Some(KnownArtifactKind::GimletSp) + ); + assert_eq!( + serde_json::from_str::("\"fhqwhgads\"") + .unwrap() + .to_known(), + None, + ); + assert!(serde_json::from_str::("null").is_err()); + + assert_eq!( + serde_json::to_string(&ArtifactKind::from_known( + KnownArtifactKind::GimletSp + )) + .unwrap(), + "\"gimlet_sp\"" + ); + assert_eq!( + serde_json::to_string(&ArtifactKind::new("fhqwhgads".to_string())) + .unwrap(), + "\"fhqwhgads\"" + ); + } + + #[test] + fn known_artifact_kind_roundtrip() { + for kind in KnownArtifactKind::iter() { + let as_string = kind.to_string(); + let kind2 = as_string.parse::().unwrap_or_else( + |error| panic!("error parsing kind {as_string}: {error}"), + ); + assert_eq!(kind, kind2); + } + } +} diff --git a/bin/Cargo.toml b/bin/Cargo.toml new file mode 100644 index 0000000..69c6c03 --- /dev/null +++ b/bin/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "tufaceous" +version.workspace = true +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[test]] +name = "manifest-tests" +harness = false + +[dependencies] +anyhow = { workspace = true, features = ["backtrace"] } +camino.workspace = true +chrono.workspace = true +clap.workspace = true +console.workspace = true +humantime.workspace = true +semver.workspace = true +slog.workspace = true +slog-async.workspace = true +slog-envlogger.workspace = true +slog-term.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tufaceous-artifact.workspace = true +tufaceous-lib.workspace = true + +[dev-dependencies] +assert_cmd.workspace = true +datatest-stable.workspace = true +dropshot.workspace = true +fs-err.workspace = true +predicates.workspace = true +tempfile.workspace = true +tokio = { workspace = true, features = ["test-util"] } + +[lints] +workspace = true diff --git a/bin/README.adoc b/bin/README.adoc new file mode 100644 index 0000000..86e4cfc --- /dev/null +++ b/bin/README.adoc @@ -0,0 +1,36 @@ +# tufaceous + +Rack update repository generation tool. + +## TUF, keys and lifetime + +Rack update repositories use TUF. Consider reading https://theupdateframework.io/overview/[the TUF overview] and https://theupdateframework.io/metadata/[a high level summary of the metadata mandated by the specification]. + +The only keys currently supported by tufaceous are Ed25519 keys. Support for hardware-backed keys is planned. + +Each role has an expiration date. The default is one week, suitable for development testing. This can be modified with the `--expiry` option. + +## init + +Create a new repository in the current directory with `tufaceous init`. + +To change the target directory, use `-r/--repo`. This is accepted on all subcommands, and needs to come before the subcommand because Clap is picky. + +This will generate a new Ed25519 private key and display it on stderr if no keys are provided. + +Currently if keys are provided, they are allowed to sign all roles. For the time being if you need more advanced editing of the root role, use https://crates.io/crates/tuftool[tuftool]'s `root` subcommands. + +## add zones + +Usage: + +---- +tuftool [-r PATH/TO/REPO] add-zone [--name NAME] ZONE_TAR_GZ VERSION +---- + +Example: + +---- +$ tuftool add-zone out/nexus.tar.gz 0.0.0 +added zone nexus, version 0.0.0 +---- diff --git a/bin/manifests/fake.toml b/bin/manifests/fake.toml new file mode 100644 index 0000000..74b1b57 --- /dev/null +++ b/bin/manifests/fake.toml @@ -0,0 +1,87 @@ +# This is an artifact manifest that generates fake entries for all components. +# This is completely non-functional and is only useful for testing archive +# extraction in other parts of the repository. + +system_version = "1.0.0" + +[[artifact.gimlet_sp]] +name = "fake-gimlet-sp" +version = "1.0.0" +source = { kind = "fake", size = "1MiB" } + +[[artifact.gimlet_rot]] +name = "fake-gimlet-rot" +version = "1.0.0" +[artifact.gimlet_rot.source] +kind = "composite-rot" +archive_a = { kind = "fake", size = "512KiB" } +archive_b = { kind = "fake", size = "512KiB" } + +[[artifact.host]] +name = "fake-host" +version = "1.0.0" +[artifact.host.source] +kind = "composite-host" +phase_1 = { kind = "fake", size = "512KiB" } +phase_2 = { kind = "fake", size = "1MiB" } + +[[artifact.trampoline]] +name = "fake-trampoline" +version = "1.0.0" +[artifact.trampoline.source] +kind = "composite-host" +phase_1 = { kind = "fake", size = "512KiB" } +phase_2 = { kind = "fake", size = "1MiB" } + +[[artifact.control_plane]] +name = "fake-control-plane" +version = "1.0.0" +[artifact.control_plane.source] +kind = "composite-control-plane" +zones = [ + { kind = "fake", name = "zone1", size = "1MiB" }, + { kind = "fake", name = "zone2", size = "1MiB" }, +] + +[[artifact.psc_sp]] +name = "fake-psc-sp" +version = "1.0.0" +source = { kind = "fake", size = "1MiB" } + +[[artifact.psc_rot]] +name = "fake-psc-rot" +version = "1.0.0" +[artifact.psc_rot.source] +kind = "composite-rot" +archive_a = { kind = "fake", size = "512KiB" } +archive_b = { kind = "fake", size = "512KiB" } + +[[artifact.switch_sp]] +name = "fake-switch-sp" +version = "1.0.0" +source = { kind = "fake", size = "1MiB" } + +[[artifact.switch_rot]] +name = "fake-switch-rot" +version = "1.0.0" +[artifact.switch_rot.source] +kind = "composite-rot" +archive_a = { kind = "fake", size = "512KiB" } +archive_b = { kind = "fake", size = "512KiB" } + +[[artifact.gimlet_rot_bootloader]] +name = "fake-gimlet-rot-bootloader" +version = "1.0.0" +source = { kind = "fake", size = "1MiB" } + +[[artifact.psc_rot_bootloader]] +name = "fake-psc-rot-bootloader" +version = "1.0.0" +source = { kind = "fake", size = "1MiB" } + +[[artifact.switch_rot_bootloader]] +name = "fake-switch-rot-bootloader" +version = "1.0.0" +source = { kind = "fake", size = "1MiB" } + + diff --git a/bin/src/date.rs b/bin/src/date.rs new file mode 100644 index 0000000..52d6f4c --- /dev/null +++ b/bin/src/date.rs @@ -0,0 +1,21 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::Result; +use chrono::{DateTime, Duration, Timelike, Utc}; + +/// Parser for datelike command line arguments. Can accept a duration (e.g. +/// "1w") or an ISO8601 timestamp. +pub(crate) fn parse_duration_or_datetime(s: &str) -> Result> { + match humantime::parse_duration(s) { + Ok(duration) => { + // Remove nanoseconds from the timestamp to keep it less + // overwhelming. `Timelike::with_nanosecond` returns None only when + // passed a value over 2 billion + let now = Utc::now().with_nanosecond(0).unwrap(); + Ok(now + Duration::from_std(duration)?) + } + Err(_) => Ok(s.parse()?), + } +} diff --git a/bin/src/dispatch.rs b/bin/src/dispatch.rs new file mode 100644 index 0000000..b6766c0 --- /dev/null +++ b/bin/src/dispatch.rs @@ -0,0 +1,258 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::{bail, Context, Result}; +use camino::Utf8PathBuf; +use chrono::{DateTime, Utc}; +use clap::{CommandFactory, Parser}; +use semver::Version; +use tufaceous_artifact::ArtifactKind; +use tufaceous_lib::assemble::{ArtifactManifest, OmicronRepoAssembler}; +use tufaceous_lib::{AddArtifact, ArchiveExtractor, Key, OmicronRepo}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(subcommand)] + command: Command, + + #[clap( + short = 'k', + long = "key", + env = "TUFACEOUS_KEY", + required = false, + global = true + )] + keys: Vec, + + #[clap(long, value_parser = crate::date::parse_duration_or_datetime, default_value = "7d", global = true)] + expiry: DateTime, + + /// TUF repository path (default: current working directory) + #[clap(short = 'r', long, global = true)] + repo: Option, +} + +impl Args { + /// Executes these arguments. + pub async fn exec(self, log: &slog::Logger) -> Result<()> { + let repo_path = match self.repo { + Some(repo) => repo, + None => std::env::current_dir()?.try_into()?, + }; + + match self.command { + Command::Init { system_version, no_generate_key } => { + let keys = maybe_generate_keys(self.keys, no_generate_key)?; + + let repo = OmicronRepo::initialize( + log, + &repo_path, + system_version, + keys, + self.expiry, + ) + .await?; + slog::info!( + log, + "Initialized TUF repository in {}", + repo.repo_path() + ); + Ok(()) + } + Command::Add { kind, allow_unknown_kinds, path, name, version } => { + if !allow_unknown_kinds { + // Try converting kind to a known kind. + if kind.to_known().is_none() { + // Simulate a failure to parse (though ideally there would + // be a way to also specify the underlying error -- there + // doesn't appear to be a public API to do so in clap 4). + let mut error = clap::Error::new( + clap::error::ErrorKind::ValueValidation, + ) + .with_cmd(&Args::command()); + error.insert( + clap::error::ContextKind::InvalidArg, + clap::error::ContextValue::String( + "".to_owned(), + ), + ); + error.insert( + clap::error::ContextKind::InvalidValue, + clap::error::ContextValue::String(kind.to_string()), + ); + error.exit(); + } + } + + let repo = OmicronRepo::load_untrusted_ignore_expiration( + log, &repo_path, + ) + .await?; + let mut editor = repo.into_editor().await?; + + let new_artifact = + AddArtifact::from_path(kind, name, version, path)?; + + editor + .add_artifact(&new_artifact) + .context("error adding artifact")?; + editor.sign_and_finish(self.keys, self.expiry).await?; + println!( + "added {} {}, version {}", + new_artifact.kind(), + new_artifact.name(), + new_artifact.version() + ); + Ok(()) + } + Command::Archive { output_path } => { + // The filename must end with "zip". + if output_path.extension() != Some("zip") { + bail!("output path `{output_path}` must end with .zip"); + } + + let repo = OmicronRepo::load_untrusted_ignore_expiration( + log, &repo_path, + ) + .await?; + repo.archive(&output_path)?; + + Ok(()) + } + Command::Extract { archive_file, dest } => { + let mut extractor = ArchiveExtractor::from_path(&archive_file)?; + extractor.extract(&dest)?; + + // Now load the repository and ensure it's valid. + let repo = OmicronRepo::load_untrusted(log, &dest) + .await + .with_context(|| { + format!( + "error loading extracted repository at `{dest}` \ + (extracted files are still available)" + ) + })?; + repo.read_artifacts().await.with_context(|| { + format!( + "error loading artifacts.json from extracted archive \ + at `{dest}`" + ) + })?; + + Ok(()) + } + Command::Assemble { + manifest_path, + output_path, + build_dir, + no_generate_key, + skip_all_present, + } => { + // The filename must end with "zip". + if output_path.extension() != Some("zip") { + bail!("output path `{output_path}` must end with .zip"); + } + + let manifest = ArtifactManifest::from_path(&manifest_path) + .context("error reading manifest")?; + if !skip_all_present { + manifest.verify_all_present()?; + } + + let keys = maybe_generate_keys(self.keys, no_generate_key)?; + let mut assembler = OmicronRepoAssembler::new( + log, + manifest, + keys, + self.expiry, + output_path, + ); + if let Some(dir) = build_dir { + assembler.set_build_dir(dir); + } + + assembler.build().await?; + + Ok(()) + } + } + } +} + +#[derive(Debug, Parser)] +enum Command { + /// Create a new rack update TUF repository + Init { + /// The system version. + system_version: Version, + + /// Disable random key generation and exit if no keys are provided + #[clap(long)] + no_generate_key: bool, + }, + Add { + /// The kind of artifact this is. + kind: ArtifactKind, + + /// Allow artifact kinds that aren't known to tufaceous + #[clap(long)] + allow_unknown_kinds: bool, + + /// Path to the artifact. + path: Utf8PathBuf, + + /// Override the name for this artifact (default: filename with extension stripped) + #[clap(long)] + name: Option, + + /// Artifact version. + version: Version, + }, + /// Archives this repository to a zip file. + Archive { + /// The path to write the archive to (must end with .zip). + output_path: Utf8PathBuf, + }, + /// Validates and extracts a repository created by the `archive` command. + Extract { + /// The file to extract. + archive_file: Utf8PathBuf, + + /// The destination to extract the file to. + dest: Utf8PathBuf, + }, + /// Assembles a repository from a provided manifest. + Assemble { + /// Path to artifact manifest. + manifest_path: Utf8PathBuf, + + /// The path to write the archive to (must end with .zip). + output_path: Utf8PathBuf, + + /// Directory to use for building artifacts [default: temporary directory] + #[clap(long)] + build_dir: Option, + + /// Disable random key generation and exit if no keys are provided + #[clap(long)] + no_generate_key: bool, + + /// Skip checking to ensure all expected artifacts are present. + #[clap(long)] + skip_all_present: bool, + }, +} + +fn maybe_generate_keys( + keys: Vec, + no_generate_key: bool, +) -> Result> { + Ok(if !no_generate_key && keys.is_empty() { + let key = Key::generate_ed25519()?; + crate::hint::generated_key(&key); + vec![key] + } else { + keys + }) +} diff --git a/bin/src/hint.rs b/bin/src/hint.rs new file mode 100644 index 0000000..74385e3 --- /dev/null +++ b/bin/src/hint.rs @@ -0,0 +1,29 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use tufaceous_lib::Key; + +fn print_hint(hint: &str) { + for line in hint.trim().lines() { + eprintln!("{}", console::style(format!("hint: {}", line)).yellow()); + } +} + +pub(crate) fn generated_key(key: &Key) { + print_hint(&format!( + r#" +Generated a random key: + + {key} + +To modify this repository, you will need this key. Use the -k/--key +command line flag or the TUFACEOUS_KEY environment variable: + + export TUFACEOUS_KEY={key} + +To prevent this default behavior, use --no-generate-key. + "#, + key = console::style(key).italic() + )) +} diff --git a/bin/src/lib.rs b/bin/src/lib.rs new file mode 100644 index 0000000..65ff581 --- /dev/null +++ b/bin/src/lib.rs @@ -0,0 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +mod date; +mod dispatch; +mod hint; + +pub use dispatch::*; diff --git a/bin/src/main.rs b/bin/src/main.rs new file mode 100644 index 0000000..014817e --- /dev/null +++ b/bin/src/main.rs @@ -0,0 +1,35 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::Result; +use clap::Parser; +use slog::Drain; +use tufaceous::Args; + +#[tokio::main] +async fn main() -> Result<()> { + let log = setup_log(); + let args = Args::parse(); + args.exec(&log).await +} + +fn setup_log() -> slog::Logger { + let stderr_drain = stderr_env_drain("RUST_LOG"); + let drain = slog_async::Async::new(stderr_drain).build().fuse(); + slog::Logger::root(drain, slog::o!()) +} + +fn stderr_env_drain(env_var: &str) -> impl Drain { + let stderr_decorator = slog_term::TermDecorator::new().build(); + let stderr_drain = + slog_term::FullFormat::new(stderr_decorator).build().fuse(); + let mut builder = slog_envlogger::LogBuilder::new(stderr_drain); + if let Ok(s) = std::env::var(env_var) { + builder = builder.parse(&s); + } else { + // Log at the info level by default. + builder = builder.filter(None, slog::FilterLevel::Info); + } + builder.build() +} diff --git a/bin/tests/integration-tests/command_tests.rs b/bin/tests/integration-tests/command_tests.rs new file mode 100644 index 0000000..1c365b8 --- /dev/null +++ b/bin/tests/integration-tests/command_tests.rs @@ -0,0 +1,161 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::path::Path; + +use anyhow::Result; +use assert_cmd::Command; +use camino::Utf8PathBuf; +use dropshot::test_util::LogContext; +use dropshot::{ConfigLogging, ConfigLoggingIfExists, ConfigLoggingLevel}; +use predicates::prelude::*; +use tufaceous_artifact::{ArtifactKind, KnownArtifactKind}; +use tufaceous_lib::{Key, OmicronRepo}; + +#[tokio::test] +async fn test_init_and_add() -> Result<()> { + let log_config = ConfigLogging::File { + level: ConfigLoggingLevel::Trace, + path: "UNUSED".into(), + if_exists: ConfigLoggingIfExists::Fail, + }; + let logctx = LogContext::new("test_init_and_add", &log_config); + let tempdir = tempfile::tempdir().unwrap(); + let key = Key::generate_ed25519()?; + + let mut cmd = make_cmd_with_repo(tempdir.path(), &key); + cmd.args(["init", "0.0.0"]); + cmd.assert().success(); + + // Create a couple of stub files on disk. + let nexus_path = tempdir.path().join("nexus.tar.gz"); + fs_err::write(&nexus_path, "test")?; + let unknown_path = tempdir.path().join("my-unknown-kind.tar.gz"); + fs_err::write(&unknown_path, "unknown test")?; + + let mut cmd = make_cmd_with_repo(tempdir.path(), &key); + cmd.args(["add", "gimlet_sp"]); + cmd.arg(&nexus_path); + cmd.arg("42.0.0"); + cmd.assert().success(); + + // Try adding an unknown kind without --allow-unknown-kinds. + let mut cmd = make_cmd_with_repo(tempdir.path(), &key); + cmd.args(["add", "my_unknown_kind"]); + cmd.arg(&nexus_path); + cmd.arg("0.0.0"); + cmd.assert().failure().stderr(predicate::str::contains( + "invalid value 'my_unknown_kind' for ''", + )); + + // Try adding one with --allow-unknown-kinds. + let mut cmd = make_cmd_with_repo(tempdir.path(), &key); + cmd.args(["add", "my_unknown_kind", "--allow-unknown-kinds"]); + cmd.arg(&unknown_path); + cmd.arg("0.1.0"); + cmd.assert().success(); + + // Now read the repository and ensure the list of expected artifacts. + let repo_path: Utf8PathBuf = tempdir.path().join("repo").try_into()?; + let repo = OmicronRepo::load_untrusted(&logctx.log, &repo_path).await?; + + let artifacts = repo.read_artifacts().await?; + assert_eq!( + artifacts.artifacts.len(), + 2, + "repo should contain exactly 2 artifacts: {artifacts:?}" + ); + + let mut artifacts_iter = artifacts.artifacts.into_iter(); + let artifact = artifacts_iter.next().unwrap(); + assert_eq!(artifact.name, "nexus", "artifact name"); + assert_eq!(artifact.version, "42.0.0".parse().unwrap(), "artifact version"); + assert_eq!( + artifact.kind, + ArtifactKind::from_known(KnownArtifactKind::GimletSp), + "artifact kind" + ); + assert_eq!( + artifact.target, "gimlet_sp-nexus-42.0.0.tar.gz", + "artifact target" + ); + + let artifact = artifacts_iter.next().unwrap(); + assert_eq!(artifact.name, "my-unknown-kind", "artifact name"); + assert_eq!(artifact.version, "0.1.0".parse().unwrap(), "artifact version"); + assert_eq!( + artifact.kind, + ArtifactKind::new("my_unknown_kind".to_owned()), + "artifact kind" + ); + assert_eq!( + artifact.target, "my_unknown_kind-my-unknown-kind-0.1.0.tar.gz", + "artifact target" + ); + + // Create an archive from the given path. + let archive_path = tempdir.path().join("archive.zip"); + let mut cmd = make_cmd_with_repo(tempdir.path(), &key); + cmd.arg("archive"); + cmd.arg(&archive_path); + cmd.assert().success(); + + // Extract the archive to a new directory. + let dest_path = tempdir.path().join("dest"); + let mut cmd = make_cmd_with_repo(tempdir.path(), &key); + cmd.arg("extract"); + cmd.arg(&archive_path); + cmd.arg(&dest_path); + + cmd.assert().success(); + + logctx.cleanup_successful(); + Ok(()) +} + +#[test] +fn test_assemble_fake() -> Result<()> { + let log_config = ConfigLogging::File { + level: ConfigLoggingLevel::Trace, + path: "UNUSED".into(), + if_exists: ConfigLoggingIfExists::Fail, + }; + let logctx = LogContext::new("test_assemble_fake", &log_config); + let tempdir = tempfile::tempdir().unwrap(); + let key = Key::generate_ed25519()?; + + let archive_path = tempdir.path().join("archive.zip"); + + let mut cmd = make_cmd(&key); + cmd.args(["assemble", "manifests/fake.toml"]); + cmd.arg(&archive_path); + cmd.assert().success(); + + // Extract the archive to a new directory. + let dest_path = tempdir.path().join("dest"); + let mut cmd = make_cmd(&key); + cmd.arg("extract"); + cmd.arg(&archive_path); + cmd.arg(&dest_path); + + cmd.assert().success(); + + logctx.cleanup_successful(); + Ok(()) +} + +fn make_cmd(key: &Key) -> Command { + let mut cmd = Command::cargo_bin("tufaceous").unwrap(); + cmd.env("TUFACEOUS_KEY", key.to_string()); + + cmd +} + +fn make_cmd_with_repo(tempdir: &Path, key: &Key) -> Command { + let mut cmd = make_cmd(key); + cmd.arg("--repo"); + cmd.arg(tempdir.join("repo")); + + cmd +} diff --git a/bin/tests/integration-tests/main.rs b/bin/tests/integration-tests/main.rs new file mode 100644 index 0000000..fdf11b8 --- /dev/null +++ b/bin/tests/integration-tests/main.rs @@ -0,0 +1,5 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +mod command_tests; diff --git a/bin/tests/manifest-tests.rs b/bin/tests/manifest-tests.rs new file mode 100644 index 0000000..e099f31 --- /dev/null +++ b/bin/tests/manifest-tests.rs @@ -0,0 +1,18 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::path::Path; + +use tufaceous_lib::assemble::ArtifactManifest; + +fn check_manifest(path: &Path) -> datatest_stable::Result<()> { + let path = path.try_into()?; + let manifest = + ArtifactManifest::from_path(path).expect("failed to load manifest"); + manifest.verify_all_present()?; + + Ok(()) +} + +datatest_stable::harness!(check_manifest, "manifests", r"^.*\.toml"); diff --git a/brand-metadata/Cargo.toml b/brand-metadata/Cargo.toml new file mode 100644 index 0000000..ea410cc --- /dev/null +++ b/brand-metadata/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tufaceous-brand-metadata" +version.workspace = true +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +semver.workspace = true +serde.workspace = true +serde_json.workspace = true +tar.workspace = true + +[lints] +workspace = true diff --git a/brand-metadata/src/lib.rs b/brand-metadata/src/lib.rs new file mode 100644 index 0000000..074cdcd --- /dev/null +++ b/brand-metadata/src/lib.rs @@ -0,0 +1,150 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Handling of `oxide.json` metadata files in tarballs. +//! +//! `oxide.json` is originally defined by the omicron1(7) zone brand, which +//! lives at . tufaceous +//! extended this format with additional archive types for identifying other +//! types of tarballs. + +use std::io::{Error, ErrorKind, Read, Result, Write}; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Metadata { + v: String, + + // helios-build-utils defines a top-level `i` field for extra information, + // but omicron-package doesn't use this for the package name and version. + // We can also benefit from having rich types for these extra fields, so + // any additional top-level fields (including `i`) that exist for a given + // archive type should be deserialized as part of `ArchiveType`. + #[serde(flatten)] + t: ArchiveType, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case", tag = "t")] +pub enum ArchiveType { + // Originally defined in helios-build-utils (part of helios-omicron-brand): + Baseline, + Layer(LayerInfo), + Os, + + // tufaceous extensions: + Rot, + ControlPlane, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LayerInfo { + pub pkg: String, + pub version: semver::Version, +} + +impl Metadata { + pub fn new(archive_type: ArchiveType) -> Metadata { + Metadata { v: "1".into(), t: archive_type } + } + + pub fn append_to_tar( + &self, + a: &mut tar::Builder, + mtime: u64, + ) -> Result<()> { + let mut b = serde_json::to_vec(self)?; + b.push(b'\n'); + + let mut h = tar::Header::new_ustar(); + h.set_entry_type(tar::EntryType::Regular); + h.set_username("root")?; + h.set_uid(0); + h.set_groupname("root")?; + h.set_gid(0); + h.set_path("oxide.json")?; + h.set_mode(0o444); + h.set_size(b.len().try_into().unwrap()); + h.set_mtime(mtime); + h.set_cksum(); + + a.append(&h, b.as_slice())?; + Ok(()) + } + + /// Read `Metadata` from a tar archive. + /// + /// `oxide.json` is generally the first file in the archive, so this should + /// be a just-opened archive with no entries already read. + pub fn read_from_tar(a: &mut tar::Archive) -> Result { + for entry in a.entries()? { + let mut entry = entry?; + if entry.path()? == std::path::Path::new("oxide.json") { + return Ok(serde_json::from_reader(&mut entry)?); + } + } + Err(Error::new(ErrorKind::InvalidData, "oxide.json is not present")) + } + + pub fn archive_type(&self) -> &ArchiveType { + &self.t + } + + pub fn is_layer(&self) -> bool { + matches!(&self.t, ArchiveType::Layer(_)) + } + + pub fn layer_info(&self) -> Result<&LayerInfo> { + match &self.t { + ArchiveType::Layer(info) => Ok(info), + _ => Err(Error::new( + ErrorKind::InvalidData, + "archive is not the \"layer\" type", + )), + } + } + + pub fn is_baseline(&self) -> bool { + matches!(&self.t, ArchiveType::Baseline) + } + + pub fn is_os(&self) -> bool { + matches!(&self.t, ArchiveType::Os) + } + + pub fn is_rot(&self) -> bool { + matches!(&self.t, ArchiveType::Rot) + } + + pub fn is_control_plane(&self) -> bool { + matches!(&self.t, ArchiveType::ControlPlane) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize() { + let metadata: Metadata = serde_json::from_str( + r#"{"v":"1","t":"layer","pkg":"nexus","version":"12.0.0-0.ci+git3a2ed5e97b3"}"#, + ) + .unwrap(); + assert!(metadata.is_layer()); + let info = metadata.layer_info().unwrap(); + assert_eq!(info.pkg, "nexus"); + assert_eq!(info.version, "12.0.0-0.ci+git3a2ed5e97b3".parse().unwrap()); + + let metadata: Metadata = serde_json::from_str( + r#"{"v":"1","t":"os","i":{"checksum":"42eda100ee0e3bf44b9d0bb6a836046fa3133c378cd9d3a4ba338c3ba9e56eb7","name":"ci 3a2ed5e/9d37813 2024-12-20 08:54"}}"#, + ).unwrap(); + assert!(metadata.is_os()); + + let metadata: Metadata = + serde_json::from_str(r#"{"v":"1","t":"control_plane"}"#).unwrap(); + assert!(metadata.is_control_plane()); + } +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 0000000..c542515 --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "tufaceous-lib" +version.workspace = true +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +aws-lc-rs.workspace = true +base64.workspace = true +buf-list.workspace = true +bytes.workspace = true +camino.workspace = true +camino-tempfile.workspace = true +chrono.workspace = true +debug-ignore.workspace = true +flate2.workspace = true +fs-err.workspace = true +futures.workspace = true +hex.workspace = true +hubtools.workspace = true +itertools.workspace = true +parse-size.workspace = true +rand.workspace = true +semver.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_path_to_error.workspace = true +sha2.workspace = true +slog.workspace = true +tar.workspace = true +tokio.workspace = true +toml.workspace = true +tough.workspace = true +tufaceous-artifact.workspace = true +tufaceous-brand-metadata.workspace = true +url.workspace = true +zip.workspace = true + +[dev-dependencies] +dropshot.workspace = true +tokio = { workspace = true, features = ["macros", "test-util"] } + +[lints] +workspace = true diff --git a/lib/src/archive.rs b/lib/src/archive.rs new file mode 100644 index 0000000..7825935 --- /dev/null +++ b/lib/src/archive.rs @@ -0,0 +1,294 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Support for reading and writing zip archives. + +use std::fmt; +use std::io::{BufReader, BufWriter, Cursor, Read, Seek}; + +use anyhow::{anyhow, bail, Context, Result}; +use buf_list::BufList; +use bytes::Bytes; +use camino::{Utf8Component, Utf8Path, Utf8PathBuf}; +use debug_ignore::DebugIgnore; +use fs_err::File; +use zip::write::{FileOptions, SimpleFileOptions}; +use zip::{CompressionMethod, ZipArchive, ZipWriter}; + +/// A builder for TUF repo archives. +#[derive(Debug)] +pub(crate) struct ArchiveBuilder { + writer: DebugIgnore>>, + // Stored for better error messages. + output_path: Utf8PathBuf, +} + +/// Defines the base directory for TUF repo archives created by this tool. +/// +/// The usual convention is that the base dir is the name of the archive +/// (e.g. foo-1.0 for foo-1.0.zip). but just using a consistent name here +/// simplifies the code that extracts the archive. +pub const ZIP_BASE_DIR: &str = "repo"; + +impl ArchiveBuilder { + /// Creates a new `ArchiveBuilder`, writing to the given path. + pub fn new(output_path: Utf8PathBuf) -> Result { + // The filename must end with "zip". + if output_path.extension() != Some("zip") { + bail!("output path `{output_path}` must end with .zip"); + } + + let file = File::create(&output_path)?; + let writer = ZipWriter::new(BufWriter::new(file)); + Ok(Self { writer: writer.into(), output_path }) + } + + /// Writes the given path to the archive at the name `name`. + /// + /// The name has [`ZIP_BASE_DIR`] prepended to it. + pub fn write_file( + &mut self, + path: &Utf8Path, + name: &Utf8Path, + ) -> Result<()> { + let name = Utf8Path::new(ZIP_BASE_DIR).join(name); + + self.writer.start_file(name.as_str(), Self::file_options())?; + let mut reader = fs_err::File::open(path)?; + std::io::copy(&mut reader, &mut *self.writer).with_context(|| { + format!( + "error writing `{path}` to archive at `{}`", + self.output_path + ) + })?; + Ok(()) + } + + pub fn finish(self) -> Result<()> { + let Self { writer, output_path } = self; + + let zip_file = writer.0.finish().with_context(|| { + format!("error finalizing archive at `{}`", output_path) + })?; + zip_file.into_inner().with_context(|| { + format!("error writing archive at `{}`", output_path) + })?; + + Ok(()) + } + + fn file_options() -> SimpleFileOptions { + // The main purpose of the zip archive is to transmit archives that are + // already compressed, so there's no point trying to re-compress them. + FileOptions::default().compression_method(CompressionMethod::Stored) + } +} + +/// An extractor for archives created by tufaceous. +/// +/// Ideally we'd just be able to read the TUF repo out of a zip archive in +/// memory, but sadly that isn't possible today due to a missing lifetime +/// parameter on `Transport::fetch`. See [this +/// issue](https://github.com/awslabs/tough/pull/563). +#[derive(Debug)] +pub struct ArchiveExtractor { + archive: ZipArchive, +} + +impl ArchiveExtractor> { + /// Builds an extractor from the given path. + /// + /// The archive must be a zip file generated by tufaceous. + pub fn from_path(zip_path: &Utf8Path) -> Result { + let reader = BufReader::new(File::open(zip_path)?); + Self::new(reader).with_context(|| { + format!("error opening zip archive at `{zip_path}`") + }) + } +} + +impl<'a> ArchiveExtractor> { + /// Loads an archived repository from memory as borrowed bytes. + pub fn from_borrowed_bytes(archive: &'a [u8]) -> Result { + let reader = Cursor::new(archive); + Self::new(reader).context("error opening zip archive from memory") + } +} + +impl ArchiveExtractor> { + /// Loads an archived repository from memory as owned bytes. + pub fn from_owned_bytes(archive: impl Into) -> Result { + let reader = Cursor::new(archive.into()); + Self::new(reader).context("error opening zip archive from memory") + } +} + +impl<'a> ArchiveExtractor> { + /// Loads an archived repository from memory as a borrowed BufList. + pub fn from_borrowed_buf_list(archive: &'a BufList) -> Result { + let reader = buf_list::Cursor::new(archive); + Self::new(reader).context("error opening zip archive from memory") + } +} + +impl ArchiveExtractor> { + /// Loads an archived repository from memory as an owned BufList. + pub fn from_owned_buf_list(archive: BufList) -> Result { + let reader = buf_list::Cursor::new(archive); + Self::new(reader).context("error opening zip archive from memory") + } +} + +impl ArchiveExtractor +where + R: Read + Seek, +{ + /// Creates a new `ArchiveExtractor` from the given reader. + pub fn new(reader: R) -> Result> { + // Validate the archive to ensure all paths are correctly formed. + let archive = Self::validate(ZipArchive::new(reader)?)?; + + Ok(Self { archive }) + } + + fn validate(mut archive: ZipArchive) -> Result> { + for i in 0..archive.len() { + let zip_file = archive.by_index(i).with_context(|| { + format!("error reading file number `{i} from archive") + })?; + if !zip_file.is_file() { + bail!("archive must consist only of files, not directories"); + } + let path = Utf8Path::new(zip_file.name()); + validate_path(path).map_err(|error| { + anyhow!("invalid path in archive `{path}`: {error}") + })?; + } + + Ok(archive) + } + + /// Extracts this archive into the specified directory. + /// + /// Once this is completed, use + /// [`OmicronRepo::load_untrusted`](crate::OmicronRepo::load_untrusted) to + /// load the archive from `output_dir`. + /// + /// [`ZIP_BASE_DIR`] will be stripped from output paths. + pub fn extract(&mut self, output_dir: &Utf8Path) -> Result<()> { + for i in 0..self.archive.len() { + let mut zip_file = self.archive.by_index(i).with_context(|| { + format!("error reading file number `{i} from archive") + })?; + // SAFETY: file names have already been checked in `Self::validate`. + let file_name = Utf8Path::new(zip_file.name()).to_owned(); + let dest_path = output_dir.join( + file_name + .strip_prefix(ZIP_BASE_DIR) + .expect("checked in Self::validate"), + ); + // The file is in a directory. + fs_err::create_dir_all( + dest_path.parent().expect("at least 1 component"), + )?; + + let mut writer = File::create(&dest_path)?; + std::io::copy(&mut zip_file, &mut writer).with_context(|| { + format!( + "error writing `{file_name}` in archive to `{dest_path}`" + ) + })?; + } + + Ok(()) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum InvalidPath<'a> { + AbsolutePath, + ExactlyBaseDir, + IncorrectBaseDir, + InvalidComponent(Utf8Component<'a>), +} + +impl fmt::Display for InvalidPath<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InvalidPath::AbsolutePath => { + write!(f, "path is absolute -- expected relative paths") + } + InvalidPath::ExactlyBaseDir => { + write!(f, "path is exactly `{ZIP_BASE_DIR}` -- expected `{ZIP_BASE_DIR}/`") + } + InvalidPath::IncorrectBaseDir => { + write!(f, "invalid base directory -- must be `{ZIP_BASE_DIR}`") + } + InvalidPath::InvalidComponent(component) => { + write!(f, "invalid component `{component}`") + } + } + } +} + +fn validate_path(path: &Utf8Path) -> Result<(), InvalidPath<'_>> { + if path.is_absolute() { + return Err(InvalidPath::AbsolutePath); + } + if path == ZIP_BASE_DIR { + return Err(InvalidPath::ExactlyBaseDir); + } + if !path.starts_with(ZIP_BASE_DIR) { + return Err(InvalidPath::IncorrectBaseDir); + } + + for component in path.components() { + if !matches!(component, Utf8Component::Normal(_)) { + return Err(InvalidPath::InvalidComponent(component)); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_path() { + let valid = ["repo/foo", "repo/foo/bar", "repo/foo/./bar", "repo//foo"]; + let invalid = [ + ("repo", InvalidPath::ExactlyBaseDir), + ("repo/", InvalidPath::ExactlyBaseDir), + ("repo/.", InvalidPath::ExactlyBaseDir), + ("not-repo", InvalidPath::IncorrectBaseDir), + ("not-repo/foo", InvalidPath::IncorrectBaseDir), + ( + "repo/..", + InvalidPath::InvalidComponent(Utf8Component::ParentDir), + ), + ("/repo/foo", InvalidPath::AbsolutePath), + ]; + + for path in valid { + validate_path(Utf8Path::new(path)).unwrap_or_else(|err| { + panic!("expected path `{path}` to be valid: {err}") + }); + } + + for (path, expected) in invalid { + eprintln!("testing invalid path: `{path}`"); + let actual = match validate_path(Utf8Path::new(path)) { + Ok(()) => panic!("expected path `{path}` to be invalid"), + Err(error) => error, + }; + + assert_eq!( + actual, expected, + "for path `{path}`, InvalidPath error should match" + ); + } + } +} diff --git a/lib/src/artifact.rs b/lib/src/artifact.rs new file mode 100644 index 0000000..eaf507e --- /dev/null +++ b/lib/src/artifact.rs @@ -0,0 +1,429 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::io::{self, BufReader, Write}; +use std::path::Path; + +use anyhow::{bail, Context, Result}; +use buf_list::BufList; +use bytes::Bytes; +use camino::Utf8PathBuf; +use fs_err::File; +use semver::Version; +use tufaceous_artifact::ArtifactKind; +use tufaceous_brand_metadata::Metadata; + +mod composite; + +pub use composite::CompositeControlPlaneArchiveBuilder; +pub use composite::CompositeEntry; +pub use composite::CompositeHostArchiveBuilder; +pub use composite::CompositeRotArchiveBuilder; +pub use composite::MtimeSource; + +/// The location a artifact will be obtained from. +#[derive(Clone, Debug)] +pub enum ArtifactSource { + File(Utf8PathBuf), + Memory(BufList), + // We might need to support downloading data over HTTP as well +} + +/// Describes a new artifact to be added. +pub struct AddArtifact { + kind: ArtifactKind, + name: String, + version: Version, + source: ArtifactSource, +} + +impl AddArtifact { + /// Creates an [`AddArtifact`] from the provided source. + pub fn new( + kind: ArtifactKind, + name: String, + version: Version, + source: ArtifactSource, + ) -> Self { + Self { kind, name, version, source } + } + + /// Creates an [`AddArtifact`] from the path, name and version. + /// + /// If the name is `None`, it is derived from the filename of the path + /// without matching extensions. + pub fn from_path( + kind: ArtifactKind, + name: Option, + version: Version, + path: Utf8PathBuf, + ) -> Result { + let name = match name { + Some(name) => name, + None => path + .file_name() + .context("artifact path is a directory")? + .split('.') + .next() + .expect("str::split has at least 1 element") + .to_owned(), + }; + + Ok(Self { kind, name, version, source: ArtifactSource::File(path) }) + } + + /// Returns the kind of artifact this is. + pub fn kind(&self) -> &ArtifactKind { + &self.kind + } + + /// Returns the name of the new artifact. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the version of the new artifact. + pub fn version(&self) -> &Version { + &self.version + } + + /// Returns the source for this artifact. + pub fn source(&self) -> &ArtifactSource { + &self.source + } + + /// Writes this artifact to the specified writer. + pub(crate) fn write_to(&self, writer: &mut W) -> Result<()> { + match &self.source { + ArtifactSource::File(path) => { + let mut reader = File::open(path)?; + std::io::copy(&mut reader, writer)?; + } + ArtifactSource::Memory(buf_list) => { + for chunk in buf_list { + writer.write_all(chunk)?; + } + } + } + + Ok(()) + } +} + +pub(crate) fn make_filler_text(length: usize) -> Vec { + std::iter::repeat(FILLER_TEXT).flatten().copied().take(length).collect() +} + +/// Represents host phase images. +/// +/// The host and trampoline artifacts are actually tarballs, with phase 1 and +/// phase 2 images inside them. This code extracts those images out of the +/// tarballs. +#[derive(Clone, Debug)] +pub struct HostPhaseImages { + pub phase_1: Bytes, + pub phase_2: Bytes, +} + +impl HostPhaseImages { + pub fn extract(reader: R) -> Result { + let mut phase_1 = Vec::new(); + let mut phase_2 = Vec::new(); + Self::extract_into( + reader, + io::Cursor::<&mut Vec>::new(&mut phase_1), + io::Cursor::<&mut Vec>::new(&mut phase_2), + )?; + Ok(Self { phase_1: phase_1.into(), phase_2: phase_2.into() }) + } + + pub fn extract_into( + reader: R, + phase_1: W, + phase_2: W, + ) -> Result<()> { + let uncompressed = flate2::bufread::GzDecoder::new(reader); + let mut archive = tar::Archive::new(uncompressed); + + let mut oxide_json_found = false; + let mut phase_1_writer = Some(phase_1); + let mut phase_2_writer = Some(phase_2); + for entry in archive + .entries() + .context("error building list of entries from archive")? + { + let entry = entry.context("error reading entry from archive")?; + let path = entry + .header() + .path() + .context("error reading path from archive")?; + if path == Path::new(OXIDE_JSON_FILE_NAME) { + let json_bytes = read_entry(entry, OXIDE_JSON_FILE_NAME)?; + let metadata: Metadata = + serde_json::from_slice(&json_bytes).with_context(|| { + format!( + "error deserializing JSON from {OXIDE_JSON_FILE_NAME}" + ) + })?; + if !metadata.is_os() { + bail!( + "unexpected archive type: expected os, found {:?}", + metadata.archive_type(), + ) + } + oxide_json_found = true; + } else if path == Path::new(HOST_PHASE_1_FILE_NAME) { + if let Some(phase_1) = phase_1_writer.take() { + read_entry_into(entry, HOST_PHASE_1_FILE_NAME, phase_1)?; + } + } else if path == Path::new(HOST_PHASE_2_FILE_NAME) { + if let Some(phase_2) = phase_2_writer.take() { + read_entry_into(entry, HOST_PHASE_2_FILE_NAME, phase_2)?; + } + } + + if oxide_json_found + && phase_1_writer.is_none() + && phase_2_writer.is_none() + { + break; + } + } + + let mut not_found = Vec::new(); + if !oxide_json_found { + not_found.push(OXIDE_JSON_FILE_NAME); + } + + // If we didn't `.take()` the writer out of the options, we never saw + // the expected phase1/phase2 filenames. + if phase_1_writer.is_some() { + not_found.push(HOST_PHASE_1_FILE_NAME); + } + if phase_2_writer.is_some() { + not_found.push(HOST_PHASE_2_FILE_NAME); + } + + if !not_found.is_empty() { + bail!("required files not found: {}", not_found.join(", ")) + } + + Ok(()) + } +} + +fn read_entry( + entry: tar::Entry, + file_name: &str, +) -> Result { + let mut buf = Vec::new(); + read_entry_into(entry, file_name, io::Cursor::new(&mut buf))?; + Ok(buf.into()) +} + +fn read_entry_into( + mut entry: tar::Entry, + file_name: &str, + mut out: W, +) -> Result<()> { + let entry_type = entry.header().entry_type(); + if entry_type != tar::EntryType::Regular { + bail!("for {file_name}, expected regular file, found {entry_type:?}"); + } + io::copy(&mut entry, &mut out) + .with_context(|| format!("error reading {file_name} from archive"))?; + Ok(()) +} + +/// Represents RoT A/B hubris archives. +/// +/// RoT artifacts are actually tarballs, with both A and B hubris archives +/// inside them. This code extracts those archives out of the tarballs. +#[derive(Clone, Debug)] +pub struct RotArchives { + pub archive_a: Bytes, + pub archive_b: Bytes, +} + +impl RotArchives { + pub fn extract(reader: R) -> Result { + let mut archive_a = Vec::new(); + let mut archive_b = Vec::new(); + Self::extract_into( + reader, + io::Cursor::<&mut Vec>::new(&mut archive_a), + io::Cursor::<&mut Vec>::new(&mut archive_b), + )?; + Ok(Self { archive_a: archive_a.into(), archive_b: archive_b.into() }) + } + + pub fn extract_into( + reader: R, + archive_a: W, + archive_b: W, + ) -> Result<()> { + let uncompressed = flate2::bufread::GzDecoder::new(reader); + let mut archive = tar::Archive::new(uncompressed); + + let mut oxide_json_found = false; + let mut archive_a_writer = Some(archive_a); + let mut archive_b_writer = Some(archive_b); + for entry in archive + .entries() + .context("error building list of entries from archive")? + { + let entry = entry.context("error reading entry from archive")?; + let path = entry + .header() + .path() + .context("error reading path from archive")?; + if path == Path::new(OXIDE_JSON_FILE_NAME) { + let json_bytes = read_entry(entry, OXIDE_JSON_FILE_NAME)?; + let metadata: Metadata = + serde_json::from_slice(&json_bytes).with_context(|| { + format!( + "error deserializing JSON from {OXIDE_JSON_FILE_NAME}" + ) + })?; + if !metadata.is_rot() { + bail!( + "unexpected archive type: expected rot, found {:?}", + metadata.archive_type(), + ) + } + oxide_json_found = true; + } else if path == Path::new(ROT_ARCHIVE_A_FILE_NAME) { + if let Some(archive_a) = archive_a_writer.take() { + read_entry_into(entry, ROT_ARCHIVE_A_FILE_NAME, archive_a)?; + } + } else if path == Path::new(ROT_ARCHIVE_B_FILE_NAME) { + if let Some(archive_b) = archive_b_writer.take() { + read_entry_into(entry, ROT_ARCHIVE_B_FILE_NAME, archive_b)?; + } + } + + if oxide_json_found + && archive_a_writer.is_none() + && archive_b_writer.is_none() + { + break; + } + } + + let mut not_found = Vec::new(); + if !oxide_json_found { + not_found.push(OXIDE_JSON_FILE_NAME); + } + + // If we didn't `.take()` the writer out of the options, we never saw + // the expected A/B filenames. + if archive_a_writer.is_some() { + not_found.push(ROT_ARCHIVE_A_FILE_NAME); + } + if archive_b_writer.is_some() { + not_found.push(ROT_ARCHIVE_B_FILE_NAME); + } + + if !not_found.is_empty() { + bail!("required files not found: {}", not_found.join(", ")) + } + + Ok(()) + } +} + +/// Represents control plane zone images. +/// +/// The control plane artifact is actually a tarball that contains a set of zone +/// images. This code extracts those images out of the tarball. +#[derive(Clone, Debug)] +pub struct ControlPlaneZoneImages { + pub zones: Vec<(String, Bytes)>, +} + +impl ControlPlaneZoneImages { + pub fn extract(reader: R) -> Result { + let mut zones = Vec::new(); + Self::extract_into(reader, |name, reader| { + let mut buf = Vec::new(); + io::copy(reader, &mut buf)?; + zones.push((name, buf.into())); + Ok(()) + })?; + Ok(Self { zones }) + } + + pub fn extract_into(reader: R, mut handler: F) -> Result<()> + where + R: io::Read, + F: FnMut(String, &mut dyn io::Read) -> Result<()>, + { + let uncompressed = + flate2::bufread::GzDecoder::new(BufReader::new(reader)); + let mut archive = tar::Archive::new(uncompressed); + + let mut oxide_json_found = false; + let mut zone_found = false; + for entry in archive + .entries() + .context("error building list of entries from archive")? + { + let mut entry = + entry.context("error reading entry from archive")?; + let path = entry + .header() + .path() + .context("error reading path from archive")?; + if path == Path::new(OXIDE_JSON_FILE_NAME) { + let json_bytes = read_entry(entry, OXIDE_JSON_FILE_NAME)?; + let metadata: Metadata = + serde_json::from_slice(&json_bytes).with_context(|| { + format!( + "error deserializing JSON from {OXIDE_JSON_FILE_NAME}" + ) + })?; + if !metadata.is_control_plane() { + bail!( + "unexpected archive type: expected control_plane, found {:?}", + metadata.archive_type(), + ) + } + oxide_json_found = true; + } else if path.starts_with(CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY) { + if let Some(name) = path + .file_name() + .and_then(|s| s.to_str()) + .map(|s| s.to_string()) + { + handler(name, &mut entry)?; + } + zone_found = true; + } + } + + let mut not_found = Vec::new(); + if !oxide_json_found { + not_found.push(OXIDE_JSON_FILE_NAME); + } + if !not_found.is_empty() { + bail!("required files not found: {}", not_found.join(", ")) + } + if !zone_found { + bail!( + "no zone images found in `{}/`", + CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY + ); + } + + Ok(()) + } +} + +static FILLER_TEXT: &[u8; 16] = b"tufaceousfaketxt"; +static OXIDE_JSON_FILE_NAME: &str = "oxide.json"; +static HOST_PHASE_1_FILE_NAME: &str = "image/rom"; +static HOST_PHASE_2_FILE_NAME: &str = "image/zfs.img"; +static ROT_ARCHIVE_A_FILE_NAME: &str = "archive-a.zip"; +static ROT_ARCHIVE_B_FILE_NAME: &str = "archive-b.zip"; +static CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY: &str = "zones"; diff --git a/lib/src/artifact/composite.rs b/lib/src/artifact/composite.rs new file mode 100644 index 0000000..2d227a9 --- /dev/null +++ b/lib/src/artifact/composite.rs @@ -0,0 +1,212 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::collections::HashMap; +use std::io::{BufWriter, Write}; + +use anyhow::{anyhow, bail, Context, Result}; +use camino::Utf8Path; +use flate2::write::GzEncoder; +use flate2::Compression; +use sha2::{Digest, Sha256}; +use tufaceous_brand_metadata::{ArchiveType, Metadata}; + +use super::{ + CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY, HOST_PHASE_1_FILE_NAME, + HOST_PHASE_2_FILE_NAME, ROT_ARCHIVE_A_FILE_NAME, ROT_ARCHIVE_B_FILE_NAME, +}; + +/// Represents a single entry in a composite artifact. +/// +/// A composite artifact is a tarball containing multiple artifacts. This +/// struct is intended for the insertion of one such entry into the artifact. +/// +/// At the moment it only accepts byte slices, but it could be extended to +/// support arbitrary readers in the future. +pub struct CompositeEntry<'a> { + pub data: &'a [u8], + pub mtime_source: MtimeSource, +} + +pub struct CompositeControlPlaneArchiveBuilder { + inner: CompositeTarballBuilder, + hashes: HashMap<[u8; 32], String>, +} + +impl CompositeControlPlaneArchiveBuilder { + pub fn new(writer: W, mtime_source: MtimeSource) -> Result { + let metadata = Metadata::new(ArchiveType::ControlPlane); + let inner = + CompositeTarballBuilder::new(writer, metadata, mtime_source)?; + Ok(Self { inner, hashes: HashMap::new() }) + } + + pub fn append_zone( + &mut self, + name: &str, + entry: CompositeEntry<'_>, + ) -> Result<()> { + let name_path = Utf8Path::new(name); + if name_path.file_name() != Some(name) { + bail!("control plane zone filenames should not contain paths"); + } + if let Some(duplicate) = + self.hashes.insert(Sha256::digest(entry.data).into(), name.into()) + { + bail!( + "duplicate zones are not allowed \ + ({name} and {duplicate} have the same checksum)" + ); + } + let path = + Utf8Path::new(CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY).join(name_path); + self.inner.append_file(path.as_str(), entry) + } + + pub fn finish(self) -> Result { + self.inner.finish() + } +} + +pub struct CompositeRotArchiveBuilder { + inner: CompositeTarballBuilder, +} + +impl CompositeRotArchiveBuilder { + pub fn new(writer: W, mtime_source: MtimeSource) -> Result { + let metadata = Metadata::new(ArchiveType::Rot); + let inner = + CompositeTarballBuilder::new(writer, metadata, mtime_source)?; + Ok(Self { inner }) + } + + pub fn append_archive_a( + &mut self, + entry: CompositeEntry<'_>, + ) -> Result<()> { + self.inner.append_file(ROT_ARCHIVE_A_FILE_NAME, entry) + } + + pub fn append_archive_b( + &mut self, + entry: CompositeEntry<'_>, + ) -> Result<()> { + self.inner.append_file(ROT_ARCHIVE_B_FILE_NAME, entry) + } + + pub fn finish(self) -> Result { + self.inner.finish() + } +} + +pub struct CompositeHostArchiveBuilder { + inner: CompositeTarballBuilder, +} + +impl CompositeHostArchiveBuilder { + pub fn new(writer: W, mtime_source: MtimeSource) -> Result { + let metadata = Metadata::new(ArchiveType::Os); + let inner = + CompositeTarballBuilder::new(writer, metadata, mtime_source)?; + Ok(Self { inner }) + } + + pub fn append_phase_1(&mut self, entry: CompositeEntry<'_>) -> Result<()> { + self.inner.append_file(HOST_PHASE_1_FILE_NAME, entry) + } + + pub fn append_phase_2(&mut self, entry: CompositeEntry<'_>) -> Result<()> { + self.inner.append_file(HOST_PHASE_2_FILE_NAME, entry) + } + + pub fn finish(self) -> Result { + self.inner.finish() + } +} + +struct CompositeTarballBuilder { + builder: tar::Builder>>, +} + +impl CompositeTarballBuilder { + fn new( + writer: W, + metadata: Metadata, + mtime_source: MtimeSource, + ) -> Result { + let mut builder = tar::Builder::new(GzEncoder::new( + BufWriter::new(writer), + Compression::fast(), + )); + metadata.append_to_tar(&mut builder, mtime_source.into_mtime())?; + Ok(Self { builder }) + } + + fn append_file( + &mut self, + path: &str, + entry: CompositeEntry<'_>, + ) -> Result<()> { + let header = + make_tar_header(path, entry.data.len(), entry.mtime_source); + self.builder + .append(&header, entry.data) + .with_context(|| format!("error append {path:?}")) + } + + fn finish(self) -> Result { + let gz_encoder = + self.builder.into_inner().context("error finalizing archive")?; + let buf_writer = + gz_encoder.finish().context("error finishing gz encoder")?; + buf_writer + .into_inner() + .map_err(|_| anyhow!("error flushing buffered archive writer")) + } +} + +fn make_tar_header( + path: &str, + size: usize, + mtime_source: MtimeSource, +) -> tar::Header { + let mtime = mtime_source.into_mtime(); + + let mut header = tar::Header::new_ustar(); + header.set_username("root").unwrap(); + header.set_uid(0); + header.set_groupname("root").unwrap(); + header.set_gid(0); + header.set_path(path).unwrap(); + header.set_size(size as u64); + header.set_mode(0o444); + header.set_entry_type(tar::EntryType::Regular); + header.set_mtime(mtime); + header.set_cksum(); + + header +} + +/// How to obtain the `mtime` field for a tar header. +#[derive(Copy, Clone, Debug)] +pub enum MtimeSource { + /// Use a fixed timestamp of zero seconds past the Unix epoch. + Zero, + + /// Use the current time. + Now, +} + +impl MtimeSource { + pub(crate) fn into_mtime(self) -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; + + match self { + Self::Zero => 0, + Self::Now => { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + } + } + } +} diff --git a/lib/src/assemble/build.rs b/lib/src/assemble/build.rs new file mode 100644 index 0000000..4cb636c --- /dev/null +++ b/lib/src/assemble/build.rs @@ -0,0 +1,135 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use anyhow::{Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use chrono::{DateTime, Utc}; + +use crate::{AddArtifact, Key, OmicronRepo}; + +use super::ArtifactManifest; + +/// Assembles a TUF repo from a list of artifacts. +#[derive(Debug)] +pub struct OmicronRepoAssembler { + log: slog::Logger, + manifest: ArtifactManifest, + build_dir: Option, + keys: Vec, + expiry: DateTime, + output_path: Utf8PathBuf, +} + +impl OmicronRepoAssembler { + pub fn new( + log: &slog::Logger, + manifest: ArtifactManifest, + keys: Vec, + expiry: DateTime, + output_path: Utf8PathBuf, + ) -> Self { + Self { + log: log.new(slog::o!("component" => "OmicronRepoAssembler")), + manifest, + build_dir: None, + keys, + expiry, + output_path, + } + } + + pub fn set_build_dir(&mut self, build_dir: Utf8PathBuf) -> &mut Self { + self.build_dir = Some(build_dir); + self + } + + pub async fn build(&self) -> Result<()> { + let (build_dir, is_temp) = match &self.build_dir { + Some(dir) => (dir.clone(), false), + None => { + // Create a new temporary directory. + let dir = camino_tempfile::Builder::new() + .prefix("tufaceous") + .tempdir()?; + // This will cause the directory to be preserved -- we're going + // to clean it up if it's successful. + let path = dir.into_path(); + (path, true) + } + }; + + slog::info!(self.log, "assembling repository in `{build_dir}`"); + + match self.build_impl(&build_dir).await { + Ok(()) => { + if is_temp { + slog::debug!(self.log, "assembly successful, cleaning up"); + // Log, but otherwise ignore, errors while cleaning up. + if let Err(error) = fs_err::remove_dir_all(&build_dir) { + slog::warn!( + self.log, + "failed to clean up temporary directory {build_dir}: {error}" + ) + } + } + + slog::info!( + self.log, + "artifacts assembled and archived to `{}`", + self.output_path + ); + } + Err(error) => { + slog::error!(self.log, "assembly failed: {error:?}"); + slog::info!( + self.log, + "failing build directory preserved: `{build_dir}`" + ); + } + } + + Ok(()) + } + + async fn build_impl(&self, build_dir: &Utf8Path) -> Result<()> { + let mut repository = OmicronRepo::initialize( + &self.log, + build_dir, + self.manifest.system_version.clone(), + self.keys.clone(), + self.expiry, + ) + .await? + .into_editor() + .await?; + + // Add all the artifacts. + for (kind, entries) in &self.manifest.artifacts { + for data in entries { + let new_artifact = AddArtifact::new( + (*kind).into(), + data.name.clone(), + data.version.clone(), + data.source.clone(), + ); + repository.add_artifact(&new_artifact).with_context(|| { + format!("error adding artifact with kind `{kind}`") + })?; + } + } + + // Write out the repository. + repository.sign_and_finish(self.keys.clone(), self.expiry).await?; + + // Now reopen the repository to archive it into a zip file. + let repo2 = OmicronRepo::load_untrusted(&self.log, build_dir) + .await + .context("error reopening repository to archive")?; + repo2 + .archive(&self.output_path) + .context("error archiving repository")?; + + Ok(()) + } +} diff --git a/lib/src/assemble/manifest.rs b/lib/src/assemble/manifest.rs new file mode 100644 index 0000000..a5dddfd --- /dev/null +++ b/lib/src/assemble/manifest.rs @@ -0,0 +1,674 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::collections::{BTreeMap, BTreeSet}; +use std::str::FromStr; + +use anyhow::{bail, ensure, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use parse_size::parse_size; +use semver::Version; +use serde::{Deserialize, Serialize}; +use tufaceous_artifact::KnownArtifactKind; + +use crate::{ + make_filler_text, ArtifactSource, CompositeControlPlaneArchiveBuilder, + CompositeEntry, CompositeHostArchiveBuilder, CompositeRotArchiveBuilder, + MtimeSource, +}; + +static FAKE_MANIFEST_TOML: &str = + include_str!("../../../bin/manifests/fake.toml"); + +/// A list of components in a TUF repo representing a single update. +#[derive(Clone, Debug)] +pub struct ArtifactManifest { + pub system_version: Version, + pub artifacts: BTreeMap>, +} + +impl ArtifactManifest { + /// Reads a manifest in from a TOML file. + pub fn from_path(path: &Utf8Path) -> Result { + let input = fs_err::read_to_string(path)?; + let base_dir = path + .parent() + .with_context(|| format!("path `{path}` did not have a parent"))?; + Self::from_str(base_dir, &input) + } + + /// Deserializes a manifest from an input string. + pub fn from_str(base_dir: &Utf8Path, input: &str) -> Result { + let manifest = DeserializedManifest::from_str(input)?; + Self::from_deserialized(base_dir, manifest) + } + + /// Creates a manifest from a [`DeserializedManifest`]. + pub fn from_deserialized( + base_dir: &Utf8Path, + manifest: DeserializedManifest, + ) -> Result { + // Replace all paths in the deserialized manifest with absolute ones, + // and do some processing to support flexible manifests: + // + // 1. assemble any composite artifacts from their pieces + // 2. replace any "fake" artifacts with in-memory buffers + // + // Currently both of those transformations produce + // `ArtifactSource::Memory(_)` variants (i.e., composite and fake + // artifacts all sit in-memory until we're done with the manifest), + // which puts some limits on how large the inputs to the manifest can + // practically be. If this becomes onerous, we could instead write the + // transformed artifacts to temporary files. + // + // We do some additional error checking here to make sure the + // `CompositeZZZ` variants are only used with their corresponding + // `KnownArtifactKind`s. It would be nicer to enforce this more + // statically and let serde do these checks, but that seems relatively + // tricky in comparison to these checks. + + Ok(ArtifactManifest { + system_version: manifest.system_version, + artifacts: manifest + .artifacts + .into_iter() + .map(|(kind, entries)| { + Self::parse_deserialized_entries(base_dir, kind, entries) + }) + .collect::>()?, + }) + } + + fn parse_deserialized_entries( + base_dir: &Utf8Path, + kind: KnownArtifactKind, + entries: Vec, + ) -> Result<(KnownArtifactKind, Vec)> { + let entries = entries + .into_iter() + .map(|data| { + let source = match data.source { + DeserializedArtifactSource::File { path } => { + ArtifactSource::File(base_dir.join(path)) + } + DeserializedArtifactSource::Fake { size } => { + let fake_data = + FakeDataAttributes::new(kind, &data.version) + .make_data(size as usize); + ArtifactSource::Memory(fake_data.into()) + } + DeserializedArtifactSource::CompositeHost { + phase_1, + phase_2, + } => { + ensure!( + matches!( + kind, + KnownArtifactKind::Host + | KnownArtifactKind::Trampoline + ), + "`composite_host` source cannot be used with \ + artifact kind {kind:?}" + ); + + let mtime_source = + if phase_1.is_fake() && phase_2.is_fake() { + // Ensure stability of fake artifacts. + MtimeSource::Zero + } else { + MtimeSource::Now + }; + + let mut builder = CompositeHostArchiveBuilder::new( + Vec::new(), + mtime_source, + )?; + phase_1.with_entry( + FakeDataAttributes::new(kind, &data.version), + |entry| builder.append_phase_1(entry), + )?; + phase_2.with_entry( + FakeDataAttributes::new(kind, &data.version), + |entry| builder.append_phase_2(entry), + )?; + ArtifactSource::Memory(builder.finish()?.into()) + } + DeserializedArtifactSource::CompositeRot { + archive_a, + archive_b, + } => { + ensure!( + matches!( + kind, + KnownArtifactKind::GimletRot + | KnownArtifactKind::SwitchRot + | KnownArtifactKind::PscRot + ), + "`composite_rot` source cannot be used with \ + artifact kind {kind:?}" + ); + + let mtime_source = + if archive_a.is_fake() && archive_b.is_fake() { + // Ensure stability of fake artifacts. + MtimeSource::Zero + } else { + MtimeSource::Now + }; + + let mut builder = CompositeRotArchiveBuilder::new( + Vec::new(), + mtime_source, + )?; + archive_a.with_entry( + FakeDataAttributes::new(kind, &data.version), + |entry| builder.append_archive_a(entry), + )?; + archive_b.with_entry( + FakeDataAttributes::new(kind, &data.version), + |entry| builder.append_archive_b(entry), + )?; + ArtifactSource::Memory(builder.finish()?.into()) + } + DeserializedArtifactSource::CompositeControlPlane { + zones, + } => { + ensure!( + kind == KnownArtifactKind::ControlPlane, + "`composite_control_plane` source cannot be \ + used with artifact kind {kind:?}" + ); + + // Ensure stability of fake artifacts. + let mtime_source = if zones.iter().all(|z| z.is_fake()) + { + MtimeSource::Zero + } else { + MtimeSource::Now + }; + + let data = Vec::new(); + let mut builder = + CompositeControlPlaneArchiveBuilder::new( + data, + mtime_source, + )?; + + for zone in zones { + zone.with_name_and_entry(|name, entry| { + builder.append_zone(name, entry) + })?; + } + ArtifactSource::Memory(builder.finish()?.into()) + } + }; + let data = ArtifactData { + name: data.name, + version: data.version, + source, + }; + Ok(data) + }) + .collect::>()?; + Ok((kind, entries)) + } + + /// Returns a fake manifest. Useful for testing. + pub fn new_fake() -> Self { + // The base directory doesn't matter for fake manifests. + Self::from_str(".".into(), FAKE_MANIFEST_TOML) + .expect("the fake manifest is a valid manifest") + } + + /// Checks that all expected artifacts are present, returning an error with + /// details if any artifacts are missing. + pub fn verify_all_present(&self) -> Result<()> { + let all_artifacts: BTreeSet<_> = KnownArtifactKind::iter() + .filter(|k| !matches!(k, KnownArtifactKind::Zone)) + .collect(); + let present_artifacts: BTreeSet<_> = + self.artifacts.keys().copied().collect(); + + let missing = &all_artifacts - &present_artifacts; + if !missing.is_empty() { + bail!( + "manifest has missing artifacts: {}", + itertools::join(missing, ", ") + ); + } + + Ok(()) + } +} + +#[derive(Debug)] +struct FakeDataAttributes<'a> { + kind: KnownArtifactKind, + version: &'a Version, +} + +impl<'a> FakeDataAttributes<'a> { + fn new(kind: KnownArtifactKind, version: &'a Version) -> Self { + Self { kind, version } + } + + fn make_data(&self, size: usize) -> Vec { + use hubtools::{CabooseBuilder, HubrisArchiveBuilder}; + + let board = match self.kind { + KnownArtifactKind::GimletRotBootloader + | KnownArtifactKind::PscRotBootloader + | KnownArtifactKind::SwitchRotBootloader => "SimRotStage0", + // non-Hubris artifacts: just make fake data + KnownArtifactKind::Host + | KnownArtifactKind::Trampoline + | KnownArtifactKind::ControlPlane + | KnownArtifactKind::Zone => return make_filler_text(size), + + // hubris artifacts: build a fake archive (SimGimletSp and + // SimGimletRot are used by sp-sim) + KnownArtifactKind::GimletSp => "SimGimletSp", + KnownArtifactKind::GimletRot => "SimRot", + KnownArtifactKind::PscSp => "fake-psc-sp", + KnownArtifactKind::PscRot => "fake-psc-rot", + KnownArtifactKind::SwitchSp => "SimSidecarSp", + KnownArtifactKind::SwitchRot => "SimRot", + }; + + // For our purposes sign = board represents what we want for the RoT + // and we don't care about the sign value for the SP + // We now have an assumption that board == name for our production + // images + let caboose = CabooseBuilder::default() + .git_commit("this-is-fake-data") + .board(board) + .version(self.version.to_string()) + .name(board) + .sign(board) + .build(); + + let mut builder = HubrisArchiveBuilder::with_fake_image(); + builder.write_caboose(caboose.as_slice()).unwrap(); + builder.build_to_vec().unwrap() + } +} + +/// Information about an individual artifact. +#[derive(Clone, Debug)] +pub struct ArtifactData { + pub name: String, + pub version: Version, + pub source: ArtifactSource, +} + +/// Deserializable version of [`ArtifactManifest`]. +/// +/// Since manifests require a base directory to be deserialized properly, +/// we don't expose the `Deserialize` impl on `ArtifactManifest, forcing +/// consumers to go through [`ArtifactManifest::from_path`] or +/// [`ArtifactManifest::from_str`]. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct DeserializedManifest { + pub system_version: Version, + #[serde(rename = "artifact")] + pub artifacts: BTreeMap>, +} + +impl DeserializedManifest { + pub fn from_path(path: &Utf8Path) -> Result { + let input = fs_err::read_to_string(path)?; + Self::from_str(&input).with_context(|| { + format!("error deserializing manifest from {path}") + }) + } + + pub fn to_toml(&self) -> Result { + toml::to_string(self).context("error serializing manifest to TOML") + } + + /// For fake manifests, applies a set of changes to them. + /// + /// Intended for testing. + pub fn apply_tweaks(&mut self, tweaks: &[ManifestTweak]) -> Result<()> { + for tweak in tweaks { + match tweak { + ManifestTweak::SystemVersion(version) => { + self.system_version = version.clone(); + } + ManifestTweak::ArtifactVersion { kind, version } => { + let entries = + self.artifacts.get_mut(kind).with_context(|| { + format!( + "manifest does not have artifact kind \ + {kind}", + ) + })?; + for entry in entries { + entry.version = version.clone(); + } + } + ManifestTweak::ArtifactContents { kind, size_delta } => { + let entries = + self.artifacts.get_mut(kind).with_context(|| { + format!( + "manifest does not have artifact kind \ + {kind}", + ) + })?; + + for entry in entries { + entry.source.apply_size_delta(*size_delta)?; + } + } + } + } + + Ok(()) + } + + /// Returns the fake manifest. + pub fn fake() -> Self { + Self::from_str(FAKE_MANIFEST_TOML).unwrap() + } + + /// Returns a version of the fake manifest with a set of changes applied. + /// + /// This is primarily intended for testing. + pub fn tweaked_fake(tweaks: &[ManifestTweak]) -> Self { + let mut manifest = Self::fake(); + manifest + .apply_tweaks(tweaks) + .expect("builtin fake manifest should accept all tweaks"); + + manifest + } +} + +impl FromStr for DeserializedManifest { + type Err = anyhow::Error; + + fn from_str(input: &str) -> Result { + let de = toml::Deserializer::new(input); + serde_path_to_error::deserialize(de) + .context("error deserializing manifest") + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct DeserializedArtifactData { + pub name: String, + pub version: Version, + pub source: DeserializedArtifactSource, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(tag = "kind", rename_all = "kebab-case")] +pub enum DeserializedArtifactSource { + File { + path: Utf8PathBuf, + }, + Fake { + #[serde(deserialize_with = "deserialize_byte_size")] + size: u64, + }, + CompositeHost { + phase_1: DeserializedFileArtifactSource, + phase_2: DeserializedFileArtifactSource, + }, + CompositeRot { + archive_a: DeserializedFileArtifactSource, + archive_b: DeserializedFileArtifactSource, + }, + CompositeControlPlane { + zones: Vec, + }, +} + +impl DeserializedArtifactSource { + fn apply_size_delta(&mut self, size_delta: i64) -> Result<()> { + match self { + DeserializedArtifactSource::File { .. } => { + bail!("cannot apply size delta to `file` source") + } + DeserializedArtifactSource::Fake { size } => { + *size = (*size).saturating_add_signed(size_delta); + Ok(()) + } + DeserializedArtifactSource::CompositeHost { phase_1, phase_2 } => { + phase_1.apply_size_delta(size_delta)?; + phase_2.apply_size_delta(size_delta)?; + Ok(()) + } + DeserializedArtifactSource::CompositeRot { + archive_a, + archive_b, + } => { + archive_a.apply_size_delta(size_delta)?; + archive_b.apply_size_delta(size_delta)?; + Ok(()) + } + DeserializedArtifactSource::CompositeControlPlane { zones } => { + for zone in zones { + zone.apply_size_delta(size_delta)?; + } + Ok(()) + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum DeserializedFileArtifactSource { + File { + path: Utf8PathBuf, + }, + Fake { + #[serde(deserialize_with = "deserialize_byte_size")] + size: u64, + }, +} + +impl DeserializedFileArtifactSource { + fn is_fake(&self) -> bool { + matches!(self, DeserializedFileArtifactSource::Fake { .. }) + } + + fn with_entry(&self, fake_attr: FakeDataAttributes, f: F) -> Result + where + F: FnOnce(CompositeEntry<'_>) -> Result, + { + let (data, mtime_source) = match self { + DeserializedFileArtifactSource::File { path } => { + let data = std::fs::read(path) + .with_context(|| format!("failed to read {path}"))?; + // For now, always use the current time as the source. (Maybe + // change this to use the mtime on disk in the future?) + (data, MtimeSource::Now) + } + DeserializedFileArtifactSource::Fake { size } => { + (fake_attr.make_data(*size as usize), MtimeSource::Zero) + } + }; + let entry = CompositeEntry { data: &data, mtime_source }; + f(entry) + } + + fn apply_size_delta(&mut self, size_delta: i64) -> Result<()> { + match self { + DeserializedFileArtifactSource::File { .. } => { + bail!("cannot apply size delta to `file` source") + } + DeserializedFileArtifactSource::Fake { size } => { + *size = (*size).saturating_add_signed(size_delta); + Ok(()) + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum DeserializedControlPlaneZoneSource { + File { + path: Utf8PathBuf, + #[serde(skip_serializing_if = "Option::is_none")] + file_name: Option, + }, + Fake { + name: String, + #[serde(deserialize_with = "deserialize_byte_size")] + size: u64, + }, +} + +impl DeserializedControlPlaneZoneSource { + fn is_fake(&self) -> bool { + matches!(self, DeserializedControlPlaneZoneSource::Fake { .. }) + } + + fn with_name_and_entry(&self, f: F) -> Result + where + F: FnOnce(&str, CompositeEntry<'_>) -> Result, + { + let (name, data, mtime_source) = match self { + DeserializedControlPlaneZoneSource::File { path, file_name } => { + let data = std::fs::read(path) + .with_context(|| format!("failed to read {path}"))?; + let name = file_name + .as_deref() + .or_else(|| path.file_name()) + .with_context(|| { + format!("zone path missing file name: {path}") + })?; + // For now, always use the current time as the source. (Maybe + // change this to use the mtime on disk in the future?) + (name.to_owned(), data, MtimeSource::Now) + } + DeserializedControlPlaneZoneSource::Fake { name, size } => { + use flate2::{write::GzEncoder, Compression}; + use tufaceous_brand_metadata::{ + ArchiveType, LayerInfo, Metadata, + }; + + let mut tar = tar::Builder::new(GzEncoder::new( + Vec::new(), + Compression::fast(), + )); + + let metadata = Metadata::new(ArchiveType::Layer(LayerInfo { + pkg: name.clone(), + version: semver::Version::new(0, 0, 0), + })); + metadata.append_to_tar(&mut tar, 0)?; + + let mut h = tar::Header::new_ustar(); + h.set_entry_type(tar::EntryType::Regular); + h.set_path("fake")?; + h.set_mode(0o444); + h.set_size(*size); + h.set_mtime(0); + h.set_cksum(); + tar.append(&h, make_filler_text(*size as usize).as_slice())?; + + let data = tar.into_inner()?.finish()?; + (format!("{name}.tar.gz"), data, MtimeSource::Zero) + } + }; + let entry = CompositeEntry { data: &data, mtime_source }; + f(&name, entry) + } + + fn apply_size_delta(&mut self, size_delta: i64) -> Result<()> { + match self { + DeserializedControlPlaneZoneSource::File { .. } => { + bail!("cannot apply size delta to `file` source") + } + DeserializedControlPlaneZoneSource::Fake { size, .. } => { + (*size) = (*size).saturating_add_signed(size_delta); + Ok(()) + } + } + } +} +/// A change to apply to a manifest. +#[derive(Clone, Debug)] +pub enum ManifestTweak { + /// Update the system version. + SystemVersion(Version), + + /// Update the versions for this artifact. + ArtifactVersion { kind: KnownArtifactKind, version: Version }, + + /// Update the contents of this artifact (only support changing the size). + ArtifactContents { kind: KnownArtifactKind, size_delta: i64 }, +} + +fn deserialize_byte_size<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + // Attempt to deserialize the size as either a string or an integer. + + struct Visitor; + + impl serde::de::Visitor<'_> for Visitor { + type Value = u64; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + formatter + .write_str("a string representing a byte size or an integer") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + parse_size(value).map_err(|_| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(value), + &self, + ) + }) + } + + // TOML uses i64, not u64 + fn visit_i64(self, value: i64) -> Result + where + E: serde::de::Error, + { + Ok(value as u64) + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + Ok(value) + } + } + + deserializer.deserialize_any(Visitor) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Ensure that the fake manifest roundtrips after serialization and + // deserialization. + #[test] + fn fake_roundtrip() { + let manifest = DeserializedManifest::fake(); + let toml = toml::to_string(&manifest).unwrap(); + let deserialized = DeserializedManifest::from_str(&toml) + .expect("fake manifest is a valid manifest"); + assert_eq!(manifest, deserialized); + } +} diff --git a/lib/src/assemble/mod.rs b/lib/src/assemble/mod.rs new file mode 100644 index 0000000..f8b5b2f --- /dev/null +++ b/lib/src/assemble/mod.rs @@ -0,0 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +mod build; +mod manifest; + +pub use build::*; +pub use manifest::*; diff --git a/lib/src/key.rs b/lib/src/key.rs new file mode 100644 index 0000000..b3a98d0 --- /dev/null +++ b/lib/src/key.rs @@ -0,0 +1,84 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::fmt::{self, Display}; +use std::str::FromStr; + +use anyhow::{bail, Result}; +use aws_lc_rs::rand::SystemRandom; +use aws_lc_rs::signature::Ed25519KeyPair; +use base64::{engine::general_purpose::URL_SAFE, Engine}; +use tough::async_trait; +use tough::key_source::KeySource; +use tough::sign::{Sign, SignKeyPair}; + +pub(crate) fn boxed_keys(keys: Vec) -> Vec> { + keys.into_iter().map(|k| Box::new(k) as Box).collect() +} + +#[derive(Debug, Clone)] +pub enum Key { + Ed25519 { pkcs8: Vec }, +} + +impl Key { + pub fn generate_ed25519() -> Result { + let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?; + Ok(Key::Ed25519 { pkcs8: pkcs8.as_ref().to_vec() }) + } + + fn as_sign_key_pair(&self) -> Result { + match self { + Key::Ed25519 { pkcs8 } => { + Ok(SignKeyPair::ED25519(Ed25519KeyPair::from_pkcs8(pkcs8)?)) + } + } + } + + pub(crate) fn as_tuf_key(&self) -> Result { + Ok(self.as_sign_key_pair()?.tuf_key()) + } +} + +#[async_trait] +impl KeySource for Key { + async fn as_sign( + &self, + ) -> Result, Box> + { + Ok(Box::new(self.as_sign_key_pair()?)) + } + + async fn write( + &self, + _value: &str, + _key_id_hex: &str, + ) -> Result<(), Box> { + Err("cannot write key back to key source".into()) + } +} + +impl FromStr for Key { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.split_once(':') { + Some(("ed25519", base64)) => { + Ok(Key::Ed25519 { pkcs8: URL_SAFE.decode(base64)? }) + } + Some((kind, _)) => bail!("Invalid key source kind: {}", kind), + None => bail!("Invalid key source (format is `kind:data`)"), + } + } +} + +impl Display for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Key::Ed25519 { pkcs8 } => { + write!(f, "ed25519:{}", URL_SAFE.encode(pkcs8)) + } + } + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 0000000..bf6fd5e --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1,16 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +mod archive; +mod artifact; +pub mod assemble; +mod key; +mod repository; +mod root; +mod target; + +pub use archive::*; +pub use artifact::*; +pub use key::*; +pub use repository::*; diff --git a/lib/src/repository.rs b/lib/src/repository.rs new file mode 100644 index 0000000..f2426c3 --- /dev/null +++ b/lib/src/repository.rs @@ -0,0 +1,430 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::collections::BTreeSet; +use std::num::NonZeroU64; + +use anyhow::{anyhow, bail, Context, Result}; +use buf_list::BufList; +use camino::{Utf8Path, Utf8PathBuf}; +use chrono::{DateTime, Utc}; +use fs_err as fs; +use futures::TryStreamExt; +use semver::Version; +use tough::editor::signed::SignedRole; +use tough::editor::RepositoryEditor; +use tough::schema::{Root, Target}; +use tough::{ExpirationEnforcement, Repository, RepositoryLoader, TargetName}; +use tufaceous_artifact::{Artifact, ArtifactsDocument}; +use url::Url; + +use crate::key::Key; +use crate::target::TargetWriter; +use crate::{AddArtifact, ArchiveBuilder}; + +/// A TUF repository describing Omicron. +pub struct OmicronRepo { + log: slog::Logger, + repo: Repository, + repo_path: Utf8PathBuf, +} + +impl OmicronRepo { + /// Initializes a new repository at the given path, writing it to disk. + pub async fn initialize( + log: &slog::Logger, + repo_path: &Utf8Path, + system_version: Version, + keys: Vec, + expiry: DateTime, + ) -> Result { + let root = crate::root::new_root(keys.clone(), expiry).await?; + let editor = OmicronRepoEditor::initialize( + repo_path.to_owned(), + root, + system_version, + ) + .await?; + + editor + .sign_and_finish(keys, expiry) + .await + .context("error signing new repository")?; + + // In theory we "trust" the key we just used to sign this repository, + // but the code path is equivalent to `load_untrusted`. + Self::load_untrusted(log, repo_path).await + } + + /// Loads a repository from the given path. + /// + /// This method enforces expirations. To load without expiration enforcement, use + /// [`Self::load_untrusted_ignore_expiration`]. + pub async fn load_untrusted( + log: &slog::Logger, + repo_path: &Utf8Path, + ) -> Result { + Self::load_untrusted_impl(log, repo_path, ExpirationEnforcement::Safe) + .await + } + + /// Loads a repository from the given path, ignoring expiration. + /// + /// Use cases for this include: + /// + /// 1. When you're editing an existing repository and will re-sign it afterwards. + /// 2. In an environment in which time isn't available. + pub async fn load_untrusted_ignore_expiration( + log: &slog::Logger, + repo_path: &Utf8Path, + ) -> Result { + Self::load_untrusted_impl(log, repo_path, ExpirationEnforcement::Unsafe) + .await + } + + async fn load_untrusted_impl( + log: &slog::Logger, + repo_path: &Utf8Path, + exp: ExpirationEnforcement, + ) -> Result { + let log = log.new(slog::o!("component" => "OmicronRepo")); + let repo_path = repo_path.canonicalize_utf8()?; + let root_json = repo_path.join("metadata").join("1.root.json"); + let root = tokio::fs::read(&root_json) + .await + .with_context(|| format!("error reading from {root_json}"))?; + + let repo = RepositoryLoader::new( + &root, + Url::from_file_path(repo_path.join("metadata")) + .expect("the canonical path is not absolute?"), + Url::from_file_path(repo_path.join("targets")) + .expect("the canonical path is not absolute?"), + ) + .expiration_enforcement(exp) + .load() + .await?; + + Ok(Self { log, repo, repo_path }) + } + + /// Returns a canonicalized form of the repository path. + pub fn repo_path(&self) -> &Utf8Path { + &self.repo_path + } + + /// Returns the repository. + pub fn repo(&self) -> &Repository { + &self.repo + } + + /// Reads the artifacts document from the repo. + pub async fn read_artifacts(&self) -> Result { + let reader = self + .repo + .read_target(&"artifacts.json".try_into()?) + .await? + .ok_or_else(|| anyhow!("artifacts.json should be present"))?; + let buf_list = reader + .try_collect::() + .await + .context("error reading from artifacts.json")?; + serde_json::from_reader(buf_list::Cursor::new(&buf_list)) + .context("error deserializing artifacts.json") + } + + /// Archives the repository to the given path as a zip file. + /// + /// ## Why zip and not tar? + /// + /// The main reason is that zip supports random access to files and tar does + /// not. + /// + /// In principle it should be possible to read the repository out of a zip + /// file from memory, but we ran into [this + /// issue](https://github.com/awslabs/tough/pull/563) while implementing it. + /// Once that is resolved (or we write our own TUF crate) it should be + /// possible to do that. + /// + /// Regardless of this roadblock, we don't want to foreclose that option + /// forever, so this code uses zip rather than having to deal with a + /// migration in the future. + pub fn archive(&self, output_path: &Utf8Path) -> Result<()> { + let mut builder = ArchiveBuilder::new(output_path.to_owned())?; + + let metadata_dir = self.repo_path.join("metadata"); + + // Gather metadata files. + for entry in metadata_dir.read_dir_utf8().with_context(|| { + format!("error reading entries from {metadata_dir}") + })? { + let entry = + entry.context("error reading entry from {metadata_dir}")?; + let file_name = entry.file_name(); + if file_name.ends_with(".root.json") + || file_name == "timestamp.json" + || file_name.ends_with(".snapshot.json") + || file_name.ends_with(".targets.json") + { + // This is a valid metadata file. + builder.write_file( + entry.path(), + &Utf8Path::new("metadata").join(file_name), + )?; + } + } + + let targets_dir = self.repo_path.join("targets"); + + // Gather all targets. + for (name, target) in self.repo.targets().signed.targets_iter() { + let target_filename = self.target_filename(target, name); + let target_path = targets_dir.join(&target_filename); + slog::trace!(self.log, "adding {} to archive", name.resolved()); + builder.write_file( + &target_path, + &Utf8Path::new("targets").join(&target_filename), + )?; + } + + builder.finish()?; + + Ok(()) + } + + /// Converts `self` into an `OmicronRepoEditor`, which can be used to perform + /// modifications to the repository. + pub async fn into_editor(self) -> Result { + OmicronRepoEditor::new(self).await + } + + /// Prepends the target digest to the name if using consistent snapshots. Returns both the + /// digest and the filename. + /// + /// Adapted from tough's source. + fn target_filename(&self, target: &Target, name: &TargetName) -> String { + let sha256 = &target.hashes.sha256.clone().into_vec(); + if self.repo.root().signed.consistent_snapshot { + format!("{}.{}", hex::encode(sha256), name.resolved()) + } else { + name.resolved().to_owned() + } + } +} + +/// An [`OmicronRepo`] than can be edited. +/// +/// Created by [`OmicronRepo::into_editor`]. +pub struct OmicronRepoEditor { + editor: RepositoryEditor, + repo_path: Utf8PathBuf, + artifacts: ArtifactsDocument, + + // Set of `TargetName::resolved()` names for every target that existed when + // the repo was opened. We use this to ensure we don't overwrite an existing + // target when adding new artifacts. + existing_target_names: BTreeSet, +} + +impl OmicronRepoEditor { + async fn new(repo: OmicronRepo) -> Result { + let artifacts = repo.read_artifacts().await?; + + let existing_target_names = repo + .repo + .targets() + .signed + .targets_iter() + .map(|(name, _)| name.resolved().to_string()) + .collect::>(); + + let editor = RepositoryEditor::from_repo( + repo.repo_path + .join("metadata") + .join(format!("{}.root.json", repo.repo.root().signed.version)), + repo.repo, + ) + .await?; + + Ok(Self { + editor, + repo_path: repo.repo_path, + artifacts, + existing_target_names, + }) + } + + async fn initialize( + repo_path: Utf8PathBuf, + root: SignedRole, + system_version: Version, + ) -> Result { + let metadata_dir = repo_path.join("metadata"); + let targets_dir = repo_path.join("targets"); + let root_path = metadata_dir + .join(format!("{}.root.json", root.signed().signed.version)); + + fs::create_dir_all(&metadata_dir)?; + fs::create_dir_all(&targets_dir)?; + fs::write(&root_path, root.buffer())?; + + let editor = RepositoryEditor::new(&root_path).await?; + + Ok(Self { + editor, + repo_path, + artifacts: ArtifactsDocument::empty(system_version), + existing_target_names: BTreeSet::new(), + }) + } + + /// Adds an artifact to the repository. + pub fn add_artifact(&mut self, new_artifact: &AddArtifact) -> Result<()> { + let target_name = format!( + "{}-{}-{}.tar.gz", + new_artifact.kind(), + new_artifact.name(), + new_artifact.version(), + ); + + // make sure we're not overwriting an existing target (either one that + // existed when we opened the repo, or one that's been added via this + // method) + if !self.existing_target_names.insert(target_name.clone()) { + bail!( + "a target named {target_name} already exists in the repository", + ); + } + + self.artifacts.artifacts.push(Artifact { + name: new_artifact.name().to_owned(), + version: new_artifact.version().to_owned(), + kind: new_artifact.kind().clone(), + target: target_name.clone(), + }); + + let targets_dir = self.repo_path.join("targets"); + + let mut file = TargetWriter::new(&targets_dir, target_name.clone())?; + new_artifact.write_to(&mut file).with_context(|| { + format!("error writing artifact `{target_name}") + })?; + file.finish(&mut self.editor)?; + + Ok(()) + } + + /// Consumes self, signing the repository and writing out this repository to disk. + pub async fn sign_and_finish( + mut self, + keys: Vec, + expiry: DateTime, + ) -> Result<()> { + let targets_dir = self.repo_path.join("targets"); + + let mut file = TargetWriter::new(&targets_dir, "artifacts.json")?; + serde_json::to_writer_pretty(&mut file, &self.artifacts)?; + file.finish(&mut self.editor)?; + + update_versions(&mut self.editor, expiry)?; + + let signed = self + .editor + .sign(&crate::key::boxed_keys(keys)) + .await + .context("error signing keys")?; + signed + .write(self.repo_path.join("metadata")) + .await + .context("error writing repository")?; + Ok(()) + } +} + +fn update_versions( + editor: &mut RepositoryEditor, + expiry: DateTime, +) -> Result<()> { + let version = u64::try_from(Utc::now().timestamp()) + .and_then(NonZeroU64::try_from) + .expect("bad epoch"); + editor.snapshot_version(version); + editor.targets_version(version)?; + editor.timestamp_version(version); + editor.snapshot_expires(expiry); + editor.targets_expires(expiry)?; + editor.timestamp_expires(expiry); + Ok(()) +} + +#[cfg(test)] +mod tests { + use buf_list::BufList; + use camino_tempfile::Utf8TempDir; + use chrono::Days; + use dropshot::test_util::LogContext; + use dropshot::{ConfigLogging, ConfigLoggingIfExists, ConfigLoggingLevel}; + + use crate::ArtifactSource; + + use super::*; + + #[tokio::test] + async fn reject_artifacts_with_the_same_filename() { + let log_config = ConfigLogging::File { + level: ConfigLoggingLevel::Trace, + path: "UNUSED".into(), + if_exists: ConfigLoggingIfExists::Fail, + }; + let logctx = LogContext::new( + "reject_artifacts_with_the_same_filename", + &log_config, + ); + let tempdir = Utf8TempDir::new().unwrap(); + let mut repo = OmicronRepo::initialize( + &logctx.log, + tempdir.path(), + "0.0.0".parse().unwrap(), + vec![Key::generate_ed25519().unwrap()], + Utc::now() + Days::new(1), + ) + .await + .unwrap() + .into_editor() + .await + .unwrap(); + + // Targets are uniquely identified by their kind/name/version triple; + // trying to add two artifacts with identical triples should fail. + let kind = "test-kind"; + let name = "test-artifact-name"; + let version = "1.0.0"; + + repo.add_artifact(&AddArtifact::new( + kind.parse().unwrap(), + name.to_string(), + version.parse().unwrap(), + ArtifactSource::Memory(BufList::new()), + )) + .unwrap(); + + let err = repo + .add_artifact(&AddArtifact::new( + kind.parse().unwrap(), + name.to_string(), + version.parse().unwrap(), + ArtifactSource::Memory(BufList::new()), + )) + .unwrap_err() + .to_string(); + + assert!(err.contains("a target named")); + assert!(err.contains(kind)); + assert!(err.contains(name)); + assert!(err.contains(version)); + assert!(err.contains("already exists")); + + logctx.cleanup_successful(); + } +} diff --git a/lib/src/root.rs b/lib/src/root.rs new file mode 100644 index 0000000..ef446fc --- /dev/null +++ b/lib/src/root.rs @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::collections::HashMap; +use std::num::NonZeroU64; + +use anyhow::Result; +use aws_lc_rs::rand::SystemRandom; +use chrono::{DateTime, Utc}; +use tough::editor::signed::SignedRole; +use tough::schema::{KeyHolder, RoleKeys, RoleType, Root}; + +use crate::key::Key; + +pub(crate) async fn new_root( + keys: Vec, + expires: DateTime, +) -> Result> { + let mut root = Root { + spec_version: "1.0.0".to_string(), + consistent_snapshot: true, + version: NonZeroU64::new(1).unwrap(), + expires, + keys: HashMap::new(), + roles: HashMap::new(), + _extra: HashMap::new(), + }; + for key in &keys { + let key = key.as_tuf_key()?; + root.keys.insert(key.key_id()?, key); + } + for kind in [ + RoleType::Root, + RoleType::Snapshot, + RoleType::Targets, + RoleType::Timestamp, + ] { + root.roles.insert( + kind, + RoleKeys { + keyids: root.keys.keys().cloned().collect(), + threshold: NonZeroU64::new(1).unwrap(), + _extra: HashMap::new(), + }, + ); + } + + let keys = crate::key::boxed_keys(keys); + Ok(SignedRole::new( + root.clone(), + &KeyHolder::Root(root), + &keys, + &SystemRandom::new(), + ) + .await?) +} diff --git a/lib/src/target.rs b/lib/src/target.rs new file mode 100644 index 0000000..38ab89f --- /dev/null +++ b/lib/src/target.rs @@ -0,0 +1,72 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::collections::HashMap; +use std::io::Write; + +use anyhow::Result; +use camino::Utf8PathBuf; +use camino_tempfile::NamedUtf8TempFile; +use sha2::{Digest, Sha256}; +use tough::editor::RepositoryEditor; +use tough::schema::{Hashes, Target}; + +pub(crate) struct TargetWriter { + file: NamedUtf8TempFile, + targets_dir: Utf8PathBuf, + name: String, + length: u64, + hasher: Sha256, +} + +impl TargetWriter { + pub(crate) fn new( + targets_dir: impl Into, + name: impl Into, + ) -> Result { + let targets_dir = targets_dir.into(); + Ok(TargetWriter { + file: NamedUtf8TempFile::new_in(&targets_dir)?, + targets_dir, + name: name.into(), + length: 0, + hasher: Sha256::default(), + }) + } + + pub(crate) fn finish(self, editor: &mut RepositoryEditor) -> Result<()> { + let digest = self.hasher.finalize(); + self.file.persist(self.targets_dir.join(format!( + "{}.{}", + hex::encode(digest), + self.name + )))?; + editor.add_target( + self.name, + Target { + length: self.length, + hashes: Hashes { + sha256: digest.to_vec().into(), + _extra: HashMap::new(), + }, + custom: HashMap::new(), + _extra: HashMap::new(), + }, + )?; + Ok(()) + } +} + +impl Write for TargetWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let n = self.file.write(buf)?; + self.length += u64::try_from(n).unwrap(); + self.hasher.update(&buf[..n]); + Ok(n) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.file.flush() + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..ec68e8a --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,6 @@ +[toolchain] +# We choose a specific toolchain (rather than "stable") for repeatability. The +# intent is to keep this up-to-date with recently-released stable Rust. + +channel = "1.85.0" +profile = "default" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..41d1d58 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,6 @@ +# --------------------------------------------------------------------------- +# Stable features that we customize locally +# --------------------------------------------------------------------------- +max_width = 80 +use_small_heuristics = "max" +edition = "2021"