tinywasm_wasmparser/validator/
names.rs

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