wasmparser/validator/
names.rs

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