wasmtime_environ/component/
names.rs

1use crate::prelude::*;
2use anyhow::{bail, Result};
3use core::hash::Hash;
4use semver::Version;
5use serde_derive::{Deserialize, Serialize};
6
7/// A semver-aware map for imports/exports of a component.
8///
9/// This data structure is used when looking up the names of imports/exports of
10/// a component to enable semver-compatible matching of lookups. This will
11/// enable lookups of `a:b/c@0.2.0` to match entries defined as `a:b/c@0.2.1`
12/// which is currently considered a key feature of WASI's compatibility story.
13///
14/// On the outside this looks like a map of `K` to `V`.
15#[derive(Clone, Serialize, Deserialize, Debug)]
16pub struct NameMap<K: Clone + Hash + Eq + Ord, V> {
17    /// A map of keys to the value that they define.
18    ///
19    /// Note that this map is "exact" where the name here is the exact name that
20    /// was specified when the `insert` was called. This doesn't have any
21    /// semver-mangling or anything like that.
22    ///
23    /// This map is always consulted first during lookups.
24    definitions: IndexMap<K, V>,
25
26    /// An auxiliary map tracking semver-compatible names. This is a map from
27    /// "semver compatible alternate name" to a name present in `definitions`
28    /// and the semver version it was registered at.
29    ///
30    /// An example map would be:
31    ///
32    /// ```text
33    /// {
34    ///     "a:b/c@0.2": ("a:b/c@0.2.1", 0.2.1),
35    ///     "a:b/c@2": ("a:b/c@2.0.0+abc", 2.0.0+abc),
36    /// }
37    /// ```
38    ///
39    /// As names are inserted into `definitions` each name may have up to one
40    /// semver-compatible name with extra numbers/info chopped off which is
41    /// inserted into this map. This map is the lookup table from `@0.2` to
42    /// `@0.2.x` where `x` is what was inserted manually.
43    ///
44    /// The `Version` here is tracked to ensure that when multiple versions on
45    /// one track are defined that only the maximal version here is retained.
46    alternate_lookups: IndexMap<K, (K, Version)>,
47}
48
49impl<K, V> NameMap<K, V>
50where
51    K: Clone + Hash + Eq + Ord,
52{
53    /// Inserts the `name` specified into this map.
54    ///
55    /// The name is intern'd through the `cx` argument and shadowing is
56    /// controlled by the `allow_shadowing` variable.
57    ///
58    /// This function will automatically insert an entry in
59    /// `self.alternate_lookups` if `name` is a semver-looking name.
60    ///
61    /// Returns an error if `allow_shadowing` is `false` and the `name` is
62    /// already present in this map (by exact match). Otherwise returns the
63    /// intern'd version of `name`.
64    pub fn insert<I>(&mut self, name: &str, cx: &mut I, allow_shadowing: bool, item: V) -> Result<K>
65    where
66        I: NameMapIntern<Key = K>,
67    {
68        // Always insert `name` and `item` as an exact definition.
69        let key = cx.intern(name);
70        if let Some(prev) = self.definitions.insert(key.clone(), item) {
71            if !allow_shadowing {
72                self.definitions.insert(key, prev);
73                bail!("map entry `{name}` defined twice")
74            }
75        }
76
77        // If `name` is a semver-looking thing, like `a:b/c@1.0.0`, then also
78        // insert an entry in the semver-compatible map under a key such as
79        // `a:b/c@1`.
80        //
81        // This key is used during `get` later on.
82        if let Some((alternate_key, version)) = alternate_lookup_key(name) {
83            let alternate_key = cx.intern(alternate_key);
84            if let Some((prev_key, prev_version)) = self
85                .alternate_lookups
86                .insert(alternate_key.clone(), (key.clone(), version.clone()))
87            {
88                // Prefer the latest version, so only do this if we're
89                // greater than the prior version.
90                if version < prev_version {
91                    self.alternate_lookups
92                        .insert(alternate_key, (prev_key, prev_version));
93                }
94            }
95        }
96        Ok(key)
97    }
98
99    /// Looks up `name` within this map, using the interning specified by
100    /// `cx`.
101    ///
102    /// This may return a definition even if `name` wasn't exactly defined in
103    /// this map, such as looking up `a:b/c@0.2.0` when the map only has
104    /// `a:b/c@0.2.1` defined.
105    pub fn get<I>(&self, name: &str, cx: &I) -> Option<&V>
106    where
107        I: NameMapIntern<Key = K>,
108    {
109        // First look up an exact match and if that's found return that. This
110        // enables defining multiple versions in the map and the requested
111        // version is returned if it matches exactly.
112        let candidate = cx.lookup(name).and_then(|k| self.definitions.get(&k));
113        if let Some(def) = candidate {
114            return Some(def);
115        }
116
117        // Failing that, then try to look for a semver-compatible alternative.
118        // This looks up the key based on `name`, if any, and then looks to see
119        // if that was intern'd in `strings`. Given all that look to see if it
120        // was defined in `alternate_lookups` and finally at the end that exact
121        // key is then used to look up again in `self.definitions`.
122        let (alternate_name, _version) = alternate_lookup_key(name)?;
123        let alternate_key = cx.lookup(alternate_name)?;
124        let (exact_key, _version) = self.alternate_lookups.get(&alternate_key)?;
125        self.definitions.get(exact_key)
126    }
127
128    /// Returns an iterator over inserted values in this map.
129    ///
130    /// Note that the iterator return yields intern'd keys and additionally does
131    /// not do anything special with semver names and such, it only literally
132    /// yields what's been inserted with [`NameMap::insert`].
133    pub fn raw_iter(&self) -> impl Iterator<Item = (&K, &V)> {
134        self.definitions.iter()
135    }
136
137    /// TODO
138    pub fn raw_get_mut(&mut self, key: &K) -> Option<&mut V> {
139        self.definitions.get_mut(key)
140    }
141}
142
143impl<K, V> Default for NameMap<K, V>
144where
145    K: Clone + Hash + Eq + Ord,
146{
147    fn default() -> NameMap<K, V> {
148        NameMap {
149            definitions: Default::default(),
150            alternate_lookups: Default::default(),
151        }
152    }
153}
154
155/// A helper trait used in conjunction with [`NameMap`] to optionally intern
156/// keys to non-strings.
157pub trait NameMapIntern {
158    /// The key that this interning context generates.
159    type Key;
160
161    /// Inserts `s` into `self` and returns the intern'd key `Self::Key`.
162    fn intern(&mut self, s: &str) -> Self::Key;
163
164    /// Looks up `s` in `self` returning `Some` if it was found or `None` if
165    /// it's not present.
166    fn lookup(&self, s: &str) -> Option<Self::Key>;
167}
168
169/// For use with [`NameMap`] when no interning should happen and instead string
170/// keys are copied as-is.
171pub struct NameMapNoIntern;
172
173impl NameMapIntern for NameMapNoIntern {
174    type Key = String;
175
176    fn intern(&mut self, s: &str) -> String {
177        s.to_string()
178    }
179
180    fn lookup(&self, s: &str) -> Option<String> {
181        Some(s.to_string())
182    }
183}
184
185/// Determines a version-based "alternate lookup key" for the `name` specified.
186///
187/// Some examples are:
188///
189/// * `foo` => `None`
190/// * `foo:bar/baz` => `None`
191/// * `foo:bar/baz@1.1.2` => `Some(foo:bar/baz@1)`
192/// * `foo:bar/baz@0.1.0` => `Some(foo:bar/baz@0.1)`
193/// * `foo:bar/baz@0.0.1` => `None`
194/// * `foo:bar/baz@0.1.0-rc.2` => `None`
195///
196/// This alternate lookup key is intended to serve the purpose where a
197/// semver-compatible definition can be located, if one is defined, at perhaps
198/// either a newer or an older version.
199fn alternate_lookup_key(name: &str) -> Option<(&str, Version)> {
200    let at = name.find('@')?;
201    let version_string = &name[at + 1..];
202    let version = Version::parse(version_string).ok()?;
203    if !version.pre.is_empty() {
204        // If there's a prerelease then don't consider that compatible with any
205        // other version number.
206        None
207    } else if version.major != 0 {
208        // If the major number is nonzero then compatibility is up to the major
209        // version number, so return up to the first decimal.
210        let first_dot = version_string.find('.')? + at + 1;
211        Some((&name[..first_dot], version))
212    } else if version.minor != 0 {
213        // Like the major version if the minor is nonzero then patch releases
214        // are all considered to be on a "compatible track".
215        let first_dot = version_string.find('.')? + at + 1;
216        let second_dot = name[first_dot + 1..].find('.')? + first_dot + 1;
217        Some((&name[..second_dot], version))
218    } else {
219        // If the patch number is the first nonzero entry then nothing can be
220        // compatible with this patch, e.g. 0.0.1 isn't' compatible with
221        // any other version inherently.
222        None
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::{NameMap, NameMapNoIntern};
229
230    #[test]
231    fn alternate_lookup_key() {
232        fn alt(s: &str) -> Option<&str> {
233            super::alternate_lookup_key(s).map(|(s, _)| s)
234        }
235
236        assert_eq!(alt("x"), None);
237        assert_eq!(alt("x:y/z"), None);
238        assert_eq!(alt("x:y/z@1.0.0"), Some("x:y/z@1"));
239        assert_eq!(alt("x:y/z@1.1.0"), Some("x:y/z@1"));
240        assert_eq!(alt("x:y/z@1.1.2"), Some("x:y/z@1"));
241        assert_eq!(alt("x:y/z@2.1.2"), Some("x:y/z@2"));
242        assert_eq!(alt("x:y/z@2.1.2+abc"), Some("x:y/z@2"));
243        assert_eq!(alt("x:y/z@0.1.2"), Some("x:y/z@0.1"));
244        assert_eq!(alt("x:y/z@0.1.3"), Some("x:y/z@0.1"));
245        assert_eq!(alt("x:y/z@0.2.3"), Some("x:y/z@0.2"));
246        assert_eq!(alt("x:y/z@0.2.3+abc"), Some("x:y/z@0.2"));
247        assert_eq!(alt("x:y/z@0.0.1"), None);
248        assert_eq!(alt("x:y/z@0.0.1-pre"), None);
249        assert_eq!(alt("x:y/z@0.1.0-pre"), None);
250        assert_eq!(alt("x:y/z@1.0.0-pre"), None);
251    }
252
253    #[test]
254    fn name_map_smoke() {
255        let mut map = NameMap::default();
256        let mut intern = NameMapNoIntern;
257
258        map.insert("a", &mut intern, false, 0).unwrap();
259        map.insert("b", &mut intern, false, 1).unwrap();
260
261        assert!(map.insert("a", &mut intern, false, 0).is_err());
262        assert!(map.insert("a", &mut intern, true, 0).is_ok());
263
264        assert_eq!(map.get("a", &intern), Some(&0));
265        assert_eq!(map.get("b", &intern), Some(&1));
266        assert_eq!(map.get("c", &intern), None);
267
268        map.insert("a:b/c@1.0.0", &mut intern, false, 2).unwrap();
269        map.insert("a:b/c@1.0.1", &mut intern, false, 3).unwrap();
270        assert_eq!(map.get("a:b/c@1.0.0", &intern), Some(&2));
271        assert_eq!(map.get("a:b/c@1.0.1", &intern), Some(&3));
272        assert_eq!(map.get("a:b/c@1.0.2", &intern), Some(&3));
273        assert_eq!(map.get("a:b/c@1.1.0", &intern), Some(&3));
274    }
275}