Coverage Report

Created: 2026-09-28 06:22

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 <cctype>
9
#include <string>
10
#include <string_view>
11
12
namespace WasmEdge {
13
namespace Validator {
14
namespace Component {
15
16
using namespace std::literals;
17
18
// label ::= <first-fragment> ( '-' <fragment> )*, each a word or an acronym.
19
22.4k
bool ExternName::isKebabString(std::string_view Input) noexcept {
20
22.4k
  bool IsFirstPart = true;
21
22.4k
  bool Uppercase = false;
22
22.4k
  bool Lowercase = false;
23
22.4k
  bool Digit = false;
24
25
91.9k
  for (char C : Input) {
26
91.9k
    if (islower(static_cast<unsigned char>(C))) {
27
25.6k
      if (Uppercase) {
28
51
        return false;
29
51
      }
30
25.6k
      Lowercase = true;
31
66.2k
    } else if (isupper(static_cast<unsigned char>(C))) {
32
10.6k
      if (Lowercase) {
33
67
        return false;
34
67
      }
35
10.5k
      Uppercase = true;
36
55.6k
    } else if (isdigit(static_cast<unsigned char>(C))) {
37
33.6k
      if (IsFirstPart && !(Uppercase || Lowercase)) {
38
33
        return false;
39
33
      }
40
33.5k
      Digit = true;
41
33.5k
    } else if (C == '-') {
42
21.7k
      if (Uppercase || Lowercase || Digit) {
43
21.7k
        IsFirstPart = false;
44
21.7k
        Uppercase = false;
45
21.7k
        Lowercase = false;
46
21.7k
        Digit = false;
47
21.7k
      } else {
48
42
        return false;
49
42
      }
50
21.7k
    } else {
51
298
      return false;
52
298
    }
53
91.9k
  }
54
55
21.9k
  return Input.size() > 0 && Input.back() != '-';
56
22.4k
}
57
58
9.75k
Expect<void> ExternName::parse(std::string_view Name) noexcept {
59
9.75k
  OriName = Name;
60
9.75k
  NoTagName = {};
61
9.75k
  NameKind = Kind::Invalid;
62
9.75k
  NameDetail = {};
63
9.75k
  Rest = Name;
64
65
9.75k
  if (!Rest.empty() && Rest[0] == '[') {
66
231
    return parsePlainName();
67
231
  }
68
69
9.52k
  if (tryRead("unlocked-dep="sv)) {
70
98
    return parseUnlockedDep();
71
98
  }
72
9.42k
  if (tryRead("locked-dep="sv)) {
73
40
    return parseLockedDep();
74
40
  }
75
9.38k
  if (tryRead("url="sv)) {
76
58
    return parseUrlName();
77
58
  }
78
9.32k
  if (tryRead("integrity="sv)) {
79
111
    return parseHashName();
80
111
  }
81
9.21k
  if (Rest.find(':') != std::string_view::npos) {
82
578
    return parseInterfaceName();
83
578
  }
84
8.63k
  return parsePlainName();
85
9.21k
}
86
87
// Consumes Prefix from Rest, or leaves Rest untouched and returns false.
88
64.4k
bool ExternName::tryRead(std::string_view Prefix) noexcept {
89
64.4k
  if (Prefix.size() > Rest.size()) {
90
51.0k
    return false;
91
51.0k
  }
92
13.3k
  if (Prefix != Rest.substr(0, Prefix.size())) {
93
12.5k
    return false;
94
12.5k
  }
95
96
791
  Rest.remove_prefix(Prefix.size());
97
791
  return true;
98
13.3k
}
99
100
// Consumes up to and including Delim, and reports the text before it.
101
244
bool ExternName::readUntil(char Delim, std::string_view &Output) noexcept {
102
244
  size_t Pos = Rest.find(Delim);
103
244
  if (Pos == Rest.npos) {
104
8
    return false;
105
8
  }
106
107
236
  Output = Rest.substr(0, Pos);
108
236
  Rest.remove_prefix(Pos + 1);
109
236
  return true;
110
244
}
111
112
// Consumes and returns the leading run of label characters ([0-9A-Za-z-]).
113
968
std::string_view ExternName::readLabelChars() noexcept {
114
968
  size_t Pos = 0;
115
6.73k
  while (Pos < Rest.size() &&
116
6.60k
         (isalnum(static_cast<unsigned char>(Rest[Pos])) || Rest[Pos] == '-')) {
117
5.77k
    Pos++;
118
5.77k
  }
119
968
  std::string_view Output = Rest.substr(0, Pos);
120
968
  Rest.remove_prefix(Pos);
121
968
  return Output;
122
968
}
123
124
// plainname ::= <label> | '[constructor|method|static]' <label> ('.' <label>)?
125
8.87k
Expect<void> ExternName::parsePlainName() noexcept {
126
8.87k
  if (tryRead("[constructor]"sv)) {
127
67
    if (!isKebabString(Rest)) {
128
47
      spdlog::error(ErrCode::Value::ComponentNameNotKebab);
129
47
      spdlog::error("    Component name: label '{}' is not in kebab case"sv,
130
47
                    Rest);
131
47
      return Unexpect(ErrCode::Value::ComponentNameNotKebab);
132
47
    }
133
20
    NoTagName = Rest;
134
20
    NameDetail.Resource = Rest;
135
20
    NameKind = Kind::Constructor;
136
20
    return {};
137
67
  }
138
139
  // A '[method]' or '[static]' name needs a '.' between two kebab labels.
140
8.80k
  auto ReadResourceAndLabel = [this](std::string_view &Resource,
141
8.80k
                                     std::string_view &Label) -> Expect<void> {
142
137
    NoTagName = Rest;
143
137
    if (!readUntil('.', Resource)) {
144
3
      spdlog::error(ErrCode::Value::NameFailedToFindDot);
145
3
      spdlog::error("    Component name: failed to find `.` character"sv);
146
3
      return Unexpect(ErrCode::Value::NameFailedToFindDot);
147
3
    }
148
134
    if (!isKebabString(Resource)) {
149
35
      spdlog::error(ErrCode::Value::ComponentNameNotKebab);
150
35
      spdlog::error("    Component name: label '{}' is not in kebab case"sv,
151
35
                    Resource);
152
35
      return Unexpect(ErrCode::Value::ComponentNameNotKebab);
153
35
    }
154
99
    if (!isKebabString(Rest)) {
155
47
      spdlog::error(ErrCode::Value::ComponentNameNotKebab);
156
47
      spdlog::error("    Component name: label '{}' is not in kebab case"sv,
157
47
                    Rest);
158
47
      return Unexpect(ErrCode::Value::ComponentNameNotKebab);
159
47
    }
160
52
    Label = Rest;
161
52
    return {};
162
99
  };
163
164
8.80k
  if (tryRead("[method]"sv)) {
165
68
    std::string_view Resource, Label;
166
68
    EXPECTED_TRY(ReadResourceAndLabel(Resource, Label));
167
20
    NameDetail.Resource = Resource;
168
20
    NameDetail.Method = Label;
169
20
    NameKind = Kind::Method;
170
20
    return {};
171
68
  }
172
173
8.73k
  if (tryRead("[static]"sv)) {
174
69
    std::string_view Resource, Label;
175
69
    EXPECTED_TRY(ReadResourceAndLabel(Resource, Label));
176
32
    NameDetail.Resource = Resource;
177
32
    NameDetail.Method = Label;
178
32
    NameKind = Kind::Static;
179
32
    return {};
180
69
  }
181
182
8.66k
  if (!Rest.empty() && Rest[0] == '[') {
183
27
    spdlog::error(ErrCode::Value::ComponentInvalidName);
184
27
    spdlog::error("    Component name: unknown annotation"sv);
185
27
    return Unexpect(ErrCode::Value::ComponentInvalidName);
186
27
  }
187
188
8.63k
  if (!isKebabString(Rest)) {
189
282
    spdlog::error(ErrCode::Value::ComponentNameNotKebab);
190
282
    spdlog::error("    Component name: label '{}' is not in kebab case"sv,
191
282
                  Rest);
192
282
    return Unexpect(ErrCode::Value::ComponentNameNotKebab);
193
282
  }
194
8.35k
  NameKind = Kind::Label;
195
8.35k
  return {};
196
8.63k
}
197
198
// depname ::= 'unlocked-dep=<' <pkgpath> <verrange>? '>'
199
98
Expect<void> ExternName::parseUnlockedDep() noexcept {
200
98
  if (!tryRead("<"sv)) {
201
14
    spdlog::error(ErrCode::Value::NameExpectedOpenAngle);
202
14
    spdlog::error("    Component name: expected `<` after unlocked-dep="sv);
203
14
    return Unexpect(ErrCode::Value::NameExpectedOpenAngle);
204
14
  }
205
206
84
  EXPECTED_TRY(parsePkgPath("@>"sv));
207
208
51
  std::string_view VersionRange;
209
51
  if (!Rest.empty() && Rest[0] == '@') {
210
45
    auto VerStart = Rest;
211
45
    Rest.remove_prefix(1);
212
213
45
    if (!Rest.empty() && Rest[0] == '*') {
214
7
      Rest.remove_prefix(1);
215
38
    } else if (!Rest.empty() && Rest[0] == '{') {
216
34
      size_t ClosePos = Rest.find('}');
217
34
      if (ClosePos == Rest.npos) {
218
3
        spdlog::error(ErrCode::Value::ComponentInvalidName);
219
3
        spdlog::error(
220
3
            "    Component name: expected `}` in unlocked-dep version range"sv);
221
3
        return Unexpect(ErrCode::Value::ComponentInvalidName);
222
3
      }
223
31
      EXPECTED_TRY(checkVersionRange(Rest.substr(1, ClosePos - 1)));
224
0
      Rest.remove_prefix(ClosePos + 1);
225
4
    } else {
226
4
      spdlog::error(ErrCode::Value::NameExpectedOpenBrace);
227
4
      spdlog::error(
228
4
          "    Component name: expected `{` at start of version range"sv);
229
4
      return Unexpect(ErrCode::Value::NameExpectedOpenBrace);
230
4
    }
231
7
    VersionRange = VerStart.substr(0, VerStart.size() - Rest.size());
232
7
  }
233
234
13
  if (!tryRead(">"sv)) {
235
7
    spdlog::error(ErrCode::Value::NameExpectedCloseAngle);
236
7
    spdlog::error("    Component name: expected `>` closing unlocked-dep"sv);
237
7
    return Unexpect(ErrCode::Value::NameExpectedCloseAngle);
238
7
  }
239
6
  if (!Rest.empty()) {
240
3
    spdlog::error(ErrCode::Value::NameTrailingCharacters);
241
3
    spdlog::error(
242
3
        "    Component name: trailing characters found after unlocked-dep"sv);
243
3
    return Unexpect(ErrCode::Value::NameTrailingCharacters);
244
3
  }
245
246
3
  NameDetail.VersionRange = VersionRange;
247
3
  NameKind = Kind::UnlockedDep;
248
3
  return {};
249
6
}
250
251
// depname ::= 'locked-dep=<' <pkgname> '>' ( ',' <hashname> )?
252
40
Expect<void> ExternName::parseLockedDep() noexcept {
253
40
  if (!tryRead("<"sv)) {
254
15
    spdlog::error(ErrCode::Value::NameExpectedOpenAngle);
255
15
    spdlog::error("    Component name: expected `<` after locked-dep="sv);
256
15
    return Unexpect(ErrCode::Value::NameExpectedOpenAngle);
257
15
  }
258
259
25
  EXPECTED_TRY(parsePkgPath("@>"sv));
260
261
15
  std::string_view Version;
262
15
  if (!Rest.empty() && Rest[0] == '@') {
263
10
    Rest.remove_prefix(1);
264
10
    size_t VerEnd = Rest.find('>');
265
10
    if (VerEnd == Rest.npos) {
266
3
      spdlog::error(ErrCode::Value::NameExpectedCloseAngle);
267
3
      spdlog::error(
268
3
          "    Component name: expected `>` after version in locked-dep"sv);
269
3
      return Unexpect(ErrCode::Value::NameExpectedCloseAngle);
270
3
    }
271
7
    Version = Rest.substr(0, VerEnd);
272
7
    Rest.remove_prefix(VerEnd);
273
7
    if (!scanSemver(Version)) {
274
7
      spdlog::error(ErrCode::Value::NameNotValidSemver);
275
7
      spdlog::error(
276
7
          "    Component name: locked-dep version '{}' is not a valid semver"sv,
277
7
          Version);
278
7
      return Unexpect(ErrCode::Value::NameNotValidSemver);
279
7
    }
280
7
  }
281
282
5
  if (!tryRead(">"sv)) {
283
0
    spdlog::error(ErrCode::Value::NameExpectedCloseAngle);
284
0
    spdlog::error("    Component name: expected `>` closing locked-dep"sv);
285
0
    return Unexpect(ErrCode::Value::NameExpectedCloseAngle);
286
0
  }
287
288
5
  EXPECTED_TRY(auto Integrity, parseIntegritySuffix());
289
290
2
  NameDetail.Version = Version;
291
2
  NameDetail.Integrity = Integrity;
292
2
  NameKind = Kind::LockedDep;
293
2
  return {};
294
5
}
295
296
// urlname ::= 'url=<' <nonbrackets> '>' ( ',' <hashname> )?
297
58
Expect<void> ExternName::parseUrlName() noexcept {
298
58
  if (!tryRead("<"sv)) {
299
5
    spdlog::error(ErrCode::Value::NameExpectedOpenAngle);
300
5
    spdlog::error("    Component name: expected `<` after url="sv);
301
5
    return Unexpect(ErrCode::Value::NameExpectedOpenAngle);
302
5
  }
303
304
53
  size_t ClosePos = Rest.find('>');
305
53
  if (ClosePos == Rest.npos) {
306
7
    spdlog::error(ErrCode::Value::NameFailedToFindCloseAngle);
307
7
    spdlog::error("    Component name: failed to find `>` closing url"sv);
308
7
    return Unexpect(ErrCode::Value::NameFailedToFindCloseAngle);
309
7
  }
310
311
46
  std::string_view Url = Rest.substr(0, ClosePos);
312
46
  if (Url.find('<') != Url.npos) {
313
6
    spdlog::error(ErrCode::Value::NameUrlContainsOpenAngle);
314
6
    spdlog::error("    Component name: url cannot contain `<`"sv);
315
6
    return Unexpect(ErrCode::Value::NameUrlContainsOpenAngle);
316
6
  }
317
40
  Rest.remove_prefix(ClosePos + 1);
318
319
40
  EXPECTED_TRY(auto Integrity, parseIntegritySuffix());
320
321
19
  NameDetail.Url = Url;
322
19
  NameDetail.Integrity = Integrity;
323
19
  NameKind = Kind::Url;
324
19
  return {};
325
40
}
326
327
// hashname ::= 'integrity=<' <integrity-metadata> '>'
328
111
Expect<void> ExternName::parseHashName() noexcept {
329
111
  if (!tryRead("<"sv)) {
330
7
    spdlog::error(ErrCode::Value::NameExpectedOpenAngle);
331
7
    spdlog::error("    Component name: expected `<` after integrity="sv);
332
7
    return Unexpect(ErrCode::Value::NameExpectedOpenAngle);
333
7
  }
334
335
104
  EXPECTED_TRY(auto Integrity, parseIntegrityBody());
336
337
11
  NameDetail.Integrity = Integrity;
338
11
  NameKind = Kind::Integrity;
339
11
  return {};
340
104
}
341
342
// interfacename ::= <words> ':' <label> '/' <label> <interfaceversion>?
343
578
Expect<void> ExternName::parseInterfaceName() noexcept {
344
578
  size_t ColonPos = Rest.find(':');
345
578
  std::string_view Namespace = Rest.substr(0, ColonPos);
346
578
  Rest.remove_prefix(ColonPos + 1);
347
578
  EXPECTED_TRY(checkWordsLabel(Namespace, "namespace"sv));
348
349
497
  std::string_view Package = readLabelChars();
350
497
  EXPECTED_TRY(checkWordsLabel(Package, "package"sv));
351
352
  // Nested namespaces (`a:b:c/d`) are feature-gated; only a projection follows.
353
467
  if (Rest.empty() || Rest[0] != '/') {
354
19
    spdlog::error(ErrCode::Value::NameExpectedSlashAfterPackage);
355
19
    spdlog::error("    Component name: expected `/` after package name"sv);
356
19
    return Unexpect(ErrCode::Value::NameExpectedSlashAfterPackage);
357
19
  }
358
448
  Rest.remove_prefix(1);
359
360
448
  std::string_view Interface = readLabelChars();
361
448
  if (!isKebabString(Interface)) {
362
30
    spdlog::error(ErrCode::Value::ComponentNameNotKebab);
363
30
    spdlog::error("    Component name: label '{}' is not in kebab case"sv,
364
30
                  Interface);
365
30
    return Unexpect(ErrCode::Value::ComponentNameNotKebab);
366
30
  }
367
368
  // Nested projections (`a:b/c/d`) are feature-gated; only a version follows.
369
418
  if (!Rest.empty() && Rest[0] != '@') {
370
12
    spdlog::error(ErrCode::Value::NameTrailingCharacters);
371
12
    spdlog::error(
372
12
        "    Component name: trailing characters found after projection"sv);
373
12
    return Unexpect(ErrCode::Value::NameTrailingCharacters);
374
12
  }
375
376
406
  std::string_view Version;
377
406
  if (!Rest.empty()) {
378
312
    Rest.remove_prefix(1);
379
312
    Version = Rest;
380
312
    if (!isCanonVersion(Version)) {
381
292
      if (auto Res = scanSemver(Version); !Res) {
382
132
        spdlog::error(Res.error().getEnum());
383
132
        spdlog::error("    Component name: version '{}' is not valid"sv,
384
132
                      Version);
385
132
        return Unexpect(Res);
386
132
      }
387
292
    }
388
312
  }
389
390
274
  NameDetail.Namespace = Namespace;
391
274
  NameDetail.Package = Package;
392
274
  NameDetail.Interface = Interface;
393
274
  NameDetail.Version = Version;
394
274
  NameKind = Kind::InterfaceType;
395
274
  return {};
396
406
}
397
398
// Parses 'namespace:package' into NameDetail, stopping at StopChars.
399
109
Expect<void> ExternName::parsePkgPath(std::string_view StopChars) noexcept {
400
109
  size_t ColonPos = Rest.find(':');
401
109
  size_t StopPos = Rest.find_first_of(StopChars);
402
109
  if (ColonPos == Rest.npos || (StopPos != Rest.npos && StopPos < ColonPos)) {
403
    // No namespace delimiter: diagnose the leading label run first.
404
23
    std::string_view Label = readLabelChars();
405
23
    EXPECTED_TRY(checkWordsLabel(Label, "namespace"sv));
406
9
    spdlog::error(ErrCode::Value::ComponentInvalidName);
407
9
    spdlog::error("    Component name: expected `:` after namespace"sv);
408
9
    return Unexpect(ErrCode::Value::ComponentInvalidName);
409
23
  }
410
411
86
  std::string_view Namespace = Rest.substr(0, ColonPos);
412
86
  Rest.remove_prefix(ColonPos + 1);
413
86
  EXPECTED_TRY(checkWordsLabel(Namespace, "namespace"sv));
414
415
80
  size_t PkgEnd = Rest.find_first_of(StopChars);
416
80
  std::string_view Package =
417
80
      (PkgEnd == Rest.npos) ? Rest : Rest.substr(0, PkgEnd);
418
80
  Rest.remove_prefix(Package.size());
419
80
  EXPECTED_TRY(checkWordsLabel(Package, "package"sv));
420
69
  if (PkgEnd == std::string_view::npos) {
421
    // The package name ran to end of input without a closing delimiter.
422
3
    spdlog::error(ErrCode::Value::NameExpectedCloseAngle);
423
3
    spdlog::error("    Component name: expected `>` after package path"sv);
424
3
    return Unexpect(ErrCode::Value::NameExpectedCloseAngle);
425
3
  }
426
427
66
  NameDetail.Namespace = Namespace;
428
66
  NameDetail.Package = Package;
429
66
  return {};
430
69
}
431
432
// Parses the `<integrity-metadata> '>'` body, which must end the name.
433
107
Expect<std::string_view> ExternName::parseIntegrityBody() noexcept {
434
107
  std::string_view Data;
435
107
  if (!readUntil('>', Data)) {
436
5
    spdlog::error(ErrCode::Value::NameFailedToFindCloseAngle);
437
5
    spdlog::error("    Component name: failed to find `>` closing integrity"sv);
438
5
    return Unexpect(ErrCode::Value::NameFailedToFindCloseAngle);
439
5
  }
440
102
  EXPECTED_TRY(checkIntegrityMetadata(Data));
441
16
  if (!Rest.empty()) {
442
5
    spdlog::error(ErrCode::Value::NameTrailingCharacters);
443
5
    spdlog::error(
444
5
        "    Component name: trailing characters found after integrity"sv);
445
5
    return Unexpect(ErrCode::Value::NameTrailingCharacters);
446
5
  }
447
11
  return Data;
448
16
}
449
450
// Parses the optional ',' <hashname> suffix; an exhausted input yields none.
451
45
Expect<std::string_view> ExternName::parseIntegritySuffix() noexcept {
452
45
  if (Rest.empty()) {
453
21
    return std::string_view{};
454
21
  }
455
24
  if (Rest[0] != ',') {
456
13
    spdlog::error(ErrCode::Value::NameTrailingCharacters);
457
13
    spdlog::error("    Component name: trailing characters found"sv);
458
13
    return Unexpect(ErrCode::Value::NameTrailingCharacters);
459
13
  }
460
11
  Rest.remove_prefix(1);
461
11
  if (!tryRead("integrity=<"sv)) {
462
8
    spdlog::error(ErrCode::Value::NameExpectedIntegrity);
463
8
    spdlog::error("    Component name: expected `integrity=<` after `,`"sv);
464
8
    return Unexpect(ErrCode::Value::NameExpectedIntegrity);
465
8
  }
466
3
  return parseIntegrityBody();
467
11
}
468
469
// words ::= <first-word> ( '-' <word> )*: a kebab label that is all lowercase.
470
Expect<void> ExternName::checkWordsLabel(std::string_view Label,
471
1.26k
                                         std::string_view What) const noexcept {
472
1.26k
  if (!isKebabString(Label)) {
473
122
    spdlog::error(ErrCode::Value::ComponentNameNotKebab);
474
122
    spdlog::error("    Component name: label '{}' is not in kebab case"sv,
475
122
                  Label);
476
122
    return Unexpect(ErrCode::Value::ComponentNameNotKebab);
477
122
  }
478
  // A kebab label already has the `<words>` shape, so only case is left.
479
4.99k
  for (char C : Label) {
480
4.99k
    if (isupper(static_cast<unsigned char>(C))) {
481
20
      spdlog::error(ErrCode::Value::ComponentPackageNameNotLowercase);
482
20
      spdlog::error("    Component name: {} '{}' is not lowercase"sv, What,
483
20
                    Label);
484
20
      return Unexpect(ErrCode::Value::ComponentPackageNameNotLowercase);
485
20
    }
486
4.99k
  }
487
1.12k
  return {};
488
1.14k
}
489
490
// verrange body ::= <verlower> | <verupper> | <verlower> ' ' <verupper>
491
Expect<void>
492
31
ExternName::checkVersionRange(std::string_view Body) const noexcept {
493
31
  if (Body.substr(0, 2) == ">="sv) {
494
3
    Body.remove_prefix(2);
495
3
    size_t SpacePos = Body.find(' ');
496
3
    std::string_view Lower =
497
3
        (SpacePos == Body.npos) ? Body : Body.substr(0, SpacePos);
498
3
    if (!scanSemver(Lower)) {
499
3
      spdlog::error(ErrCode::Value::NameNotValidSemver);
500
3
      spdlog::error(
501
3
          "    Component name: version range lower bound '{}' is not valid"sv,
502
3
          Lower);
503
3
      return Unexpect(ErrCode::Value::NameNotValidSemver);
504
3
    }
505
0
    if (SpacePos == Body.npos) {
506
0
      return {};
507
0
    }
508
0
    Body.remove_prefix(SpacePos + 1);
509
0
    if (Body.substr(0, 1) != "<"sv) {
510
0
      spdlog::error(ErrCode::Value::NameExpectedOpenAngle);
511
0
      spdlog::error(
512
0
          "    Component name: expected `<` before version range upper bound"sv);
513
0
      return Unexpect(ErrCode::Value::NameExpectedOpenAngle);
514
0
    }
515
0
    Body.remove_prefix(1);
516
0
    if (!scanSemver(Body)) {
517
0
      spdlog::error(ErrCode::Value::NameNotValidSemver);
518
0
      spdlog::error(
519
0
          "    Component name: version range upper bound '{}' is not valid"sv,
520
0
          Body);
521
0
      return Unexpect(ErrCode::Value::NameNotValidSemver);
522
0
    }
523
0
    return {};
524
0
  }
525
526
28
  if (Body.substr(0, 1) == "<"sv) {
527
4
    Body.remove_prefix(1);
528
4
    if (!scanSemver(Body)) {
529
4
      spdlog::error(ErrCode::Value::NameNotValidSemver);
530
4
      spdlog::error(
531
4
          "    Component name: version range upper bound '{}' is not valid"sv,
532
4
          Body);
533
4
      return Unexpect(ErrCode::Value::NameNotValidSemver);
534
4
    }
535
0
    return {};
536
4
  }
537
538
24
  spdlog::error(ErrCode::Value::NameExpectedVersionRangeOp);
539
24
  spdlog::error(
540
24
      "    Component name: expected `>=` or `<` at start of version range"sv);
541
24
  return Unexpect(ErrCode::Value::NameExpectedVersionRangeOp);
542
28
}
543
544
// semversuffix ::= [0-9A-Za-z.+-]*
545
Expect<void>
546
19
ExternName::checkVersionSuffix(std::string_view Suffix) const noexcept {
547
79
  for (char C : Suffix) {
548
79
    if (!isalnum(static_cast<unsigned char>(C)) && C != '.' && C != '+' &&
549
32
        C != '-') {
550
15
      spdlog::error(ErrCode::Value::ComponentVersionSuffixInvalid);
551
15
      spdlog::error("    `versionsuffix` `{}` is not a semver suffix"sv,
552
15
                    Suffix);
553
15
      return Unexpect(ErrCode::Value::ComponentVersionSuffixInvalid);
554
15
    }
555
79
  }
556
4
  if (NameKind != Kind::InterfaceType || NameDetail.Version.empty() ||
557
4
      !isCanonVersion(NameDetail.Version)) {
558
4
    spdlog::error(ErrCode::Value::ComponentVersionSuffixInvalid);
559
4
    spdlog::error("    `versionsuffix` needs a preceding canonical version"sv);
560
4
    return Unexpect(ErrCode::Value::ComponentVersionSuffixInvalid);
561
4
  }
562
0
  std::string Full(NameDetail.Version);
563
0
  Full.append(Suffix);
564
0
  if (!scanSemver(Full)) {
565
0
    spdlog::error(ErrCode::Value::ComponentVersionSuffixInvalid);
566
0
    spdlog::error("    `{}` and `versionsuffix` `{}` are not a valid semver"sv,
567
0
                  NameDetail.Version, Suffix);
568
0
    return Unexpect(ErrCode::Value::ComponentVersionSuffixInvalid);
569
0
  }
570
0
  return {};
571
0
}
572
573
// canonversion ::= [1-9] [0-9]* | '0.' [1-9] [0-9]* | '0.0.' [1-9] [0-9]*
574
312
bool ExternName::isCanonVersion(std::string_view V) const noexcept {
575
312
  if (V.substr(0, 4) == "0.0."sv) {
576
48
    V.remove_prefix(4);
577
264
  } else if (V.substr(0, 2) == "0."sv) {
578
63
    V.remove_prefix(2);
579
63
  }
580
312
  if (V.empty() || V[0] < '1' || V[0] > '9') {
581
46
    return false;
582
46
  }
583
705
  for (char C : V) {
584
705
    if (!isdigit(static_cast<unsigned char>(C))) {
585
246
      return false;
586
246
    }
587
705
  }
588
20
  return true;
589
266
}
590
591
// Scans MAJOR.MINOR.PATCH[-prerelease][+build] per semver.org 2.0, unlogged.
592
306
Expect<void> ExternName::scanSemver(std::string_view V) const noexcept {
593
306
  if (V.empty()) {
594
4
    return Unexpect(ErrCode::Value::NameEmptyString);
595
4
  }
596
597
1.01k
  for (uint32_t I = 0; I < 3; I++) {
598
806
    if (I > 0) {
599
504
      if (V.empty()) {
600
8
        return Unexpect(ErrCode::Value::NameUnexpectedEnd);
601
8
      }
602
496
      if (V[0] != '.') {
603
31
        return Unexpect(ErrCode::Value::NameUnexpectedCharacter);
604
31
      }
605
465
      V.remove_prefix(1);
606
465
    }
607
767
    size_t Len = 0;
608
2.04k
    while (Len < V.size() && isdigit(static_cast<unsigned char>(V[Len]))) {
609
1.27k
      Len++;
610
1.27k
    }
611
767
    if (Len == 0) {
612
43
      return Unexpect(V.empty() ? ErrCode::Value::NameUnexpectedEnd
613
43
                                : ErrCode::Value::NameUnexpectedCharacter);
614
43
    }
615
724
    if (Len > 1 && V[0] == '0') {
616
14
      return Unexpect(ErrCode::Value::ComponentInvalidName);
617
14
    }
618
710
    V.remove_prefix(Len);
619
710
  }
620
621
206
  if (V.empty()) {
622
28
    return {};
623
28
  }
624
178
  if (V[0] != '-' && V[0] != '+') {
625
14
    return Unexpect(ErrCode::Value::NameUnexpectedCharacter);
626
14
  }
627
628
164
  if (V[0] == '-') {
629
132
    V.remove_prefix(1);
630
132
    size_t PlusPos = V.find('+');
631
132
    std::string_view PreRelease =
632
132
        (PlusPos == V.npos) ? V : V.substr(0, PlusPos);
633
132
    EXPECTED_TRY(scanSemverIdentifiers(PreRelease, true));
634
112
    if (PlusPos == V.npos) {
635
100
      return {};
636
100
    }
637
12
    V.remove_prefix(PlusPos);
638
12
  }
639
640
  // Here V starts with '+': scan the build metadata identifiers.
641
44
  V.remove_prefix(1);
642
44
  return scanSemverIdentifiers(V, false);
643
164
}
644
645
// Scans a dot-separated identifier list, rejecting leading zeros when asked.
646
Expect<void>
647
ExternName::scanSemverIdentifiers(std::string_view Idents,
648
176
                                  bool CheckLeadingZeros) const noexcept {
649
176
  size_t Start = 0;
650
281
  while (true) {
651
281
    size_t DotPos = Idents.find('.', Start);
652
281
    std::string_view Ident = (DotPos == Idents.npos)
653
281
                                 ? Idents.substr(Start)
654
281
                                 : Idents.substr(Start, DotPos - Start);
655
281
    if (Ident.empty()) {
656
6
      return Unexpect(ErrCode::Value::NameEmptyIdentifierSegment);
657
6
    }
658
275
    bool AllDigits = true;
659
949
    for (char C : Ident) {
660
949
      if (!isdigit(static_cast<unsigned char>(C))) {
661
500
        AllDigits = false;
662
500
        if (!isalnum(static_cast<unsigned char>(C)) && C != '-') {
663
23
          return Unexpect(ErrCode::Value::NameUnexpectedCharacter);
664
23
        }
665
500
      }
666
949
    }
667
252
    if (CheckLeadingZeros && AllDigits && Ident.size() > 1 && Ident[0] == '0') {
668
3
      return Unexpect(ErrCode::Value::ComponentInvalidName);
669
3
    }
670
249
    if (DotPos == Idents.npos) {
671
144
      return {};
672
144
    }
673
105
    Start = DotPos + 1;
674
105
  }
675
176
}
676
677
// integrity-metadata ::= WSP-separated (sha256|sha384|sha512) '-' base64-value.
678
Expect<void>
679
102
ExternName::checkIntegrityMetadata(std::string_view Input) const noexcept {
680
  // base64-value ::= [A-Za-z0-9+/]+ ( '=' | '==' )?, padded only at the end.
681
106
  auto IsBase64 = [](std::string_view S) noexcept {
682
106
    if (S.empty()) {
683
6
      return false;
684
6
    }
685
100
    size_t Equals = 0;
686
685
    for (size_t I = 0; I < S.size(); I++) {
687
619
      char C = S[I];
688
619
      if ((isalnum(static_cast<unsigned char>(C)) || C == '+' || C == '/') &&
689
579
          Equals == 0) {
690
576
        continue;
691
576
      }
692
43
      if (C == '=' && I > 0 && Equals < 2) {
693
9
        Equals++;
694
9
        continue;
695
9
      }
696
34
      return false;
697
43
    }
698
66
    return true;
699
100
  };
700
701
212
  while (!Input.empty() && Input.front() == ' ') {
702
110
    Input.remove_prefix(1);
703
110
  }
704
200
  while (!Input.empty() && Input.back() == ' ') {
705
98
    Input.remove_suffix(1);
706
98
  }
707
102
  if (Input.empty()) {
708
4
    spdlog::error(ErrCode::Value::NameIntegrityEmpty);
709
4
    spdlog::error("    Component name: integrity hash cannot be empty"sv);
710
4
    return Unexpect(ErrCode::Value::NameIntegrityEmpty);
711
4
  }
712
713
164
  while (!Input.empty()) {
714
292
    while (!Input.empty() && Input.front() == ' ') {
715
144
      Input.remove_prefix(1);
716
144
    }
717
148
    if (Input.empty()) {
718
0
      break;
719
0
    }
720
721
148
    size_t TokenEnd = Input.find(' ');
722
148
    std::string_view Token =
723
148
        (TokenEnd == Input.npos) ? Input : Input.substr(0, TokenEnd);
724
148
    Input =
725
148
        (TokenEnd == Input.npos) ? std::string_view{} : Input.substr(TokenEnd);
726
727
148
    size_t OptPos = Token.find('?');
728
148
    std::string_view HashExpr =
729
148
        (OptPos == Token.npos) ? Token : Token.substr(0, OptPos);
730
731
148
    size_t DashPos = HashExpr.find('-');
732
148
    if (DashPos == HashExpr.npos) {
733
22
      spdlog::error(ErrCode::Value::NameExpectedDashAfterHash);
734
22
      spdlog::error("    Component name: expected `-` after hash algorithm"sv);
735
22
      return Unexpect(ErrCode::Value::NameExpectedDashAfterHash);
736
22
    }
737
126
    std::string_view Algo = HashExpr.substr(0, DashPos);
738
126
    if (Algo != "sha256"sv && Algo != "sha384"sv && Algo != "sha512"sv) {
739
20
      spdlog::error(ErrCode::Value::NameUnknownHashAlgorithm);
740
20
      spdlog::error("    Component name: unrecognized hash algorithm '{}'"sv,
741
20
                    Algo);
742
20
      return Unexpect(ErrCode::Value::NameUnknownHashAlgorithm);
743
20
    }
744
106
    if (!IsBase64(HashExpr.substr(DashPos + 1))) {
745
40
      spdlog::error(ErrCode::Value::NameInvalidBase64);
746
40
      spdlog::error("    Component name: hash value is not valid base64"sv);
747
40
      return Unexpect(ErrCode::Value::NameInvalidBase64);
748
40
    }
749
106
  }
750
751
16
  return {};
752
98
}
753
754
} // namespace Component
755
} // namespace Validator
756
} // namespace WasmEdge