From a9f6f22d141fc76403424d76d3080ee5cad2f5cd Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 7 Oct 2026 08:57:13 -0400 Subject: [PATCH 1/3] Refine wit - schema-version -> option - digest.algorithm: string -> digest-algorithm Signed-off-by: Scott Andrews --- components/client/src/lib.rs | 145 +++++++++++++++++++---------------- wit/client.wit | 31 ++++---- 2 files changed, 94 insertions(+), 82 deletions(-) diff --git a/components/client/src/lib.rs b/components/client/src/lib.rs index f183348..f008ce8 100644 --- a/components/client/src/lib.rs +++ b/components/client/src/lib.rs @@ -11,13 +11,12 @@ use wit_bindgen::StreamReader; use crate::{ componentized::http::client::{self as http, HttpResponse}, exports::componentized::oci::client::{ - Config, Digest, ErrorCode, Guest, Instant, Manifest, + Config, Digest, DigestAlgorithm, ErrorCode, Guest, Instant, Manifest, MediaType::{self, Other}, MediaTypeSuffix, OciDescriptorV1, OciImageConfigV1, OciImageConfigV1Config, OciImageConfigV1ContentAddresses, OciImageConfigV1HistoryEntry, OciImageIndexManifestV1, OciImageIndexManifestV1Manifest, OciImageIndexManifestV1ManifestPlatform, - OciImageManifestV1, Reference, RetryAfter, SchemaVersion, WasmConfigV0, - WasmConfigV0Component, + OciImageManifestV1, Reference, RetryAfter, WasmConfigV0, WasmConfigV0Component, }, }; @@ -63,7 +62,7 @@ impl Guest for OCIClient { } = Self::get(url, vec![]).await?; match status { - 200 => Self::compute_digest("sha256", &body.collect().await), + 200 => Self::compute_digest(&DigestAlgorithm::Sha256, &body.collect().await), _ => Err(Self::decode_transport_error(body, status, headers).await), } } @@ -330,6 +329,10 @@ impl OCIClient { "invalid checksum digest format: {encoded}" ))))? } + Ok(Digest { + algorithm: DigestAlgorithm::Sha256, + encoded, + }) } "sha512" => { let re = Regex::new(r"^[a-f0-9]{128}$").unwrap(); @@ -338,16 +341,18 @@ impl OCIClient { "invalid checksum digest format: {encoded}" ))))? } + Ok(Digest { + algorithm: DigestAlgorithm::Sha512, + encoded, + }) } - _ => Err(ErrorCode::Other(Some(format!( + algorithm => Err(ErrorCode::Other(Some(format!( "unsupported digest algorithm: {algorithm}" ))))?, } - - Ok(Digest { algorithm, encoded }) } - fn compute_digest(algorithm: &str, bytes: &[u8]) -> Result { + fn compute_digest(algorithm: &DigestAlgorithm, bytes: &[u8]) -> Result { fn hash_hex(bytes: &[u8]) -> String { let mut hasher = D::new(); hasher.update(bytes); @@ -358,19 +363,20 @@ impl OCIClient { .collect() } - let encoded = match algorithm { - "sha256" => hash_hex::(bytes), - "sha512" => hash_hex::(bytes), + match algorithm { + DigestAlgorithm::Sha256 => Ok(Digest { + algorithm: algorithm.clone(), + encoded: hash_hex::(bytes), + }), + DigestAlgorithm::Sha512 => Ok(Digest { + algorithm: algorithm.clone(), + encoded: hash_hex::(bytes), + }), _ => Err(ErrorCode::DigestInvalid(format!( "unsupported algorithm: {}", algorithm - )))?, - }; - - Ok(Digest { - algorithm: algorithm.to_string(), - encoded, - }) + ))), + } } fn assert_digest(expected_digest: Digest, bytes: &[u8]) -> Result<(), ErrorCode> { @@ -584,12 +590,9 @@ impl OCIClient { fn parse_oci_image_index_v1(v: &Value, field: &str) -> Result, ErrorCode> { Self::parse_object(v, field, |v, _field| { Ok(Manifest::OciImageIndexV1(OciImageIndexManifestV1 { - schema_version: Self::required( - Self::parse_schema_version( - &v["schemaVersion"], - &format!("{field}.schemaVersion"), - ), - "schemaVersion", + schema_version: Self::parse_u8( + &v["schemaVersion"], + &format!("{field}.schemaVersion"), )?, media_type: Self::required( Self::parse_media_type(&v["mediaType"], &format!("{field}.mediaType")), @@ -716,11 +719,8 @@ impl OCIClient { fn parse_oci_image_manifest_v1(v: &Value, field: &str) -> Result, ErrorCode> { Self::parse_object(v, field, |v, _field| { Ok(Manifest::OciImageV1(OciImageManifestV1 { - schema_version: Self::required( - Self::parse_schema_version( - &v["schemaVersion"], - &format!("{field}.schemaVersion"), - ), + schema_version: Self::parse_u8( + &v["schemaVersion"], &format!("{field}.schemaVersion"), )?, media_type: Self::required( @@ -1034,16 +1034,6 @@ impl OCIClient { } } - fn parse_schema_version(v: &Value, field: &str) -> Result, ErrorCode> { - match Self::parse_u8(v, field)? { - Some(version) => match version { - 2 => Ok(Some(SchemaVersion::V2)), - _ => Ok(Some(SchemaVersion::Other(Some(version as u8)))), - }, - None => Ok(None), - } - } - fn parse_digest(v: &Value, field: &str) -> Result, ErrorCode> { match Self::parse_string(v, field)? { Some(digest) => Ok(Some(Self::digest(digest)?)), @@ -1116,9 +1106,9 @@ struct TransportError { impl From for ErrorCode { fn from(value: http::ErrorCode) -> Self { match value { - http::ErrorCode::RedirectLimitExceeded((_, count)) => Self::Other(Some(format!( - "too many redirects, stopped after {count}" - ))), + http::ErrorCode::RedirectLimitExceeded((_, count)) => { + Self::Other(Some(format!("too many redirects, stopped after {count}"))) + } http::ErrorCode::RedirectRequiresBody(_) => Self::Other(Some( "redirect requires resending the request body".to_string(), )), @@ -1146,6 +1136,16 @@ impl Display for Digest { } } +impl Display for DigestAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Sha256 => f.write_str("sha256"), + Self::Sha512 => f.write_str("sha512"), + Self::Other(algorithm) => f.write_str(algorithm), + } + } +} + impl PartialEq for Reference { fn eq(&self, other: &Self) -> bool { self.registry == other.registry @@ -1161,6 +1161,17 @@ impl PartialEq for Digest { } } +impl PartialEq for DigestAlgorithm { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (DigestAlgorithm::Sha256, DigestAlgorithm::Sha256) => true, + (DigestAlgorithm::Sha512, DigestAlgorithm::Sha512) => true, + (DigestAlgorithm::Other(this), DigestAlgorithm::Other(that)) => this == that, + _ => false, + } + } +} + impl PartialEq for ErrorCode { fn eq(&self, other: &Self) -> bool { match (self, other) { @@ -1264,16 +1275,6 @@ impl PartialEq for MediaTypeSuffix { } } -impl PartialEq for SchemaVersion { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (SchemaVersion::V2, SchemaVersion::V2) => true, - (SchemaVersion::Other(this), SchemaVersion::Other(other)) => this == other, - _ => false, - } - } -} - impl PartialEq for Manifest { fn eq(&self, other: &Self) -> bool { match (self, other) { @@ -1452,9 +1453,17 @@ mod tests { Some(tag.to_string()) } - fn make_digest(algorithm: &str, encoded: &str) -> Option { + fn make_digest_sha256(encoded: &str) -> Option { + make_digest(DigestAlgorithm::Sha256, encoded) + } + + fn make_digest_sha512(encoded: &str) -> Option { + make_digest(DigestAlgorithm::Sha512, encoded) + } + + fn make_digest(algorithm: DigestAlgorithm, encoded: &str) -> Option { Some(Digest { - algorithm: algorithm.to_string(), + algorithm, encoded: encoded.to_string(), }) } @@ -1608,7 +1617,7 @@ mod tests { "index.docker.io", "foo/bar", None, - make_digest("sha256", sha256), + make_digest_sha256(sha256), ), description: "valid sha256 digest", }, @@ -1618,7 +1627,7 @@ mod tests { "index.docker.io", "foo/bar", None, - make_digest("sha512", sha512), + make_digest_sha512(sha512), ), description: "valid sha512 digest", }, @@ -1628,7 +1637,7 @@ mod tests { "index.docker.io", "foo/bar", make_tag("v1"), - make_digest("sha256", sha256), + make_digest_sha256(sha256), ), description: "valid tag and digest", }, @@ -1667,7 +1676,7 @@ mod tests { "index.docker.io", "foo/bar", make_tag("v1"), - make_digest("sha256", sha256), + make_digest_sha256(sha256), ), description: "when both a tag and a digest are present, the digest wins and the tag is dropped", }, @@ -1763,7 +1772,7 @@ mod tests { artifact_type: None, config: OciDescriptorV1 { annotations: None, - digest: Digest { algorithm: "sha256".to_string(), encoded: "80d83bbdaa82cff96584c99217c29fd17bc7e5f0424c0cb26aa3831ea18132b4".to_string() }, + digest: Digest { algorithm: DigestAlgorithm::Sha256, encoded: "80d83bbdaa82cff96584c99217c29fd17bc7e5f0424c0cb26aa3831ea18132b4".to_string() }, media_type: MediaType::ApplicationVndWasmConfigV0(MediaTypeSuffix::Json), size: 345, urls: None, @@ -1774,7 +1783,7 @@ mod tests { annotations: Some(BTreeMap::from([ ("org.opencontainers.image.title".to_string(), "client.wasm".to_string()), ])), - digest: Digest { algorithm: "sha256".to_string(), encoded: "ee7ff5c9588e997b4a54b6d351b52a5ca4f6980377f59e48c778f48a23b483db".to_string() }, + digest: Digest { algorithm: DigestAlgorithm::Sha256, encoded: "ee7ff5c9588e997b4a54b6d351b52a5ca4f6980377f59e48c778f48a23b483db".to_string() }, media_type: MediaType::ApplicationWasm, size: 1894585, urls: None, @@ -1782,7 +1791,7 @@ mod tests { data: None, }], media_type: MediaType::ApplicationVndOciImageManifestV1(MediaTypeSuffix::Json), - schema_version: SchemaVersion::V2, + schema_version: Some(2 as u8), subject: None, })), ); @@ -1815,7 +1824,7 @@ mod tests { ] }), "").unwrap(), Some(Manifest::OciImageIndexV1(OciImageIndexManifestV1 { - schema_version: SchemaVersion::V2, + schema_version: Some(2 as u8), media_type: MediaType::ApplicationVndOciImageIndexV1(MediaTypeSuffix::Json), artifact_type: None, manifests: vec![ @@ -1831,7 +1840,7 @@ mod tests { variant:None, }), subject:None, - digest: Digest { algorithm: "sha256".to_string(), encoded: "9434033b4008b51c0c9270dda9315ea4229901fee28a7980085091e9fd4b62b8".to_string() }, + digest: Digest { algorithm: DigestAlgorithm::Sha256, encoded: "9434033b4008b51c0c9270dda9315ea4229901fee28a7980085091e9fd4b62b8".to_string() }, urls: None, data: None, artifact_type: Some(MediaType::Other("application/vnd.docker.container.image.v1+json".to_string())) @@ -1846,7 +1855,7 @@ mod tests { os_features:None,os_version:None,variant:None, }), subject:None, - digest: Digest { algorithm: "sha256".to_string(), encoded: "eac7a2bcae76b2bc5b5fed23033ffba56283462e315c2035b6ab5b2c8c80bd34".to_string() }, + digest: Digest { algorithm: DigestAlgorithm::Sha256, encoded: "eac7a2bcae76b2bc5b5fed23033ffba56283462e315c2035b6ab5b2c8c80bd34".to_string() }, urls: None, data: None, artifact_type: Some(MediaType::Other("application/vnd.docker.container.image.v1+json".to_string())), @@ -1948,9 +1957,9 @@ mod tests { rootfs: OciImageConfigV1ContentAddresses { type_: "layers".to_string(), diff_ids: vec![ - Digest{algorithm:"sha256".to_string(), encoded:"458136df58646e7146e8240b685e4e6bfffa019ba10d3c221ff59b3928f54d8c".to_string()}, - Digest{algorithm:"sha256".to_string(), encoded:"ffe56a1c5f3878e9b5f803842adb9e2ce81584b6bd027e8599582aefe14a975b".to_string()}, - Digest{algorithm:"sha256".to_string(), encoded:"38217aaa0c148dcb7f3af90a384d30ccb4a7736a9f15b75a5d6a9f28c586964b".to_string()}, + Digest{algorithm:DigestAlgorithm::Sha256, encoded:"458136df58646e7146e8240b685e4e6bfffa019ba10d3c221ff59b3928f54d8c".to_string()}, + Digest{algorithm:DigestAlgorithm::Sha256, encoded:"ffe56a1c5f3878e9b5f803842adb9e2ce81584b6bd027e8599582aefe14a975b".to_string()}, + Digest{algorithm:DigestAlgorithm::Sha256, encoded:"38217aaa0c148dcb7f3af90a384d30ccb4a7736a9f15b75a5d6a9f28c586964b".to_string()}, ] }, config: Some(OciImageConfigV1Config{ @@ -2015,7 +2024,7 @@ mod tests { architecture: "wasm".to_string(), os: "wasip2".to_string(), layer_digests: vec![Digest { - algorithm: "sha256".to_string(), + algorithm: DigestAlgorithm::Sha256, encoded: "ee7ff5c9588e997b4a54b6d351b52a5ca4f6980377f59e48c778f48a23b483db" .to_string() }], diff --git a/wit/client.wit b/wit/client.wit index 5ecd069..cdf5a74 100644 --- a/wit/client.wit +++ b/wit/client.wit @@ -45,15 +45,6 @@ interface client { delay-seconds(u32), } - /// schmea version for OCI manifests, should always be 2 - @since(version = 0.1.0-dev) - variant schema-version { - /// literal 2. Prefixed with a 'v' to bypass wit naming constrains - v2, - /// a non-2 value, the parsed manifest may be incomplete or otherwise inaccurate. - other(option), - } - /// common media types for oci artifacts @since(version = 0.1.0-dev) variant media-type { @@ -129,8 +120,8 @@ interface client { /// https://github.com/opencontainers/image-spec/blob/main/manifest.md @since(version = 0.1.0-dev) record oci-image-manifest-v1 { - /// manifest schema version - schema-version: schema-version, + /// manifest schema version, must be 2 + schema-version: option, /// must contain the media type 'application/vnd.oci.image.manifest.v1+json'. media-type: media-type, /// type of an artifact when the manifest is used for an artifact @@ -149,8 +140,8 @@ interface client { /// https://github.com/opencontainers/image-spec/blob/main/image-index.md @since(version = 0.1.0-dev) record oci-image-index-manifest-v1 { - /// manifest schema version - schema-version: schema-version, + /// manifest schema version, must be 2 + schema-version: option, /// must contain the media type 'application/vnd.oci.image.index.v1+json'. media-type: media-type, /// type of an artifact when the manifest is used for an artifact @@ -340,11 +331,23 @@ interface client { @since(version = 0.1.0-dev) record digest { /// hashing algorithm. `sha256` is strongly recommended - algorithm: string, + algorithm: digest-algorithm, /// digest value. Format is defined by the algorithm. encoded: string, } + /// Content digest algorithm + variant digest-algorithm { + /// 256-bit secure hash + /// https://csrc.nist.gov/pubs/fips/180-4/upd1/final + sha256, + /// 512-bit secure hash + /// https://csrc.nist.gov/pubs/fips/180-4/upd1/final + sha512, + /// other algorithms may not be supported by the registry or client + other(string), + } + /// Parse a string image reference like `ubuntu` or `ghcr.io/componentized/oci:1.0.0` for use by the other methods. @since(version = 0.1.0-dev) parse-reference: func(reference: string) -> result; From 08425d0178542ad716836a9da5c852dd00a4a8da Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 7 Oct 2026 16:56:42 -0400 Subject: [PATCH 2/3] get-blob now returns a stream Signed-off-by: Scott Andrews --- Cargo.lock | 2459 ++++++++++++++++++++++---- Cargo.toml | 5 + components/client/Cargo.toml | 6 +- components/client/src/lib.rs | 708 +++++--- components/client/tests/blobs.rs | 215 +++ components/client/tests/manifests.rs | 206 +++ crates/test-harness/Cargo.toml | 12 + crates/test-harness/src/lib.rs | 451 +++++ wit/client.wit | 25 +- 9 files changed, 3423 insertions(+), 664 deletions(-) create mode 100644 components/client/tests/blobs.rs create mode 100644 components/client/tests/manifests.rs create mode 100644 crates/test-harness/Cargo.toml create mode 100644 crates/test-harness/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b150c33..0868ee6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + [[package]] name = "aho-corasick" version = "1.1.5" @@ -11,6 +20,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -32,12 +53,44 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.12.1" @@ -52,6 +105,33 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cap-primitives" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] [[package]] name = "cc" @@ -60,6 +140,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -69,6 +151,17 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + [[package]] name = "chrono" version = "0.5.0-alpha.1" @@ -76,7 +169,7 @@ source = "git+https://github.com/chronotope/chrono.git?branch=0.5.x#16c45fa8eab2 dependencies = [ "android-tzdata", "iana-time-zone", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -88,11 +181,23 @@ dependencies = [ "regex", "serde", "serde_json", - "sha2", + "sha2 0.11.0", + "test-harness", + "tokio", "url", + "wasmtime", "wit-bindgen", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.21", +] + [[package]] name = "const-oid" version = "0.10.2" @@ -106,655 +211,2098 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "cpufeatures" -version = "0.3.1" +name = "core_detect" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" dependencies = [ - "libc", + "cfg-if", ] [[package]] -name = "crypto-common" -version = "0.2.2" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "hybrid-array", + "libc", ] [[package]] -name = "digest" -version = "0.11.3" +name = "cpufeatures" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "libc", ] [[package]] -name = "displaydoc" -version = "0.2.7" +name = "cranelift-assembler-x64" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +checksum = "658e8623bea025d142fb11c299b8c6572ffe9fd663b52887c4dc124735cc130b" dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", + "cranelift-assembler-x64-meta", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "cranelift-assembler-x64-meta" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "a4b12b8c55c31c3d8598c2de9a657c3b978f905f6886c43e02216e0b15fe45a3" +dependencies = [ + "cranelift-srcgen", +] [[package]] -name = "find-msvc-tools" -version = "0.1.13" +name = "cranelift-bforest" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" +checksum = "894b984f82c5a36f05fdd4973fbf4d37d69cd5cdf8c16e8ffe3af4a9cdfe193c" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] [[package]] -name = "foldhash" -version = "0.2.0" +name = "cranelift-bitset" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "3a1e4c34abe94193ada09f821f474be87ac8a1fbbacd87058571ef04c1bf4009" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "cranelift-codegen" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "63826cef0bf70e61d56cc930aa4ee423638f66dc3cfc10d683e73138a6d177b8" dependencies = [ - "percent-encoding", + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", ] [[package]] -name = "futures-core" -version = "0.3.34" +name = "cranelift-codegen-meta" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +checksum = "26de7849112293681a19f7525c689e1d73dc6eb2687355170fb7458c4da8d353" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] [[package]] -name = "futures-task" -version = "0.3.34" +name = "cranelift-codegen-shared" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +checksum = "df315a8b1cfd6e620971e3d4249bd2c4528167717e8f48a6317a87c55891bcb8" [[package]] -name = "futures-util" -version = "0.3.34" +name = "cranelift-control" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +checksum = "2c3fff7610fb17f228f16598e1a044db79a12806ece7496ff2c69cf1a27a79a5" dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", + "arbitrary", ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "cranelift-entity" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "6158b1ac381792ad11e670abbcd15db8b1737e84bfa8a7f2bd7eb41388a92530" dependencies = [ - "foldhash", + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", ] [[package]] -name = "heck" -version = "0.5.0" +name = "cranelift-frontend" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "099361a0919067086dcde624a281ce719834cebf8544d6c24646f5e924107234" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] [[package]] -name = "httpdate" -version = "1.0.3" +name = "cranelift-isle" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "2a2f30ea3fd5a11f7d3473976d3b62b5b3f2b62fa943c85e0331d88bf505082c" [[package]] -name = "hybrid-array" -version = "0.4.15" +name = "cranelift-native" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +checksum = "397508a8d817dc74019603ac5a4c4e40b4df627e5aed4f2f8088345d4c01d401" dependencies = [ - "typenum", + "cranelift-codegen", + "libc", + "target-lexicon", ] [[package]] -name = "iana-time-zone" -version = "0.1.65" +name = "cranelift-srcgen" +version = "0.136.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] +checksum = "b4944ef05513e1c1a85a945cf8f29d95ddea8bcbb19e0bfd4e71063450967420" [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "crc32fast" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ - "cc", + "cfg-if", ] [[package]] -name = "icu_collections" -version = "2.3.0" +name = "crossbeam-deque" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", + "crossbeam-epoch", + "crossbeam-utils", ] [[package]] -name = "icu_locale_core" -version = "2.3.0" +name = "crossbeam-epoch" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "crossbeam-utils", ] [[package]] -name = "icu_normalizer" -version = "2.3.0" +name = "crossbeam-utils" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] -name = "icu_normalizer_data" -version = "2.3.0" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] [[package]] -name = "icu_properties" -version = "2.3.0" +name = "crypto-common" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", + "hybrid-array", ] [[package]] -name = "icu_properties_data" -version = "2.3.0" +name = "debugid" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] [[package]] -name = "icu_provider" -version = "2.3.1" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "block-buffer 0.10.4", + "crypto-common 0.1.7", ] [[package]] -name = "id-arena" -version = "2.3.0" +name = "digest" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] [[package]] -name = "idna" -version = "1.1.0" +name = "directories-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", + "cfg-if", + "dirs-sys-next", ] [[package]] -name = "idna_adapter" -version = "1.2.2" +name = "dirs-sys-next" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ - "icu_normalizer", - "icu_properties", + "libc", + "redox_users", + "winapi", ] [[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ - "equivalent", - "hashbrown", + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.6", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b66b7448cb92d709f2924de98b5ba5b5ce4ea08e6d6b9c3b403fcd88d122ca2" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185b9b4cd91f85110f48f8c0314757dfb2bb48c40f5d91055ea666cbbcbf317b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", "serde", "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "test-harness" +version = "0.1.0" +dependencies = [ + "sha2 0.11.0", + "tokio", + "wasmtime", + "wasmtime-wasi", +] + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[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.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "tokio-macros" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] [[package]] -name = "js-sys" -version = "0.3.105" +name = "toml" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] [[package]] -name = "libc" -version = "0.2.189" +name = "toml_parser" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] [[package]] -name = "litemap" -version = "0.8.3" +name = "toml_writer" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] -name = "log" -version = "0.4.33" +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] [[package]] -name = "macro-string" -version = "0.3.0" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 2.0.119", ] [[package]] -name = "memchr" -version = "2.8.3" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] [[package]] -name = "once_cell" -version = "1.21.4" +name = "typenum" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "unicode-width" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] -name = "potential_utf" -version = "0.1.6" +name = "url" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ - "zerovec", + "form_urlencoded", + "idna", + "percent-encoding", + "serde", ] [[package]] -name = "prettyplease" -version = "0.3.0" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", "proc-macro2", + "quote", "syn 3.0.6", + "wasm-bindgen-shared", ] [[package]] -name = "proc-macro2" -version = "1.0.107" +name = "wasm-bindgen-shared" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] [[package]] -name = "quote" -version = "1.0.47" +name = "wasm-compose" +version = "0.258.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "27dc71f96d21a8b9153da9005c603f4f20d5781b7e6d2675ea0ca5def8713b75" dependencies = [ - "proc-macro2", + "anyhow", + "heck", + "indexmap", + "log", + "petgraph", + "smallvec", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wat", ] [[package]] -name = "regex" -version = "1.13.1" +name = "wasm-encoder" +version = "0.258.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +checksum = "f64765e43c000fae008a48db8b288e175e237fc9ce20a26586836878c1bee89f" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "leb128fmt", + "wasmparser 0.258.3", ] [[package]] -name = "regex-automata" -version = "0.4.18" +name = "wasm-encoder" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "leb128fmt", + "wasmparser 0.259.0", ] [[package]] -name = "regex-syntax" -version = "0.8.11" +name = "wasm-encoder" +version = "0.261.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +checksum = "2608e8bb6d67fd68f5a8d0eb1363d6e7bcbc1f8ded5a0bd3a1e382462b876b22" +dependencies = [ + "leb128fmt", + "wasmparser 0.261.0", +] [[package]] -name = "rustversion" -version = "1.0.23" +name = "wasm-metadata" +version = "0.258.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "47e10808eb27efde692f1b1706089fe19296728bf872291f5b4e0d50fba88f2f" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", +] [[package]] -name = "semver" -version = "1.0.28" +name = "wasm-metadata" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +checksum = "4e7bf119eb01f4a246864c10bd87c8f26c566350abb25aa4d5273c415bbab338" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.259.0", + "wasmparser 0.259.0", +] [[package]] -name = "serde" -version = "1.0.229" +name = "wasmparser" +version = "0.258.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +checksum = "fe9269dd817e0e7021bf534ccd9761bc2d64778b6708e41228b8439d75eaa0b1" dependencies = [ - "serde_core", + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f20f20e44f7e8aeb6744823ea9d869ede51e51be4fdaedede2852282e54d2d8" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a457ba8670a3db3d357c1aeac3ed4fe378a76054a221789f0df0ea507e292b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasmtime" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1801b9c9f91e16e352ed77656824e7c4556a7538dbfc1a50b37243f0e12ea045" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "bytes", + "cc", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wat", + "windows-sys 0.61.2", + "wit-parser 0.258.3", +] + +[[package]] +name = "wasmtime-environ" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f397969b8daf208e45120036be74956e99364ce916d5b5d67f9e9f0fe40e5477" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", "serde_derive", + "sha2 0.10.9", + "smallvec", + "target-lexicon", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", ] [[package]] -name = "serde_core" -version = "1.0.229" +name = "wasmtime-internal-cache" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +checksum = "fdf564a08f96acec55a335e3185d1df98ba3feee47e178f5bc2429e723d30d0f" dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix", + "serde", "serde_derive", + "sha2 0.10.9", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", ] [[package]] -name = "serde_derive" -version = "1.0.229" +name = "wasmtime-internal-component-macro" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "f47c428b868b59d51a9f77e24bff02cc321f8ec0faeba5c40f7e4c1cddd43a64" dependencies = [ + "anyhow", "proc-macro2", "quote", - "syn 3.0.6", + "syn 2.0.119", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.258.3", ] [[package]] -name = "serde_json" -version = "1.0.151" +name = "wasmtime-internal-component-util" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fe14bd5ea6444a09f6baa3865559c93c91158930e7fe6d127429a9372f6165" + +[[package]] +name = "wasmtime-internal-core" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "2053fd1d90080e502ca4ea39d86c3528587a40220b9572a874c8b742c219a00c" dependencies = [ - "itoa", - "memchr", + "anyhow", + "hashbrown 0.17.1", + "libm", "serde", - "serde_core", - "zmij", ] [[package]] -name = "sha2" -version = "0.11.0" +name = "wasmtime-internal-cranelift" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "15e96d6c55401e68e40e3f74078ec96f92fe9b8a7e90e885fb87265e69eea559" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.21", + "wasmparser 0.258.3", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "slab" -version = "0.4.12" +name = "wasmtime-internal-fiber" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "1b913875bab1b78fada489e55d5e0a10b53286fb4bb09f5657212f549c9af36c" +dependencies = [ + "cc", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] [[package]] -name = "smallvec" -version = "1.16.1" +name = "wasmtime-internal-jit-debug" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +checksum = "0356a1a09dee1239904e09e962456d9f6edfa5da61c5e3e1b0e4669710f48fba" +dependencies = [ + "cc", + "object", + "rustix", + "wasmtime-internal-versioned-export-macros", +] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "wasmtime-internal-jit-icache-coherence" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "5dead9a2d7b5d835698a2e1a32006e7d0b5711f4697b24bab1564bb2488e3d7a" +dependencies = [ + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] [[package]] -name = "syn" -version = "2.0.119" +name = "wasmtime-internal-unwinder" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "69ffac8fc893de2958b263d9d7274cf4c531ba9ff582558348d4e28d97b8a0f2" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", ] [[package]] -name = "syn" -version = "3.0.6" +name = "wasmtime-internal-versioned-export-macros" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "9a20f0dcb6c6a006363d3f85b5c633675730487b0dc23726b2d40495872407fb" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.119", ] [[package]] -name = "synstructure" -version = "0.14.0" +name = "wasmtime-internal-wit-bindgen" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +checksum = "80758f73f5f5daacd8a6d1a8513336a4f9a58d26c4f1c73eb80502cb143e2626" dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-component 0.258.3", + "wit-parser 0.258.3", ] [[package]] -name = "tinystr" -version = "0.8.4" +name = "wasmtime-wasi" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "bac947c8478d8ea84ef4120d04f984012a514e493fe16fa5bb72906d7e55addb" dependencies = [ - "displaydoc", - "zerovec", + "async-trait", + "bitflags", + "bytes", + "cap-primitives", + "futures", + "rand", + "rustix", + "rustix-linux-procfs", + "thiserror 2.0.21", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", + "winx", ] [[package]] -name = "typenum" -version = "1.20.1" +name = "wasmtime-wasi-io" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +checksum = "e65d70efbe70631acbb4c6cf46eea543c75908f01e2fd09550e5a89631693304" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", +] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "wast" +version = "35.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] [[package]] -name = "url" -version = "2.5.8" +name = "wast" +version = "261.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "776443145731a4062e5b0d392892a2005909b6ab72d9fdc3cad53dd1a714e44a" dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.261.0", ] [[package]] -name = "utf8_iter" -version = "1.0.4" +name = "wat" +version = "1.261.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "a7b4d1a49ea73a8f3326e74e3a05db667001b16bd1035ed3356fc1a0ed05ca7f" +dependencies = [ + "wast 261.0.0", +] [[package]] -name = "wasm-bindgen" -version = "0.2.128" +name = "wiggle" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +checksum = "7f7c3a2e8ec4789bb82e4bf04b0dba44b02ce05beba6cbc6eb992b8f0b93c9cd" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "bitflags", + "thiserror 2.0.21", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.128" +name = "wiggle-generate" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +checksum = "761bf518bf5972dd0ddb8c3c693b6a52ed826e25d10bfae89b1368327c86030d" dependencies = [ + "heck", + "proc-macro2", "quote", - "wasm-bindgen-macro-support", + "syn 2.0.119", + "wasmtime-environ", + "witx", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.128" +name = "wiggle-macro" +version = "49.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +checksum = "0bdaa435787bc4bb662020aea37538745fc60b4629d7f37864a09195cec0e1eb" dependencies = [ - "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", - "wasm-bindgen-shared", + "syn 2.0.119", + "wiggle-generate", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.128" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "unicode-ident", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "wasm-encoder" -version = "0.259.0" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" -dependencies = [ - "leb128fmt", - "wasmparser", -] +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "wasm-metadata" -version = "0.259.0" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7bf119eb01f4a246864c10bd87c8f26c566350abb25aa4d5273c415bbab338" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", + "windows-sys 0.61.2", ] [[package]] -name = "wasmparser" -version = "0.259.0" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" -dependencies = [ - "bitflags", - "hashbrown", - "indexmap", - "semver", -] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" @@ -815,20 +2363,64 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -837,48 +2429,112 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.62.0" @@ -886,6 +2542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53cb4b5556c3a791e86838ea287782bdafa704d55b0e68b5b81a3a16b9ea5f4b" dependencies = [ "bitflags", + "futures", "wit-bindgen-rust-macro", ] @@ -897,7 +2554,7 @@ checksum = "72ad22a37ecbc0e1fdca34d2b670ba41368cb0920735643e00fdf39a16ee5431" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.259.0", ] [[package]] @@ -911,9 +2568,9 @@ dependencies = [ "indexmap", "prettyplease", "syn 3.0.6", - "wasm-metadata", + "wasm-metadata 0.259.0", "wit-bindgen-core", - "wit-component", + "wit-component 0.259.0", ] [[package]] @@ -932,6 +2589,25 @@ dependencies = [ "wit-bindgen-rust", ] +[[package]] +name = "wit-component" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fca88950af0c1f42bc77e17315e48798b8d68340171b6d204f62bc19a878a6" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.258.3", + "wasm-metadata 0.258.3", + "wasmparser 0.258.3", + "wit-parser 0.258.3", +] + [[package]] name = "wit-component" version = "0.259.0" @@ -945,10 +2621,29 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "wasm-encoder 0.259.0", + "wasm-metadata 0.259.0", + "wasmparser 0.259.0", + "wit-parser 0.259.0", +] + +[[package]] +name = "wit-parser" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2127663dcf3ee16614fb0e3c33d5c6c66fec9dad317a8b0fdd3869487a533" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.258.3", ] [[package]] @@ -958,7 +2653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6390f4f02493ce678d509c859cf1698e38929fb75c8012a6ee2f7d6b6cb66cd7" dependencies = [ "anyhow", - "hashbrown", + "hashbrown 0.17.1", "id-arena", "indexmap", "log", @@ -967,7 +2662,19 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser", + "wasmparser 0.259.0", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", ] [[package]] @@ -1058,3 +2765,31 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.0+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 3411f01..a8d8a46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "components/client", + "crates/test-harness", ] [workspace.dependencies] @@ -11,5 +12,9 @@ regex = "1.10" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.11" +test-harness = { path = "./crates/test-harness" } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] } url = "2.5" +wasmtime = { version = "49", features = ["component-model-async"] } +wasmtime-wasi = { version = "49", features = ["p3"] } wit-bindgen = "0.62.0" diff --git a/components/client/Cargo.toml b/components/client/Cargo.toml index 17cb7e5..ac05b95 100644 --- a/components/client/Cargo.toml +++ b/components/client/Cargo.toml @@ -15,5 +15,9 @@ serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } url = { workspace = true } -wit-bindgen = { workspace = true } +wit-bindgen = { workspace = true, features = ["async-spawn"] } +[dev-dependencies] +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/client/src/lib.rs b/components/client/src/lib.rs index f008ce8..6b6add1 100644 --- a/components/client/src/lib.rs +++ b/components/client/src/lib.rs @@ -5,8 +5,9 @@ use std::{collections::BTreeMap, fmt::Display, time::UNIX_EPOCH}; use regex::Regex; use serde::Deserialize; use serde_json::Value; +use sha2::Digest as _; use url::Url; -use wit_bindgen::StreamReader; +use wit_bindgen::{FutureReader, StreamReader, StreamResult}; use crate::{ componentized::http::client::{self as http, HttpResponse}, @@ -22,6 +23,14 @@ use crate::{ pub(crate) struct OCIClient; +/// the size of each read from the registry while streaming a blob +const BLOB_CHUNK_SIZE: usize = 64 * 1024; + +/// the manifest media types the client accepts, a registry may serve a different manifest for a +/// tag depending on the media types accepted +const MANIFEST_ACCEPT: &str = + "application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json"; + impl Guest for OCIClient { #[allow(async_fn_in_trait)] fn parse_reference(reference: String) -> Result { @@ -41,60 +50,58 @@ impl Guest for OCIClient { #[allow(async_fn_in_trait)] async fn resolve_digest(reference: Reference) -> Result { - let Reference { - registry, - repository, - tag, - digest, - } = reference; - + let (url, digest) = Self::manifest_url(reference); if let Some(digest) = digest { return Ok(digest); } + let headers = vec![("Accept".to_string(), MANIFEST_ACCEPT.to_string())]; - let tag = tag.unwrap_or("latest".to_string()); - let url = format!("https://{registry}/v2/{repository}/manifests/{tag}"); - let http::HttpResponse { - status, - headers, - body, - .. - } = Self::get(url, vec![]).await?; + // the same manifest `get-manifest` gets for the tag + let bytes = Transport::fetch(url, headers, None) + .await? + .collect() + .await?; - match status { - 200 => Self::compute_digest(&DigestAlgorithm::Sha256, &body.collect().await), - _ => Err(Self::decode_transport_error(body, status, headers).await), - } + let mut hasher = DigestHasher::new(&DigestAlgorithm::Sha256)?; + hasher.update(&bytes); + Ok(hasher.finalize()) } #[allow(async_fn_in_trait)] - async fn get_blob(reference: Reference) -> Result, ErrorCode> { - let Reference { - registry, - repository, - tag: _, - digest, - } = reference; - let digest = match digest { - Some(digest) => digest, - None => Err(ErrorCode::BlobUnknown("digest required".to_string()))?, - }; - let url = format!("https://{registry}/v2/{repository}/blobs/{digest}"); - let http::HttpResponse { - status, - headers, - body, - .. - } = Self::get(url, vec![]).await?; - - let raw = match status { - 200 => Ok(body.collect().await), - _ => Err(Self::decode_transport_error(body, status, headers).await), - }?; - - Self::assert_digest(digest, &raw)?; + async fn get_blob( + reference: Reference, + ) -> Result<(StreamReader, FutureReader>), ErrorCode> { + let (url, digest) = Self::blob_url(reference)?; + let mut blob = Transport::fetch(url, vec![], Some(digest)).await?; + + let (mut content_tx, content_rx) = wit_stream::new(); + // written when the task ends without verifying the content, e.g. the caller dropped the + // stream before reading all of the content + let (verified_tx, verified_rx) = wit_future::new(|| { + Err(ErrorCode::DigestInvalid( + "blob content was not verified, the stream ended early".to_string(), + )) + }); + wit_bindgen::spawn_local(async move { + let verified = loop { + match blob.next().await { + Ok(Some(chunk)) => { + if !content_tx.write_all(chunk).await.is_empty() { + // the caller dropped the stream + return; + } + } + Ok(None) => break Ok(()), + Err(err) => break Err(err), + } + }; + // the stream ends before the future resolves, a caller reads to the end of the + // stream and then awaits the future + drop(content_tx); + verified_tx.write(verified).await.ok(); + }); - Ok(raw) + Ok((content_rx, verified_rx)) } #[allow(async_fn_in_trait)] @@ -102,7 +109,12 @@ impl Guest for OCIClient { reference: Reference, default_media_type: Option, ) -> Result { - let blob = Self::get_blob(reference).await?; + // configs are small, buffered to decode + let (url, digest) = Self::blob_url(reference)?; + let blob = Transport::fetch(url, vec![], Some(digest)) + .await? + .collect() + .await?; Self::required( Self::parse_config( @@ -116,37 +128,13 @@ impl Guest for OCIClient { #[allow(async_fn_in_trait)] async fn get_manifest(reference: Reference) -> Result { - let Reference { - registry, - repository, - tag, - digest, - } = reference; - let version = match digest.clone() { - Some(digest) => digest.to_string(), - None => match tag { - Some(tag) => tag, - None => "latest".to_string(), - }, - }; - let url = format!("https://{registry}/v2/{repository}/manifests/{version}"); - let http::HttpResponse { - status, - headers, - body, - .. - } = Self::get(url, vec![ - ("Accept".to_string(), "application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json".to_string()) - ]).await?; - - let raw = match status { - 200 => body.collect().await, - _ => Err(Self::decode_transport_error(body, status, headers).await)?, - }; - if let Some(digest) = digest { - // check if manifest was requested by digest, ignore if requested by tag - Self::assert_digest(digest, &raw)? - } + let (url, digest) = Self::manifest_url(reference); + let headers = vec![("Accept".to_string(), MANIFEST_ACCEPT.to_string())]; + // a manifest requested by tag has no digest to verify + let raw = Transport::fetch(url, headers, digest) + .await? + .collect() + .await?; Self::required( Self::parse_manifest(&serde_json::from_slice(&raw)?, "$manifest"), "$manifest", @@ -155,116 +143,37 @@ impl Guest for OCIClient { } impl OCIClient { - async fn get( - url: String, - mut headers: Vec<(String, String)>, - ) -> Result { - let response = http::get(url.clone(), headers.clone(), None).await?; - if response.status != 401 { - return Ok(response); - } - - let challenge = response - .headers - .iter() - .find(|(k, _)| k.eq_ignore_ascii_case("www-authenticate")) - .and_then(|(_, v)| Self::parse_www_authenticate(v)); - let Some((scheme, params)) = challenge else { - return Ok(response); + /// The url of a blob in the registry, and its digest. The reference must be digested. + fn blob_url(reference: Reference) -> Result<(String, Digest), ErrorCode> { + let Reference { + registry, + repository, + tag: _, + digest, + } = reference; + let digest = match digest { + Some(digest) => digest, + None => Err(ErrorCode::BlobUnknown("digest required".to_string()))?, }; - if !scheme.eq_ignore_ascii_case("bearer") { - // other schemes (e.g. basic) require credentials, which are not supported yet - return Ok(response); - } - - // https://distribution.github.io/distribution/spec/auth/token/ - let token = Self::fetch_token(¶ms).await?; - headers.retain(|(k, _)| !k.eq_ignore_ascii_case("authorization")); - headers.push(("Authorization".to_string(), format!("Bearer {token}"))); - - // a second 401 is returned to the caller and decoded as a transport error - Ok(http::get(url, headers, None).await?) - } - - async fn fetch_token(params: &BTreeMap) -> Result { - let realm = params.get("realm").ok_or_else(|| { - ErrorCode::Unauthorized("bearer challenge is missing realm".to_string()) - })?; - let mut url = Url::parse(realm) - .map_err(|e| ErrorCode::Unauthorized(format!("invalid token realm {realm}: {e}")))?; - { - let mut query = url.query_pairs_mut(); - for key in ["service", "scope"] { - if let Some(value) = params.get(key) { - query.append_pair(key, value); - } - } - } - - let http::HttpResponse { status, body, .. } = - http::get(url.to_string(), vec![], None).await?; - let body = body.collect().await; - if status != 200 { - return Err(ErrorCode::Unauthorized(format!( - "token request to {realm} failed with status {status}" - ))); - } - - let TokenResponse { - token, - access_token, - } = serde_json::from_slice(&body)?; - token.or(access_token).ok_or_else(|| { - ErrorCode::Unauthorized(format!("token response from {realm} is missing token")) - }) + let url = format!("https://{registry}/v2/{repository}/blobs/{digest}"); + Ok((url, digest)) } - /// Parses a single `WWW-Authenticate` challenge into its scheme and - /// lower-cased auth-params, e.g. - /// `Bearer realm="https://auth.example/token",service="example",scope="repository:foo:pull"` - fn parse_www_authenticate(value: &str) -> Option<(String, BTreeMap)> { - let value = value.trim(); - let (scheme, rest) = value.split_once(char::is_whitespace).unwrap_or((value, "")); - if scheme.is_empty() { - return None; - } - - let mut params = BTreeMap::new(); - let mut chars = rest.chars().peekable(); - loop { - while chars.next_if(|c| c.is_whitespace() || *c == ',').is_some() {} - if chars.peek().is_none() { - break; - } - - let mut key = String::new(); - while let Some(c) = chars.next_if(|c| *c != '=' && *c != ',') { - key.push(c); - } - if chars.next_if_eq(&'=').is_none() { - // token68 or malformed param, not used by registries - continue; - } - while chars.next_if(|c| c.is_whitespace()).is_some() {} - - let mut val = String::new(); - if chars.next_if_eq(&'"').is_some() { - while let Some(c) = chars.next() { - match c { - '\\' => val.extend(chars.next()), - '"' => break, - _ => val.push(c), - } - } - } else { - while let Some(c) = chars.next_if(|c| *c != ',') { - val.push(c); - } - } - params.insert(key.trim().to_ascii_lowercase(), val.trim().to_string()); - } - - Some((scheme.to_string(), params)) + /// The url of a manifest in the registry. + fn manifest_url(reference: Reference) -> (String, Option) { + let Reference { + registry, + repository, + tag, + digest, + } = reference; + let version = match (digest.clone(), tag) { + (Some(digest), _) => digest.to_string(), + (None, Some(tag)) => tag, + (None, None) => "latest".to_string(), + }; + let url = format!("https://{registry}/v2/{repository}/manifests/{version}"); + (url, digest) } fn tag_reference(reference: String) -> Result { @@ -352,43 +261,6 @@ impl OCIClient { } } - fn compute_digest(algorithm: &DigestAlgorithm, bytes: &[u8]) -> Result { - fn hash_hex(bytes: &[u8]) -> String { - let mut hasher = D::new(); - hasher.update(bytes); - hasher - .finalize() - .iter() - .map(|b| format!("{b:02x}")) - .collect() - } - - match algorithm { - DigestAlgorithm::Sha256 => Ok(Digest { - algorithm: algorithm.clone(), - encoded: hash_hex::(bytes), - }), - DigestAlgorithm::Sha512 => Ok(Digest { - algorithm: algorithm.clone(), - encoded: hash_hex::(bytes), - }), - _ => Err(ErrorCode::DigestInvalid(format!( - "unsupported algorithm: {}", - algorithm - ))), - } - } - - fn assert_digest(expected_digest: Digest, bytes: &[u8]) -> Result<(), ErrorCode> { - let actual_digest = Self::compute_digest(&expected_digest.algorithm, bytes)?; - if expected_digest != actual_digest { - Err(ErrorCode::DigestInvalid(format!( - "digest mismatch: expected = {expected_digest}, actual = {actual_digest}", - )))? - } - Ok(()) - } - fn parse_repository_reference(reference: String) -> Result { if reference.len() == 0 { return Err(ErrorCode::Other(Some( @@ -454,75 +326,6 @@ impl OCIClient { }) } - async fn decode_transport_error( - body: StreamReader, - status: u16, - headers: Vec<(String, String)>, - ) -> ErrorCode { - if status == 429 { - let after = headers - .iter() - .find(|(k, _)| k.eq_ignore_ascii_case("Retry-After")) - .map(|(_, v)| { - if let Ok(seconds) = v.parse::() { - return Some(RetryAfter::DelaySeconds(seconds)); - } - if let Ok(date) = httpdate::parse_http_date(v) { - let seconds = date - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() - .try_into() - .unwrap(); - if seconds == 0 { - return None; - } - return Some(RetryAfter::Date(Instant { - seconds: seconds, - nanoseconds: 0, - })); - } - None - }) - .flatten(); - return ErrorCode::Toomanyrequests(after); - } - - let body = body.collect().await; - - let transport_errors: serde_json::Result = serde_json::from_slice(&body); - if transport_errors.is_err() { - return ErrorCode::Other(Some(format!( - "transport error with http status {status}: {}", - transport_errors.unwrap_err() - ))); - } - let transport_errors = transport_errors.unwrap(); - if transport_errors.errors.len() == 0 { - return ErrorCode::Other(Some( - "transport error with http status {status}: unspecified errors".to_string(), - )); - } - let error = transport_errors.errors.get(0).unwrap(); - - match error.code.as_str() { - "BLOB_UNKNOWN" => ErrorCode::BlobUnknown(error.message.to_string()), - "BLOB_UPLOAD_INVALID" => ErrorCode::BlobUploadInvalid(error.message.to_string()), - "BLOB_UPLOAD_UNKNOWN" => ErrorCode::BlobUploadUnknown(error.message.to_string()), - "DIGEST_INVALID" => ErrorCode::DigestInvalid(error.message.to_string()), - "MANIFEST_BLOB_UNKNOWN" => ErrorCode::ManifestBlobUnknown(error.message.to_string()), - "MANIFEST_INVALID" => ErrorCode::ManifestInvalid(error.message.to_string()), - "MANIFEST_UNKNOWN" => ErrorCode::ManifestUnknown(error.message.to_string()), - "NAME_INVALID" => ErrorCode::NameInvalid(error.message.to_string()), - "NAME_UNKNOWN" => ErrorCode::NameUnknown(error.message.to_string()), - "SIZE_INVALID" => ErrorCode::SizeInvalid(error.message.to_string()), - "UNAUTHORIZED" => ErrorCode::Unauthorized(error.message.to_string()), - "DENIED" => ErrorCode::Denied(error.message.to_string()), - "UNSUPPORTED" => ErrorCode::Unsupported(error.message.to_string()), - _ => ErrorCode::Other(Some(error.message.to_string())), - } - } - fn normalize_media_type(media_type: &str) -> MediaType { match media_type { "application/vnd.oci.descriptor.v1+json" => { @@ -1103,6 +906,321 @@ struct TransportError { // detail: Option, } +/// Requests to the registry, authenticating with a bearer token when the registry challenges +/// the request and decoding the registry's errors. The content of a response streams, e.g. a blob +/// or a manifest, verified against its digest. The end of the content is only reported once the +/// content matches the digest. Every digest the client verifies is verified here. +/// +/// Exported functions build on this rather than on `get-blob`, a component can't read a future +/// it writes itself. +struct Transport { + body: StreamReader, + /// the expected digest and the hasher computing the actual digest, taken once verified + verification: Option<(Digest, DigestHasher)>, + /// the outcome of the verification, returned again by every later read so a mismatch is + /// never followed by what looks like the verified end of the content + verified: Result<(), ErrorCode>, + /// the registry closed the body, the content is verified on the next read + ended: bool, +} + +impl Transport { + /// Requests content from the registry, verified against the digest when there is one, e.g. + /// not for a manifest requested by tag. Errors from the registry are returned before the + /// content is read. + async fn fetch( + url: String, + headers: Vec<(String, String)>, + digest: Option, + ) -> Result { + // an unsupported algorithm fails before the request, the content couldn't be verified + let verification = match digest { + Some(digest) => { + let hasher = DigestHasher::new(&digest.algorithm)?; + Some((digest, hasher)) + } + None => None, + }; + let http::HttpResponse { + status, + headers, + body, + .. + } = Self::get(url, headers).await?; + if status != 200 { + return Err(Self::decode_transport_error(body, status, headers).await); + } + + Ok(Self { + body, + verification, + verified: Ok(()), + ended: false, + }) + } + + /// The next chunk of content, `None` once all of the content is read and matches the + /// digest, `digest-invalid` when it does not. + async fn next(&mut self) -> Result>, ErrorCode> { + while !self.ended { + let (status, chunk) = self.body.read(Vec::with_capacity(BLOB_CHUNK_SIZE)).await; + self.ended = status == StreamResult::Dropped; + if !chunk.is_empty() { + if let Some((_, hasher)) = &mut self.verification { + hasher.update(&chunk); + } + return Ok(Some(chunk)); + } + } + if let Some((digest, hasher)) = self.verification.take() { + self.verified = Self::assert_digest(digest, hasher.finalize()); + } + self.verified.clone().map(|()| None) + } + + /// All of the content, once it matches the digest. + async fn collect(mut self) -> Result, ErrorCode> { + let mut content = vec![]; + while let Some(chunk) = self.next().await? { + content.extend(chunk); + } + Ok(content) + } + + async fn get( + url: String, + mut headers: Vec<(String, String)>, + ) -> Result { + let response = http::get(url.clone(), headers.clone(), None).await?; + if response.status != 401 { + return Ok(response); + } + + let challenge = response + .headers + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case("www-authenticate")) + .and_then(|(_, v)| Self::parse_www_authenticate(v)); + let Some((scheme, params)) = challenge else { + return Ok(response); + }; + if !scheme.eq_ignore_ascii_case("bearer") { + // other schemes (e.g. basic) require credentials, which are not supported yet + return Ok(response); + } + + // https://distribution.github.io/distribution/spec/auth/token/ + let token = Self::fetch_token(¶ms).await?; + headers.retain(|(k, _)| !k.eq_ignore_ascii_case("authorization")); + headers.push(("Authorization".to_string(), format!("Bearer {token}"))); + + // a second 401 is returned to the caller and decoded as a transport error + Ok(http::get(url, headers, None).await?) + } + + async fn fetch_token(params: &BTreeMap) -> Result { + let realm = params.get("realm").ok_or_else(|| { + ErrorCode::Unauthorized("bearer challenge is missing realm".to_string()) + })?; + let mut url = Url::parse(realm) + .map_err(|e| ErrorCode::Unauthorized(format!("invalid token realm {realm}: {e}")))?; + { + let mut query = url.query_pairs_mut(); + for key in ["service", "scope"] { + if let Some(value) = params.get(key) { + query.append_pair(key, value); + } + } + } + + let http::HttpResponse { status, body, .. } = + http::get(url.to_string(), vec![], None).await?; + let body = body.collect().await; + if status != 200 { + return Err(ErrorCode::Unauthorized(format!( + "token request to {realm} failed with status {status}" + ))); + } + + let TokenResponse { + token, + access_token, + } = serde_json::from_slice(&body)?; + token.or(access_token).ok_or_else(|| { + ErrorCode::Unauthorized(format!("token response from {realm} is missing token")) + }) + } + + /// Parses a single `WWW-Authenticate` challenge into its scheme and + /// lower-cased auth-params, e.g. + /// `Bearer realm="https://auth.example/token",service="example",scope="repository:foo:pull"` + fn parse_www_authenticate(value: &str) -> Option<(String, BTreeMap)> { + let value = value.trim(); + let (scheme, rest) = value.split_once(char::is_whitespace).unwrap_or((value, "")); + if scheme.is_empty() { + return None; + } + + let mut params = BTreeMap::new(); + let mut chars = rest.chars().peekable(); + loop { + while chars.next_if(|c| c.is_whitespace() || *c == ',').is_some() {} + if chars.peek().is_none() { + break; + } + + let mut key = String::new(); + while let Some(c) = chars.next_if(|c| *c != '=' && *c != ',') { + key.push(c); + } + if chars.next_if_eq(&'=').is_none() { + // token68 or malformed param, not used by registries + continue; + } + while chars.next_if(|c| c.is_whitespace()).is_some() {} + + let mut val = String::new(); + if chars.next_if_eq(&'"').is_some() { + while let Some(c) = chars.next() { + match c { + '\\' => val.extend(chars.next()), + '"' => break, + _ => val.push(c), + } + } + } else { + while let Some(c) = chars.next_if(|c| *c != ',') { + val.push(c); + } + } + params.insert(key.trim().to_ascii_lowercase(), val.trim().to_string()); + } + + Some((scheme.to_string(), params)) + } + + async fn decode_transport_error( + body: StreamReader, + status: u16, + headers: Vec<(String, String)>, + ) -> ErrorCode { + if status == 429 { + let after = headers + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case("Retry-After")) + .map(|(_, v)| { + if let Ok(seconds) = v.parse::() { + return Some(RetryAfter::DelaySeconds(seconds)); + } + if let Ok(date) = httpdate::parse_http_date(v) { + let seconds = date + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + .try_into() + .unwrap(); + if seconds == 0 { + return None; + } + return Some(RetryAfter::Date(Instant { + seconds: seconds, + nanoseconds: 0, + })); + } + None + }) + .flatten(); + return ErrorCode::Toomanyrequests(after); + } + + let body = body.collect().await; + + let transport_errors: serde_json::Result = serde_json::from_slice(&body); + if transport_errors.is_err() { + return ErrorCode::Other(Some(format!( + "transport error with http status {status}: {}", + transport_errors.unwrap_err() + ))); + } + let transport_errors = transport_errors.unwrap(); + if transport_errors.errors.len() == 0 { + return ErrorCode::Other(Some(format!( + "transport error with http status {status}: unspecified errors" + ))); + } + let error = transport_errors.errors.get(0).unwrap(); + + match error.code.as_str() { + "BLOB_UNKNOWN" => ErrorCode::BlobUnknown(error.message.to_string()), + "BLOB_UPLOAD_INVALID" => ErrorCode::BlobUploadInvalid(error.message.to_string()), + "BLOB_UPLOAD_UNKNOWN" => ErrorCode::BlobUploadUnknown(error.message.to_string()), + "DIGEST_INVALID" => ErrorCode::DigestInvalid(error.message.to_string()), + "MANIFEST_BLOB_UNKNOWN" => ErrorCode::ManifestBlobUnknown(error.message.to_string()), + "MANIFEST_INVALID" => ErrorCode::ManifestInvalid(error.message.to_string()), + "MANIFEST_UNKNOWN" => ErrorCode::ManifestUnknown(error.message.to_string()), + "NAME_INVALID" => ErrorCode::NameInvalid(error.message.to_string()), + "NAME_UNKNOWN" => ErrorCode::NameUnknown(error.message.to_string()), + "SIZE_INVALID" => ErrorCode::SizeInvalid(error.message.to_string()), + "UNAUTHORIZED" => ErrorCode::Unauthorized(error.message.to_string()), + "DENIED" => ErrorCode::Denied(error.message.to_string()), + "UNSUPPORTED" => ErrorCode::Unsupported(error.message.to_string()), + _ => ErrorCode::Other(Some(error.message.to_string())), + } + } + + fn assert_digest(expected_digest: Digest, actual_digest: Digest) -> Result<(), ErrorCode> { + if expected_digest != actual_digest { + Err(ErrorCode::DigestInvalid(format!( + "digest mismatch: expected = {expected_digest}, actual = {actual_digest}", + )))? + } + Ok(()) + } +} + +/// Computes a digest incrementally, e.g. as content streams from the registry +enum DigestHasher { + Sha256(sha2::Sha256), + Sha512(sha2::Sha512), +} + +impl DigestHasher { + fn new(algorithm: &DigestAlgorithm) -> Result { + match algorithm { + DigestAlgorithm::Sha256 => Ok(Self::Sha256(sha2::Sha256::new())), + DigestAlgorithm::Sha512 => Ok(Self::Sha512(sha2::Sha512::new())), + _ => Err(ErrorCode::DigestInvalid(format!( + "unsupported algorithm: {}", + algorithm + ))), + } + } + + fn update(&mut self, bytes: &[u8]) { + match self { + Self::Sha256(hasher) => hasher.update(bytes), + Self::Sha512(hasher) => hasher.update(bytes), + } + } + + fn finalize(self) -> Digest { + fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() + } + + match self { + Self::Sha256(hasher) => Digest { + algorithm: DigestAlgorithm::Sha256, + encoded: hex(&hasher.finalize()), + }, + Self::Sha512(hasher) => Digest { + algorithm: DigestAlgorithm::Sha512, + encoded: hex(&hasher.finalize()), + }, + } + } +} + impl From for ErrorCode { fn from(value: http::ErrorCode) -> Self { match value { @@ -1702,7 +1820,7 @@ mod tests { }; assert_eq!( - OCIClient::parse_www_authenticate( + Transport::parse_www_authenticate( r#"Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/alpine:pull,push""# ), Some(( @@ -1715,7 +1833,7 @@ mod tests { )), ); assert_eq!( - OCIClient::parse_www_authenticate( + Transport::parse_www_authenticate( r#"Basic Realm = "a \"quoted\" realm" , charset=UTF-8"# ), Some(( @@ -1724,10 +1842,10 @@ mod tests { )), ); assert_eq!( - OCIClient::parse_www_authenticate("Bearer"), + Transport::parse_www_authenticate("Bearer"), Some(("Bearer".to_string(), params(&[]))), ); - assert_eq!(OCIClient::parse_www_authenticate(" "), None); + assert_eq!(Transport::parse_www_authenticate(" "), None); } #[test] diff --git a/components/client/tests/blobs.rs b/components/client/tests/blobs.rs new file mode 100644 index 0000000..d8c005a --- /dev/null +++ b/components/client/tests/blobs.rs @@ -0,0 +1,215 @@ +//! Tests for the blobs streamed by `client`, against a scripted registry. + +use test_harness::{ + Config, Digest, ErrorCode, Harness, Reference, RegistryResponse, read, resolve, sha256, +}; + +/// Content larger than the client reads from the registry at once, so it streams in chunks. +fn content() -> Vec { + (0..1024 * 1024 + 7).map(|i| (i % 251) as u8).collect() +} + +fn reference(digest: Option) -> Reference { + Reference { + registry: "registry.example".to_string(), + repository: "componentized/oci".to_string(), + tag: None, + digest, + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn streams_blob() -> wasmtime::Result<()> { + let mut client = Harness::new() + .registry(|_| RegistryResponse::ok(content())) + .build() + .await?; + let (bytes, verified) = client + .run(async move |accessor, client| { + let (stream, verified) = client + .call_get_blob(accessor, reference(Some(sha256(&content())))) + .await? + .expect("get-blob"); + let bytes = read(accessor, stream, None).await?; + let verified = resolve(accessor, verified).await?; + Ok((bytes, verified)) + }) + .await?; + assert_eq!(bytes, content()); + assert!(matches!(verified, Ok(())), "{verified:?}"); + let requests = client.requests(); + assert_eq!(requests.len(), 1); + assert_eq!( + requests[0].url, + format!( + "https://registry.example/v2/componentized/oci/blobs/sha256:{}", + sha256(&content()).encoded + ) + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn rejects_blob_not_matching_digest() -> wasmtime::Result<()> { + let mut tampered = content(); + tampered[512 * 1024] ^= 0xff; + let mut client = Harness::new() + .registry(move |_| RegistryResponse::ok(tampered.clone())) + .build() + .await?; + let (bytes, verified) = client + .run(async move |accessor, client| { + let (stream, verified) = client + .call_get_blob(accessor, reference(Some(sha256(&content())))) + .await? + .expect("get-blob"); + let bytes = read(accessor, stream, None).await?; + let verified = resolve(accessor, verified).await?; + Ok((bytes, verified)) + }) + .await?; + // the content streams before it can be verified, the future reports the mismatch + assert_eq!(bytes.len(), content().len()); + match verified { + Err(ErrorCode::DigestInvalid(message)) => { + assert!(message.contains("digest mismatch"), "{message}") + } + other => panic!("expected digest-invalid, got {other:?}"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn rejects_truncated_blob() -> wasmtime::Result<()> { + let mut client = Harness::new() + .registry(|_| RegistryResponse::ok(content()[..1000].to_vec())) + .build() + .await?; + let verified = client + .run(async move |accessor, client| { + let (stream, verified) = client + .call_get_blob(accessor, reference(Some(sha256(&content())))) + .await? + .expect("get-blob"); + read(accessor, stream, None).await?; + resolve(accessor, verified).await + }) + .await?; + assert!( + matches!(verified, Err(ErrorCode::DigestInvalid(_))), + "{verified:?}" + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn blob_not_verified_when_stream_dropped() -> wasmtime::Result<()> { + let mut client = Harness::new() + .registry(|_| RegistryResponse::ok(content())) + .build() + .await?; + let (bytes, verified) = client + .run(async move |accessor, client| { + let (stream, verified) = client + .call_get_blob(accessor, reference(Some(sha256(&content())))) + .await? + .expect("get-blob"); + let bytes = read(accessor, stream, Some(1)).await?; + let verified = resolve(accessor, verified).await?; + Ok((bytes, verified)) + }) + .await?; + assert!(bytes.len() < content().len()); + match verified { + Err(ErrorCode::DigestInvalid(message)) => { + assert!(message.contains("not verified"), "{message}") + } + other => panic!("expected digest-invalid, got {other:?}"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn returns_registry_error_before_streaming() -> wasmtime::Result<()> { + let mut client = Harness::new() + .registry(|_| RegistryResponse::error(404, "BLOB_UNKNOWN", "blob unknown to registry")) + .build() + .await?; + let result = client + .run(async move |accessor, client| { + Ok(client + .call_get_blob(accessor, reference(Some(sha256(&content())))) + .await? + .map(|_| ())) + }) + .await?; + assert!( + matches!(&result, Err(ErrorCode::BlobUnknown(message)) if message == "blob unknown to registry"), + "{result:?}" + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn requires_digest() -> wasmtime::Result<()> { + let mut client = Harness::new().build().await?; + let result = client + .run(async move |accessor, client| { + Ok(client + .call_get_blob(accessor, reference(None)) + .await? + .map(|_| ())) + }) + .await?; + assert!( + matches!(&result, Err(ErrorCode::BlobUnknown(message)) if message == "digest required"), + "{result:?}" + ); + assert_eq!(client.requests(), vec![]); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn rejects_config_not_matching_digest() -> wasmtime::Result<()> { + let config = br#"{"architecture":"wasm","os":"wasip2","layerDigests":[]}"#; + let mut client = Harness::new() + .registry(|_| { + RegistryResponse::ok(&br#"{"architecture":"wasm","os":"wasip3","layerDigests":[]}"#[..]) + }) + .build() + .await?; + let result = client + .run(async move |accessor, client| { + Ok(client + .call_get_config(accessor, reference(Some(sha256(config))), None) + .await? + .map(|_| ())) + }) + .await?; + assert!( + matches!(result, Err(ErrorCode::DigestInvalid(_))), + "{result:?}" + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn gets_config() -> wasmtime::Result<()> { + let config = br#"{"mediaType":"application/vnd.wasm.config.v0+json","architecture":"wasm","os":"wasip2","layerDigests":[]}"#; + let mut client = Harness::new() + .registry(|_| RegistryResponse::ok(&config[..])) + .build() + .await?; + let result = client + .run(async move |accessor, client| { + Ok(client + .call_get_config(accessor, reference(Some(sha256(config))), None) + .await?) + }) + .await?; + match result { + Ok(Config::WasmV0(config)) => assert_eq!(config.os, "wasip2"), + other => panic!("expected a wasm config, got {other:?}"), + } + Ok(()) +} diff --git a/components/client/tests/manifests.rs b/components/client/tests/manifests.rs new file mode 100644 index 0000000..af03398 --- /dev/null +++ b/components/client/tests/manifests.rs @@ -0,0 +1,206 @@ +//! Tests for the manifests fetched by `client`, against a scripted registry. + +use test_harness::{ + Digest, DigestAlgorithm, ErrorCode, Harness, Manifest, Reference, RegistryResponse, sha256, +}; + +const MANIFEST: &[u8] = br#"{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.wasm.config.v0+json", + "digest": "sha256:80d83bbdaa82cff96584c99217c29fd17bc7e5f0424c0cb26aa3831ea18132b4", + "size": 345 + }, + "layers": [ + { + "mediaType": "application/wasm", + "digest": "sha256:ee7ff5c9588e997b4a54b6d351b52a5ca4f6980377f59e48c778f48a23b483db", + "size": 1894585 + } + ] +}"#; + +/// Whether the digests are the same, the generated `Digest` has no `PartialEq`. +fn same_digest(a: &Digest, b: &Digest) -> bool { + matches!( + (&a.algorithm, &b.algorithm), + (DigestAlgorithm::Sha256, DigestAlgorithm::Sha256) + | (DigestAlgorithm::Sha512, DigestAlgorithm::Sha512) + ) && a.encoded == b.encoded +} + +fn reference(tag: Option<&str>, digest: Option) -> Reference { + Reference { + registry: "registry.example".to_string(), + repository: "componentized/oci".to_string(), + tag: tag.map(str::to_string), + digest, + } +} + +async fn get_manifest( + registry: impl FnMut(&test_harness::RegistryRequest) -> RegistryResponse + Send + 'static, + reference: Reference, +) -> wasmtime::Result<( + Result, + Vec, +)> { + let mut client = Harness::new().registry(registry).build().await?; + let result = client + .run(async move |accessor, client| client.call_get_manifest(accessor, reference).await) + .await?; + Ok((result, client.requests())) +} + +#[tokio::test(flavor = "multi_thread")] +async fn gets_manifest_by_digest() -> wasmtime::Result<()> { + let digest = sha256(MANIFEST); + let (result, requests) = get_manifest( + |_| RegistryResponse::ok(MANIFEST), + reference(None, Some(digest.clone())), + ) + .await?; + assert!(matches!(result, Ok(Manifest::OciImageV1(_))), "{result:?}"); + assert_eq!(requests.len(), 1); + assert_eq!( + requests[0].url, + format!( + "https://registry.example/v2/componentized/oci/manifests/sha256:{}", + digest.encoded + ) + ); + assert!( + requests[0] + .headers + .iter() + .any(|(name, value)| name == "Accept" + && value.contains("application/vnd.oci.image.manifest.v1+json")), + "{:?}", + requests[0].headers + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn rejects_manifest_not_matching_digest() -> wasmtime::Result<()> { + let tampered = String::from_utf8(MANIFEST.to_vec()) + .unwrap() + .replace("1894585", "1894586"); + let (result, _) = get_manifest( + move |_| RegistryResponse::ok(tampered.clone()), + reference(None, Some(sha256(MANIFEST))), + ) + .await?; + match result { + Err(ErrorCode::DigestInvalid(message)) => { + assert!(message.contains("digest mismatch"), "{message}") + } + other => panic!("expected digest-invalid, got {other:?}"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn gets_manifest_by_tag_without_verifying() -> wasmtime::Result<()> { + let (result, requests) = get_manifest( + |_| RegistryResponse::ok(MANIFEST), + reference(Some("v1"), None), + ) + .await?; + assert!(matches!(result, Ok(Manifest::OciImageV1(_))), "{result:?}"); + assert_eq!( + requests[0].url, + "https://registry.example/v2/componentized/oci/manifests/v1" + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn returns_registry_error_for_manifest() -> wasmtime::Result<()> { + let (result, _) = get_manifest( + |_| RegistryResponse::error(404, "MANIFEST_UNKNOWN", "manifest unknown"), + reference(Some("v1"), None), + ) + .await?; + assert!( + matches!(&result, Err(ErrorCode::ManifestUnknown(message)) if message == "manifest unknown"), + "{result:?}" + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn resolves_digest_of_tagged_manifest() -> wasmtime::Result<()> { + let mut client = Harness::new() + .registry(|_| RegistryResponse::ok(MANIFEST)) + .build() + .await?; + let result = client + .run(async move |accessor, client| { + client + .call_resolve_digest(accessor, reference(Some("v1"), None)) + .await + }) + .await?; + assert!( + matches!(&result, Ok(digest) if same_digest(digest, &sha256(MANIFEST))), + "{result:?}" + ); + let requests = client.requests(); + assert_eq!( + requests[0].url, + "https://registry.example/v2/componentized/oci/manifests/v1" + ); + // the same manifest get-manifest gets for the tag + assert!( + requests[0] + .headers + .iter() + .any(|(name, value)| name == "Accept" + && value.contains("application/vnd.oci.image.manifest.v1+json")), + "{:?}", + requests[0].headers + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn resolves_digested_reference_without_request() -> wasmtime::Result<()> { + let digest = sha256(MANIFEST); + let mut client = Harness::new().build().await?; + let result = client + .run({ + let digest = digest.clone(); + async move |accessor, client| { + client + .call_resolve_digest(accessor, reference(Some("v1"), Some(digest))) + .await + } + }) + .await?; + assert!( + matches!(&result, Ok(resolved) if same_digest(resolved, &digest)), + "{result:?}" + ); + assert_eq!(client.requests(), vec![]); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn reports_status_of_unspecified_registry_error() -> wasmtime::Result<()> { + let (result, _) = get_manifest( + |_| RegistryResponse { + status: 500, + headers: vec![], + body: br#"{"errors":[]}"#.to_vec(), + }, + reference(Some("v1"), None), + ) + .await?; + assert!( + matches!(&result, Err(ErrorCode::Other(Some(message))) if message == "transport error with http status 500: unspecified errors"), + "{result:?}" + ); + Ok(()) +} diff --git a/crates/test-harness/Cargo.toml b/crates/test-harness/Cargo.toml new file mode 100644 index 0000000..443c18b --- /dev/null +++ b/crates/test-harness/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test-harness" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +publish = false + +[dependencies] +sha2 = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } +wasmtime-wasi = { workspace = true } diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs new file mode 100644 index 0000000..084990d --- /dev/null +++ b/crates/test-harness/src/lib.rs @@ -0,0 +1,451 @@ +//! Test harness for the client component. +//! +//! A [`Harness`] instantiates the client component (`target/components/client/client.wasm`) with a +//! host `componentized:http/client`. Requests the component sends never reach the network, they +//! are recorded and answered by a scripted registry. + +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::process::Command; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +use tokio::sync::{mpsc, oneshot}; +use wasmtime::component::{ + Accessor, Component, FutureConsumer, FutureReader, HasData, Lift, Linker, ResourceTable, + Source, StreamConsumer, StreamReader, StreamResult, +}; +use wasmtime::error::Context as _; +use wasmtime::{Engine, Result, Store, StoreContextMut, bail, format_err}; +use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; + +use crate::bindings::componentized::http::client as http_client; + +pub mod bindings { + wasmtime::component::bindgen!({ + path: "../../components/wit", + world: "componentized:oci-components/client", + imports: { + "componentized:http/client": async | store, + }, + exports: { default: async | store }, + with: { + "wasi:clocks": wasmtime_wasi::p3::bindings::clocks, + }, + }); +} + +pub use bindings::exports::componentized::oci::client::{ + Config, Digest, DigestAlgorithm, ErrorCode, Guest as Client, Manifest, Reference, +}; + +/// The sha256 digest of the content. +pub fn sha256(content: &[u8]) -> Digest { + use sha2::Digest as _; + Digest { + algorithm: DigestAlgorithm::Sha256, + encoded: sha2::Sha256::digest(content) + .iter() + .map(|b| format!("{b:02x}")) + .collect(), + } +} + +/// Root of the workspace, where the Makefile lives. +fn workspace_dir() -> PathBuf { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + dir.canonicalize().unwrap_or(dir) +} + +/// Rebuild the client component in `target/components/` with make, once for this process. +fn ensure_built() -> Result { + static BUILT: Mutex = Mutex::new(false); + + let target = "target/components/client/client.wasm"; + // hold the lock while building so concurrent tests don't run make over each other + let mut built = BUILT.lock().unwrap_or_else(|err| err.into_inner()); + if !*built { + let output = Command::new("make") + .arg("-C") + .arg(workspace_dir()) + .arg(target) + .output() + .context("failed to run make")?; + if !output.status.success() { + bail!( + "failed to build {target}:\n{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + *built = true; + } + Ok(workspace_dir().join(target)) +} + +/// A request the client sent to the registry. +#[derive(Clone, Debug, PartialEq)] +pub struct RegistryRequest { + pub url: String, + pub headers: Vec<(String, String)>, +} + +/// The registry's response to a request. +#[derive(Clone, Debug, PartialEq)] +pub struct RegistryResponse { + pub status: u16, + pub headers: Vec<(String, String)>, + pub body: Vec, +} + +impl RegistryResponse { + /// A 200 response with the body. + pub fn ok(body: impl Into>) -> Self { + Self { + status: 200, + headers: vec![], + body: body.into(), + } + } + + /// An error response with an OCI distribution spec error, e.g. `BLOB_UNKNOWN`. + pub fn error(status: u16, code: &str, message: &str) -> Self { + Self { + status, + headers: vec![("Content-Type".to_string(), "application/json".to_string())], + body: format!(r#"{{"errors":[{{"code":"{code}","message":"{message}"}}]}}"#) + .into_bytes(), + } + } +} + +type Responder = dyn FnMut(&RegistryRequest) -> RegistryResponse + Send; + +pub struct Ctx { + wasi: WasiCtx, + table: ResourceTable, + responder: Box, + requests: Arc>>, +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.table, + } + } +} + +/// The host `componentized:http/client`, requests are recorded and answered by the registry. +struct HttpClient; + +impl HasData for HttpClient { + type Data<'a> = &'a mut Ctx; +} + +impl http_client::Host for Ctx {} + +impl HttpClient { + fn respond( + accessor: &Accessor, + url: String, + headers: Vec<(String, String)>, + body: Option>, + ) -> Result { + let err = |err: wasmtime::Error| http_client::ErrorCode::Other(Some(err.to_string())); + accessor.with(|mut store| { + if let Some(mut body) = body { + body.close(&mut store).map_err(err)?; + } + let ctx = store.get(); + let request = RegistryRequest { url, headers }; + let response = (ctx.responder)(&request); + ctx.requests.lock().unwrap().push(request); + + let body = StreamReader::new(&mut store, response.body).map_err(err)?; + let trailers = + FutureReader::new(&mut store, async { Ok::<_, wasmtime::Error>(Ok(vec![])) }) + .map_err(err)?; + Ok(http_client::HttpResponse { + status: response.status, + headers: response.headers, + body, + trailers, + }) + }) + } +} + +impl http_client::HostWithStore for HttpClient { + async fn request( + accessor: &Accessor, + _method: http_client::Method, + url: String, + headers: Vec<(String, String)>, + body: Option>, + _options: Option, + ) -> Result { + Self::respond(accessor, url, headers, body) + } + + async fn get( + accessor: &Accessor, + url: String, + headers: Vec<(String, String)>, + _options: Option, + ) -> Result { + Self::respond(accessor, url, headers, None) + } + + async fn post( + accessor: &Accessor, + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + _options: Option, + ) -> Result { + Self::respond(accessor, url, headers, Some(body)) + } + + async fn put( + accessor: &Accessor, + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + _options: Option, + ) -> Result { + Self::respond(accessor, url, headers, Some(body)) + } + + async fn delete( + accessor: &Accessor, + url: String, + headers: Vec<(String, String)>, + _options: Option, + ) -> Result { + Self::respond(accessor, url, headers, None) + } + + async fn patch( + accessor: &Accessor, + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + _options: Option, + ) -> Result { + Self::respond(accessor, url, headers, Some(body)) + } + + async fn head( + accessor: &Accessor, + url: String, + headers: Vec<(String, String)>, + _options: Option, + ) -> Result { + Self::respond(accessor, url, headers, None) + } + + async fn options( + accessor: &Accessor, + url: String, + headers: Vec<(String, String)>, + _options: Option, + ) -> Result { + Self::respond(accessor, url, headers, None) + } + + async fn trace( + accessor: &Accessor, + url: String, + headers: Vec<(String, String)>, + _options: Option, + ) -> Result { + Self::respond(accessor, url, headers, None) + } + + async fn query( + accessor: &Accessor, + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + _options: Option, + ) -> Result { + Self::respond(accessor, url, headers, Some(body)) + } +} + +/// Builds a client instance for a test. +pub struct Harness { + responder: Box, +} + +impl Harness { + /// Test the client component, by default every request is answered with a 404. + pub fn new() -> Self { + Self { + responder: Box::new(|_| RegistryResponse::error(404, "NAME_UNKNOWN", "not found")), + } + } + + /// Answer the requests the client sends with the responses. + pub fn registry( + mut self, + responder: impl FnMut(&RegistryRequest) -> RegistryResponse + Send + 'static, + ) -> Self { + self.responder = Box::new(responder); + self + } + + /// Instantiate the client. + pub async fn build(self) -> Result { + let mut config = wasmtime::Config::new(); + config.wasm_component_model_async(true); + // manifests and configs have `map` annotations and labels + config.wasm_component_model_map(true); + let engine = Engine::new(&config)?; + + let path = ensure_built()?; + let component = Component::from_file(&engine, &path) + .with_context(|| format!("failed to load {}", path.display()))?; + + let mut linker = Linker::new(&engine); + wasmtime_wasi::p3::add_to_linker(&mut linker)?; + http_client::add_to_linker::<_, HttpClient>(&mut linker, |ctx| ctx)?; + + let requests = Arc::new(Mutex::new(vec![])); + let mut store = Store::new( + &engine, + Ctx { + wasi: WasiCtxBuilder::new().inherit_stdio().build(), + table: ResourceTable::new(), + responder: self.responder, + requests: requests.clone(), + }, + ); + let client = bindings::Client::instantiate_async(&mut store, &component, &linker) + .await + .context("failed to instantiate the client")?; + + Ok(TestSubject { + store, + client, + requests, + }) + } +} + +impl Default for Harness { + fn default() -> Self { + Self::new() + } +} + +/// An instantiated client. +pub struct TestSubject { + store: Store, + client: bindings::Client, + requests: Arc>>, +} + +impl TestSubject { + /// The requests the client sent to the registry. + pub fn requests(&self) -> Vec { + self.requests.lock().unwrap().clone() + } + + /// Run a test body against the client's exported `componentized:oci/client`. + pub async fn run( + &mut self, + f: impl AsyncFnOnce(&Accessor, &Client) -> Result + Send, + ) -> Result { + let client = self.client.componentized_oci_client(); + self.store + .run_concurrent(async move |accessor| f(accessor, client).await) + .await? + } +} + +/// Read the content of a guest byte stream, until it closes, or until at least `limit` bytes are +/// read, then the stream is dropped. +pub async fn read( + accessor: &Accessor, + stream: StreamReader, + limit: Option, +) -> Result> { + let (tx, mut rx) = mpsc::unbounded_channel(); + accessor.with(|store| { + stream.pipe( + store, + BytesConsumer { + tx, + remaining: limit.unwrap_or(usize::MAX), + }, + ) + })?; + let mut bytes = vec![]; + while let Some(chunk) = rx.recv().await { + bytes.extend(chunk); + } + Ok(bytes) +} + +/// Wait for the value of a guest future. +pub async fn resolve( + accessor: &Accessor, + future: FutureReader, +) -> Result { + let (tx, rx) = oneshot::channel(); + accessor.with(|store| future.pipe(store, OneshotConsumer(Some(tx))))?; + rx.await + .map_err(|_| format_err!("future closed without a value")) +} + +struct BytesConsumer { + tx: mpsc::UnboundedSender>, + remaining: usize, +} + +impl StreamConsumer for BytesConsumer { + type Item = u8; + + fn poll_consume( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + store: StoreContextMut, + source: Source<'_, u8>, + _finish: bool, + ) -> Poll> { + let this = self.get_mut(); + let mut source = source.as_direct(store); + let chunk = source.remaining().to_vec(); + source.mark_read(chunk.len()); + this.remaining = this.remaining.saturating_sub(chunk.len()); + if this.tx.send(chunk).is_err() || this.remaining == 0 { + return Poll::Ready(Ok(StreamResult::Dropped)); + } + Poll::Ready(Ok(StreamResult::Completed)) + } +} + +struct OneshotConsumer(Option>); + +impl FutureConsumer for OneshotConsumer { + type Item = T; + + fn poll_consume( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + store: StoreContextMut, + mut source: Source<'_, T>, + _finish: bool, + ) -> Poll> { + let mut item = None; + source.read(store, &mut item)?; + if let (Some(item), Some(tx)) = (item, self.get_mut().0.take()) { + // the receiver is only dropped when the test stopped waiting + let _ = tx.send(item); + } + Poll::Ready(Ok(())) + } +} diff --git a/wit/client.wit b/wit/client.wit index cdf5a74..a228ea9 100644 --- a/wit/client.wit +++ b/wit/client.wit @@ -348,23 +348,36 @@ interface client { other(string), } - /// Parse a string image reference like `ubuntu` or `ghcr.io/componentized/oci:1.0.0` for use by the other methods. + /// Parse a string image reference like `ubuntu` or + /// `ghcr.io/componentized/oci:1.0.0` for use by the other methods. @since(version = 0.1.0-dev) parse-reference: func(reference: string) -> result; - /// Resolves a tagged reference to a digest with the registry. Digested references are returned immediately. + /// Resolves a tagged reference to a digest with the registry. Digested + /// references are returned immediately. @since(version = 0.1.0-dev) resolve-digest: async func(reference: reference) -> result; - /// Gets a blob from the registry. The reference must be digested. Manifests must be fetched with `get-manifest()` + /// Gets a blob from the registry. The reference must be digested. + /// Manifests must be fetched with `get-manifest()`. + /// + /// The content streams as it's received from the registry. Errors from the + /// registry are returned before the content starts. The content is + /// verified against the digest as it streams, the future resolves once the + /// stream ends, with `digest-invalid` when the content does not match the + /// digest. Content read from the stream must not be trusted until the + /// future resolves successfully. @since(version = 0.1.0-dev) - get-blob: async func(reference: reference) -> result, error-code>; + get-blob: async func(reference: reference) -> result, future>>, error-code>; - /// Gets a config from the registry decoding by media type. In many cases, the media type cannot be inferred and must be passed explicitly. The reference must be digested. + /// Gets a config from the registry decoding by media type. In many cases, + /// the media type cannot be inferred and must be passed explicitly. The + /// reference must be digested. @since(version = 0.1.0-dev) get-config: async func(reference: reference, default-media-type: option) -> result; - /// Gets a manifest from the registry decoding by media type. The reference may be tagged or digested. + /// Gets a manifest from the registry decoding by media type. The reference + /// may be tagged or digested. @since(version = 0.1.0-dev) get-manifest: async func(reference: reference) -> result; From 3a2b6bf04a1fbd7840c73a1b486a1425f15dc51d Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 7 Oct 2026 17:15:49 -0400 Subject: [PATCH 3/3] drop regex crate, it's very large Signed-off-by: Scott Andrews --- components/client/Cargo.toml | 2 +- components/client/src/lib.rs | 92 ++++++++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/components/client/Cargo.toml b/components/client/Cargo.toml index ac05b95..0a4e9a3 100644 --- a/components/client/Cargo.toml +++ b/components/client/Cargo.toml @@ -10,7 +10,6 @@ crate-type = ["cdylib"] [dependencies] chrono = { workspace = true } httpdate = { workspace = true } -regex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } @@ -18,6 +17,7 @@ url = { workspace = true } wit-bindgen = { workspace = true, features = ["async-spawn"] } [dev-dependencies] +regex = { workspace = true } test-harness = { workspace = true } tokio = { workspace = true } wasmtime = { workspace = true } diff --git a/components/client/src/lib.rs b/components/client/src/lib.rs index 6b6add1..8e8c486 100644 --- a/components/client/src/lib.rs +++ b/components/client/src/lib.rs @@ -1,8 +1,7 @@ #![cfg_attr(not(test), no_main)] -use std::{collections::BTreeMap, fmt::Display, time::UNIX_EPOCH}; +use std::{collections::BTreeMap, fmt::Display, ops::RangeInclusive, time::UNIX_EPOCH}; -use regex::Regex; use serde::Deserialize; use serde_json::Value; use sha2::Digest as _; @@ -195,8 +194,7 @@ impl OCIClient { let mut reference = Self::parse_repository_reference(base)?; if tag != "" { - let re = Regex::new(r"^[a-zA-Z0-9_\-.]{1,128}$").unwrap(); - if !re.is_match(&tag) { + if !is_tag(&tag) { Err(ErrorCode::Other(Some(format!("invalid tag format: {tag}"))))? } reference.tag = Some(tag); @@ -232,8 +230,7 @@ impl OCIClient { match algorithm.as_str() { "sha256" => { - let re = Regex::new(r"^[a-f0-9]{64}$").unwrap(); - if !re.is_match(&encoded) { + if !is_lower_hex(&encoded, 64) { Err(ErrorCode::Other(Some(format!( "invalid checksum digest format: {encoded}" ))))? @@ -244,8 +241,7 @@ impl OCIClient { }) } "sha512" => { - let re = Regex::new(r"^[a-f0-9]{128}$").unwrap(); - if !re.is_match(&encoded) { + if !is_lower_hex(&encoded, 128) { Err(ErrorCode::Other(Some(format!( "invalid checksum digest format: {encoded}" ))))? @@ -291,12 +287,7 @@ impl OCIClient { repository = format!("library/{repository}"); } - if !Regex::new(r"^[a-z0-9_\-./]{1,255}$") - .unwrap() - .is_match(&repository) - || repository.starts_with('/') - || repository.ends_with('/') - { + if !is_repository(&repository) || repository.starts_with('/') || repository.ends_with('/') { Err(ErrorCode::Other(Some(format!( "invalid repository format: {repository}" ))))? @@ -1221,6 +1212,32 @@ impl DigestHasher { } } +/// Whether every byte of the value is allowed and its length is within the range, the same as an +/// anchored regex of a single ASCII character class, e.g. `^[a-f0-9]{64}$`. A non-ASCII +/// character is never allowed, so the length in bytes is the length in characters. +fn is_ascii_class(value: &str, len: RangeInclusive, allowed: impl Fn(u8) -> bool) -> bool { + len.contains(&value.len()) && value.bytes().all(allowed) +} + +/// A tag, `^[a-zA-Z0-9_\-.]{1,128}$` +fn is_tag(value: &str) -> bool { + is_ascii_class(value, 1..=128, |b| { + b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.') + }) +} + +/// The encoded value of a digest, `^[a-f0-9]{len}$` +fn is_lower_hex(value: &str, len: usize) -> bool { + is_ascii_class(value, len..=len, |b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) +} + +/// The characters of a repository, `^[a-z0-9_\-./]{1,255}$` +fn is_repository(value: &str) -> bool { + is_ascii_class(value, 1..=255, |b| { + b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'-' | b'.' | b'/') + }) +} + impl From for ErrorCode { fn from(value: http::ErrorCode) -> Self { match value { @@ -1592,6 +1609,53 @@ mod tests { )))) } + #[test] + fn test_ascii_classes_match_regex() { + use regex::Regex; + + let checks: [(&str, fn(&str) -> bool); 4] = [ + (r"^[a-zA-Z0-9_\-.]{1,128}$", is_tag), + (r"^[a-f0-9]{64}$", |v| is_lower_hex(v, 64)), + (r"^[a-f0-9]{128}$", |v| is_lower_hex(v, 128)), + (r"^[a-z0-9_\-./]{1,255}$", is_repository), + ]; + + // every character class boundary, and characters a regex could treat differently + let alphabet: Vec = "09afgzAFGZ_-./:@ \n\t\0\x7féÅ☃🦀a0".chars().collect(); + let mut inputs: Vec = vec![String::new()]; + for len in [1, 2, 63, 64, 65, 127, 128, 129, 254, 255, 256] { + for c in &alphabet { + inputs.push(c.to_string().repeat(len)); + // a valid prefix and a single other character at the end, e.g. a trailing newline + inputs.push(format!("{}{c}", "a".repeat(len - 1))); + inputs.push(format!("{c}{}", "a".repeat(len - 1))); + } + } + // deterministic pseudo-random mixes of the alphabet + let mut state: u64 = 0x2545f4914f6cdd1d; + for _ in 0..20_000 { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + let len = (state % 300) as usize; + let input = (0..len) + .map(|i| alphabet[((state >> (i % 48)) as usize + i) % alphabet.len()]) + .collect(); + inputs.push(input); + } + + for (pattern, check) in checks { + let re = Regex::new(pattern).unwrap(); + for input in &inputs { + assert_eq!( + check(input), + re.is_match(input), + "{pattern} disagrees for {input:?}" + ); + } + } + } + #[test] fn test_parse_reference() { let sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";