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