Coverage Report

Created: 2026-06-30 06:10

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