From b8126924a9f0a8fe87f799c8d336dc561680671e Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Mon, 27 May 2024 17:38:49 -0400 Subject: [PATCH 1/3] Add wasm-pkg-common crate --- Cargo.lock | 32 ++- crates/wasm-pkg-common/Cargo.toml | 16 ++ crates/wasm-pkg-common/src/config.rs | 281 ++++++++++++++++++++++ crates/wasm-pkg-common/src/config/toml.rs | 158 ++++++++++++ crates/wasm-pkg-common/src/label.rs | 65 +++++ crates/wasm-pkg-common/src/lib.rs | 31 +++ crates/wasm-pkg-common/src/package.rs | 50 ++++ crates/wasm-pkg-common/src/registry.rs | 45 ++++ 8 files changed, 668 insertions(+), 10 deletions(-) create mode 100644 crates/wasm-pkg-common/Cargo.toml create mode 100644 crates/wasm-pkg-common/src/config.rs create mode 100644 crates/wasm-pkg-common/src/config/toml.rs create mode 100644 crates/wasm-pkg-common/src/label.rs create mode 100644 crates/wasm-pkg-common/src/lib.rs create mode 100644 crates/wasm-pkg-common/src/package.rs create mode 100644 crates/wasm-pkg-common/src/registry.rs diff --git a/Cargo.lock b/Cargo.lock index 5a85ce7..c973143 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2778,9 +2778,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde 1.0.201", ] @@ -3229,21 +3229,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" dependencies = [ "serde 1.0.201", "serde_spanned", "toml_datetime", - "toml_edit 0.22.12", + "toml_edit 0.22.13", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde 1.0.201", ] @@ -3261,9 +3261,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ "indexmap 2.2.6", "serde 1.0.201", @@ -3763,6 +3763,18 @@ dependencies = [ "wasmparser 0.209.1", ] +[[package]] +name = "wasm-pkg-common" +version = "0.3.0" +dependencies = [ + "dirs", + "http", + "serde 1.0.201", + "thiserror", + "toml 0.8.13", + "tracing", +] + [[package]] name = "wasm-pkg-loader" version = "0.3.0" @@ -3785,7 +3797,7 @@ dependencies = [ "thiserror", "tokio", "tokio-util", - "toml 0.8.12", + "toml 0.8.13", "tracing", "tracing-subscriber", "url", diff --git a/crates/wasm-pkg-common/Cargo.toml b/crates/wasm-pkg-common/Cargo.toml new file mode 100644 index 0000000..5a633fe --- /dev/null +++ b/crates/wasm-pkg-common/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wasm-pkg-common" +description = "Wasm Package common types and configuration" +repository = "https://github.com/bytecodealliance/wasm-pkg-tools/tree/main/crates/wasm-pkg-common" +edition.workspace = true +version.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +dirs = "5.0.1" +http = "1.1.0" +serde = { version = "1.0", features = ["derive"] } +toml = "0.8.13" +thiserror = "1.0" +tracing = "0.1" \ No newline at end of file diff --git a/crates/wasm-pkg-common/src/config.rs b/crates/wasm-pkg-common/src/config.rs new file mode 100644 index 0000000..8676fbf --- /dev/null +++ b/crates/wasm-pkg-common/src/config.rs @@ -0,0 +1,281 @@ +use std::{ + collections::{hash_map::Entry, HashMap}, + io::ErrorKind, + path::Path, +}; + +use serde::{Deserialize, Serialize}; + +use crate::{label::Label, package::PackageRef, Error, Registry}; + +mod toml; + +pub const DEFAULT_REGISTRY: &str = "bytecodealliance.org"; + +/// Wasm Package registry configuration. +/// +/// Most consumers are expected to start with [`Config::global_defaults`] to +/// provide a consistent baseline user experience. Where needed, these defaults +/// can be overridden with application-specific config via [`Config::merge`] or +/// other mutation methods. +#[derive(Debug)] +pub struct Config { + default_registry: Option, + namespace_registries: HashMap, + package_registries: HashMap, + registry_configs: HashMap, +} + +impl Default for Config { + fn default() -> Self { + Self { + default_registry: Some(DEFAULT_REGISTRY.parse().unwrap()), + namespace_registries: Default::default(), + package_registries: Default::default(), + registry_configs: Default::default(), + } + } +} + +impl Config { + /// Returns an empty config. + /// + /// Note that this may differ from the `Default` implementation, which + /// includes hard-coded global defaults. + pub fn new() -> Self { + Self { + default_registry: Default::default(), + namespace_registries: Default::default(), + package_registries: Default::default(), + registry_configs: Default::default(), + } + } + + /// Loads config from several default sources. + /// + /// The following sources are loaded in this order, with later sources + /// merged into (overriding) earlier sources. + /// - Hard-coded defaults + /// - User-global config file (e.g. `~/.config/wasm-pkg/config.toml`) + /// + /// Note: This list is expected to expand in the future to include + /// "workspace" config files like `./.wasm-pkg/config.toml`. + pub fn global_defaults() -> Result { + let mut config = Self::default(); + if let Some(global_config) = Self::read_global_config()? { + config.merge(global_config); + } + Ok(config) + } + + /// Reads config from + pub fn read_global_config() -> Result, Error> { + let Some(config_dir) = dirs::config_dir() else { + return Ok(None); + }; + let path = config_dir.join("wasm-pkg").join("config.toml"); + let contents = match std::fs::read_to_string(path) { + Ok(contents) => contents, + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None), + Err(err) => return Err(Error::ConfigFileIoError(err)), + }; + Ok(Some(Self::from_toml(&contents)?)) + } + + /// Reads config from a TOML file at the given path. + pub fn from_file(path: impl AsRef) -> Result { + let contents = std::fs::read_to_string(path).map_err(Error::ConfigFileIoError)?; + Self::from_toml(&contents) + } + + /// Parses config from the given TOML contents. + pub fn from_toml(contents: &str) -> Result { + let toml_cfg: toml::TomlConfig = + ::toml::from_str(contents).map_err(Error::invalid_config)?; + Ok(toml_cfg.into()) + } + + /// Merges the given other config into this one. + pub fn merge(&mut self, other: Self) { + let Self { + default_registry, + namespace_registries, + package_registries, + registry_configs, + } = other; + if default_registry.is_some() { + self.default_registry = default_registry; + } + self.namespace_registries.extend(namespace_registries); + self.package_registries.extend(package_registries); + for (registry, config) in registry_configs { + match self.registry_configs.entry(registry) { + Entry::Occupied(mut occupied) => occupied.get_mut().merge(config), + Entry::Vacant(vacant) => { + vacant.insert(config); + } + } + } + } + + /// Resolves a [`Registry`] for the given [`PackageRef`]. + /// + /// Resolution returns the first of these that matches: + /// - A package registry exactly matching the package + /// - A namespace registry matching the package's namespace + /// - The default registry + pub fn resolve_registry(&self, package: &PackageRef) -> Option<&Registry> { + if let Some(reg) = self.package_registries.get(package) { + Some(reg) + } else if let Some(reg) = self.namespace_registries.get(package.namespace()) { + Some(reg) + } else if let Some(reg) = self.default_registry.as_ref() { + Some(reg) + } else { + None + } + } + + /// Returns the default registry. + pub fn default_registry(&self) -> Option<&Registry> { + self.default_registry.as_ref() + } + + /// Sets the default registry. + pub fn set_default_registry(&mut self, registry: Option) { + self.default_registry = registry; + } + + /// Returns a registry for the given namespace. + /// + /// Does not fall back to the default registry; see [`Self::resolve`]. + pub fn namespace_registry(&self, namespace: &Label) -> Option<&Registry> { + self.namespace_registries.get(namespace) + } + + /// Sets a registry for the given namespace. + pub fn set_namespace_registry(&mut self, namespace: Label, registry: Registry) { + self.namespace_registries.insert(namespace, registry); + } + + /// Returns a registry configured for the given exact package. + /// + /// Does not fall back to namespace or default registries; see [`Self::resolve`]. + pub fn package_registry(&self, package: &PackageRef) -> Option<&Registry> { + self.package_registries.get(package) + } + + /// Sets a registry for the given exact package. + pub fn set_package_registry(&mut self, package: PackageRef, registry: Registry) { + self.package_registries.insert(package, registry); + } + + /// Returns [`RegistryConfig`] for the given registry. + pub fn registry_config(&self, registry: &Registry) -> Option<&RegistryConfig> { + self.registry_configs.get(registry) + } + + /// Returns a mutable [`RegistryConfig`] for the given registry, inserting + /// an empty one if needed. + pub fn get_or_insert_registry_config_mut( + &mut self, + registry: &Registry, + ) -> &mut RegistryConfig { + if !self.registry_configs.contains_key(registry) { + self.registry_configs + .insert(registry.clone(), Default::default()); + } + self.registry_configs.get_mut(registry).unwrap() + } +} + +#[derive(Default)] +pub struct RegistryConfig { + backend_type: Option, + backend_configs: HashMap, +} + +impl RegistryConfig { + /// Merges the given other config into this one. + pub fn merge(&mut self, other: Self) { + let Self { + backend_type, + backend_configs, + } = other; + if backend_type.is_some() { + self.backend_type = backend_type; + } + for (ty, config) in backend_configs { + match self.backend_configs.entry(ty) { + Entry::Occupied(mut occupied) => occupied.get_mut().extend(config), + Entry::Vacant(vacant) => { + vacant.insert(config); + } + } + } + } + + /// Returns the backend type override. + pub fn backend_type(&self) -> Option<&str> { + self.backend_type.as_deref() + } + + /// Sets the backend type override. + pub fn set_backend_type(&mut self, backend_type: Option) { + self.backend_type = backend_type; + } + + /// Returns an iterator of configured backend types. + pub fn configured_backend_types(&self) -> impl Iterator { + self.backend_configs.keys().map(|ty| ty.as_str()) + } + + /// Attempts to deserialize backend config with the given type. + /// + /// Returns `Ok(None)` if no configuration was provided. + /// Returns `Err` if configuration was provided but deserialization failed. + pub fn backend_config<'a, T: Deserialize<'a>>( + &'a self, + backend_type: &str, + ) -> Result, Error> { + let Some(table) = self.backend_configs.get(backend_type) else { + return Ok(None); + }; + let config = table.clone().try_into().map_err(Error::invalid_config)?; + Ok(Some(config)) + } + + /// Set the backend config of the given type by serializing the given config. + pub fn set_backend_config( + &mut self, + backend_type: String, + backend_config: T, + ) -> Result<(), Error> { + let table = ::toml::Table::try_from(backend_config).map_err(Error::invalid_config)?; + self.backend_configs.insert(backend_type, table); + Ok(()) + } +} + +impl std::fmt::Debug for RegistryConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RegistryConfig") + .field("backend_type", &self.backend_type) + .field( + "backend_configs", + &DebugBackendConfigs(&self.backend_configs), + ) + .finish() + } +} + +// Redact backend configs, which may contain sensitive values. +struct DebugBackendConfigs<'a>(&'a HashMap); + +impl<'a> std::fmt::Debug for DebugBackendConfigs<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map() + .entries(self.0.keys().map(|ty| (ty, &""))) + .finish() + } +} diff --git a/crates/wasm-pkg-common/src/config/toml.rs b/crates/wasm-pkg-common/src/config/toml.rs new file mode 100644 index 0000000..eca737b --- /dev/null +++ b/crates/wasm-pkg-common/src/config/toml.rs @@ -0,0 +1,158 @@ +// TODO: caused by inner bytes::Bytes; probably fixed in Rust 1.79 +#![allow(clippy::mutable_key_type)] + +use std::collections::HashMap; + +use serde::{Deserialize, Deserializer}; + +use crate::{label::Label, package::PackageRef, Error, Registry}; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TomlConfig { + default_registry: Option, + #[serde(default)] + package_registries: HashMap, + #[serde(default)] + registry: HashMap, +} + +impl From for super::Config { + fn from(value: TomlConfig) -> Self { + let TomlConfig { + default_registry, + package_registries: pattern_registries, + registry, + } = value; + + let mut namespace_registries: HashMap = Default::default(); + let mut package_registries: HashMap = Default::default(); + for (pattern, registry) in pattern_registries { + match pattern { + PackagePattern::NamespaceWildcard(namespace) => { + namespace_registries.insert(namespace, registry); + } + PackagePattern::ExactPackage(package) => { + package_registries.insert(package, registry); + } + } + } + + let registry_configs = registry + .into_iter() + .map(|(reg, config)| (reg, config.into())) + .collect(); + + Self { + default_registry, + namespace_registries, + package_registries, + registry_configs, + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Deserialize)] +#[serde(try_from = "String")] +enum PackagePattern { + NamespaceWildcard(Label), + ExactPackage(PackageRef), +} + +impl TryFrom for PackagePattern { + type Error = Error; + + fn try_from(mut value: String) -> Result { + if value.ends_with(":*") { + value.truncate(value.len() - 2); + Ok(PackagePattern::NamespaceWildcard(value.try_into()?)) + } else if value.contains(':') { + Ok(PackagePattern::ExactPackage(value.try_into()?)) + } else { + Err(Error::InvalidPackagePattern( + "keys must be full package names or :* wildcards".into(), + )) + } + } +} + +#[derive(Deserialize)] +struct TomlRegistryConfig { + #[serde(rename = "type")] + type_: Option, + #[serde(flatten)] + backend_configs: HashMap, +} + +impl From for super::RegistryConfig { + fn from(value: TomlRegistryConfig) -> Self { + let TomlRegistryConfig { + type_, + backend_configs, + } = value; + Self { + backend_type: type_, + backend_configs, + } + } +} + +impl<'de> Deserialize<'de> for Registry { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + Registry::try_from(String::deserialize(deserializer)?).map_err(D::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn smoke_test() { + let toml_config = toml::toml! { + default_registry = "example.com" + + [package_registries] + "wasi:*" = "wasi.dev" + "example:foo" = "example.com" + + [registry."wasi.dev".oci] + auth = { username = "open", password = "sesame" } + + [registry."example.com"] + type = "test" + test = { token = "top_secret" } + }; + let wasi_dev: Registry = "wasi.dev".parse().unwrap(); + let example_com: Registry = "example.com".parse().unwrap(); + + let toml_cfg: TomlConfig = toml_config.try_into().unwrap(); + let cfg = crate::config::Config::from(toml_cfg); + + assert_eq!(cfg.default_registry(), Some(&example_com)); + assert_eq!( + cfg.resolve_registry(&"wasi:http".parse().unwrap()), + Some(&wasi_dev) + ); + assert_eq!( + cfg.resolve_registry(&"example:foo".parse().unwrap()), + Some(&example_com) + ); + + #[derive(Deserialize)] + struct TestConfig { + token: String, + } + let test_cfg: TestConfig = cfg + .registry_config(&example_com) + .unwrap() + .backend_config("test") + .unwrap() + .unwrap(); + assert_eq!(test_cfg.token, "top_secret"); + } +} diff --git a/crates/wasm-pkg-common/src/label.rs b/crates/wasm-pkg-common/src/label.rs new file mode 100644 index 0000000..4a6c497 --- /dev/null +++ b/crates/wasm-pkg-common/src/label.rs @@ -0,0 +1,65 @@ +/// A Component Model kebab-case label. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Label(String); + +impl AsRef for Label { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for Label { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Debug for Label { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl TryFrom for Label { + type Error = InvalidLabel; + + fn try_from(label: String) -> Result { + if label.is_empty() { + return Err(InvalidLabel::Empty); + } + for word in label.split('-') { + let mut chars = word.chars(); + match chars.next() { + None => return Err(InvalidLabel::EmptyWord), + Some(ch) if !ch.is_ascii_lowercase() => { + return Err(InvalidLabel::InvalidWordFirstChar) + } + _ => (), + } + if !chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()) { + return Err(InvalidLabel::InvalidChar); + } + } + Ok(Self(label)) + } +} + +impl std::str::FromStr for Label { + type Err = InvalidLabel; + + fn from_str(s: &str) -> Result { + s.to_owned().try_into() + } +} + +#[derive(Debug, thiserror::Error)] +pub enum InvalidLabel { + #[error("labels may not be empty")] + Empty, + #[error("dash-separated words may not be empty")] + EmptyWord, + #[error("dash-separated words may contain only lowercase alphanumeric ASCII characters")] + InvalidChar, + #[error("dash-separated words must begin with an ASCII lowercase letter")] + InvalidWordFirstChar, +} diff --git a/crates/wasm-pkg-common/src/lib.rs b/crates/wasm-pkg-common/src/lib.rs new file mode 100644 index 0000000..afedeae --- /dev/null +++ b/crates/wasm-pkg-common/src/lib.rs @@ -0,0 +1,31 @@ +use http::uri::InvalidUri; + +pub mod config; +mod label; +mod package; +mod registry; + +use label::InvalidLabel; +pub use registry::Registry; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("error reading config file: {0}")] + ConfigFileIoError(#[source] std::io::Error), + #[error("invalid config: {0}")] + InvalidConfig(#[source] Box), + #[error("invalid package pattern: {0}")] + InvalidPackagePattern(String), + #[error("invalid label: {0}")] + InvalidLabel(#[from] InvalidLabel), + #[error("invalid package ref: {0}")] + InvalidPackageRef(String), + #[error("invalid registry: {0}")] + InvalidRegistry(#[from] InvalidUri), +} + +impl Error { + fn invalid_config(err: impl std::error::Error + 'static) -> Self { + Self::InvalidConfig(Box::new(err)) + } +} diff --git a/crates/wasm-pkg-common/src/package.rs b/crates/wasm-pkg-common/src/package.rs new file mode 100644 index 0000000..7e50e2c --- /dev/null +++ b/crates/wasm-pkg-common/src/package.rs @@ -0,0 +1,50 @@ +use crate::{label::Label, Error}; + +/// A package reference, consisting of kebab-case namespace and name, e.g. `wasm-pkg:loader`. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PackageRef { + namespace: Label, + name: Label, +} + +impl PackageRef { + /// Returns the namespace of the package. + pub fn namespace(&self) -> &Label { + &self.namespace + } + + /// Returns the name of the package. + pub fn name(&self) -> &Label { + &self.name + } +} + +impl std::fmt::Display for PackageRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.namespace, self.name) + } +} + +impl TryFrom for PackageRef { + type Error = Error; + + fn try_from(mut value: String) -> Result { + let Some(colon) = value.find(':') else { + return Err(Error::InvalidPackageRef("missing expected ':'".into())); + }; + let name = value.split_off(colon + 1); + value.truncate(colon); + Ok(Self { + namespace: value.parse()?, + name: name.parse()?, + }) + } +} + +impl std::str::FromStr for PackageRef { + type Err = Error; + + fn from_str(s: &str) -> Result { + s.to_string().try_into() + } +} diff --git a/crates/wasm-pkg-common/src/registry.rs b/crates/wasm-pkg-common/src/registry.rs new file mode 100644 index 0000000..3a091a7 --- /dev/null +++ b/crates/wasm-pkg-common/src/registry.rs @@ -0,0 +1,45 @@ +use http::uri::Authority; + +use crate::Error; + +/// A registry identifier, which should be a valid HTTP Host. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Registry(Authority); + +impl Registry { + pub fn host(&self) -> &str { + self.0.host() + } + + pub fn port(&self) -> Option { + self.0.port_u16() + } +} + +impl AsRef for Registry { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl std::fmt::Display for Registry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::str::FromStr for Registry { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(Self(s.parse()?)) + } +} + +impl TryFrom for Registry { + type Error = Error; + + fn try_from(value: String) -> Result { + Ok(Self(value.try_into()?)) + } +} From 78f42ff278ea2397664b10af9c5affc6987bc78b Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Wed, 29 May 2024 09:52:43 -0400 Subject: [PATCH 2/3] common: Add RegistryMetadata --- crates/wasm-pkg-common/Cargo.toml | 1 + crates/wasm-pkg-common/src/lib.rs | 4 +- crates/wasm-pkg-common/src/registry.rs | 202 +++++++++++++++++++++++++ 3 files changed, 206 insertions(+), 1 deletion(-) diff --git a/crates/wasm-pkg-common/Cargo.toml b/crates/wasm-pkg-common/Cargo.toml index 5a633fe..f11a36a 100644 --- a/crates/wasm-pkg-common/Cargo.toml +++ b/crates/wasm-pkg-common/Cargo.toml @@ -11,6 +11,7 @@ license.workspace = true dirs = "5.0.1" http = "1.1.0" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" toml = "0.8.13" thiserror = "1.0" tracing = "0.1" \ No newline at end of file diff --git a/crates/wasm-pkg-common/src/lib.rs b/crates/wasm-pkg-common/src/lib.rs index afedeae..f0b128b 100644 --- a/crates/wasm-pkg-common/src/lib.rs +++ b/crates/wasm-pkg-common/src/lib.rs @@ -3,7 +3,7 @@ use http::uri::InvalidUri; pub mod config; mod label; mod package; -mod registry; +pub mod registry; use label::InvalidLabel; pub use registry::Registry; @@ -22,6 +22,8 @@ pub enum Error { InvalidPackageRef(String), #[error("invalid registry: {0}")] InvalidRegistry(#[from] InvalidUri), + #[error("invalid registry metadata: {0}")] + InvalidRegistryMetadata(#[source] serde_json::Error), } impl Error { diff --git a/crates/wasm-pkg-common/src/registry.rs b/crates/wasm-pkg-common/src/registry.rs index 3a091a7..666d468 100644 --- a/crates/wasm-pkg-common/src/registry.rs +++ b/crates/wasm-pkg-common/src/registry.rs @@ -1,4 +1,10 @@ +use std::{ + borrow::Cow, + collections::{BTreeSet, HashMap}, +}; + use http::uri::Authority; +use serde::{de::DeserializeOwned, Deserialize}; use crate::Error; @@ -7,10 +13,12 @@ use crate::Error; pub struct Registry(Authority); impl Registry { + /// Returns the registry host, without port number. pub fn host(&self) -> &str { self.0.host() } + /// Returns the registry port number, if given. pub fn port(&self) -> Option { self.0.port_u16() } @@ -43,3 +51,197 @@ impl TryFrom for Registry { Ok(Self(value.try_into()?)) } } + +/// Well-Known URI (RFC 8615) path for registry metadata. +pub const REGISTRY_METADATA_PATH: &str = "/.well-known/wasm-pkg/registry.json"; + +type JsonObject = serde_json::Map; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegistryMetadata { + /// The registry's preferred protocol. + preferred_protocol: Option, + + /// Protocol-specific configuration. + #[serde(flatten)] + protocol_configs: HashMap, + + // Backward-compatibility aliases: + /// OCI Registry + oci_registry: Option, + /// OCI Namespace Prefix + oci_namespace_prefix: Option, + /// Warg URL + warg_url: Option, +} + +const OCI_PROTOCOL: &str = "oci"; +const WARG_PROTOCOL: &str = "warg"; + +impl RegistryMetadata { + /// Returns the registry's preferred protocol. + /// + /// The preferred protocol is: + /// - the `preferredProtocol` metadata field, if given + /// - the protocol configuration key, if only one configuration is given + pub fn preferred_protocol(&self) -> Option<&str> { + if let Some(protocol) = self.preferred_protocol.as_deref() { + return Some(protocol); + } + if self.protocol_configs.len() == 1 { + return self.protocol_configs.keys().next().map(|x| x.as_str()); + } + None + } + + /// Returns an iterator of protocols configured by the registry. + pub fn configured_protocols(&self) -> impl Iterator> { + let mut protos: BTreeSet = self.protocol_configs.keys().cloned().collect(); + // Backward-compatibility aliases + if self.oci_registry.is_some() || self.oci_namespace_prefix.is_some() { + protos.insert(OCI_PROTOCOL.into()); + } + if self.warg_url.is_some() { + protos.insert(WARG_PROTOCOL.into()); + } + protos.into_iter().map(Into::into) + } + + /// Deserializes protocol config for the given protocol. + /// + /// Returns `Ok(None)` if no configuration is available for the given + /// protocol. + /// Returns `Err` if configuration is available for the given protocol but + /// deserialization fails. + pub fn protocol_config(&self, protocol: &str) -> Result, Error> { + let mut config = self.protocol_configs.get(protocol).cloned(); + + // Backward-compatibility aliases + let mut maybe_set = |key: &str, val: &Option| { + if let Some(value) = val { + config + .get_or_insert_with(Default::default) + .insert(key.into(), value.clone().into()); + } + }; + match protocol { + OCI_PROTOCOL => { + maybe_set("registry", &self.oci_registry); + maybe_set("namespacePrefix", &self.oci_namespace_prefix); + } + WARG_PROTOCOL => { + maybe_set("url", &self.warg_url); + } + _ => {} + } + + if config.is_none() { + return Ok(None); + } + Ok(Some( + serde_json::from_value(config.unwrap().into()) + .map_err(Error::InvalidRegistryMetadata)?, + )) + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[derive(Deserialize, Debug, PartialEq)] + #[serde(rename_all = "camelCase")] + struct OtherProtocolConfig { + key: String, + } + + #[test] + fn smoke_test() { + let meta: RegistryMetadata = serde_json::from_value(json!({ + "oci": {"registry": "oci.example.com"}, + "warg": {"url": "https://warg.example.com"}, + })) + .unwrap(); + assert_eq!(meta.preferred_protocol(), None); + assert_eq!( + meta.configured_protocols().collect::>(), + ["oci", "warg"] + ); + let oci_config: JsonObject = meta.protocol_config("oci").unwrap().unwrap(); + assert_eq!(oci_config["registry"], "oci.example.com"); + let warg_config: JsonObject = meta.protocol_config("warg").unwrap().unwrap(); + assert_eq!(warg_config["url"], "https://warg.example.com"); + let other_config: Option = meta.protocol_config("other").unwrap(); + assert_eq!(other_config, None); + } + + #[test] + fn preferred_protocol_explicit() { + let meta: RegistryMetadata = serde_json::from_value(json!({ + "preferredProtocol": "warg", + "oci": {"registry": "oci.example.com"}, + "warg": {"url": "https://warg.example.com"}, + })) + .unwrap(); + assert_eq!(meta.preferred_protocol(), Some("warg")); + } + + #[test] + fn preferred_protocol_implicit() { + let meta: RegistryMetadata = serde_json::from_value(json!({ + "oci": {"registry": "oci.example.com"}, + })) + .unwrap(); + assert_eq!(meta.preferred_protocol(), Some("oci")); + } + + #[test] + fn basic_backward_compat_test() { + let meta: RegistryMetadata = serde_json::from_value(json!({ + "ociRegistry": "oci.example.com", + "ociNamespacePrefix": "prefix/", + "wargUrl": "https://warg.example.com", + })) + .unwrap(); + assert_eq!( + meta.configured_protocols().collect::>(), + ["oci", "warg"] + ); + let oci_config: JsonObject = meta.protocol_config("oci").unwrap().unwrap(); + assert_eq!(oci_config["registry"], "oci.example.com"); + assert_eq!(oci_config["namespacePrefix"], "prefix/"); + let warg_config: JsonObject = meta.protocol_config("warg").unwrap().unwrap(); + assert_eq!(warg_config["url"], "https://warg.example.com"); + } + + #[test] + fn merged_backward_compat_test() { + let meta: RegistryMetadata = serde_json::from_value(json!({ + "wargUrl": "https://warg.example.com", + "other": {"key": "value"} + })) + .unwrap(); + assert_eq!( + meta.configured_protocols().collect::>(), + ["other", "warg"] + ); + let warg_config: JsonObject = meta.protocol_config("warg").unwrap().unwrap(); + assert_eq!(warg_config["url"], "https://warg.example.com"); + let other_config: OtherProtocolConfig = meta.protocol_config("other").unwrap().unwrap(); + assert_eq!(other_config.key, "value"); + } + + #[test] + fn bad_protocol_config() { + let meta: RegistryMetadata = serde_json::from_value(json!({ + "other": {"bad": "config"} + })) + .unwrap(); + assert_eq!(meta.configured_protocols().collect::>(), ["other"]); + let res = meta.protocol_config::("other"); + assert!(res.is_err(), "{res:?}"); + } +} From c4936b7610d9fa429612a9fb49566033d1294a5d Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Wed, 29 May 2024 16:08:13 -0400 Subject: [PATCH 3/3] common: Split [package_registries] into [namespace_registries] and [package_registry_overrides] --- Cargo.lock | 1 + Cargo.toml | 1 + crates/wasm-pkg-common/src/config.rs | 24 ++++----- crates/wasm-pkg-common/src/config/toml.rs | 66 +++++------------------ crates/wasm-pkg-common/src/label.rs | 11 +++- crates/wasm-pkg-common/src/package.rs | 11 +++- crates/wasm-pkg-common/src/registry.rs | 11 +++- 7 files changed, 55 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c973143..44ebe39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3770,6 +3770,7 @@ dependencies = [ "dirs", "http", "serde 1.0.201", + "serde_json", "thiserror", "toml 0.8.13", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 96739af..9bd4b2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ tracing-subscriber = { version = "0.3.18", default-features = false, features = "fmt", "env-filter", ] } +wasm-pkg-common = { version = "0.3.0", path = "crates/wasm-pkg-common" } wasm-pkg-loader = { version = "0.3.0", path = "crates/wasm-pkg-loader" } diff --git a/crates/wasm-pkg-common/src/config.rs b/crates/wasm-pkg-common/src/config.rs index 8676fbf..b9b01f6 100644 --- a/crates/wasm-pkg-common/src/config.rs +++ b/crates/wasm-pkg-common/src/config.rs @@ -22,7 +22,7 @@ pub const DEFAULT_REGISTRY: &str = "bytecodealliance.org"; pub struct Config { default_registry: Option, namespace_registries: HashMap, - package_registries: HashMap, + package_registry_overrides: HashMap, registry_configs: HashMap, } @@ -31,7 +31,7 @@ impl Default for Config { Self { default_registry: Some(DEFAULT_REGISTRY.parse().unwrap()), namespace_registries: Default::default(), - package_registries: Default::default(), + package_registry_overrides: Default::default(), registry_configs: Default::default(), } } @@ -46,7 +46,7 @@ impl Config { Self { default_registry: Default::default(), namespace_registries: Default::default(), - package_registries: Default::default(), + package_registry_overrides: Default::default(), registry_configs: Default::default(), } } @@ -100,14 +100,14 @@ impl Config { let Self { default_registry, namespace_registries, - package_registries, + package_registry_overrides: package_registries, registry_configs, } = other; if default_registry.is_some() { self.default_registry = default_registry; } self.namespace_registries.extend(namespace_registries); - self.package_registries.extend(package_registries); + self.package_registry_overrides.extend(package_registries); for (registry, config) in registry_configs { match self.registry_configs.entry(registry) { Entry::Occupied(mut occupied) => occupied.get_mut().merge(config), @@ -125,7 +125,7 @@ impl Config { /// - A namespace registry matching the package's namespace /// - The default registry pub fn resolve_registry(&self, package: &PackageRef) -> Option<&Registry> { - if let Some(reg) = self.package_registries.get(package) { + if let Some(reg) = self.package_registry_overrides.get(package) { Some(reg) } else if let Some(reg) = self.namespace_registries.get(package.namespace()) { Some(reg) @@ -158,16 +158,16 @@ impl Config { self.namespace_registries.insert(namespace, registry); } - /// Returns a registry configured for the given exact package. + /// Returns a registry override configured for the given package. /// /// Does not fall back to namespace or default registries; see [`Self::resolve`]. - pub fn package_registry(&self, package: &PackageRef) -> Option<&Registry> { - self.package_registries.get(package) + pub fn package_registry_override(&self, package: &PackageRef) -> Option<&Registry> { + self.package_registry_overrides.get(package) } - /// Sets a registry for the given exact package. - pub fn set_package_registry(&mut self, package: PackageRef, registry: Registry) { - self.package_registries.insert(package, registry); + /// Sets a registry override for the given package. + pub fn set_package_registry_override(&mut self, package: PackageRef, registry: Registry) { + self.package_registry_overrides.insert(package, registry); } /// Returns [`RegistryConfig`] for the given registry. diff --git a/crates/wasm-pkg-common/src/config/toml.rs b/crates/wasm-pkg-common/src/config/toml.rs index eca737b..b984cda 100644 --- a/crates/wasm-pkg-common/src/config/toml.rs +++ b/crates/wasm-pkg-common/src/config/toml.rs @@ -3,16 +3,18 @@ use std::collections::HashMap; -use serde::{Deserialize, Deserializer}; +use serde::Deserialize; -use crate::{label::Label, package::PackageRef, Error, Registry}; +use crate::{label::Label, package::PackageRef, Registry}; #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct TomlConfig { default_registry: Option, #[serde(default)] - package_registries: HashMap, + namespace_registries: HashMap, + #[serde(default)] + package_registry_overrides: HashMap, #[serde(default)] registry: HashMap, } @@ -21,23 +23,11 @@ impl From for super::Config { fn from(value: TomlConfig) -> Self { let TomlConfig { default_registry, - package_registries: pattern_registries, + namespace_registries, + package_registry_overrides, registry, } = value; - let mut namespace_registries: HashMap = Default::default(); - let mut package_registries: HashMap = Default::default(); - for (pattern, registry) in pattern_registries { - match pattern { - PackagePattern::NamespaceWildcard(namespace) => { - namespace_registries.insert(namespace, registry); - } - PackagePattern::ExactPackage(package) => { - package_registries.insert(package, registry); - } - } - } - let registry_configs = registry .into_iter() .map(|(reg, config)| (reg, config.into())) @@ -46,36 +36,12 @@ impl From for super::Config { Self { default_registry, namespace_registries, - package_registries, + package_registry_overrides, registry_configs, } } } -#[derive(Debug, PartialEq, Eq, Hash, Deserialize)] -#[serde(try_from = "String")] -enum PackagePattern { - NamespaceWildcard(Label), - ExactPackage(PackageRef), -} - -impl TryFrom for PackagePattern { - type Error = Error; - - fn try_from(mut value: String) -> Result { - if value.ends_with(":*") { - value.truncate(value.len() - 2); - Ok(PackagePattern::NamespaceWildcard(value.try_into()?)) - } else if value.contains(':') { - Ok(PackagePattern::ExactPackage(value.try_into()?)) - } else { - Err(Error::InvalidPackagePattern( - "keys must be full package names or :* wildcards".into(), - )) - } - } -} - #[derive(Deserialize)] struct TomlRegistryConfig { #[serde(rename = "type")] @@ -97,16 +63,6 @@ impl From for super::RegistryConfig { } } -impl<'de> Deserialize<'de> for Registry { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - use serde::de::Error; - Registry::try_from(String::deserialize(deserializer)?).map_err(D::Error::custom) - } -} - #[cfg(test)] mod tests { use super::*; @@ -116,8 +72,10 @@ mod tests { let toml_config = toml::toml! { default_registry = "example.com" - [package_registries] - "wasi:*" = "wasi.dev" + [namespace_registries] + wasi = "wasi.dev" + + [package_registry_overrides] "example:foo" = "example.com" [registry."wasi.dev".oci] diff --git a/crates/wasm-pkg-common/src/label.rs b/crates/wasm-pkg-common/src/label.rs index 4a6c497..637cbef 100644 --- a/crates/wasm-pkg-common/src/label.rs +++ b/crates/wasm-pkg-common/src/label.rs @@ -1,5 +1,8 @@ +use serde::{Deserialize, Serialize}; + /// A Component Model kebab-case label. -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(into = "String", try_from = "String")] pub struct Label(String); impl AsRef for Label { @@ -20,6 +23,12 @@ impl std::fmt::Debug for Label { } } +impl From