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        let mut is_first = true;
57        let mut has_digit = false;
58        for c in self.chars() {
59            match c {
60                'a'..='z' if !lower && !upper => lower = true,
61                'A'..='Z' if !lower && !upper => upper = true,
62                '0'..='9' if !lower && !upper && !is_first => has_digit = true,
63                'a'..='z' if lower => {}
64                'A'..='Z' if upper => {}
65                '0'..='9' if lower || upper => has_digit = true,
66                '-' if lower || upper || has_digit => {
67                    lower = false;
68                    upper = false;
69                    is_first = false;
70                    has_digit = false;
71                }
72                _ => return false,
73            }
74        }
75
76        !self.is_empty() && !self.ends_with('-')
77    }
78}
79
80impl Deref for KebabStr {
81    type Target = str;
82
83    fn deref(&self) -> &str {
84        self.as_str()
85    }
86}
87
88impl PartialEq for KebabStr {
89    fn eq(&self, other: &Self) -> bool {
90        if self.len() != other.len() {
91            return false;
92        }
93
94        self.chars()
95            .zip(other.chars())
96            .all(|(a, b)| a.to_ascii_lowercase() == b.to_ascii_lowercase())
97    }
98}
99
100impl PartialEq<KebabString> for KebabStr {
101    fn eq(&self, other: &KebabString) -> bool {
102        self.eq(other.as_kebab_str())
103    }
104}
105
106impl Ord for KebabStr {
107    fn cmp(&self, other: &Self) -> Ordering {
108        let self_chars = self.chars().map(|c| c.to_ascii_lowercase());
109        let other_chars = other.chars().map(|c| c.to_ascii_lowercase());
110        self_chars.cmp(other_chars)
111    }
112}
113
114impl PartialOrd for KebabStr {
115    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
116        Some(self.cmp(other))
117    }
118}
119
120impl Hash for KebabStr {
121    fn hash<H: Hasher>(&self, state: &mut H) {
122        self.len().hash(state);
123
124        for b in self.chars() {
125            b.to_ascii_lowercase().hash(state);
126        }
127    }
128}
129
130impl fmt::Display for KebabStr {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        (self as &str).fmt(f)
133    }
134}
135
136impl ToOwned for KebabStr {
137    type Owned = KebabString;
138
139    fn to_owned(&self) -> Self::Owned {
140        self.to_kebab_string()
141    }
142}
143
144/// Represents an owned kebab string for validation.
145///
146/// This is a wrapper around `String` that ensures the string is
147/// a valid kebab case string according to the component model
148/// specification.
149///
150/// It also provides an equality and hashing implementation
151/// that ignores ASCII case.
152#[derive(Debug, Clone, Eq)]
153pub struct KebabString(String);
154
155impl KebabString {
156    /// Creates a new kebab string.
157    ///
158    /// Returns `None` if the given string is not a valid kebab string.
159    pub fn new(s: impl Into<String>) -> Option<Self> {
160        let s = s.into();
161        if KebabStr::new(&s).is_some() {
162            Some(Self(s))
163        } else {
164            None
165        }
166    }
167
168    /// Gets the underlying string.
169    pub fn as_str(&self) -> &str {
170        self.0.as_str()
171    }
172
173    /// Converts the kebab string to a kebab string slice.
174    pub fn as_kebab_str(&self) -> &KebabStr {
175        // Safety: internal string is always valid kebab-case
176        KebabStr::new_unchecked(self.as_str())
177    }
178}
179
180impl Deref for KebabString {
181    type Target = KebabStr;
182
183    fn deref(&self) -> &Self::Target {
184        self.as_kebab_str()
185    }
186}
187
188impl Borrow<KebabStr> for KebabString {
189    fn borrow(&self) -> &KebabStr {
190        self.as_kebab_str()
191    }
192}
193
194impl Ord for KebabString {
195    fn cmp(&self, other: &Self) -> Ordering {
196        self.as_kebab_str().cmp(other.as_kebab_str())
197    }
198}
199
200impl PartialOrd for KebabString {
201    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
202        self.as_kebab_str().partial_cmp(other.as_kebab_str())
203    }
204}
205
206impl PartialEq for KebabString {
207    fn eq(&self, other: &Self) -> bool {
208        self.as_kebab_str().eq(other.as_kebab_str())
209    }
210}
211
212impl PartialEq<KebabStr> for KebabString {
213    fn eq(&self, other: &KebabStr) -> bool {
214        self.as_kebab_str().eq(other)
215    }
216}
217
218impl Hash for KebabString {
219    fn hash<H: Hasher>(&self, state: &mut H) {
220        self.as_kebab_str().hash(state)
221    }
222}
223
224impl fmt::Display for KebabString {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        self.as_kebab_str().fmt(f)
227    }
228}
229
230impl From<KebabString> for String {
231    fn from(s: KebabString) -> String {
232        s.0
233    }
234}
235
236/// An import or export name in the component model which is backed by `T`,
237/// which defaults to `String`.
238///
239/// This name can be either:
240///
241/// * a plain label or "kebab string": `a-b-c`
242/// * a plain method name : `[method]a-b.c-d`
243/// * a plain static method name : `[static]a-b.c-d`
244/// * a plain constructor: `[constructor]a-b`
245/// * an interface name: `wasi:cli/reactor@0.1.0`
246/// * a dependency name: `locked-dep=foo:bar/baz`
247/// * a URL name: `url=https://..`
248/// * a hash name: `integrity=sha256:...`
249///
250/// # Equality and hashing
251///
252/// Note that this type the `[method]...` and `[static]...` variants are
253/// considered equal and hash to the same value. This enables disallowing
254/// clashes between the two where method name overlap cannot happen.
255#[derive(Clone)]
256pub struct ComponentName {
257    raw: String,
258    kind: ParsedComponentNameKind,
259}
260
261#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
262enum ParsedComponentNameKind {
263    Label,
264    Constructor,
265    Method,
266    Static,
267    Interface,
268    Dependency,
269    Url,
270    Hash,
271}
272
273/// Created via [`ComponentName::kind`] and classifies a name.
274#[derive(Debug, Clone)]
275pub enum ComponentNameKind<'a> {
276    /// `a-b-c`
277    Label(&'a KebabStr),
278    /// `[constructor]a-b`
279    Constructor(&'a KebabStr),
280    /// `[method]a-b.c-d`
281    #[allow(missing_docs)]
282    Method(ResourceFunc<'a>),
283    /// `[static]a-b.c-d`
284    #[allow(missing_docs)]
285    Static(ResourceFunc<'a>),
286    /// `wasi:http/types@2.0`
287    #[allow(missing_docs)]
288    Interface(InterfaceName<'a>),
289    /// `locked-dep=foo:bar/baz`
290    #[allow(missing_docs)]
291    Dependency(DependencyName<'a>),
292    /// `url=https://...`
293    #[allow(missing_docs)]
294    Url(UrlName<'a>),
295    /// `integrity=sha256:...`
296    #[allow(missing_docs)]
297    Hash(HashName<'a>),
298}
299
300const CONSTRUCTOR: &str = "[constructor]";
301const METHOD: &str = "[method]";
302const STATIC: &str = "[static]";
303
304impl ComponentName {
305    /// Attempts to parse `name` as a valid component name, returning `Err` if
306    /// it's not valid.
307    pub fn new(name: &str, offset: usize) -> Result<ComponentName> {
308        Self::new_with_features(name, offset, WasmFeatures::default())
309    }
310
311    /// Attempts to parse `name` as a valid component name, returning `Err` if
312    /// it's not valid.
313    ///
314    /// `features` can be used to enable or disable validation of certain forms
315    /// of supported import names.
316    pub fn new_with_features(name: &str, offset: usize, features: WasmFeatures) -> Result<Self> {
317        let mut parser = ComponentNameParser {
318            next: name,
319            offset,
320            features,
321        };
322        let kind = parser.parse()?;
323        if !parser.next.is_empty() {
324            bail!(offset, "trailing characters found: `{}`", parser.next);
325        }
326        Ok(ComponentName {
327            raw: name.to_string(),
328            kind,
329        })
330    }
331
332    /// Returns the [`ComponentNameKind`] corresponding to this name.
333    pub fn kind(&self) -> ComponentNameKind<'_> {
334        use ComponentNameKind::*;
335        use ParsedComponentNameKind as PK;
336        match self.kind {
337            PK::Label => Label(KebabStr::new_unchecked(&self.raw)),
338            PK::Constructor => Constructor(KebabStr::new_unchecked(&self.raw[CONSTRUCTOR.len()..])),
339            PK::Method => Method(ResourceFunc(&self.raw[METHOD.len()..])),
340            PK::Static => Static(ResourceFunc(&self.raw[STATIC.len()..])),
341            PK::Interface => Interface(InterfaceName(&self.raw)),
342            PK::Dependency => Dependency(DependencyName(&self.raw)),
343            PK::Url => Url(UrlName(&self.raw)),
344            PK::Hash => Hash(HashName(&self.raw)),
345        }
346    }
347
348    /// Returns the raw underlying name as a string.
349    pub fn as_str(&self) -> &str {
350        &self.raw
351    }
352}
353
354impl From<ComponentName> for String {
355    fn from(name: ComponentName) -> String {
356        name.raw
357    }
358}
359
360impl Hash for ComponentName {
361    fn hash<H: Hasher>(&self, hasher: &mut H) {
362        self.kind().hash(hasher)
363    }
364}
365
366impl PartialEq for ComponentName {
367    fn eq(&self, other: &ComponentName) -> bool {
368        self.kind().eq(&other.kind())
369    }
370}
371
372impl Eq for ComponentName {}
373
374impl Ord for ComponentName {
375    fn cmp(&self, other: &ComponentName) -> Ordering {
376        self.kind().cmp(&other.kind())
377    }
378}
379
380impl PartialOrd for ComponentName {
381    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
382        self.kind.partial_cmp(&other.kind)
383    }
384}
385
386impl fmt::Display for ComponentName {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        self.raw.fmt(f)
389    }
390}
391
392impl fmt::Debug for ComponentName {
393    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394        self.raw.fmt(f)
395    }
396}
397
398impl ComponentNameKind<'_> {
399    /// Returns the [`ParsedComponentNameKind`] of the [`ComponentNameKind`].
400    fn kind(&self) -> ParsedComponentNameKind {
401        match self {
402            Self::Label(_) => ParsedComponentNameKind::Label,
403            Self::Constructor(_) => ParsedComponentNameKind::Constructor,
404            Self::Method(_) => ParsedComponentNameKind::Method,
405            Self::Static(_) => ParsedComponentNameKind::Static,
406            Self::Interface(_) => ParsedComponentNameKind::Interface,
407            Self::Dependency(_) => ParsedComponentNameKind::Dependency,
408            Self::Url(_) => ParsedComponentNameKind::Url,
409            Self::Hash(_) => ParsedComponentNameKind::Hash,
410        }
411    }
412}
413
414impl Ord for ComponentNameKind<'_> {
415    fn cmp(&self, other: &Self) -> Ordering {
416        use ComponentNameKind::*;
417
418        match (self, other) {
419            (Label(lhs), Label(rhs)) => lhs.cmp(rhs),
420            (Constructor(lhs), Constructor(rhs)) => lhs.cmp(rhs),
421            (Method(lhs) | Static(lhs), Method(rhs) | Static(rhs)) => lhs.cmp(rhs),
422
423            // `[..]l.l` is equivalent to `l`
424            (Label(plain), Method(method) | Static(method))
425            | (Method(method) | Static(method), Label(plain))
426                if *plain == method.resource() && *plain == method.method() =>
427            {
428                Ordering::Equal
429            }
430
431            (Interface(lhs), Interface(rhs)) => lhs.cmp(rhs),
432            (Dependency(lhs), Dependency(rhs)) => lhs.cmp(rhs),
433            (Url(lhs), Url(rhs)) => lhs.cmp(rhs),
434            (Hash(lhs), Hash(rhs)) => lhs.cmp(rhs),
435
436            (Label(_), _)
437            | (Constructor(_), _)
438            | (Method(_), _)
439            | (Static(_), _)
440            | (Interface(_), _)
441            | (Dependency(_), _)
442            | (Url(_), _)
443            | (Hash(_), _) => self.kind().cmp(&other.kind()),
444        }
445    }
446}
447
448impl PartialOrd for ComponentNameKind<'_> {
449    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
450        Some(self.cmp(other))
451    }
452}
453
454impl Hash for ComponentNameKind<'_> {
455    fn hash<H: Hasher>(&self, hasher: &mut H) {
456        use ComponentNameKind::*;
457        match self {
458            Label(name) => (0u8, name).hash(hasher),
459            Constructor(name) => (1u8, name).hash(hasher),
460
461            Method(name) | Static(name) => {
462                // `l.l` hashes the same as `l` since they're equal above,
463                // otherwise everything is hashed as `a.b` with a unique
464                // prefix.
465                if name.resource() == name.method() {
466                    (0u8, name.resource()).hash(hasher)
467                } else {
468                    (2u8, name).hash(hasher)
469                }
470            }
471
472            Interface(name) => (3u8, name).hash(hasher),
473            Dependency(name) => (4u8, name).hash(hasher),
474            Url(name) => (5u8, name).hash(hasher),
475            Hash(name) => (6u8, name).hash(hasher),
476        }
477    }
478}
479
480impl PartialEq for ComponentNameKind<'_> {
481    fn eq(&self, other: &ComponentNameKind<'_>) -> bool {
482        self.cmp(other) == Ordering::Equal
483    }
484}
485
486impl Eq for ComponentNameKind<'_> {}
487
488/// A resource name and its function, stored as `a.b`.
489#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
490pub struct ResourceFunc<'a>(&'a str);
491
492impl<'a> ResourceFunc<'a> {
493    /// Returns the underlying string as `a.b`
494    pub fn as_str(&self) -> &'a str {
495        self.0
496    }
497
498    /// Returns the resource name or the `a` in `a.b`
499    pub fn resource(&self) -> &'a KebabStr {
500        let dot = self.0.find('.').unwrap();
501        KebabStr::new_unchecked(&self.0[..dot])
502    }
503
504    /// Returns the method name or the `b` in `a.b`
505    pub fn method(&self) -> &'a KebabStr {
506        let dot = self.0.find('.').unwrap();
507        KebabStr::new_unchecked(&self.0[dot + 1..])
508    }
509}
510
511/// An interface name, stored as `a:b/c@1.2.3`
512#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
513pub struct InterfaceName<'a>(&'a str);
514
515impl<'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)]
558pub struct DependencyName<'a>(&'a str);
559
560impl<'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)]
569pub struct UrlName<'a>(&'a str);
570
571impl<'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)]
580pub struct HashName<'a>(&'a str);
581
582impl<'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.
594struct ComponentNameParser<'a> {
595    next: &'a str,
596    offset: usize,
597    features: WasmFeatures,
598}
599
600impl<'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.cm_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.cm_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
912fn is_base64(s: &str) -> bool {
913    if s.is_empty() {
914        return false;
915    }
916    let mut equals = 0;
917    for (i, byte) 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)]
928mod 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_some());
948        assert!(KebabStr::new("0-a").is_none());
949        assert!(KebabStr::new("a-b--c").is_none());
950        assert!(KebabStr::new("a0-000-3d4a-54FF").is_some());
951        assert!(KebabStr::new("a0-000-3d4A-54Ff").is_none());
952    }
953
954    #[test]
955    fn name_smoke() {
956        assert!(parse_kebab_name("a").is_some());
957        assert!(parse_kebab_name("[foo]a").is_none());
958        assert!(parse_kebab_name("[constructor]a").is_some());
959        assert!(parse_kebab_name("[method]a").is_none());
960        assert!(parse_kebab_name("[method]a.b").is_some());
961        assert!(parse_kebab_name("[method]a-0.b-1").is_some());
962        assert!(parse_kebab_name("[method]a.b.c").is_none());
963        assert!(parse_kebab_name("[static]a.b").is_some());
964        assert!(parse_kebab_name("[static]a").is_none());
965    }
966
967    #[test]
968    fn name_equality() {
969        assert_eq!(parse_kebab_name("a"), parse_kebab_name("a"));
970        assert_ne!(parse_kebab_name("a"), parse_kebab_name("b"));
971        assert_eq!(
972            parse_kebab_name("[constructor]a"),
973            parse_kebab_name("[constructor]a")
974        );
975        assert_ne!(
976            parse_kebab_name("[constructor]a"),
977            parse_kebab_name("[constructor]b")
978        );
979        assert_eq!(
980            parse_kebab_name("[method]a.b"),
981            parse_kebab_name("[method]a.b")
982        );
983        assert_ne!(
984            parse_kebab_name("[method]a.b"),
985            parse_kebab_name("[method]b.b")
986        );
987        assert_eq!(
988            parse_kebab_name("[static]a.b"),
989            parse_kebab_name("[static]a.b")
990        );
991        assert_ne!(
992            parse_kebab_name("[static]a.b"),
993            parse_kebab_name("[static]b.b")
994        );
995
996        assert_eq!(
997            parse_kebab_name("[static]a.b"),
998            parse_kebab_name("[method]a.b")
999        );
1000        assert_eq!(
1001            parse_kebab_name("[method]a.b"),
1002            parse_kebab_name("[static]a.b")
1003        );
1004
1005        assert_ne!(
1006            parse_kebab_name("[method]b.b"),
1007            parse_kebab_name("[static]a.b")
1008        );
1009
1010        let mut s = HashSet::new();
1011        assert!(s.insert(parse_kebab_name("a")));
1012        assert!(s.insert(parse_kebab_name("[constructor]a")));
1013        assert!(s.insert(parse_kebab_name("[method]a.b")));
1014        assert!(!s.insert(parse_kebab_name("[static]a.b")));
1015        assert!(s.insert(parse_kebab_name("[static]b.b")));
1016    }
1017}