1 use crate::error::{Result, bail};
2 use crate::prelude::*;
3 use core::hash::Hash;
4 use semver::Version;
5 use 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/[email protected]` to match entries defined as `a:b/[email protected]`
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)]
16 pub 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/[email protected]": ("a:b/[email protected]", 0.2.1),
35     ///     "a:b/c@2": ("a:b/[email protected]+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 
49 impl<K, V> NameMap<K, V>
50 where
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`.
insert<I>(&mut self, name: &str, cx: &mut I, allow_shadowing: bool, item: V) -> Result<K> where I: NameMapIntern<Key = K>,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/[email protected]`, 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/[email protected]` when the map only has
104     /// `a:b/[email protected]` defined.
get<I>(&self, name: &str, cx: &I) -> Option<&V> where I: NameMapIntern<Key = K>,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`].
raw_iter(&self) -> impl Iterator<Item = (&K, &V)>133     pub fn raw_iter(&self) -> impl Iterator<Item = (&K, &V)> {
134         self.definitions.iter()
135     }
136 
137     /// TODO
raw_get_mut(&mut self, key: &K) -> Option<&mut V>138     pub fn raw_get_mut(&mut self, key: &K) -> Option<&mut V> {
139         self.definitions.get_mut(key)
140     }
141 }
142 
143 impl<K, V> Default for NameMap<K, V>
144 where
145     K: Clone + Hash + Eq + Ord,
146 {
default() -> NameMap<K, V>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.
157 pub 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`.
intern(&mut self, s: &str) -> Self::Key162     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.
lookup(&self, s: &str) -> Option<Self::Key>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.
171 pub struct NameMapNoIntern;
172 
173 impl NameMapIntern for NameMapNoIntern {
174     type Key = String;
175 
intern(&mut self, s: &str) -> String176     fn intern(&mut self, s: &str) -> String {
177         s.to_string()
178     }
179 
lookup(&self, s: &str) -> Option<String>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.
alternate_lookup_key(name: &str) -> Option<(&str, Version)>199 fn 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)]
227 mod tests {
228     use super::{NameMap, NameMapNoIntern};
229 
230     #[test]
alternate_lookup_key()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/[email protected]"), Some("x:y/z@1"));
239         assert_eq!(alt("x:y/[email protected]"), Some("x:y/z@1"));
240         assert_eq!(alt("x:y/[email protected]"), Some("x:y/z@1"));
241         assert_eq!(alt("x:y/[email protected]"), Some("x:y/z@2"));
242         assert_eq!(alt("x:y/[email protected]+abc"), Some("x:y/z@2"));
243         assert_eq!(alt("x:y/[email protected]"), Some("x:y/[email protected]"));
244         assert_eq!(alt("x:y/[email protected]"), Some("x:y/[email protected]"));
245         assert_eq!(alt("x:y/[email protected]"), Some("x:y/[email protected]"));
246         assert_eq!(alt("x:y/[email protected]+abc"), Some("x:y/[email protected]"));
247         assert_eq!(alt("x:y/[email protected]"), None);
248         assert_eq!(alt("x:y/[email protected]"), None);
249         assert_eq!(alt("x:y/[email protected]"), None);
250         assert_eq!(alt("x:y/[email protected]"), None);
251     }
252 
253     #[test]
name_map_smoke()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/[email protected]", &mut intern, false, 2).unwrap();
269         map.insert("a:b/[email protected]", &mut intern, false, 3).unwrap();
270         assert_eq!(map.get("a:b/[email protected]", &intern), Some(&2));
271         assert_eq!(map.get("a:b/[email protected]", &intern), Some(&3));
272         assert_eq!(map.get("a:b/[email protected]", &intern), Some(&3));
273         assert_eq!(map.get("a:b/[email protected]", &intern), Some(&3));
274     }
275 }
276