Coverage Report

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