Coverage Report

Created: 2026-08-08 06:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/WasmEdge/lib/validator/component_name.cpp
Line
Count
Source
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright The WasmEdge Authors
3
4
#include "validator/component_name.h"
5
6
#include "spdlog/spdlog.h"
7
8
#include <algorithm>
9
#include <cctype>
10
#include <string_view>
11
12
namespace WasmEdge {
13
namespace Validator {
14
15
using namespace std::literals;
16
17
// label          ::= <first-fragment> ( '-' <fragment> )*
18
// first-fragment ::= <first-word> | <first-acronym>
19
// first-word     ::= [a-z] [0-9a-z]*
20
// first-acronym  ::= [A-Z] [0-9A-Z]*
21
// fragment       ::= <word> | <acronym>
22
// word           ::= [0-9a-z]+
23
// acronym        ::= [0-9A-Z]+
24
15.5k
bool isKebabString(std::string_view Input) {
25
15.5k
  bool IsFirstPart = true;
26
15.5k
  bool Uppercase = false;
27
15.5k
  bool Lowercase = false;
28
15.5k
  bool Digit = false;
29
30
45.6k
  for (char C : Input) {
31
45.6k
    if (islower(C)) {
32
12.9k
      if (Uppercase)
33
17
        return false;
34
12.9k
      Lowercase = true;
35
32.6k
    } else if (isupper(C)) {
36
12.6k
      if (Lowercase)
37
25
        return false;
38
12.6k
      Uppercase = true;
39
19.9k
    } else if (isdigit(C)) {
40
18.0k
      if (IsFirstPart && !(Uppercase || Lowercase))
41
17
        return false;
42
18.0k
      Digit = true;
43
18.0k
    } else if (C == '-') {
44
1.64k
      if (Uppercase || Lowercase || Digit) {
45
1.62k
        IsFirstPart = false;
46
1.62k
        Uppercase = false;
47
1.62k
        Lowercase = false;
48
1.62k
        Digit = false;
49
1.62k
      } else {
50
16
        return false;
51
16
      }
52
1.64k
    } else {
53
247
      return false;
54
247
    }
55
45.6k
  }
56
57
15.2k
  return Input.size() > 0 && Input.back() != '-';
58
15.5k
}
59
60
namespace {
61
62
// words      ::= <first-word> ( '-' <word> )*
63
// first-word ::= [a-z] [0-9a-z]*
64
// word       ::= [0-9a-z]+
65
4.06k
bool isLowercaseKebabString(std::string_view Input) {
66
4.06k
  if (Input.empty() || !islower(Input[0]))
67
17
    return false;
68
7.60k
  for (char C : Input) {
69
7.60k
    if (C != '-' && !islower(C) && !isdigit(C))
70
30
      return false;
71
7.60k
  }
72
4.01k
  return Input.back() != '-' && Input.find("--"sv) == Input.npos;
73
4.04k
}
74
75
5.71k
bool isEOF(std::string_view Input) { return Input.empty(); }
76
77
10.7k
bool readUntil(std::string_view &Input, char Delim, std::string_view &Output) {
78
10.7k
  size_t Pos = Input.find(Delim);
79
10.7k
  if (Pos == Input.npos) {
80
8.48k
    return false;
81
8.48k
  }
82
83
2.30k
  Output = Input.substr(0, Pos);
84
2.30k
  Input.remove_prefix(Pos + 1);
85
2.30k
  return true;
86
10.7k
}
87
88
88.5k
bool tryRead(std::string_view Prefix, std::string_view &Name) {
89
88.5k
  if (Prefix.size() > Name.size())
90
69.5k
    return false;
91
18.9k
  if (Prefix != Name.substr(0, Prefix.size()))
92
17.8k
    return false;
93
94
1.07k
  Name.remove_prefix(Prefix.size());
95
1.07k
  return true;
96
18.9k
}
97
98
3.83k
bool tryReadKebab(std::string_view &Input, std::string_view &Output) {
99
3.83k
  size_t Pos = 0;
100
11.6k
  while (Pos < Input.size()) {
101
11.6k
    if (isalnum(Input[Pos]) || Input[Pos] == '-') {
102
7.85k
      Pos++;
103
7.85k
    } else {
104
3.76k
      break;
105
3.76k
    }
106
11.6k
  }
107
3.83k
  Output = Input.substr(0, Pos);
108
3.83k
  Input.remove_prefix(Pos);
109
3.83k
  return isKebabString(Output);
110
3.83k
}
111
112
// integrity-metadata = *WSP hash-with-options *(1*WSP hash-with-options) *WSP
113
// hash-with-options   = hash-expression *("?" option-expression)
114
// hash-expression     = hash-algorithm "-" base64-value
115
// hash-algorithm      = "sha256" / "sha384" / "sha512"
116
// base64-value        = *VCHAR (visible chars, no whitespace)
117
71
bool isIntegrityMetadata(std::string_view Input) {
118
155
  while (!Input.empty() && Input.front() == ' ')
119
84
    Input.remove_prefix(1);
120
192
  while (!Input.empty() && Input.back() == ' ')
121
121
    Input.remove_suffix(1);
122
71
  if (Input.empty())
123
2
    return false;
124
125
69
  bool HasToken = false;
126
125
  while (!Input.empty()) {
127
139
    while (!Input.empty() && Input.front() == ' ')
128
50
      Input.remove_prefix(1);
129
89
    if (Input.empty())
130
0
      break;
131
132
89
    size_t TokenEnd = Input.find(' ');
133
89
    std::string_view Token =
134
89
        (TokenEnd == Input.npos) ? Input : Input.substr(0, TokenEnd);
135
89
    Input =
136
89
        (TokenEnd == Input.npos) ? std::string_view{} : Input.substr(TokenEnd);
137
138
89
    size_t OptPos = Token.find('?');
139
89
    std::string_view HashExpr =
140
89
        (OptPos == Token.npos) ? Token : Token.substr(0, OptPos);
141
142
89
    bool ValidAlgo = false;
143
89
    static constexpr std::string_view Algos[3] = {"sha256-", "sha384-",
144
89
                                                  "sha512-"};
145
169
    for (auto AlgoSV : Algos) {
146
169
      if (HashExpr.size() > AlgoSV.size() &&
147
115
          HashExpr.substr(0, AlgoSV.size()) == AlgoSV) {
148
66
        auto Value = HashExpr.substr(AlgoSV.size());
149
66
        if (std::all_of(Value.begin(), Value.end(),
150
757
                        [](char C) { return C >= 0x21 && C <= 0x7E; })) {
151
56
          ValidAlgo = true;
152
56
        }
153
66
        break;
154
66
      }
155
169
    }
156
89
    if (!ValidAlgo)
157
33
      return false;
158
159
56
    HasToken = true;
160
56
  }
161
162
36
  return HasToken;
163
69
}
164
165
// Parses a non-negative integer without leading zeros.
166
// Returns the end position, or npos on failure.
167
1.16k
size_t parseNumeric(std::string_view V) {
168
1.16k
  if (V.empty())
169
2
    return std::string_view::npos;
170
1.15k
  if (V[0] == '0') {
171
225
    return 1;
172
225
  }
173
933
  if (V[0] >= '1' && V[0] <= '9') {
174
872
    size_t Pos = 1;
175
2.24k
    while (Pos < V.size() && isdigit(V[Pos]))
176
1.37k
      Pos++;
177
872
    return Pos;
178
872
  }
179
61
  return std::string_view::npos;
180
933
}
181
182
// canonversion ::= [1-9] [0-9]*
183
//                | '0.' [1-9] [0-9]*
184
//                | '0.0.' [1-9] [0-9]*
185
390
bool isCanonVersion(std::string_view V) {
186
390
  if (V.empty())
187
1
    return false;
188
189
  // canonversion ::= [1-9] [0-9]* | '0.' [1-9] [0-9]* | '0.0.' [1-9] [0-9]*
190
560
  for (int I = 0; I < 3; I++) {
191
560
    if (V[0] >= '1' && V[0] <= '9') {
192
294
      size_t End = parseNumeric(V);
193
294
      return End == V.size();
194
294
    }
195
266
    if (!tryRead("0."sv, V) || V.empty())
196
95
      return false;
197
266
  }
198
199
0
  return false;
200
389
}
201
202
// Validates a dot-separated pre-release or build identifier segment.
203
// Each identifier is [0-9A-Za-z-]+.
204
// Numeric identifiers must not have leading zeros.
205
236
bool isPreReleaseOrBuild(std::string_view V, bool CheckLeadingZeros) {
206
236
  if (V.empty())
207
4
    return false;
208
232
  size_t Start = 0;
209
695
  while (Start < V.size()) {
210
632
    size_t DotPos = V.find('.', Start);
211
632
    std::string_view Ident =
212
632
        (DotPos == V.npos) ? V.substr(Start) : V.substr(Start, DotPos - Start);
213
632
    if (Ident.empty())
214
3
      return false;
215
3.24k
    for (char C : Ident) {
216
3.24k
      if (!isalnum(C) && C != '-')
217
35
        return false;
218
3.24k
    }
219
594
    if (CheckLeadingZeros) {
220
397
      bool AllDigits = std::all_of(Ident.begin(), Ident.end(),
221
899
                                   [](char C) { return isdigit(C); });
222
397
      if (AllDigits && Ident.size() > 1 && Ident[0] == '0')
223
3
        return false;
224
397
    }
225
591
    if (DotPos == V.npos)
226
128
      break;
227
463
    Start = DotPos + 1;
228
463
  }
229
191
  return true;
230
232
}
231
232
// MAJOR.MINOR.PATCH[-prerelease][+build] per semver.org 2.0
233
345
bool isValidSemver(std::string_view V) {
234
345
  if (V.empty())
235
1
    return false;
236
237
  // Parse MAJOR.MINOR.PATCH
238
1.08k
  for (int I = 0; I < 3; I++) {
239
866
    size_t End = parseNumeric(V);
240
866
    if (End == std::string_view::npos)
241
63
      return false;
242
803
    if (I < 2) {
243
582
      if (End >= V.size() || V[End] != '.')
244
60
        return false;
245
522
      V.remove_prefix(End + 1);
246
522
    } else {
247
221
      V.remove_prefix(End);
248
221
    }
249
803
  }
250
251
221
  if (V.empty())
252
39
    return true;
253
254
182
  if (V[0] == '-') {
255
146
    V.remove_prefix(1);
256
146
    size_t PlusPos = V.find('+');
257
146
    std::string_view PreRelease =
258
146
        (PlusPos == V.npos) ? V : V.substr(0, PlusPos);
259
146
    if (!isPreReleaseOrBuild(PreRelease, true))
260
20
      return false;
261
126
    if (PlusPos == V.npos)
262
58
      return true;
263
68
    V.remove_prefix(PlusPos);
264
68
  }
265
266
104
  if (!V.empty() && V[0] == '+') {
267
90
    V.remove_prefix(1);
268
90
    return isPreReleaseOrBuild(V, false);
269
90
  }
270
271
14
  return V.empty();
272
104
}
273
274
390
bool isVersion(std::string_view V) {
275
390
  return isCanonVersion(V) || isValidSemver(V);
276
390
}
277
278
797
Unexpected<ErrCode> reportError(std::string_view Reason) {
279
797
  spdlog::error(ErrCode::Value::ComponentInvalidName);
280
797
  spdlog::error("    Component name: {}"sv, Reason);
281
797
  return Unexpect(ErrCode::Value::ComponentInvalidName);
282
797
}
283
284
// hashname ::= 'integrity=<' <integrity-metadata> '>'
285
// Parses optional ',integrity=<...>' suffix from Next.
286
// If Next is empty, returns true with empty Integrity.
287
// On success, Next is consumed and Integrity is set.
288
Expect<void> tryParseIntegritySuffix(std::string_view &Next,
289
37
                                     std::string_view &Integrity) {
290
37
  if (Next.empty()) {
291
12
    Integrity = {};
292
12
    return {};
293
12
  }
294
25
  if (!tryRead(",integrity=<"sv, Next))
295
5
    return reportError("expected ',integrity=<' after "sv);
296
20
  std::string_view IntegrityData;
297
20
  if (!readUntil(Next, '>', IntegrityData))
298
3
    return reportError("expected '>' closing integrity"sv);
299
17
  if (!isIntegrityMetadata(IntegrityData))
300
4
    return reportError("invalid integrity metadata"sv);
301
13
  if (!isEOF(Next))
302
1
    return reportError("unexpected trailing content after integrity"sv);
303
12
  Integrity = IntegrityData;
304
12
  return {};
305
13
}
306
307
// pkgpath ::= <namespace> <words>
308
// Parses 'namespace:package' from Next, stopping at delimiters in StopChars.
309
struct PkgPath {
310
  std::string_view Namespace;
311
  std::string_view Package;
312
};
313
314
Expect<PkgPath> parsePkgPath(std::string_view &Next,
315
69
                             std::string_view StopChars) {
316
69
  std::string_view Namespace;
317
69
  if (!readUntil(Next, ':', Namespace))
318
2
    return reportError("expected ':' in namespace"sv);
319
67
  if (!isLowercaseKebabString(Namespace))
320
3
    return reportError("invalid namespace"sv);
321
322
64
  size_t PkgEnd = Next.find_first_of(StopChars);
323
64
  if (PkgEnd == Next.npos)
324
3
    return reportError("unterminated package name"sv);
325
61
  std::string_view Package = Next.substr(0, PkgEnd);
326
61
  Next.remove_prefix(PkgEnd);
327
61
  if (!isLowercaseKebabString(Package))
328
5
    return reportError("invalid package name"sv);
329
330
56
  return PkgPath{Namespace, Package};
331
61
}
332
333
} // anonymous namespace
334
335
// exportname        ::= <plainname> | <interfacename>
336
// importname        ::= <exportname> | <depname> | <urlname> | <hashname>
337
9.25k
Expect<ComponentName> ComponentName::parse(std::string_view Name) {
338
9.25k
  ComponentName Result(Name);
339
9.25k
  auto Next = Name;
340
341
  // plainname         ::= <label>
342
  //                     | '[constructor]' <label>
343
  //                     | '[method]' <label> '.' <label>
344
  //                     | '[static]' <label> '.' <label>
345
346
9.25k
  if (tryRead("[constructor]"sv, Next)) {
347
375
    if (!isKebabString(Next)) {
348
34
      return reportError("invalid label after [constructor]"sv);
349
34
    }
350
341
    Result.Detail.emplace<ConstructorDetail>(ConstructorDetail{Next});
351
341
    Result.NoTagName = Next;
352
341
    Result.Kind = ComponentNameKind::Constructor;
353
341
    return Result;
354
375
  }
355
356
8.88k
  auto tryReadResourceWithLabel = [&](std::string_view Tag,
357
8.88k
                                      std::string_view &Resource,
358
17.7k
                                      std::string_view &Label) -> bool {
359
17.7k
    auto Saved = Next;
360
17.7k
    if (!tryRead(Tag, Next)) {
361
17.6k
      return false;
362
17.6k
    }
363
158
    auto TmpNoTagName = Next;
364
158
    if (!readUntil(Next, '.', Resource)) {
365
3
      Next = Saved;
366
3
      return false;
367
3
    }
368
155
    if (!isKebabString(Resource) || !isKebabString(Next)) {
369
85
      Next = Saved;
370
85
      return false;
371
85
    }
372
70
    Result.NoTagName = TmpNoTagName;
373
70
    Label = Next;
374
70
    return true;
375
155
  };
376
377
8.88k
  {
378
8.88k
    std::string_view Resource, Label;
379
8.88k
    if (tryReadResourceWithLabel("[method]"sv, Resource, Label)) {
380
2
      Result.Detail.emplace<MethodDetail>(MethodDetail{Resource, Label});
381
2
      Result.Kind = ComponentNameKind::Method;
382
2
      return Result;
383
2
    }
384
8.88k
  }
385
386
8.88k
  {
387
8.88k
    std::string_view Resource, Label;
388
8.88k
    if (tryReadResourceWithLabel("[static]"sv, Resource, Label)) {
389
68
      Result.Detail.emplace<StaticDetail>(StaticDetail{Resource, Label});
390
68
      Result.Kind = ComponentNameKind::Static;
391
68
      return Result;
392
68
    }
393
8.88k
  }
394
395
8.81k
  if (tryRead("[async]"sv, Next)) {
396
1
    Result.NoTagName = Next;
397
1
    return reportError("[async] not supported yet"sv);
398
1
  }
399
400
8.81k
  if (tryRead("[async method]"sv, Next)) {
401
1
    Result.NoTagName = Next;
402
1
    return reportError("[async method] not supported yet"sv);
403
1
  }
404
405
8.81k
  if (tryRead("[async static]"sv, Next)) {
406
1
    Result.NoTagName = Next;
407
1
    return reportError("[async static] not supported yet"sv);
408
1
  }
409
410
8.81k
  if (Next.size() != 0 && Next[0] == '[') {
411
122
    return reportError("unknown annotation"sv);
412
122
  }
413
8.68k
  Result.NoTagName = Next;
414
415
  // depname ::= 'unlocked-dep=<' <pkgnamequery> '>'
416
  //           | 'locked-dep=<' <pkgname> '>' ( ',' <hashname> )?
417
418
8.68k
  if (tryRead("unlocked-dep="sv, Next)) {
419
41
    if (!tryRead("<"sv, Next))
420
7
      return reportError("expected '<' after unlocked-dep="sv);
421
422
61
    EXPECTED_TRY(auto Path, parsePkgPath(Next, "@>"sv));
423
424
    // verrange ::= '@*'
425
    //            | '@{' verlower '}'
426
    //            | '@{' verupper '}'
427
    //            | '@{' verlower ' ' verupper '}'
428
61
    std::string_view VersionRange;
429
61
    if (!Next.empty() && Next[0] == '@') {
430
26
      auto VerStart = Next;
431
26
      Next.remove_prefix(1);
432
26
      if (Next.empty())
433
0
        return reportError(
434
0
            "expected version range after '@' in unlocked-dep"sv);
435
436
26
      if (Next[0] == '*') {
437
3
        Next.remove_prefix(1);
438
23
      } else if (Next[0] == '{') {
439
15
        size_t ClosePos = Next.find('}');
440
15
        if (ClosePos == Next.npos)
441
1
          return reportError("expected '}' in unlocked-dep version range"sv);
442
14
        auto RangeBody = Next.substr(1, ClosePos - 1);
443
444
14
        auto ValidateRange = [](std::string_view Body) -> bool {
445
14
          if (Body.empty())
446
1
            return false;
447
13
          auto Remaining = Body;
448
449
13
          if (tryRead(">="sv, Remaining)) {
450
2
            size_t SpacePos = Remaining.find(' ');
451
2
            std::string_view Lower = (SpacePos == Remaining.npos)
452
2
                                         ? Remaining
453
2
                                         : Remaining.substr(0, SpacePos);
454
2
            if (!isValidSemver(Lower))
455
2
              return false;
456
0
            if (SpacePos == Remaining.npos)
457
0
              return true;
458
0
            Remaining.remove_prefix(SpacePos + 1);
459
0
            if (!tryRead("<"sv, Remaining))
460
0
              return false;
461
0
            return isValidSemver(Remaining);
462
0
          }
463
464
11
          if (tryRead("<"sv, Remaining)) {
465
1
            return isValidSemver(Remaining);
466
1
          }
467
468
10
          return false;
469
11
        };
470
471
14
        if (!ValidateRange(RangeBody))
472
14
          return reportError("invalid version range in unlocked-dep"sv);
473
474
0
        Next.remove_prefix(ClosePos + 1);
475
8
      } else {
476
8
        return reportError("expected '*' or '{' after '@' in unlocked-dep"sv);
477
8
      }
478
3
      VersionRange = VerStart.substr(0, VerStart.size() - Next.size());
479
3
    }
480
481
4
    if (!tryRead(">"sv, Next))
482
3
      return reportError("expected '>' closing unlocked-dep"sv);
483
484
1
    if (!isEOF(Next))
485
1
      return reportError("unexpected trailing content after unlocked-dep"sv);
486
487
0
    Result.Detail.emplace<UnlockedDepDetail>(
488
0
        UnlockedDepDetail{Path.Namespace, Path.Package, VersionRange});
489
0
    Result.Kind = ComponentNameKind::UnlockedDep;
490
0
    return Result;
491
1
  }
492
493
8.64k
  if (tryRead("locked-dep="sv, Next)) {
494
43
    if (!tryRead("<"sv, Next))
495
8
      return reportError("expected '<' after locked-dep="sv);
496
497
64
    EXPECTED_TRY(auto Path, parsePkgPath(Next, "@>"sv));
498
499
64
    std::string_view Version;
500
64
    if (!Next.empty() && Next[0] == '@') {
501
6
      Next.remove_prefix(1);
502
6
      size_t VerEnd = Next.find('>');
503
6
      if (VerEnd == Next.npos)
504
1
        return reportError("expected '>' after version in locked-dep"sv);
505
5
      Version = Next.substr(0, VerEnd);
506
5
      Next.remove_prefix(VerEnd);
507
5
      if (!isValidSemver(Version))
508
5
        return reportError("invalid semver in locked-dep"sv);
509
5
    }
510
511
23
    if (!tryRead(">"sv, Next))
512
0
      return reportError("expected '>' closing locked-dep"sv);
513
514
23
    std::string_view Integrity;
515
23
    EXPECTED_TRY(tryParseIntegritySuffix(Next, Integrity));
516
517
12
    Result.Detail.emplace<LockedDepDetail>(
518
12
        LockedDepDetail{Path.Namespace, Path.Package, Version, Integrity});
519
12
    Result.Kind = ComponentNameKind::LockedDep;
520
12
    return Result;
521
23
  }
522
523
  // urlname ::= 'url=<' <nonbrackets> '>' (',' <hashname>)?
524
  // nonbrackets ::= [^<>]*
525
8.60k
  if (tryRead("url="sv, Next)) {
526
28
    if (!tryRead("<"sv, Next))
527
9
      return reportError("expected '<' after url="sv);
528
529
19
    size_t ClosePos = Next.find('>');
530
19
    if (ClosePos == Next.npos)
531
1
      return reportError("expected '>' closing url"sv);
532
533
18
    std::string_view UrlContent = Next.substr(0, ClosePos);
534
18
    if (UrlContent.find('<') != UrlContent.npos)
535
4
      return reportError("'<' not allowed inside url"sv);
536
14
    Next.remove_prefix(ClosePos + 1);
537
538
14
    std::string_view Integrity;
539
14
    EXPECTED_TRY(tryParseIntegritySuffix(Next, Integrity));
540
541
12
    Result.Detail.emplace<UrlDetail>(UrlDetail{UrlContent, Integrity});
542
12
    Result.Kind = ComponentNameKind::Url;
543
12
    return Result;
544
14
  }
545
546
  // hashname ::= 'integrity=<' <integrity-metadata> '>'
547
8.57k
  if (tryRead("integrity="sv, Next)) {
548
68
    if (!tryRead("<"sv, Next))
549
13
      return reportError("expected '<' after integrity="sv);
550
55
    std::string_view IntegrityData;
551
55
    if (!readUntil(Next, '>', IntegrityData))
552
1
      return reportError("expected '>' closing integrity"sv);
553
54
    if (!isIntegrityMetadata(IntegrityData))
554
31
      return reportError("invalid integrity metadata"sv);
555
23
    if (!isEOF(Next))
556
2
      return reportError("unexpected trailing content after integrity"sv);
557
21
    Result.Detail.emplace<IntegrityDetail>(IntegrityDetail{IntegrityData});
558
21
    Result.Kind = ComponentNameKind::Integrity;
559
21
    return Result;
560
23
  }
561
562
  // interfacename ::= <namespace> <label> <projection> <interfaceversion>?
563
  // namespace     ::= <words> ':'
564
  // projection    ::= '/' <label>
565
  // interfaceversion ::= '@' <valid semver> | '@' <canonversion>
566
8.50k
  {
567
8.50k
    std::string_view Namespace, Package, Interface, Version;
568
569
8.50k
    int Counter = 0;
570
10.4k
    while (readUntil(Next, ':', Namespace)) {
571
2.00k
      Counter++;
572
2.00k
      if (!isLowercaseKebabString(Namespace)) {
573
38
        return reportError("invalid namespace in interface name"sv);
574
38
      }
575
2.00k
    }
576
8.47k
    if (Counter == 0) {
577
      // No ':' found — fall through to label parsing below.
578
6.52k
      goto ParseLabel;
579
6.52k
    }
580
1.94k
    if (Counter != 1) {
581
6
      return reportError("nested namespaces not supported yet"sv);
582
6
    }
583
584
    // interfacename ::= <namespace> <words> <projection> ...
585
1.93k
    if (!tryReadKebab(Next, Package) || !isLowercaseKebabString(Package)) {
586
22
      return reportError("invalid package in interface name"sv);
587
22
    }
588
589
1.91k
    Counter = 0;
590
3.80k
    while (!isEOF(Next) && Next[0] == '/') {
591
1.89k
      Next.remove_prefix(1);
592
1.89k
      Counter++;
593
1.89k
      if (!tryReadKebab(Next, Interface)) {
594
7
        return reportError("invalid projection label in interface name"sv);
595
7
      }
596
1.89k
    }
597
598
1.91k
    if (Counter == 0) {
599
32
      return reportError("expected '/' projection in interface name"sv);
600
32
    }
601
1.87k
    if (Counter != 1) {
602
3
      return reportError("nested projections not supported yet"sv);
603
3
    }
604
605
1.87k
    if (!isEOF(Next) && Next[0] == '@') {
606
390
      Next.remove_prefix(1);
607
390
      Version = Next;
608
390
      if (!isVersion(Version)) {
609
175
        return reportError("invalid version in interface name"sv);
610
175
      }
611
390
    }
612
613
1.70k
    Result.Detail.emplace<InterfaceDetail>(
614
1.70k
        InterfaceDetail{Namespace, Package, Interface, Version});
615
1.70k
    Result.Kind = ComponentNameKind::InterfaceType;
616
1.70k
    return Result;
617
1.87k
  }
618
619
6.52k
ParseLabel:
620
6.52k
  if (!isKebabString(Next)) {
621
220
    return reportError("invalid label"sv);
622
220
  }
623
6.30k
  Result.Detail.emplace<LabelDetail>();
624
6.30k
  Result.Kind = ComponentNameKind::Label;
625
6.30k
  return Result;
626
6.52k
}
627
628
} // namespace Validator
629
} // namespace WasmEdge