Coverage Report

Created: 2026-08-14 06:41

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