Path: blob/main/crates/environ/src/component/names.rs
1692 views
use crate::prelude::*;1use anyhow::{Result, bail};2use core::hash::Hash;3use semver::Version;4use serde_derive::{Deserialize, Serialize};56/// A semver-aware map for imports/exports of a component.7///8/// This data structure is used when looking up the names of imports/exports of9/// a component to enable semver-compatible matching of lookups. This will10/// enable lookups of `a:b/[email protected]` to match entries defined as `a:b/[email protected]`11/// which is currently considered a key feature of WASI's compatibility story.12///13/// On the outside this looks like a map of `K` to `V`.14#[derive(Clone, Serialize, Deserialize, Debug)]15pub struct NameMap<K: Clone + Hash + Eq + Ord, V> {16/// A map of keys to the value that they define.17///18/// Note that this map is "exact" where the name here is the exact name that19/// was specified when the `insert` was called. This doesn't have any20/// semver-mangling or anything like that.21///22/// This map is always consulted first during lookups.23definitions: IndexMap<K, V>,2425/// An auxiliary map tracking semver-compatible names. This is a map from26/// "semver compatible alternate name" to a name present in `definitions`27/// and the semver version it was registered at.28///29/// An example map would be:30///31/// ```text32/// {33/// "a:b/[email protected]": ("a:b/[email protected]", 0.2.1),34/// "a:b/c@2": ("a:b/[email protected]+abc", 2.0.0+abc),35/// }36/// ```37///38/// As names are inserted into `definitions` each name may have up to one39/// semver-compatible name with extra numbers/info chopped off which is40/// inserted into this map. This map is the lookup table from `@0.2` to41/// `@0.2.x` where `x` is what was inserted manually.42///43/// The `Version` here is tracked to ensure that when multiple versions on44/// one track are defined that only the maximal version here is retained.45alternate_lookups: IndexMap<K, (K, Version)>,46}4748impl<K, V> NameMap<K, V>49where50K: Clone + Hash + Eq + Ord,51{52/// Inserts the `name` specified into this map.53///54/// The name is intern'd through the `cx` argument and shadowing is55/// controlled by the `allow_shadowing` variable.56///57/// This function will automatically insert an entry in58/// `self.alternate_lookups` if `name` is a semver-looking name.59///60/// Returns an error if `allow_shadowing` is `false` and the `name` is61/// already present in this map (by exact match). Otherwise returns the62/// intern'd version of `name`.63pub fn insert<I>(&mut self, name: &str, cx: &mut I, allow_shadowing: bool, item: V) -> Result<K>64where65I: NameMapIntern<Key = K>,66{67// Always insert `name` and `item` as an exact definition.68let key = cx.intern(name);69if let Some(prev) = self.definitions.insert(key.clone(), item) {70if !allow_shadowing {71self.definitions.insert(key, prev);72bail!("map entry `{name}` defined twice")73}74}7576// If `name` is a semver-looking thing, like `a:b/[email protected]`, then also77// insert an entry in the semver-compatible map under a key such as78// `a:b/c@1`.79//80// This key is used during `get` later on.81if let Some((alternate_key, version)) = alternate_lookup_key(name) {82let alternate_key = cx.intern(alternate_key);83if let Some((prev_key, prev_version)) = self84.alternate_lookups85.insert(alternate_key.clone(), (key.clone(), version.clone()))86{87// Prefer the latest version, so only do this if we're88// greater than the prior version.89if version < prev_version {90self.alternate_lookups91.insert(alternate_key, (prev_key, prev_version));92}93}94}95Ok(key)96}9798/// Looks up `name` within this map, using the interning specified by99/// `cx`.100///101/// This may return a definition even if `name` wasn't exactly defined in102/// this map, such as looking up `a:b/[email protected]` when the map only has103/// `a:b/[email protected]` defined.104pub fn get<I>(&self, name: &str, cx: &I) -> Option<&V>105where106I: NameMapIntern<Key = K>,107{108// First look up an exact match and if that's found return that. This109// enables defining multiple versions in the map and the requested110// version is returned if it matches exactly.111let candidate = cx.lookup(name).and_then(|k| self.definitions.get(&k));112if let Some(def) = candidate {113return Some(def);114}115116// Failing that, then try to look for a semver-compatible alternative.117// This looks up the key based on `name`, if any, and then looks to see118// if that was intern'd in `strings`. Given all that look to see if it119// was defined in `alternate_lookups` and finally at the end that exact120// key is then used to look up again in `self.definitions`.121let (alternate_name, _version) = alternate_lookup_key(name)?;122let alternate_key = cx.lookup(alternate_name)?;123let (exact_key, _version) = self.alternate_lookups.get(&alternate_key)?;124self.definitions.get(exact_key)125}126127/// Returns an iterator over inserted values in this map.128///129/// Note that the iterator return yields intern'd keys and additionally does130/// not do anything special with semver names and such, it only literally131/// yields what's been inserted with [`NameMap::insert`].132pub fn raw_iter(&self) -> impl Iterator<Item = (&K, &V)> {133self.definitions.iter()134}135136/// TODO137pub fn raw_get_mut(&mut self, key: &K) -> Option<&mut V> {138self.definitions.get_mut(key)139}140}141142impl<K, V> Default for NameMap<K, V>143where144K: Clone + Hash + Eq + Ord,145{146fn default() -> NameMap<K, V> {147NameMap {148definitions: Default::default(),149alternate_lookups: Default::default(),150}151}152}153154/// A helper trait used in conjunction with [`NameMap`] to optionally intern155/// keys to non-strings.156pub trait NameMapIntern {157/// The key that this interning context generates.158type Key;159160/// Inserts `s` into `self` and returns the intern'd key `Self::Key`.161fn intern(&mut self, s: &str) -> Self::Key;162163/// Looks up `s` in `self` returning `Some` if it was found or `None` if164/// it's not present.165fn lookup(&self, s: &str) -> Option<Self::Key>;166}167168/// For use with [`NameMap`] when no interning should happen and instead string169/// keys are copied as-is.170pub struct NameMapNoIntern;171172impl NameMapIntern for NameMapNoIntern {173type Key = String;174175fn intern(&mut self, s: &str) -> String {176s.to_string()177}178179fn lookup(&self, s: &str) -> Option<String> {180Some(s.to_string())181}182}183184/// Determines a version-based "alternate lookup key" for the `name` specified.185///186/// Some examples are:187///188/// * `foo` => `None`189/// * `foo:bar/baz` => `None`190/// * `foo:bar/[email protected]` => `Some(foo:bar/baz@1)`191/// * `foo:bar/[email protected]` => `Some(foo:bar/[email protected])`192/// * `foo:bar/[email protected]` => `None`193/// * `foo:bar/[email protected]` => `None`194///195/// This alternate lookup key is intended to serve the purpose where a196/// semver-compatible definition can be located, if one is defined, at perhaps197/// either a newer or an older version.198fn alternate_lookup_key(name: &str) -> Option<(&str, Version)> {199let at = name.find('@')?;200let version_string = &name[at + 1..];201let version = Version::parse(version_string).ok()?;202if !version.pre.is_empty() {203// If there's a prerelease then don't consider that compatible with any204// other version number.205None206} else if version.major != 0 {207// If the major number is nonzero then compatibility is up to the major208// version number, so return up to the first decimal.209let first_dot = version_string.find('.')? + at + 1;210Some((&name[..first_dot], version))211} else if version.minor != 0 {212// Like the major version if the minor is nonzero then patch releases213// are all considered to be on a "compatible track".214let first_dot = version_string.find('.')? + at + 1;215let second_dot = name[first_dot + 1..].find('.')? + first_dot + 1;216Some((&name[..second_dot], version))217} else {218// If the patch number is the first nonzero entry then nothing can be219// compatible with this patch, e.g. 0.0.1 isn't' compatible with220// any other version inherently.221None222}223}224225#[cfg(test)]226mod tests {227use super::{NameMap, NameMapNoIntern};228229#[test]230fn alternate_lookup_key() {231fn alt(s: &str) -> Option<&str> {232super::alternate_lookup_key(s).map(|(s, _)| s)233}234235assert_eq!(alt("x"), None);236assert_eq!(alt("x:y/z"), None);237assert_eq!(alt("x:y/[email protected]"), Some("x:y/z@1"));238assert_eq!(alt("x:y/[email protected]"), Some("x:y/z@1"));239assert_eq!(alt("x:y/[email protected]"), Some("x:y/z@1"));240assert_eq!(alt("x:y/[email protected]"), Some("x:y/z@2"));241assert_eq!(alt("x:y/[email protected]+abc"), Some("x:y/z@2"));242assert_eq!(alt("x:y/[email protected]"), Some("x:y/[email protected]"));243assert_eq!(alt("x:y/[email protected]"), Some("x:y/[email protected]"));244assert_eq!(alt("x:y/[email protected]"), Some("x:y/[email protected]"));245assert_eq!(alt("x:y/[email protected]+abc"), Some("x:y/[email protected]"));246assert_eq!(alt("x:y/[email protected]"), None);247assert_eq!(alt("x:y/[email protected]"), None);248assert_eq!(alt("x:y/[email protected]"), None);249assert_eq!(alt("x:y/[email protected]"), None);250}251252#[test]253fn name_map_smoke() {254let mut map = NameMap::default();255let mut intern = NameMapNoIntern;256257map.insert("a", &mut intern, false, 0).unwrap();258map.insert("b", &mut intern, false, 1).unwrap();259260assert!(map.insert("a", &mut intern, false, 0).is_err());261assert!(map.insert("a", &mut intern, true, 0).is_ok());262263assert_eq!(map.get("a", &intern), Some(&0));264assert_eq!(map.get("b", &intern), Some(&1));265assert_eq!(map.get("c", &intern), None);266267map.insert("a:b/[email protected]", &mut intern, false, 2).unwrap();268map.insert("a:b/[email protected]", &mut intern, false, 3).unwrap();269assert_eq!(map.get("a:b/[email protected]", &intern), Some(&2));270assert_eq!(map.get("a:b/[email protected]", &intern), Some(&3));271assert_eq!(map.get("a:b/[email protected]", &intern), Some(&3));272assert_eq!(map.get("a:b/[email protected]", &intern), Some(&3));273}274}275276277