Coverage Report

Created: 2026-08-15 07:39

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