Coverage Report

Created: 2026-07-16 07:09

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
10.2k
bool isKebabString(std::string_view Input) {
25
10.2k
  bool IsFirstPart = true;
26
10.2k
  bool Uppercase = false;
27
10.2k
  bool Lowercase = false;
28
10.2k
  bool Digit = false;
29
30
34.1k
  for (char C : Input) {
31
34.1k
    if (islower(C)) {
32
6.54k
      if (Uppercase)
33
12
        return false;
34
6.53k
      Lowercase = true;
35
27.5k
    } else if (isupper(C)) {
36
7.78k
      if (Lowercase)
37
27
        return false;
38
7.75k
      Uppercase = true;
39
19.8k
    } else if (isdigit(C)) {
40
17.6k
      if (IsFirstPart && !(Uppercase || Lowercase))
41
18
        return false;
42
17.6k
      Digit = true;
43
17.6k
    } else if (C == '-') {
44
1.95k
      if (Uppercase || Lowercase || Digit) {
45
1.94k
        IsFirstPart = false;
46
1.94k
        Uppercase = false;
47
1.94k
        Lowercase = false;
48
1.94k
        Digit = false;
49
1.94k
      } else {
50
11
        return false;
51
11
      }
52
1.95k
    } else {
53
228
      return false;
54
228
    }
55
34.1k
  }
56
57
9.99k
  return Input.size() > 0 && Input.back() != '-';
58
10.2k
}
59
60
namespace {
61
62
// words      ::= <first-word> ( '-' <word> )*
63
// first-word ::= [a-z] [0-9a-z]*
64
// word       ::= [0-9a-z]+
65
5.47k
bool isLowercaseKebabString(std::string_view Input) {
66
5.47k
  if (Input.empty() || !islower(Input[0]))
67
25
    return false;
68
11.4k
  for (char C : Input) {
69
11.4k
    if (C != '-' && !islower(C) && !isdigit(C))
70
39
      return false;
71
11.4k
  }
72
5.40k
  return Input.back() != '-' && Input.find("--"sv) == Input.npos;
73
5.44k
}
74
75
7.76k
bool isEOF(std::string_view Input) { return Input.empty(); }
76
77
7.75k
bool readUntil(std::string_view &Input, char Delim, std::string_view &Output) {
78
7.75k
  size_t Pos = Input.find(Delim);
79
7.75k
  if (Pos == Input.npos) {
80
4.83k
    return false;
81
4.83k
  }
82
83
2.92k
  Output = Input.substr(0, Pos);
84
2.92k
  Input.remove_prefix(Pos + 1);
85
2.92k
  return true;
86
7.75k
}
87
88
51.2k
bool tryRead(std::string_view Prefix, std::string_view &Name) {
89
51.2k
  if (Prefix.size() > Name.size())
90
27.8k
    return false;
91
23.3k
  if (Prefix != Name.substr(0, Prefix.size()))
92
22.4k
    return false;
93
94
890
  Name.remove_prefix(Prefix.size());
95
890
  return true;
96
23.3k
}
97
98
5.24k
bool tryReadKebab(std::string_view &Input, std::string_view &Output) {
99
5.24k
  size_t Pos = 0;
100
17.4k
  while (Pos < Input.size()) {
101
17.3k
    if (isalnum(Input[Pos]) || Input[Pos] == '-') {
102
12.1k
      Pos++;
103
12.1k
    } else {
104
5.17k
      break;
105
5.17k
    }
106
17.3k
  }
107
5.24k
  Output = Input.substr(0, Pos);
108
5.24k
  Input.remove_prefix(Pos);
109
5.24k
  return isKebabString(Output);
110
5.24k
}
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
6
bool isIntegrityMetadata(std::string_view Input) {
118
7
  while (!Input.empty() && Input.front() == ' ')
119
1
    Input.remove_prefix(1);
120
6
  while (!Input.empty() && Input.back() == ' ')
121
0
    Input.remove_suffix(1);
122
6
  if (Input.empty())
123
2
    return false;
124
125
4
  bool HasToken = false;
126
4
  while (!Input.empty()) {
127
4
    while (!Input.empty() && Input.front() == ' ')
128
0
      Input.remove_prefix(1);
129
4
    if (Input.empty())
130
0
      break;
131
132
4
    size_t TokenEnd = Input.find(' ');
133
4
    std::string_view Token =
134
4
        (TokenEnd == Input.npos) ? Input : Input.substr(0, TokenEnd);
135
4
    Input =
136
4
        (TokenEnd == Input.npos) ? std::string_view{} : Input.substr(TokenEnd);
137
138
4
    size_t OptPos = Token.find('?');
139
4
    std::string_view HashExpr =
140
4
        (OptPos == Token.npos) ? Token : Token.substr(0, OptPos);
141
142
4
    bool ValidAlgo = false;
143
4
    static constexpr std::string_view Algos[3] = {"sha256-", "sha384-",
144
4
                                                  "sha512-"};
145
12
    for (auto AlgoSV : Algos) {
146
12
      if (HashExpr.size() > AlgoSV.size() &&
147
0
          HashExpr.substr(0, AlgoSV.size()) == AlgoSV) {
148
0
        auto Value = HashExpr.substr(AlgoSV.size());
149
0
        if (std::all_of(Value.begin(), Value.end(),
150
0
                        [](char C) { return C >= 0x21 && C <= 0x7E; })) {
151
0
          ValidAlgo = true;
152
0
        }
153
0
        break;
154
0
      }
155
12
    }
156
4
    if (!ValidAlgo)
157
4
      return false;
158
159
0
    HasToken = true;
160
0
  }
161
162
0
  return HasToken;
163
4
}
164
165
// Parses a non-negative integer without leading zeros.
166
// Returns the end position, or npos on failure.
167
1.30k
size_t parseNumeric(std::string_view V) {
168
1.30k
  if (V.empty())
169
3
    return std::string_view::npos;
170
1.29k
  if (V[0] == '0') {
171
301
    return 1;
172
301
  }
173
996
  if (V[0] >= '1' && V[0] <= '9') {
174
949
    size_t Pos = 1;
175
1.65k
    while (Pos < V.size() && isdigit(V[Pos]))
176
705
      Pos++;
177
949
    return Pos;
178
949
  }
179
47
  return std::string_view::npos;
180
996
}
181
182
// canonversion ::= [1-9] [0-9]*
183
//                | '0.' [1-9] [0-9]*
184
//                | '0.0.' [1-9] [0-9]*
185
404
bool isCanonVersion(std::string_view V) {
186
404
  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
614
  for (int I = 0; I < 3; I++) {
191
614
    if (V[0] >= '1' && V[0] <= '9') {
192
318
      size_t End = parseNumeric(V);
193
318
      return End == V.size();
194
318
    }
195
296
    if (!tryRead("0."sv, V) || V.empty())
196
85
      return false;
197
296
  }
198
199
0
  return false;
200
403
}
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
200
bool isPreReleaseOrBuild(std::string_view V, bool CheckLeadingZeros) {
206
200
  if (V.empty())
207
2
    return false;
208
198
  size_t Start = 0;
209
355
  while (Start < V.size()) {
210
342
    size_t DotPos = V.find('.', Start);
211
342
    std::string_view Ident =
212
342
        (DotPos == V.npos) ? V.substr(Start) : V.substr(Start, DotPos - Start);
213
342
    if (Ident.empty())
214
2
      return false;
215
1.19k
    for (char C : Ident) {
216
1.19k
      if (!isalnum(C) && C != '-')
217
26
        return false;
218
1.19k
    }
219
314
    if (CheckLeadingZeros) {
220
254
      bool AllDigits = std::all_of(Ident.begin(), Ident.end(),
221
468
                                   [](char C) { return isdigit(C); });
222
254
      if (AllDigits && Ident.size() > 1 && Ident[0] == '0')
223
1
        return false;
224
254
    }
225
313
    if (DotPos == V.npos)
226
156
      break;
227
157
    Start = DotPos + 1;
228
157
  }
229
169
  return true;
230
198
}
231
232
// MAJOR.MINOR.PATCH[-prerelease][+build] per semver.org 2.0
233
373
bool isValidSemver(std::string_view V) {
234
373
  if (V.empty())
235
2
    return false;
236
237
  // Parse MAJOR.MINOR.PATCH
238
1.25k
  for (int I = 0; I < 3; I++) {
239
982
    size_t End = parseNumeric(V);
240
982
    if (End == std::string_view::npos)
241
50
      return false;
242
932
    if (I < 2) {
243
664
      if (End >= V.size() || V[End] != '.')
244
53
        return false;
245
611
      V.remove_prefix(End + 1);
246
611
    } else {
247
268
      V.remove_prefix(End);
248
268
    }
249
932
  }
250
251
268
  if (V.empty())
252
76
    return true;
253
254
192
  if (V[0] == '-') {
255
153
    V.remove_prefix(1);
256
153
    size_t PlusPos = V.find('+');
257
153
    std::string_view PreRelease =
258
153
        (PlusPos == V.npos) ? V : V.substr(0, PlusPos);
259
153
    if (!isPreReleaseOrBuild(PreRelease, true))
260
12
      return false;
261
141
    if (PlusPos == V.npos)
262
121
      return true;
263
20
    V.remove_prefix(PlusPos);
264
20
  }
265
266
59
  if (!V.empty() && V[0] == '+') {
267
47
    V.remove_prefix(1);
268
47
    return isPreReleaseOrBuild(V, false);
269
47
  }
270
271
12
  return V.empty();
272
59
}
273
274
404
bool isVersion(std::string_view V) {
275
404
  return isCanonVersion(V) || isValidSemver(V);
276
404
}
277
278
751
Unexpected<ErrCode> reportError(std::string_view Reason) {
279
751
  spdlog::error(ErrCode::Value::ComponentInvalidName);
280
751
  spdlog::error("    Component name: {}"sv, Reason);
281
751
  return Unexpect(ErrCode::Value::ComponentInvalidName);
282
751
}
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
13
                                     std::string_view &Integrity) {
290
13
  if (Next.empty()) {
291
11
    Integrity = {};
292
11
    return {};
293
11
  }
294
2
  if (!tryRead(",integrity=<"sv, Next))
295
2
    return reportError("expected ',integrity=<' after "sv);
296
0
  std::string_view IntegrityData;
297
0
  if (!readUntil(Next, '>', IntegrityData))
298
0
    return reportError("expected '>' closing integrity"sv);
299
0
  if (!isIntegrityMetadata(IntegrityData))
300
0
    return reportError("invalid integrity metadata"sv);
301
0
  if (!isEOF(Next))
302
0
    return reportError("unexpected trailing content after integrity"sv);
303
0
  Integrity = IntegrityData;
304
0
  return {};
305
0
}
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
57
                             std::string_view StopChars) {
316
57
  std::string_view Namespace;
317
57
  if (!readUntil(Next, ':', Namespace))
318
1
    return reportError("expected ':' in namespace"sv);
319
56
  if (!isLowercaseKebabString(Namespace))
320
3
    return reportError("invalid namespace"sv);
321
322
53
  size_t PkgEnd = Next.find_first_of(StopChars);
323
53
  if (PkgEnd == Next.npos)
324
4
    return reportError("unterminated package name"sv);
325
49
  std::string_view Package = Next.substr(0, PkgEnd);
326
49
  Next.remove_prefix(PkgEnd);
327
49
  if (!isLowercaseKebabString(Package))
328
8
    return reportError("invalid package name"sv);
329
330
41
  return PkgPath{Namespace, Package};
331
49
}
332
333
} // anonymous namespace
334
335
// exportname        ::= <plainname> | <interfacename>
336
// importname        ::= <exportname> | <depname> | <urlname> | <hashname>
337
5.49k
Expect<ComponentName> ComponentName::parse(std::string_view Name) {
338
5.49k
  ComponentName Result(Name);
339
5.49k
  auto Next = Name;
340
341
  // plainname         ::= <label>
342
  //                     | '[constructor]' <label>
343
  //                     | '[method]' <label> '.' <label>
344
  //                     | '[static]' <label> '.' <label>
345
346
5.49k
  if (tryRead("[constructor]"sv, Next)) {
347
343
    if (!isKebabString(Next)) {
348
32
      return reportError("invalid label after [constructor]"sv);
349
32
    }
350
311
    Result.Detail.emplace<ConstructorDetail>(ConstructorDetail{Next});
351
311
    Result.NoTagName = Next;
352
311
    Result.Kind = ComponentNameKind::Constructor;
353
311
    return Result;
354
343
  }
355
356
5.14k
  auto tryReadResourceWithLabel = [&](std::string_view Tag,
357
5.14k
                                      std::string_view &Resource,
358
10.2k
                                      std::string_view &Label) -> bool {
359
10.2k
    auto Saved = Next;
360
10.2k
    if (!tryRead(Tag, Next)) {
361
10.1k
      return false;
362
10.1k
    }
363
135
    auto TmpNoTagName = Next;
364
135
    if (!readUntil(Next, '.', Resource)) {
365
3
      Next = Saved;
366
3
      return false;
367
3
    }
368
132
    if (!isKebabString(Resource) || !isKebabString(Next)) {
369
84
      Next = Saved;
370
84
      return false;
371
84
    }
372
48
    Result.NoTagName = TmpNoTagName;
373
48
    Label = Next;
374
48
    return true;
375
132
  };
376
377
5.14k
  {
378
5.14k
    std::string_view Resource, Label;
379
5.14k
    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
5.14k
  }
385
386
5.14k
  {
387
5.14k
    std::string_view Resource, Label;
388
5.14k
    if (tryReadResourceWithLabel("[static]"sv, Resource, Label)) {
389
46
      Result.Detail.emplace<StaticDetail>(StaticDetail{Resource, Label});
390
46
      Result.Kind = ComponentNameKind::Static;
391
46
      return Result;
392
46
    }
393
5.14k
  }
394
395
5.10k
  if (tryRead("[async]"sv, Next)) {
396
1
    Result.NoTagName = Next;
397
1
    return reportError("[async] not supported yet"sv);
398
1
  }
399
400
5.10k
  if (tryRead("[async method]"sv, Next)) {
401
0
    Result.NoTagName = Next;
402
0
    return reportError("[async method] not supported yet"sv);
403
0
  }
404
405
5.10k
  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
5.09k
  if (Next.size() != 0 && Next[0] == '[') {
411
115
    return reportError("unknown annotation"sv);
412
115
  }
413
4.98k
  Result.NoTagName = Next;
414
415
  // depname ::= 'unlocked-dep=<' <pkgnamequery> '>'
416
  //           | 'locked-dep=<' <pkgname> '>' ( ',' <hashname> )?
417
418
4.98k
  if (tryRead("unlocked-dep="sv, Next)) {
419
59
    if (!tryRead("<"sv, Next))
420
9
      return reportError("expected '<' after unlocked-dep="sv);
421
422
85
    EXPECTED_TRY(auto Path, parsePkgPath(Next, "@>"sv));
423
424
    // verrange ::= '@*'
425
    //            | '@{' verlower '}'
426
    //            | '@{' verupper '}'
427
    //            | '@{' verlower ' ' verupper '}'
428
85
    std::string_view VersionRange;
429
85
    if (!Next.empty() && Next[0] == '@') {
430
34
      auto VerStart = Next;
431
34
      Next.remove_prefix(1);
432
34
      if (Next.empty())
433
0
        return reportError(
434
0
            "expected version range after '@' in unlocked-dep"sv);
435
436
34
      if (Next[0] == '*') {
437
3
        Next.remove_prefix(1);
438
31
      } else if (Next[0] == '{') {
439
23
        size_t ClosePos = Next.find('}');
440
23
        if (ClosePos == Next.npos)
441
1
          return reportError("expected '}' in unlocked-dep version range"sv);
442
22
        auto RangeBody = Next.substr(1, ClosePos - 1);
443
444
22
        auto ValidateRange = [](std::string_view Body) -> bool {
445
22
          if (Body.empty())
446
1
            return false;
447
21
          auto Remaining = Body;
448
449
21
          if (tryRead(">="sv, Remaining)) {
450
0
            size_t SpacePos = Remaining.find(' ');
451
0
            std::string_view Lower = (SpacePos == Remaining.npos)
452
0
                                         ? Remaining
453
0
                                         : Remaining.substr(0, SpacePos);
454
0
            if (!isValidSemver(Lower))
455
0
              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
21
          if (tryRead("<"sv, Remaining)) {
465
4
            return isValidSemver(Remaining);
466
4
          }
467
468
17
          return false;
469
21
        };
470
471
22
        if (!ValidateRange(RangeBody))
472
22
          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
4.92k
  if (tryRead("locked-dep="sv, Next)) {
494
16
    if (!tryRead("<"sv, Next))
495
9
      return reportError("expected '<' after locked-dep="sv);
496
497
13
    EXPECTED_TRY(auto Path, parsePkgPath(Next, "@>"sv));
498
499
13
    std::string_view Version;
500
13
    if (!Next.empty() && Next[0] == '@') {
501
5
      Next.remove_prefix(1);
502
5
      size_t VerEnd = Next.find('>');
503
5
      if (VerEnd == Next.npos)
504
1
        return reportError("expected '>' after version in locked-dep"sv);
505
4
      Version = Next.substr(0, VerEnd);
506
4
      Next.remove_prefix(VerEnd);
507
4
      if (!isValidSemver(Version))
508
4
        return reportError("invalid semver in locked-dep"sv);
509
4
    }
510
511
1
    if (!tryRead(">"sv, Next))
512
0
      return reportError("expected '>' closing locked-dep"sv);
513
514
1
    std::string_view Integrity;
515
1
    EXPECTED_TRY(tryParseIntegritySuffix(Next, Integrity));
516
517
0
    Result.Detail.emplace<LockedDepDetail>(
518
0
        LockedDepDetail{Path.Namespace, Path.Package, Version, Integrity});
519
0
    Result.Kind = ComponentNameKind::LockedDep;
520
0
    return Result;
521
1
  }
522
523
  // urlname ::= 'url=<' <nonbrackets> '>' (',' <hashname>)?
524
  // nonbrackets ::= [^<>]*
525
4.90k
  if (tryRead("url="sv, Next)) {
526
27
    if (!tryRead("<"sv, Next))
527
12
      return reportError("expected '<' after url="sv);
528
529
15
    size_t ClosePos = Next.find('>');
530
15
    if (ClosePos == Next.npos)
531
1
      return reportError("expected '>' closing url"sv);
532
533
14
    std::string_view UrlContent = Next.substr(0, ClosePos);
534
14
    if (UrlContent.find('<') != UrlContent.npos)
535
2
      return reportError("'<' not allowed inside url"sv);
536
12
    Next.remove_prefix(ClosePos + 1);
537
538
12
    std::string_view Integrity;
539
12
    EXPECTED_TRY(tryParseIntegritySuffix(Next, Integrity));
540
541
11
    Result.Detail.emplace<UrlDetail>(UrlDetail{UrlContent, Integrity});
542
11
    Result.Kind = ComponentNameKind::Url;
543
11
    return Result;
544
12
  }
545
546
  // hashname ::= 'integrity=<' <integrity-metadata> '>'
547
4.88k
  if (tryRead("integrity="sv, Next)) {
548
10
    if (!tryRead("<"sv, Next))
549
3
      return reportError("expected '<' after integrity="sv);
550
7
    std::string_view IntegrityData;
551
7
    if (!readUntil(Next, '>', IntegrityData))
552
1
      return reportError("expected '>' closing integrity"sv);
553
6
    if (!isIntegrityMetadata(IntegrityData))
554
6
      return reportError("invalid integrity metadata"sv);
555
0
    if (!isEOF(Next))
556
0
      return reportError("unexpected trailing content after integrity"sv);
557
0
    Result.Detail.emplace<IntegrityDetail>(IntegrityDetail{IntegrityData});
558
0
    Result.Kind = ComponentNameKind::Integrity;
559
0
    return Result;
560
0
  }
561
562
  // interfacename ::= <namespace> <label> <projection> <interfaceversion>?
563
  // namespace     ::= <words> ':'
564
  // projection    ::= '/' <label>
565
  // interfaceversion ::= '@' <valid semver> | '@' <canonversion>
566
4.87k
  {
567
4.87k
    std::string_view Namespace, Package, Interface, Version;
568
569
4.87k
    int Counter = 0;
570
7.55k
    while (readUntil(Next, ':', Namespace)) {
571
2.73k
      Counter++;
572
2.73k
      if (!isLowercaseKebabString(Namespace)) {
573
47
        return reportError("invalid namespace in interface name"sv);
574
47
      }
575
2.73k
    }
576
4.82k
    if (Counter == 0) {
577
      // No ':' found — fall through to label parsing below.
578
2.16k
      goto ParseLabel;
579
2.16k
    }
580
2.66k
    if (Counter != 1) {
581
2
      return reportError("nested namespaces not supported yet"sv);
582
2
    }
583
584
    // interfacename ::= <namespace> <words> <projection> ...
585
2.65k
    if (!tryReadKebab(Next, Package) || !isLowercaseKebabString(Package)) {
586
34
      return reportError("invalid package in interface name"sv);
587
34
    }
588
589
2.62k
    Counter = 0;
590
5.19k
    while (!isEOF(Next) && Next[0] == '/') {
591
2.58k
      Next.remove_prefix(1);
592
2.58k
      Counter++;
593
2.58k
      if (!tryReadKebab(Next, Interface)) {
594
10
        return reportError("invalid projection label in interface name"sv);
595
10
      }
596
2.58k
    }
597
598
2.61k
    if (Counter == 0) {
599
45
      return reportError("expected '/' projection in interface name"sv);
600
45
    }
601
2.56k
    if (Counter != 1) {
602
3
      return reportError("nested projections not supported yet"sv);
603
3
    }
604
605
2.56k
    if (!isEOF(Next) && Next[0] == '@') {
606
404
      Next.remove_prefix(1);
607
404
      Version = Next;
608
404
      if (!isVersion(Version)) {
609
140
        return reportError("invalid version in interface name"sv);
610
140
      }
611
404
    }
612
613
2.42k
    Result.Detail.emplace<InterfaceDetail>(
614
2.42k
        InterfaceDetail{Namespace, Package, Interface, Version});
615
2.42k
    Result.Kind = ComponentNameKind::InterfaceType;
616
2.42k
    return Result;
617
2.56k
  }
618
619
2.16k
ParseLabel:
620
2.16k
  if (!isKebabString(Next)) {
621
220
    return reportError("invalid label"sv);
622
220
  }
623
1.94k
  Result.Detail.emplace<LabelDetail>();
624
1.94k
  Result.Kind = ComponentNameKind::Label;
625
1.94k
  return Result;
626
2.16k
}
627
628
} // namespace Validator
629
} // namespace WasmEdge