//! Definitions of name-related helpers and newtypes, primarily for the //! component model. use crate::prelude::*; use crate::{Result, WasmFeatures}; use core::borrow::Borrow; use core::cmp::Ordering; use core::fmt; use core::hash::{Hash, Hasher}; use core::ops::Deref; use semver::Version; /// Represents a kebab string slice used in validation. /// /// This is a wrapper around `str` that ensures the slice is /// a valid kebab case string according to the component model /// specification. /// /// It also provides an equality and hashing implementation /// that ignores ASCII case. #[derive(Debug, Eq)] #[repr(transparent)] pub struct KebabStr(str); impl KebabStr { /// Creates a new kebab string slice. /// /// Returns `None` if the given string is not a valid kebab string. pub fn new<'a>(s: impl AsRef + 'a) -> Option<&'a Self> { let s = Self::new_unchecked(s); if s.is_kebab_case() { Some(s) } else { None } } pub(crate) fn new_unchecked<'a>(s: impl AsRef + 'a) -> &'a Self { // Safety: `KebabStr` is a transparent wrapper around `str` // Therefore transmuting `&str` to `&KebabStr` is safe. #[allow(unsafe_code)] unsafe { core::mem::transmute::<_, &Self>(s.as_ref()) } } /// Gets the underlying string slice. pub fn as_str(&self) -> &str { &self.0 } /// Converts the slice to an owned string. pub fn to_kebab_string(&self) -> KebabString { KebabString(self.to_string()) } fn is_kebab_case(&self) -> bool { let mut lower = false; let mut upper = false; for c in self.chars() { match c { 'a'..='z' if !lower && !upper => lower = true, 'A'..='Z' if !lower && !upper => upper = true, 'a'..='z' if lower => {} 'A'..='Z' if upper => {} '0'..='9' if lower || upper => {} '-' if lower || upper => { lower = false; upper = false; } _ => return false, } } !self.is_empty() && !self.ends_with('-') } } impl Deref for KebabStr { type Target = str; fn deref(&self) -> &str { self.as_str() } } impl PartialEq for KebabStr { fn eq(&self, other: &Self) -> bool { if self.len() != other.len() { return false; } self.chars() .zip(other.chars()) .all(|(a, b)| a.to_ascii_lowercase() == b.to_ascii_lowercase()) } } impl PartialEq for KebabStr { fn eq(&self, other: &KebabString) -> bool { self.eq(other.as_kebab_str()) } } impl Ord for KebabStr { fn cmp(&self, other: &Self) -> Ordering { let self_chars = self.chars().map(|c| c.to_ascii_lowercase()); let other_chars = other.chars().map(|c| c.to_ascii_lowercase()); self_chars.cmp(other_chars) } } impl PartialOrd for KebabStr { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Hash for KebabStr { fn hash(&self, state: &mut H) { self.len().hash(state); for b in self.chars() { b.to_ascii_lowercase().hash(state); } } } impl fmt::Display for KebabStr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { (self as &str).fmt(f) } } impl ToOwned for KebabStr { type Owned = KebabString; fn to_owned(&self) -> Self::Owned { self.to_kebab_string() } } /// Represents an owned kebab string for validation. /// /// This is a wrapper around `String` that ensures the string is /// a valid kebab case string according to the component model /// specification. /// /// It also provides an equality and hashing implementation /// that ignores ASCII case. #[derive(Debug, Clone, Eq)] pub struct KebabString(String); impl KebabString { /// Creates a new kebab string. /// /// Returns `None` if the given string is not a valid kebab string. pub fn new(s: impl Into) -> Option { let s = s.into(); if KebabStr::new(&s).is_some() { Some(Self(s)) } else { None } } /// Gets the underlying string. pub fn as_str(&self) -> &str { self.0.as_str() } /// Converts the kebab string to a kebab string slice. pub fn as_kebab_str(&self) -> &KebabStr { // Safety: internal string is always valid kebab-case KebabStr::new_unchecked(self.as_str()) } } impl Deref for KebabString { type Target = KebabStr; fn deref(&self) -> &Self::Target { self.as_kebab_str() } } impl Borrow for KebabString { fn borrow(&self) -> &KebabStr { self.as_kebab_str() } } impl Ord for KebabString { fn cmp(&self, other: &Self) -> Ordering { self.as_kebab_str().cmp(other.as_kebab_str()) } } impl PartialOrd for KebabString { fn partial_cmp(&self, other: &Self) -> Option { self.as_kebab_str().partial_cmp(other.as_kebab_str()) } } impl PartialEq for KebabString { fn eq(&self, other: &Self) -> bool { self.as_kebab_str().eq(other.as_kebab_str()) } } impl PartialEq for KebabString { fn eq(&self, other: &KebabStr) -> bool { self.as_kebab_str().eq(other) } } impl Hash for KebabString { fn hash(&self, state: &mut H) { self.as_kebab_str().hash(state) } } impl fmt::Display for KebabString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.as_kebab_str().fmt(f) } } impl From for String { fn from(s: KebabString) -> String { s.0 } } /// An import or export name in the component model which is backed by `T`, /// which defaults to `String`. /// /// This name can be either: /// /// * a plain label or "kebab string": `a-b-c` /// * a plain method name : `[method]a-b.c-d` /// * a plain static method name : `[static]a-b.c-d` /// * a plain constructor: `[constructor]a-b` /// * an interface name: `wasi:cli/reactor@0.1.0` /// * a dependency name: `locked-dep=foo:bar/baz` /// * a URL name: `url=https://..` /// * a hash name: `integrity=sha256:...` /// /// # Equality and hashing /// /// Note that this type the `[method]...` and `[static]...` variants are /// considered equal and hash to the same value. This enables disallowing /// clashes between the two where method name overlap cannot happen. #[derive(Clone)] pub struct ComponentName { raw: String, kind: ParsedComponentNameKind, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] enum ParsedComponentNameKind { Label, Constructor, Method, Static, Interface, Dependency, Url, Hash, } /// Created via [`ComponentName::kind`] and classifies a name. #[derive(Debug, Clone)] pub enum ComponentNameKind<'a> { /// `a-b-c` Label(&'a KebabStr), /// `[constructor]a-b` Constructor(&'a KebabStr), /// `[method]a-b.c-d` #[allow(missing_docs)] Method(ResourceFunc<'a>), /// `[static]a-b.c-d` #[allow(missing_docs)] Static(ResourceFunc<'a>), /// `wasi:http/types@2.0` #[allow(missing_docs)] Interface(InterfaceName<'a>), /// `locked-dep=foo:bar/baz` #[allow(missing_docs)] Dependency(DependencyName<'a>), /// `url=https://...` #[allow(missing_docs)] Url(UrlName<'a>), /// `integrity=sha256:...` #[allow(missing_docs)] Hash(HashName<'a>), } const CONSTRUCTOR: &str = "[constructor]"; const METHOD: &str = "[method]"; const STATIC: &str = "[static]"; impl ComponentName { /// Attempts to parse `name` as a valid component name, returning `Err` if /// it's not valid. pub fn new(name: &str, offset: usize) -> Result { Self::new_with_features(name, offset, WasmFeatures::default()) } /// Attempts to parse `name` as a valid component name, returning `Err` if /// it's not valid. /// /// `features` can be used to enable or disable validation of certain forms /// of supported import names. pub fn new_with_features(name: &str, offset: usize, features: WasmFeatures) -> Result { let mut parser = ComponentNameParser { next: name, offset, features, }; let kind = parser.parse()?; if !parser.next.is_empty() { bail!(offset, "trailing characters found: `{}`", parser.next); } Ok(ComponentName { raw: name.to_string(), kind, }) } /// Returns the [`ComponentNameKind`] corresponding to this name. pub fn kind(&self) -> ComponentNameKind<'_> { use ComponentNameKind::*; use ParsedComponentNameKind as PK; match self.kind { PK::Label => Label(KebabStr::new_unchecked(&self.raw)), PK::Constructor => Constructor(KebabStr::new_unchecked(&self.raw[CONSTRUCTOR.len()..])), PK::Method => Method(ResourceFunc(&self.raw[METHOD.len()..])), PK::Static => Static(ResourceFunc(&self.raw[STATIC.len()..])), PK::Interface => Interface(InterfaceName(&self.raw)), PK::Dependency => Dependency(DependencyName(&self.raw)), PK::Url => Url(UrlName(&self.raw)), PK::Hash => Hash(HashName(&self.raw)), } } /// Returns the raw underlying name as a string. pub fn as_str(&self) -> &str { &self.raw } } impl From for String { fn from(name: ComponentName) -> String { name.raw } } impl Hash for ComponentName { fn hash(&self, hasher: &mut H) { self.kind().hash(hasher) } } impl PartialEq for ComponentName { fn eq(&self, other: &ComponentName) -> bool { self.kind().eq(&other.kind()) } } impl Eq for ComponentName {} impl Ord for ComponentName { fn cmp(&self, other: &ComponentName) -> Ordering { self.kind().cmp(&other.kind()) } } impl PartialOrd for ComponentName { fn partial_cmp(&self, other: &Self) -> Option { self.kind.partial_cmp(&other.kind) } } impl fmt::Display for ComponentName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.raw.fmt(f) } } impl fmt::Debug for ComponentName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.raw.fmt(f) } } impl ComponentNameKind<'_> { /// Returns the [`ParsedComponentNameKind`] of the [`ComponentNameKind`]. fn kind(&self) -> ParsedComponentNameKind { match self { Self::Label(_) => ParsedComponentNameKind::Label, Self::Constructor(_) => ParsedComponentNameKind::Constructor, Self::Method(_) => ParsedComponentNameKind::Method, Self::Static(_) => ParsedComponentNameKind::Static, Self::Interface(_) => ParsedComponentNameKind::Interface, Self::Dependency(_) => ParsedComponentNameKind::Dependency, Self::Url(_) => ParsedComponentNameKind::Url, Self::Hash(_) => ParsedComponentNameKind::Hash, } } } impl Ord for ComponentNameKind<'_> { fn cmp(&self, other: &Self) -> Ordering { match self.kind().cmp(&other.kind()) { Ordering::Equal => (), unequal => return unequal, } match (self, other) { (ComponentNameKind::Label(lhs), ComponentNameKind::Label(rhs)) => lhs.cmp(rhs), (ComponentNameKind::Constructor(lhs), ComponentNameKind::Constructor(rhs)) => { lhs.cmp(rhs) } (ComponentNameKind::Method(lhs), ComponentNameKind::Method(rhs)) => lhs.cmp(rhs), (ComponentNameKind::Method(lhs), ComponentNameKind::Static(rhs)) => lhs.cmp(rhs), (ComponentNameKind::Static(lhs), ComponentNameKind::Method(rhs)) => lhs.cmp(rhs), (ComponentNameKind::Static(lhs), ComponentNameKind::Static(rhs)) => lhs.cmp(rhs), (ComponentNameKind::Interface(lhs), ComponentNameKind::Interface(rhs)) => lhs.cmp(rhs), (ComponentNameKind::Dependency(lhs), ComponentNameKind::Dependency(rhs)) => { lhs.cmp(rhs) } (ComponentNameKind::Url(lhs), ComponentNameKind::Url(rhs)) => lhs.cmp(rhs), (ComponentNameKind::Hash(lhs), ComponentNameKind::Hash(rhs)) => lhs.cmp(rhs), _ => unreachable!("already compared for different kinds above"), } } } impl PartialOrd for ComponentNameKind<'_> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Hash for ComponentNameKind<'_> { fn hash(&self, hasher: &mut H) { use ComponentNameKind::*; match self { Label(name) => (0u8, name).hash(hasher), Constructor(name) => (1u8, name).hash(hasher), // for hashing method == static Method(name) | Static(name) => (2u8, name).hash(hasher), Interface(name) => (3u8, name).hash(hasher), Dependency(name) => (4u8, name).hash(hasher), Url(name) => (5u8, name).hash(hasher), Hash(name) => (6u8, name).hash(hasher), } } } impl PartialEq for ComponentNameKind<'_> { fn eq(&self, other: &ComponentNameKind<'_>) -> bool { use ComponentNameKind::*; match (self, other) { (Label(a), Label(b)) => a == b, (Label(_), _) => false, (Constructor(a), Constructor(b)) => a == b, (Constructor(_), _) => false, // method == static for the purposes of hashing so equate them here // as well. (Method(a), Method(b)) | (Static(a), Static(b)) | (Method(a), Static(b)) | (Static(a), Method(b)) => a == b, (Method(_), _) => false, (Static(_), _) => false, (Interface(a), Interface(b)) => a == b, (Interface(_), _) => false, (Dependency(a), Dependency(b)) => a == b, (Dependency(_), _) => false, (Url(a), Url(b)) => a == b, (Url(_), _) => false, (Hash(a), Hash(b)) => a == b, (Hash(_), _) => false, } } } impl Eq for ComponentNameKind<'_> {} /// A resource name and its function, stored as `a.b`. #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct ResourceFunc<'a>(&'a str); impl<'a> ResourceFunc<'a> { /// Returns the the underlying string as `a.b` pub fn as_str(&self) -> &'a str { self.0 } /// Returns the resource name or the `a` in `a.b` pub fn resource(&self) -> &'a KebabStr { let dot = self.0.find('.').unwrap(); KebabStr::new_unchecked(&self.0[..dot]) } } /// An interface name, stored as `a:b/c@1.2.3` #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct InterfaceName<'a>(&'a str); impl<'a> InterfaceName<'a> { /// Returns the entire underlying string. pub fn as_str(&self) -> &'a str { self.0 } /// Returns the `a:b` in `a:b:c/d/e` pub fn namespace(&self) -> &'a KebabStr { let colon = self.0.rfind(':').unwrap(); KebabStr::new_unchecked(&self.0[..colon]) } /// Returns the `c` in `a:b:c/d/e` pub fn package(&self) -> &'a KebabStr { let colon = self.0.rfind(':').unwrap(); let slash = self.0.find('/').unwrap(); KebabStr::new_unchecked(&self.0[colon + 1..slash]) } /// Returns the `d` in `a:b:c/d/e`. pub fn interface(&self) -> &'a KebabStr { let projection = self.projection(); let slash = projection.find('/').unwrap_or(projection.len()); KebabStr::new_unchecked(&projection[..slash]) } /// Returns the `d/e` in `a:b:c/d/e` pub fn projection(&self) -> &'a KebabStr { let slash = self.0.find('/').unwrap(); let at = self.0.find('@').unwrap_or(self.0.len()); KebabStr::new_unchecked(&self.0[slash + 1..at]) } /// Returns the `1.2.3` in `a:b:c/d/e@1.2.3` pub fn version(&self) -> Option { let at = self.0.find('@')?; Some(Version::parse(&self.0[at + 1..]).unwrap()) } } /// A dependency on an implementation either as `locked-dep=...` or /// `unlocked-dep=...` #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct DependencyName<'a>(&'a str); impl<'a> DependencyName<'a> { /// Returns entire underlying import string pub fn as_str(&self) -> &'a str { self.0 } } /// A dependency on an implementation either as `url=...` #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct UrlName<'a>(&'a str); impl<'a> UrlName<'a> { /// Returns entire underlying import string pub fn as_str(&self) -> &'a str { self.0 } } /// A dependency on an implementation either as `integrity=...`. #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct HashName<'a>(&'a str); impl<'a> HashName<'a> { /// Returns entire underlying import string. pub fn as_str(&self) -> &'a str { self.0 } } // A small helper structure to parse `self.next` which is an import or export // name. // // Methods will update `self.next` as they go along and `self.offset` is used // for error messages. struct ComponentNameParser<'a> { next: &'a str, offset: usize, features: WasmFeatures, } impl<'a> ComponentNameParser<'a> { fn parse(&mut self) -> Result { if self.eat_str(CONSTRUCTOR) { self.expect_kebab()?; return Ok(ParsedComponentNameKind::Constructor); } if self.eat_str(METHOD) { let resource = self.take_until('.')?; self.kebab(resource)?; self.expect_kebab()?; return Ok(ParsedComponentNameKind::Method); } if self.eat_str(STATIC) { let resource = self.take_until('.')?; self.kebab(resource)?; self.expect_kebab()?; return Ok(ParsedComponentNameKind::Static); } // 'unlocked-dep=<' '>' if self.eat_str("unlocked-dep=") { self.expect_str("<")?; self.pkg_name_query()?; self.expect_str(">")?; return Ok(ParsedComponentNameKind::Dependency); } // 'locked-dep=<' '>' ( ',' )? if self.eat_str("locked-dep=") { self.expect_str("<")?; self.pkg_name(false)?; self.expect_str(">")?; self.eat_optional_hash()?; return Ok(ParsedComponentNameKind::Dependency); } // 'url=<' '>' (',' )? if self.eat_str("url=") { self.expect_str("<")?; let url = self.take_up_to('>')?; if url.contains('<') { bail!(self.offset, "url cannot contain `<`"); } self.expect_str(">")?; self.eat_optional_hash()?; return Ok(ParsedComponentNameKind::Url); } // 'integrity=<' '>' if self.eat_str("integrity=") { self.expect_str("<")?; let _hash = self.parse_hash()?; self.expect_str(">")?; return Ok(ParsedComponentNameKind::Hash); } if self.next.contains(':') { self.pkg_name(true)?; Ok(ParsedComponentNameKind::Interface) } else { self.expect_kebab()?; Ok(ParsedComponentNameKind::Label) } } // pkgnamequery ::= ? fn pkg_name_query(&mut self) -> Result<()> { self.pkg_path(false)?; if self.eat_str("@") { if self.eat_str("*") { return Ok(()); } self.expect_str("{")?; let range = self.take_up_to('}')?; self.expect_str("}")?; self.semver_range(range)?; } Ok(()) } // pkgname ::= ? fn pkg_name(&mut self, require_projection: bool) -> Result<()> { self.pkg_path(require_projection)?; if self.eat_str("@") { let version = match self.eat_up_to('>') { Some(version) => version, None => self.take_rest(), }; self.semver(version)?; } Ok(()) } // pkgpath ::= +