Coverage Report

Created: 2025-12-04 07:01

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