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