/src/wasm-tools/crates/wasmparser/src/validator/names.rs
Line | Count | Source |
1 | | //! Definitions of name-related helpers and newtypes, primarily for the |
2 | | //! component model. |
3 | | |
4 | | use crate::prelude::*; |
5 | | use crate::{Result, WasmFeatures}; |
6 | | use core::cmp::Ordering; |
7 | | use core::fmt; |
8 | | use core::hash::{Hash, Hasher}; |
9 | | use core::ops::Deref; |
10 | | use semver::Version; |
11 | | |
12 | | /// Represents a kebab string slice used in validation. |
13 | | /// |
14 | | /// This is a wrapper around `str` that ensures the slice is |
15 | | /// a valid kebab case string according to the component model |
16 | | /// specification. |
17 | | /// |
18 | | /// It also provides an equality and hashing implementation |
19 | | /// that ignores ASCII case. |
20 | | #[derive(Debug, Eq, Clone, Copy)] |
21 | | #[repr(transparent)] |
22 | | pub struct KebabStr<'a>(&'a str); |
23 | | |
24 | | impl<'a> KebabStr<'a> { |
25 | | /// Creates a new kebab string slice. |
26 | | /// |
27 | | /// Returns `None` if the given string is not a valid kebab string. |
28 | 1.36M | pub fn new(s: &'a str) -> Option<Self> { |
29 | 1.36M | let s = Self::new_unchecked(s); |
30 | 1.36M | if s.is_kebab_case() { Some(s) } else { None } |
31 | 1.36M | } |
32 | | |
33 | 3.87M | pub(crate) fn new_unchecked(s: &'a str) -> Self { |
34 | 3.87M | Self(s) |
35 | 3.87M | } |
36 | | |
37 | | /// Gets the underlying string slice. |
38 | 8.21M | pub fn as_str(&self) -> &str { |
39 | 8.21M | &self.0 |
40 | 8.21M | } |
41 | | |
42 | | /// Converts the slice to an owned string. |
43 | 0 | pub fn to_kebab_string(&self) -> KebabString { |
44 | 0 | KebabString(self.to_string()) |
45 | 0 | } |
46 | | |
47 | 1.36M | fn is_kebab_case(&self) -> bool { |
48 | 1.36M | let mut lower = false; |
49 | 1.36M | let mut upper = false; |
50 | 1.36M | let mut is_first = true; |
51 | 1.36M | let mut has_digit = false; |
52 | 8.39M | for c in self.chars() { |
53 | 19.9k | match c { |
54 | 6.74M | 'a'..='z' if !lower && !upper => lower = true, |
55 | 0 | 'A'..='Z' if !lower && !upper => upper = true, |
56 | 1.62M | '0'..='9' if !lower && !upper && !is_first => has_digit = true, |
57 | 5.36M | 'a'..='z' if lower => {} |
58 | 0 | 'A'..='Z' if upper => {} |
59 | 1.62M | '0'..='9' if lower || upper => has_digit = true, |
60 | 19.9k | '-' if lower || upper || has_digit => { |
61 | 19.9k | lower = false; |
62 | 19.9k | upper = false; |
63 | 19.9k | is_first = false; |
64 | 19.9k | has_digit = false; |
65 | 19.9k | } |
66 | 0 | _ => return false, |
67 | | } |
68 | | } |
69 | | |
70 | 1.36M | !self.is_empty() && !self.ends_with('-') |
71 | 1.36M | } |
72 | | } |
73 | | |
74 | | impl Deref for KebabStr<'_> { |
75 | | type Target = str; |
76 | | |
77 | 8.02M | fn deref(&self) -> &str { |
78 | 8.02M | self.as_str() |
79 | 8.02M | } |
80 | | } |
81 | | |
82 | | impl PartialEq for KebabStr<'_> { |
83 | 159k | fn eq(&self, other: &Self) -> bool { |
84 | 159k | if self.len() != other.len() { |
85 | 21.0k | return false; |
86 | 138k | } |
87 | | |
88 | 138k | self.chars() |
89 | 138k | .zip(other.chars()) |
90 | 763k | .all(|(a, b)| a.to_ascii_lowercase() == b.to_ascii_lowercase()) |
91 | 159k | } |
92 | | } |
93 | | |
94 | | impl PartialEq<KebabString> for KebabStr<'_> { |
95 | 0 | fn eq(&self, other: &KebabString) -> bool { |
96 | 0 | self.eq(&other.as_kebab_str()) |
97 | 0 | } |
98 | | } |
99 | | |
100 | | impl Ord for KebabStr<'_> { |
101 | 41.6k | fn cmp(&self, other: &Self) -> Ordering { |
102 | 256k | let self_chars = self.chars().map(|c| c.to_ascii_lowercase()); |
103 | 249k | let other_chars = other.chars().map(|c| c.to_ascii_lowercase()); |
104 | 41.6k | self_chars.cmp(other_chars) |
105 | 41.6k | } |
106 | | } |
107 | | |
108 | | impl PartialOrd for KebabStr<'_> { |
109 | 0 | fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
110 | 0 | Some(self.cmp(other)) |
111 | 0 | } |
112 | | } |
113 | | |
114 | | impl Hash for KebabStr<'_> { |
115 | 1.45M | fn hash<H: Hasher>(&self, state: &mut H) { |
116 | 1.45M | self.len().hash(state); |
117 | | |
118 | 9.41M | for b in self.chars() { |
119 | 9.41M | b.to_ascii_lowercase().hash(state); |
120 | 9.41M | } |
121 | 1.45M | } |
122 | | } |
123 | | |
124 | | impl fmt::Display for KebabStr<'_> { |
125 | 167k | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
126 | 167k | self.as_str().fmt(f) |
127 | 167k | } |
128 | | } |
129 | | |
130 | | /// Represents an owned kebab string for validation. |
131 | | /// |
132 | | /// This is a wrapper around `String` that ensures the string is |
133 | | /// a valid kebab case string according to the component model |
134 | | /// specification. |
135 | | /// |
136 | | /// It also provides an equality and hashing implementation |
137 | | /// that ignores ASCII case. |
138 | | #[derive(Debug, Clone, Eq)] |
139 | | pub struct KebabString(String); |
140 | | |
141 | | impl KebabString { |
142 | | /// Creates a new kebab string. |
143 | | /// |
144 | | /// Returns `None` if the given string is not a valid kebab string. |
145 | 701k | pub fn new(s: impl Into<String>) -> Option<Self> { |
146 | 701k | let s = s.into(); |
147 | 701k | if KebabStr::new(&s).is_some() { |
148 | 701k | Some(Self(s)) |
149 | | } else { |
150 | 0 | None |
151 | | } |
152 | 701k | } |
153 | | |
154 | | /// Gets the underlying string. |
155 | 1.44M | pub fn as_str(&self) -> &str { |
156 | 1.44M | self.0.as_str() |
157 | 1.44M | } |
158 | | |
159 | | /// Converts the kebab string to a kebab string slice. |
160 | 1.43M | pub fn as_kebab_str(&self) -> KebabStr<'_> { |
161 | 1.43M | KebabStr::new_unchecked(self.as_str()) |
162 | 1.43M | } |
163 | | } |
164 | | |
165 | | impl Deref for KebabString { |
166 | | type Target = str; |
167 | 0 | fn deref(&self) -> &str { |
168 | 0 | self.as_str() |
169 | 0 | } |
170 | | } |
171 | | |
172 | | impl Ord for KebabString { |
173 | 0 | fn cmp(&self, other: &Self) -> Ordering { |
174 | 0 | self.as_kebab_str().cmp(&other.as_kebab_str()) |
175 | 0 | } |
176 | | } |
177 | | |
178 | | impl PartialOrd for KebabString { |
179 | 0 | fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
180 | 0 | self.as_kebab_str().partial_cmp(&other.as_kebab_str()) |
181 | 0 | } |
182 | | } |
183 | | |
184 | | impl PartialEq for KebabString { |
185 | 137k | fn eq(&self, other: &Self) -> bool { |
186 | 137k | self.as_kebab_str().eq(&other.as_kebab_str()) |
187 | 137k | } |
188 | | } |
189 | | |
190 | | impl PartialEq<KebabStr<'_>> for KebabString { |
191 | 0 | fn eq(&self, other: &KebabStr<'_>) -> bool { |
192 | 0 | self.as_kebab_str().eq(other) |
193 | 0 | } |
194 | | } |
195 | | |
196 | | impl Hash for KebabString { |
197 | 1.10M | fn hash<H: Hasher>(&self, state: &mut H) { |
198 | 1.10M | self.as_kebab_str().hash(state) |
199 | 1.10M | } |
200 | | } |
201 | | |
202 | | impl fmt::Display for KebabString { |
203 | 50.6k | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
204 | 50.6k | self.as_kebab_str().fmt(f) |
205 | 50.6k | } |
206 | | } |
207 | | |
208 | | impl From<KebabString> for String { |
209 | 70.2k | fn from(s: KebabString) -> String { |
210 | 70.2k | s.0 |
211 | 70.2k | } |
212 | | } |
213 | | |
214 | | /// An import or export name in the component model which is backed by `T`, |
215 | | /// which defaults to `String`. |
216 | | /// |
217 | | /// This name can be either: |
218 | | /// |
219 | | /// * a plain label or "kebab string": `a-b-c` |
220 | | /// * a plain method name : `[method]a-b.c-d` |
221 | | /// * a plain static method name : `[static]a-b.c-d` |
222 | | /// * a plain constructor: `[constructor]a-b` |
223 | | /// * an interface name: `wasi:cli/reactor@0.1.0` |
224 | | /// * a dependency name: `locked-dep=foo:bar/baz` |
225 | | /// * a URL name: `url=https://..` |
226 | | /// * a hash name: `integrity=sha256:...` |
227 | | /// |
228 | | /// # Equality and hashing |
229 | | /// |
230 | | /// Note that this type the `[method]...` and `[static]...` variants are |
231 | | /// considered equal and hash to the same value. This enables disallowing |
232 | | /// clashes between the two where method name overlap cannot happen. |
233 | | #[derive(Clone)] |
234 | | pub struct ComponentName { |
235 | | raw: String, |
236 | | kind: ParsedComponentNameKind, |
237 | | } |
238 | | |
239 | | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] |
240 | | enum ParsedComponentNameKind { |
241 | | Label, |
242 | | Constructor, |
243 | | Method, |
244 | | Static, |
245 | | Interface, |
246 | | Dependency, |
247 | | Url, |
248 | | Hash, |
249 | | } |
250 | | |
251 | | /// Created via [`ComponentName::kind`] and classifies a name. |
252 | | #[derive(Debug, Clone)] |
253 | | pub enum ComponentNameKind<'a> { |
254 | | /// `a-b-c` |
255 | | Label(KebabStr<'a>), |
256 | | /// `[constructor]a-b` |
257 | | Constructor(KebabStr<'a>), |
258 | | /// `[method]a-b.c-d` |
259 | | #[allow(missing_docs)] |
260 | | Method(ResourceFunc<'a>), |
261 | | /// `[static]a-b.c-d` |
262 | | #[allow(missing_docs)] |
263 | | Static(ResourceFunc<'a>), |
264 | | /// `wasi:http/types@2.0` |
265 | | #[allow(missing_docs)] |
266 | | Interface(InterfaceName<'a>), |
267 | | /// `locked-dep=foo:bar/baz` |
268 | | #[allow(missing_docs)] |
269 | | Dependency(DependencyName<'a>), |
270 | | /// `url=https://...` |
271 | | #[allow(missing_docs)] |
272 | | Url(UrlName<'a>), |
273 | | /// `integrity=sha256:...` |
274 | | #[allow(missing_docs)] |
275 | | Hash(HashName<'a>), |
276 | | } |
277 | | |
278 | | const CONSTRUCTOR: &str = "[constructor]"; |
279 | | const METHOD: &str = "[method]"; |
280 | | const STATIC: &str = "[static]"; |
281 | | |
282 | | impl ComponentName { |
283 | | /// Attempts to parse `name` as a valid component name, returning `Err` if |
284 | | /// it's not valid. |
285 | 94.0k | pub fn new(name: &str, offset: u64) -> Result<ComponentName> { |
286 | 94.0k | Self::new_with_features(name, offset, WasmFeatures::default()) |
287 | 94.0k | } |
288 | | |
289 | | /// Attempts to parse `name` as a valid component name, returning `Err` if |
290 | | /// it's not valid. |
291 | | /// |
292 | | /// `features` can be used to enable or disable validation of certain forms |
293 | | /// of supported import names. |
294 | 381k | pub fn new_with_features(name: &str, offset: u64, features: WasmFeatures) -> Result<Self> { |
295 | 381k | let mut parser = ComponentNameParser { |
296 | 381k | next: name, |
297 | 381k | offset, |
298 | 381k | features, |
299 | 381k | }; |
300 | 381k | let kind = parser.parse()?; |
301 | 381k | if !parser.next.is_empty() { |
302 | 0 | bail!(offset, "trailing characters found: `{}`", parser.next); |
303 | 381k | } |
304 | 381k | Ok(ComponentName { |
305 | 381k | raw: name.to_string(), |
306 | 381k | kind, |
307 | 381k | }) |
308 | 381k | } |
309 | | |
310 | | /// Returns the [`ComponentNameKind`] corresponding to this name. |
311 | 1.14M | pub fn kind(&self) -> ComponentNameKind<'_> { |
312 | | use ComponentNameKind::*; |
313 | | use ParsedComponentNameKind as PK; |
314 | 1.14M | match self.kind { |
315 | 858k | PK::Label => Label(KebabStr::new_unchecked(&self.raw)), |
316 | 3.82k | PK::Constructor => Constructor(KebabStr::new_unchecked(&self.raw[CONSTRUCTOR.len()..])), |
317 | 16.9k | PK::Method => Method(ResourceFunc(&self.raw[METHOD.len()..])), |
318 | 25.8k | PK::Static => Static(ResourceFunc(&self.raw[STATIC.len()..])), |
319 | 244k | PK::Interface => Interface(InterfaceName(&self.raw)), |
320 | 0 | PK::Dependency => Dependency(DependencyName(&self.raw)), |
321 | 0 | PK::Url => Url(UrlName(&self.raw)), |
322 | 0 | PK::Hash => Hash(HashName(&self.raw)), |
323 | | } |
324 | 1.14M | } |
325 | | |
326 | | /// Returns the raw underlying name as a string. |
327 | 0 | pub fn as_str(&self) -> &str { |
328 | 0 | &self.raw |
329 | 0 | } |
330 | | } |
331 | | |
332 | | impl From<ComponentName> for String { |
333 | 0 | fn from(name: ComponentName) -> String { |
334 | 0 | name.raw |
335 | 0 | } |
336 | | } |
337 | | |
338 | | impl Hash for ComponentName { |
339 | 419k | fn hash<H: Hasher>(&self, hasher: &mut H) { |
340 | 419k | self.kind().hash(hasher) |
341 | 419k | } |
342 | | } |
343 | | |
344 | | impl PartialEq for ComponentName { |
345 | 46.6k | fn eq(&self, other: &ComponentName) -> bool { |
346 | 46.6k | self.kind().eq(&other.kind()) |
347 | 46.6k | } |
348 | | } |
349 | | |
350 | | impl Eq for ComponentName {} |
351 | | |
352 | | impl Ord for ComponentName { |
353 | 0 | fn cmp(&self, other: &ComponentName) -> Ordering { |
354 | 0 | self.kind().cmp(&other.kind()) |
355 | 0 | } |
356 | | } |
357 | | |
358 | | impl PartialOrd for ComponentName { |
359 | 0 | fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
360 | 0 | self.kind.partial_cmp(&other.kind) |
361 | 0 | } |
362 | | } |
363 | | |
364 | | impl fmt::Display for ComponentName { |
365 | 17.6k | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
366 | 17.6k | self.raw.fmt(f) |
367 | 17.6k | } |
368 | | } |
369 | | |
370 | | impl fmt::Debug for ComponentName { |
371 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
372 | 0 | self.raw.fmt(f) |
373 | 0 | } |
374 | | } |
375 | | |
376 | | impl ComponentNameKind<'_> { |
377 | | /// Returns the [`ParsedComponentNameKind`] of the [`ComponentNameKind`]. |
378 | 6.73k | fn kind(&self) -> ParsedComponentNameKind { |
379 | 6.73k | match self { |
380 | 3.32k | Self::Label(_) => ParsedComponentNameKind::Label, |
381 | 216 | Self::Constructor(_) => ParsedComponentNameKind::Constructor, |
382 | 736 | Self::Method(_) => ParsedComponentNameKind::Method, |
383 | 1.50k | Self::Static(_) => ParsedComponentNameKind::Static, |
384 | 953 | Self::Interface(_) => ParsedComponentNameKind::Interface, |
385 | 0 | Self::Dependency(_) => ParsedComponentNameKind::Dependency, |
386 | 0 | Self::Url(_) => ParsedComponentNameKind::Url, |
387 | 0 | Self::Hash(_) => ParsedComponentNameKind::Hash, |
388 | | } |
389 | 6.73k | } |
390 | | } |
391 | | |
392 | | impl Ord for ComponentNameKind<'_> { |
393 | 46.6k | fn cmp(&self, other: &Self) -> Ordering { |
394 | | use ComponentNameKind::*; |
395 | | |
396 | 46.6k | match (self, other) { |
397 | 41.6k | (Label(lhs), Label(rhs)) => lhs.cmp(rhs), |
398 | 0 | (Constructor(lhs), Constructor(rhs)) => lhs.cmp(rhs), |
399 | 410 | (Method(lhs) | Static(lhs), Method(rhs) | Static(rhs)) => lhs.cmp(rhs), |
400 | | |
401 | | // `[..]l.l` is equivalent to `l` |
402 | 333 | (Label(plain), Method(method) | Static(method)) |
403 | 1.14k | | (Method(method) | Static(method), Label(plain)) |
404 | 580 | if *plain == method.resource() && *plain == method.method() => |
405 | | { |
406 | 0 | Ordering::Equal |
407 | | } |
408 | | |
409 | 1.22k | (Interface(lhs), Interface(rhs)) => lhs.cmp(rhs), |
410 | 0 | (Dependency(lhs), Dependency(rhs)) => lhs.cmp(rhs), |
411 | 0 | (Url(lhs), Url(rhs)) => lhs.cmp(rhs), |
412 | 0 | (Hash(lhs), Hash(rhs)) => lhs.cmp(rhs), |
413 | | |
414 | | (Label(_), _) |
415 | | | (Constructor(_), _) |
416 | | | (Method(_), _) |
417 | | | (Static(_), _) |
418 | | | (Interface(_), _) |
419 | | | (Dependency(_), _) |
420 | | | (Url(_), _) |
421 | 3.36k | | (Hash(_), _) => self.kind().cmp(&other.kind()), |
422 | | } |
423 | 46.6k | } |
424 | | } |
425 | | |
426 | | impl PartialOrd for ComponentNameKind<'_> { |
427 | 0 | fn partial_cmp(&self, other: &Self) -> Option<Ordering> { |
428 | 0 | Some(self.cmp(other)) |
429 | 0 | } |
430 | | } |
431 | | |
432 | | impl Hash for ComponentNameKind<'_> { |
433 | 419k | fn hash<H: Hasher>(&self, hasher: &mut H) { |
434 | | use ComponentNameKind::*; |
435 | 419k | match self { |
436 | 342k | Label(name) => (0u8, name).hash(hasher), |
437 | 1.63k | Constructor(name) => (1u8, name).hash(hasher), |
438 | | |
439 | 11.1k | Method(name) | Static(name) => { |
440 | | // `l.l` hashes the same as `l` since they're equal above, |
441 | | // otherwise everything is hashed as `a.b` with a unique |
442 | | // prefix. |
443 | 18.3k | if name.resource() == name.method() { |
444 | 0 | (0u8, name.resource()).hash(hasher) |
445 | | } else { |
446 | 18.3k | (2u8, name).hash(hasher) |
447 | | } |
448 | | } |
449 | | |
450 | 56.9k | Interface(name) => (3u8, name).hash(hasher), |
451 | 0 | Dependency(name) => (4u8, name).hash(hasher), |
452 | 0 | Url(name) => (5u8, name).hash(hasher), |
453 | 0 | Hash(name) => (6u8, name).hash(hasher), |
454 | | } |
455 | 419k | } |
456 | | } |
457 | | |
458 | | impl PartialEq for ComponentNameKind<'_> { |
459 | 46.6k | fn eq(&self, other: &ComponentNameKind<'_>) -> bool { |
460 | 46.6k | self.cmp(other) == Ordering::Equal |
461 | 46.6k | } |
462 | | } |
463 | | |
464 | | impl Eq for ComponentNameKind<'_> {} |
465 | | |
466 | | /// A resource name and its function, stored as `a.b`. |
467 | | #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] |
468 | | pub struct ResourceFunc<'a>(&'a str); |
469 | | |
470 | | impl<'a> ResourceFunc<'a> { |
471 | | /// Returns the underlying string as `a.b` |
472 | 0 | pub fn as_str(&self) -> &'a str { |
473 | 0 | self.0 |
474 | 0 | } |
475 | | |
476 | | /// Returns the resource name or the `a` in `a.b` |
477 | 33.4k | pub fn resource(&self) -> KebabStr<'a> { |
478 | 33.4k | let dot = self.0.find('.').unwrap(); |
479 | 33.4k | KebabStr::new_unchecked(&self.0[..dot]) |
480 | 33.4k | } |
481 | | |
482 | | /// Returns the method name or the `b` in `a.b` |
483 | 19.7k | pub fn method(&self) -> KebabStr<'a> { |
484 | 19.7k | let dot = self.0.find('.').unwrap(); |
485 | 19.7k | KebabStr::new_unchecked(&self.0[dot + 1..]) |
486 | 19.7k | } |
487 | | } |
488 | | |
489 | | /// An interface name, stored as `a:b/c@1.2.3` |
490 | | #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] |
491 | | pub struct InterfaceName<'a>(&'a str); |
492 | | |
493 | | impl<'a> InterfaceName<'a> { |
494 | | /// Returns the entire underlying string. |
495 | 1.20k | pub fn as_str(&self) -> &'a str { |
496 | 1.20k | self.0 |
497 | 1.20k | } |
498 | | |
499 | | /// Returns the `a:b` in `a:b:c/d/e` |
500 | 38.5k | pub fn namespace(&self) -> KebabStr<'a> { |
501 | 38.5k | let colon = self.0.rfind(':').unwrap(); |
502 | 38.5k | KebabStr::new_unchecked(&self.0[..colon]) |
503 | 38.5k | } |
504 | | |
505 | | /// Returns the `c` in `a:b:c/d/e` |
506 | 38.5k | pub fn package(&self) -> KebabStr<'a> { |
507 | 38.5k | let colon = self.0.rfind(':').unwrap(); |
508 | 38.5k | let slash = self.0.find('/').unwrap(); |
509 | 38.5k | KebabStr::new_unchecked(&self.0[colon + 1..slash]) |
510 | 38.5k | } |
511 | | |
512 | | /// Returns the `d` in `a:b:c/d/e`. |
513 | 40.8k | pub fn interface(&self) -> KebabStr<'a> { |
514 | 40.8k | let projection = self.projection(); |
515 | 40.8k | let slash = projection.find('/').unwrap_or(projection.len()); |
516 | 40.8k | KebabStr::new_unchecked(&projection.0[..slash]) |
517 | 40.8k | } |
518 | | |
519 | | /// Returns the `d/e` in `a:b:c/d/e` |
520 | 40.8k | pub fn projection(&self) -> KebabStr<'a> { |
521 | 40.8k | let slash = self.0.find('/').unwrap(); |
522 | 40.8k | let at = self.0.find('@').unwrap_or(self.0.len()); |
523 | 40.8k | KebabStr::new_unchecked(&self.0[slash + 1..at]) |
524 | 40.8k | } |
525 | | |
526 | | /// Returns the `1.2.3` in `a:b:c/d/e@1.2.3` |
527 | 93.4k | pub fn version(&self, suffix: Option<&str>) -> Result<Option<Version>, semver::Error> { |
528 | 93.4k | let Some(at) = self.0.find('@') else { |
529 | 24.4k | return Ok(None); |
530 | | }; |
531 | 69.0k | let prefix = &self.0[at + 1..]; |
532 | 69.0k | match suffix { |
533 | | // FIXME: this should perform full validation of the version |
534 | | // suffix/prefix, notably that "prefix" is indeed the "semver track" |
535 | | // that is expected. For example "1.2.3" means that prefix must be |
536 | | // "1", nothing else. This validation is deferred to a future PR. |
537 | 0 | Some(suffix) => Ok(Some(Version::parse(&format!("{prefix}{suffix}"))?)), |
538 | 69.0k | None => Ok(Some(Version::parse(prefix)?)), |
539 | | } |
540 | 93.4k | } |
541 | | } |
542 | | |
543 | | /// A dependency on an implementation either as `locked-dep=...` or |
544 | | /// `unlocked-dep=...` |
545 | | #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] |
546 | | pub struct DependencyName<'a>(&'a str); |
547 | | |
548 | | impl<'a> DependencyName<'a> { |
549 | | /// Returns entire underlying import string |
550 | 0 | pub fn as_str(&self) -> &'a str { |
551 | 0 | self.0 |
552 | 0 | } |
553 | | } |
554 | | |
555 | | /// A dependency on an implementation either as `url=...` |
556 | | #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] |
557 | | pub struct UrlName<'a>(&'a str); |
558 | | |
559 | | impl<'a> UrlName<'a> { |
560 | | /// Returns entire underlying import string |
561 | 0 | pub fn as_str(&self) -> &'a str { |
562 | 0 | self.0 |
563 | 0 | } |
564 | | } |
565 | | |
566 | | /// A dependency on an implementation either as `integrity=...`. |
567 | | #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] |
568 | | pub struct HashName<'a>(&'a str); |
569 | | |
570 | | impl<'a> HashName<'a> { |
571 | | /// Returns entire underlying import string. |
572 | 0 | pub fn as_str(&self) -> &'a str { |
573 | 0 | self.0 |
574 | 0 | } |
575 | | } |
576 | | |
577 | | // A small helper structure to parse `self.next` which is an import or export |
578 | | // name. |
579 | | // |
580 | | // Methods will update `self.next` as they go along and `self.offset` is used |
581 | | // for error messages. |
582 | | struct ComponentNameParser<'a> { |
583 | | next: &'a str, |
584 | | offset: u64, |
585 | | features: WasmFeatures, |
586 | | } |
587 | | |
588 | | impl<'a> ComponentNameParser<'a> { |
589 | 381k | fn parse(&mut self) -> Result<ParsedComponentNameKind> { |
590 | 381k | if self.eat_str(CONSTRUCTOR) { |
591 | 1.27k | self.expect_kebab()?; |
592 | 1.27k | return Ok(ParsedComponentNameKind::Constructor); |
593 | 380k | } |
594 | 380k | if self.eat_str(METHOD) { |
595 | 5.71k | let resource = self.take_until('.')?; |
596 | 5.71k | self.kebab(resource)?; |
597 | 5.71k | self.expect_kebab()?; |
598 | 5.71k | return Ok(ParsedComponentNameKind::Method); |
599 | 374k | } |
600 | 374k | if self.eat_str(STATIC) { |
601 | 7.16k | let resource = self.take_until('.')?; |
602 | 7.16k | self.kebab(resource)?; |
603 | 7.16k | self.expect_kebab()?; |
604 | 7.16k | return Ok(ParsedComponentNameKind::Static); |
605 | 367k | } |
606 | | |
607 | | // 'unlocked-dep=<' <pkgnamequery> '>' |
608 | 367k | if self.eat_str("unlocked-dep=") { |
609 | 0 | self.expect_str("<")?; |
610 | 0 | self.pkg_name_query()?; |
611 | 0 | self.expect_str(">")?; |
612 | 0 | return Ok(ParsedComponentNameKind::Dependency); |
613 | 367k | } |
614 | | |
615 | | // 'locked-dep=<' <pkgname> '>' ( ',' <hashname> )? |
616 | 367k | if self.eat_str("locked-dep=") { |
617 | 0 | self.expect_str("<")?; |
618 | 0 | self.pkg_name(false)?; |
619 | 0 | self.expect_str(">")?; |
620 | 0 | self.eat_optional_hash()?; |
621 | 0 | return Ok(ParsedComponentNameKind::Dependency); |
622 | 367k | } |
623 | | |
624 | | // 'url=<' <nonbrackets> '>' (',' <hashname>)? |
625 | 367k | if self.eat_str("url=") { |
626 | 0 | self.expect_str("<")?; |
627 | 0 | let url = self.take_up_to('>')?; |
628 | 0 | if url.contains('<') { |
629 | 0 | bail!(self.offset, "url cannot contain `<`"); |
630 | 0 | } |
631 | 0 | self.expect_str(">")?; |
632 | 0 | self.eat_optional_hash()?; |
633 | 0 | return Ok(ParsedComponentNameKind::Url); |
634 | 367k | } |
635 | | |
636 | | // 'integrity=<' <integrity-metadata> '>' |
637 | 367k | if self.eat_str("integrity=") { |
638 | 0 | self.expect_str("<")?; |
639 | 0 | let _hash = self.parse_hash()?; |
640 | 0 | self.expect_str(">")?; |
641 | 0 | return Ok(ParsedComponentNameKind::Hash); |
642 | 367k | } |
643 | | |
644 | 367k | if self.next.contains(':') { |
645 | 134k | self.pkg_name(true)?; |
646 | 134k | Ok(ParsedComponentNameKind::Interface) |
647 | | } else { |
648 | 232k | self.expect_kebab()?; |
649 | 232k | Ok(ParsedComponentNameKind::Label) |
650 | | } |
651 | 381k | } |
652 | | |
653 | | // pkgnamequery ::= <pkgpath> <verrange>? |
654 | 0 | fn pkg_name_query(&mut self) -> Result<()> { |
655 | 0 | self.pkg_path(false)?; |
656 | | |
657 | 0 | if self.eat_str("@") { |
658 | 0 | if self.eat_str("*") { |
659 | 0 | return Ok(()); |
660 | 0 | } |
661 | | |
662 | 0 | self.expect_str("{")?; |
663 | 0 | let range = self.take_up_to('}')?; |
664 | 0 | self.expect_str("}")?; |
665 | 0 | self.semver_range(range)?; |
666 | 0 | } |
667 | | |
668 | 0 | Ok(()) |
669 | 0 | } |
670 | | |
671 | | // pkgname ::= <pkgpath> <version>? |
672 | 134k | fn pkg_name(&mut self, is_interface_name: bool) -> Result<()> { |
673 | 134k | self.pkg_path(is_interface_name)?; |
674 | | |
675 | 134k | if self.eat_str("@") { |
676 | 101k | let version = match self.eat_up_to('>') { |
677 | 0 | Some(version) => version, |
678 | 101k | None => self.take_rest(), |
679 | | }; |
680 | | |
681 | | // Validation of the semver of interface names is deferred to full |
682 | | // component validation with access to the `version_suffix` field. |
683 | 101k | if !is_interface_name { |
684 | 0 | self.semver(version)?; |
685 | 101k | } |
686 | 33.5k | } |
687 | | |
688 | 134k | Ok(()) |
689 | 134k | } |
690 | | |
691 | | // pkgpath ::= <namespace>+ <label> <projection>* |
692 | 134k | fn pkg_path(&mut self, require_projection: bool) -> Result<()> { |
693 | | // There must be at least one package namespace |
694 | 134k | self.take_lowercase_kebab()?; |
695 | 134k | self.expect_str(":")?; |
696 | 134k | self.take_lowercase_kebab()?; |
697 | | |
698 | 134k | if self.features.cm_nested_names() { |
699 | | // Take the remaining package namespaces and name |
700 | 65.8k | while self.next.starts_with(':') { |
701 | 0 | self.expect_str(":")?; |
702 | 0 | self.take_lowercase_kebab()?; |
703 | | } |
704 | 69.1k | } |
705 | | |
706 | | // Take the projections |
707 | 134k | if self.next.starts_with('/') { |
708 | 134k | self.expect_str("/")?; |
709 | 134k | self.take_kebab()?; |
710 | | |
711 | 134k | if self.features.cm_nested_names() { |
712 | 65.8k | while self.next.starts_with('/') { |
713 | 0 | self.expect_str("/")?; |
714 | 0 | self.take_kebab()?; |
715 | | } |
716 | 69.1k | } |
717 | 0 | } else if require_projection { |
718 | 0 | bail!(self.offset, "expected `/` after package name"); |
719 | 0 | } |
720 | | |
721 | 134k | Ok(()) |
722 | 134k | } |
723 | | |
724 | | // verrange ::= '@*' |
725 | | // | '@{' <verlower> '}' |
726 | | // | '@{' <verupper> '}' |
727 | | // | '@{' <verlower> ' ' <verupper> '}' |
728 | | // verlower ::= '>=' <valid semver> |
729 | | // verupper ::= '<' <valid semver> |
730 | 0 | fn semver_range(&self, range: &str) -> Result<()> { |
731 | 0 | if range == "*" { |
732 | 0 | return Ok(()); |
733 | 0 | } |
734 | | |
735 | 0 | if let Some(range) = range.strip_prefix(">=") { |
736 | 0 | let (lower, upper) = range |
737 | 0 | .split_once(' ') |
738 | 0 | .map(|(l, u)| (l, Some(u))) |
739 | 0 | .unwrap_or((range, None)); |
740 | 0 | self.semver(lower)?; |
741 | | |
742 | 0 | if let Some(upper) = upper { |
743 | 0 | match upper.strip_prefix('<') { |
744 | 0 | Some(upper) => { |
745 | 0 | self.semver(upper)?; |
746 | | } |
747 | 0 | None => bail!( |
748 | 0 | self.offset, |
749 | | "expected `<` at start of version range upper bounds" |
750 | | ), |
751 | | } |
752 | 0 | } |
753 | 0 | } else if let Some(upper) = range.strip_prefix('<') { |
754 | 0 | self.semver(upper)?; |
755 | | } else { |
756 | 0 | bail!( |
757 | 0 | self.offset, |
758 | | "expected `>=` or `<` at start of version range" |
759 | | ); |
760 | | } |
761 | | |
762 | 0 | Ok(()) |
763 | 0 | } |
764 | | |
765 | 0 | fn parse_hash(&mut self) -> Result<&'a str> { |
766 | 0 | let integrity = self.take_up_to('>')?; |
767 | 0 | let mut any = false; |
768 | 0 | for hash in integrity.split_whitespace() { |
769 | 0 | any = true; |
770 | 0 | let rest = hash |
771 | 0 | .strip_prefix("sha256") |
772 | 0 | .or_else(|| hash.strip_prefix("sha384")) |
773 | 0 | .or_else(|| hash.strip_prefix("sha512")); |
774 | 0 | let rest = match rest { |
775 | 0 | Some(s) => s, |
776 | 0 | None => bail!(self.offset, "unrecognized hash algorithm: `{hash}`"), |
777 | | }; |
778 | 0 | let rest = match rest.strip_prefix('-') { |
779 | 0 | Some(s) => s, |
780 | 0 | None => bail!(self.offset, "expected `-` after hash algorithm: {hash}"), |
781 | | }; |
782 | 0 | let (base64, _options) = match rest.find('?') { |
783 | 0 | Some(i) => (&rest[..i], Some(&rest[i + 1..])), |
784 | 0 | None => (rest, None), |
785 | | }; |
786 | 0 | if !is_base64(base64) { |
787 | 0 | bail!(self.offset, "not valid base64: `{base64}`"); |
788 | 0 | } |
789 | | } |
790 | 0 | if !any { |
791 | 0 | bail!(self.offset, "integrity hash cannot be empty"); |
792 | 0 | } |
793 | 0 | Ok(integrity) |
794 | 0 | } |
795 | | |
796 | 0 | fn eat_optional_hash(&mut self) -> Result<Option<&'a str>> { |
797 | 0 | if !self.eat_str(",") { |
798 | 0 | return Ok(None); |
799 | 0 | } |
800 | 0 | self.expect_str("integrity=<")?; |
801 | 0 | let ret = self.parse_hash()?; |
802 | 0 | self.expect_str(">")?; |
803 | 0 | Ok(Some(ret)) |
804 | 0 | } |
805 | | |
806 | 3.01M | fn eat_str(&mut self, prefix: &str) -> bool { |
807 | 3.01M | match self.next.strip_prefix(prefix) { |
808 | 385k | Some(rest) => { |
809 | 385k | self.next = rest; |
810 | 385k | true |
811 | | } |
812 | 2.62M | None => false, |
813 | | } |
814 | 3.01M | } |
815 | | |
816 | 269k | fn expect_str(&mut self, prefix: &str) -> Result<()> { |
817 | 269k | if self.eat_str(prefix) { |
818 | 269k | Ok(()) |
819 | | } else { |
820 | 0 | bail!(self.offset, "expected `{prefix}` at `{}`", self.next); |
821 | | } |
822 | 269k | } |
823 | | |
824 | 12.8k | fn eat_until(&mut self, c: char) -> Option<&'a str> { |
825 | 12.8k | let ret = self.eat_up_to(c); |
826 | 12.8k | if ret.is_some() { |
827 | 12.8k | self.next = &self.next[c.len_utf8()..]; |
828 | 12.8k | } |
829 | 12.8k | ret |
830 | 12.8k | } |
831 | | |
832 | 114k | fn eat_up_to(&mut self, c: char) -> Option<&'a str> { |
833 | 114k | let i = self.next.find(c)?; |
834 | 12.8k | let (a, b) = self.next.split_at(i); |
835 | 12.8k | self.next = b; |
836 | 12.8k | Some(a) |
837 | 114k | } |
838 | | |
839 | 664k | fn kebab(&self, s: &'a str) -> Result<KebabStr<'a>> { |
840 | 664k | match KebabStr::new(s) { |
841 | 664k | Some(name) => Ok(name), |
842 | 0 | None => bail!(self.offset, "`{s}` is not in kebab case"), |
843 | | } |
844 | 664k | } |
845 | | |
846 | 0 | fn semver(&self, s: &str) -> Result<Version> { |
847 | 0 | match Version::parse(s) { |
848 | 0 | Ok(v) => Ok(v), |
849 | 0 | Err(e) => bail!(self.offset, "`{s}` is not a valid semver: {e}"), |
850 | | } |
851 | 0 | } |
852 | | |
853 | 12.8k | fn take_until(&mut self, c: char) -> Result<&'a str> { |
854 | 12.8k | match self.eat_until(c) { |
855 | 12.8k | Some(s) => Ok(s), |
856 | 0 | None => bail!(self.offset, "failed to find `{c}` character"), |
857 | | } |
858 | 12.8k | } |
859 | | |
860 | 0 | fn take_up_to(&mut self, c: char) -> Result<&'a str> { |
861 | 0 | match self.eat_up_to(c) { |
862 | 0 | Some(s) => Ok(s), |
863 | 0 | None => bail!(self.offset, "failed to find `{c}` character"), |
864 | | } |
865 | 0 | } |
866 | | |
867 | 381k | fn take_rest(&mut self) -> &'a str { |
868 | 381k | let ret = self.next; |
869 | 381k | self.next = ""; |
870 | 381k | ret |
871 | 381k | } |
872 | | |
873 | 404k | fn take_kebab(&mut self) -> Result<KebabStr<'a>> { |
874 | 404k | self.next |
875 | 2.60M | .find(|c| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-')) |
876 | 404k | .map(|i| { |
877 | 371k | let (kebab, next) = self.next.split_at(i); |
878 | 371k | self.next = next; |
879 | 371k | self.kebab(kebab) |
880 | 371k | }) |
881 | 404k | .unwrap_or_else(|| self.expect_kebab()) |
882 | 404k | } |
883 | | |
884 | 269k | fn take_lowercase_kebab(&mut self) -> Result<KebabStr<'a>> { |
885 | 269k | let kebab = self.take_kebab()?; |
886 | 269k | if let Some(c) = kebab |
887 | 269k | .chars() |
888 | 1.48M | .find(|c| c.is_alphabetic() && !c.is_lowercase()) |
889 | | { |
890 | 0 | bail!( |
891 | 0 | self.offset, |
892 | | "character `{c}` is not lowercase in package name/namespace" |
893 | | ); |
894 | 269k | } |
895 | 269k | Ok(kebab) |
896 | 269k | } |
897 | | |
898 | 280k | fn expect_kebab(&mut self) -> Result<KebabStr<'a>> { |
899 | 280k | let s = self.take_rest(); |
900 | 280k | self.kebab(s) |
901 | 280k | } |
902 | | } |
903 | | |
904 | 0 | fn is_base64(s: &str) -> bool { |
905 | 0 | if s.is_empty() { |
906 | 0 | return false; |
907 | 0 | } |
908 | 0 | let mut equals = 0; |
909 | 0 | for (i, byte) in s.as_bytes().iter().enumerate() { |
910 | 0 | match byte { |
911 | 0 | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'+' | b'/' if equals == 0 => {} |
912 | 0 | b'=' if i > 0 && equals < 2 => equals += 1, |
913 | 0 | _ => return false, |
914 | | } |
915 | | } |
916 | 0 | true |
917 | 0 | } |
918 | | |
919 | | #[cfg(test)] |
920 | | mod tests { |
921 | | use super::*; |
922 | | use std::collections::HashSet; |
923 | | |
924 | | fn parse_kebab_name(s: &str) -> Option<ComponentName> { |
925 | | ComponentName::new(s, 0).ok() |
926 | | } |
927 | | |
928 | | #[test] |
929 | | fn kebab_smoke() { |
930 | | assert!(KebabStr::new("").is_none()); |
931 | | assert!(KebabStr::new("a").is_some()); |
932 | | assert!(KebabStr::new("aB").is_none()); |
933 | | assert!(KebabStr::new("a-B").is_some()); |
934 | | assert!(KebabStr::new("a-").is_none()); |
935 | | assert!(KebabStr::new("-").is_none()); |
936 | | assert!(KebabStr::new("ΒΆ").is_none()); |
937 | | assert!(KebabStr::new("0").is_none()); |
938 | | assert!(KebabStr::new("a0").is_some()); |
939 | | assert!(KebabStr::new("a-0").is_some()); |
940 | | assert!(KebabStr::new("0-a").is_none()); |
941 | | assert!(KebabStr::new("a-b--c").is_none()); |
942 | | assert!(KebabStr::new("a0-000-3d4a-54FF").is_some()); |
943 | | assert!(KebabStr::new("a0-000-3d4A-54Ff").is_none()); |
944 | | } |
945 | | |
946 | | #[test] |
947 | | fn name_smoke() { |
948 | | assert!(parse_kebab_name("a").is_some()); |
949 | | assert!(parse_kebab_name("[foo]a").is_none()); |
950 | | assert!(parse_kebab_name("[constructor]a").is_some()); |
951 | | assert!(parse_kebab_name("[method]a").is_none()); |
952 | | assert!(parse_kebab_name("[method]a.b").is_some()); |
953 | | assert!(parse_kebab_name("[method]a-0.b-1").is_some()); |
954 | | assert!(parse_kebab_name("[method]a.b.c").is_none()); |
955 | | assert!(parse_kebab_name("[static]a.b").is_some()); |
956 | | assert!(parse_kebab_name("[static]a").is_none()); |
957 | | } |
958 | | |
959 | | #[test] |
960 | | fn name_equality() { |
961 | | assert_eq!(parse_kebab_name("a"), parse_kebab_name("a")); |
962 | | assert_ne!(parse_kebab_name("a"), parse_kebab_name("b")); |
963 | | assert_eq!( |
964 | | parse_kebab_name("[constructor]a"), |
965 | | parse_kebab_name("[constructor]a") |
966 | | ); |
967 | | assert_ne!( |
968 | | parse_kebab_name("[constructor]a"), |
969 | | parse_kebab_name("[constructor]b") |
970 | | ); |
971 | | assert_eq!( |
972 | | parse_kebab_name("[method]a.b"), |
973 | | parse_kebab_name("[method]a.b") |
974 | | ); |
975 | | assert_ne!( |
976 | | parse_kebab_name("[method]a.b"), |
977 | | parse_kebab_name("[method]b.b") |
978 | | ); |
979 | | assert_eq!( |
980 | | parse_kebab_name("[static]a.b"), |
981 | | parse_kebab_name("[static]a.b") |
982 | | ); |
983 | | assert_ne!( |
984 | | parse_kebab_name("[static]a.b"), |
985 | | parse_kebab_name("[static]b.b") |
986 | | ); |
987 | | |
988 | | assert_eq!( |
989 | | parse_kebab_name("[static]a.b"), |
990 | | parse_kebab_name("[method]a.b") |
991 | | ); |
992 | | assert_eq!( |
993 | | parse_kebab_name("[method]a.b"), |
994 | | parse_kebab_name("[static]a.b") |
995 | | ); |
996 | | |
997 | | assert_ne!( |
998 | | parse_kebab_name("[method]b.b"), |
999 | | parse_kebab_name("[static]a.b") |
1000 | | ); |
1001 | | |
1002 | | let mut s = HashSet::new(); |
1003 | | assert!(s.insert(parse_kebab_name("a"))); |
1004 | | assert!(s.insert(parse_kebab_name("[constructor]a"))); |
1005 | | assert!(s.insert(parse_kebab_name("[method]a.b"))); |
1006 | | assert!(!s.insert(parse_kebab_name("[static]a.b"))); |
1007 | | assert!(s.insert(parse_kebab_name("[static]b.b"))); |
1008 | | } |
1009 | | } |