Coverage Report

Created: 2026-11-09 06:49

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
37.7k
bool ExternName::isKebabString(std::string_view Input) noexcept {
20
37.7k
  bool IsFirstPart = true;
21
37.7k
  bool Uppercase = false;
22
37.7k
  bool Lowercase = false;
23
37.7k
  bool Digit = false;
24
25
127k
  for (char C : Input) {
26
127k
    if (islower(static_cast<unsigned char>(C))) {
27
34.5k
      if (Uppercase) {
28
29
        return false;
29
29
      }
30
34.5k
      Lowercase = true;
31
93.1k
    } else if (isupper(static_cast<unsigned char>(C))) {
32
20.0k
      if (Lowercase) {
33
66
        return false;
34
66
      }
35
19.9k
      Uppercase = true;
36
73.0k
    } else if (isdigit(static_cast<unsigned char>(C))) {
37
46.4k
      if (IsFirstPart && !(Uppercase || Lowercase)) {
38
34
        return false;
39
34
      }
40
46.3k
      Digit = true;
41
46.3k
    } else if (C == '-') {
42
26.4k
      if (Uppercase || Lowercase || Digit) {
43
26.3k
        IsFirstPart = false;
44
26.3k
        Uppercase = false;
45
26.3k
        Lowercase = false;
46
26.3k
        Digit = false;
47
26.3k
      } else {
48
44
        return false;
49
44
      }
50
26.4k
    } else {
51
220
      return false;
52
220
    }
53
127k
  }
54
55
37.3k
  return Input.size() > 0 && Input.back() != '-';
56
37.7k
}
57
58
13.8k
Expect<void> ExternName::parse(std::string_view Name) noexcept {
59
13.8k
  OriName = Name;
60
13.8k
  NoTagName = {};
61
13.8k
  NameKind = Kind::Invalid;
62
13.8k
  NameDetail = {};
63
13.8k
  Rest = Name;
64
65
13.8k
  if (!Rest.empty() && Rest[0] == '[') {
66
240
    return parsePlainName();
67
240
  }
68
69
13.6k
  if (Rest.find(':') != std::string_view::npos) {
70
620
    return parseInterfaceName();
71
620
  }
72
13.0k
  return parsePlainName();
73
13.6k
}
74
75
// Consumes Prefix from Rest, or leaves Rest untouched and returns false.
76
39.6k
bool ExternName::tryRead(std::string_view Prefix) noexcept {
77
39.6k
  if (Prefix.size() > Rest.size()) {
78
34.5k
    return false;
79
34.5k
  }
80
5.13k
  if (Prefix != Rest.substr(0, Prefix.size())) {
81
4.94k
    return false;
82
4.94k
  }
83
84
193
  Rest.remove_prefix(Prefix.size());
85
193
  return true;
86
5.13k
}
87
88
// Consumes up to and including Delim, and reports the text before it.
89
135
bool ExternName::readUntil(char Delim, std::string_view &Output) noexcept {
90
135
  size_t Pos = Rest.find(Delim);
91
135
  if (Pos == Rest.npos) {
92
4
    return false;
93
4
  }
94
95
131
  Output = Rest.substr(0, Pos);
96
131
  Rest.remove_prefix(Pos + 1);
97
131
  return true;
98
135
}
99
100
// Consumes and returns the leading run of label characters ([0-9A-Za-z-]).
101
1.05k
std::string_view ExternName::readLabelChars() noexcept {
102
1.05k
  size_t Pos = 0;
103
9.05k
  while (Pos < Rest.size() &&
104
8.91k
         (isalnum(static_cast<unsigned char>(Rest[Pos])) || Rest[Pos] == '-')) {
105
8.00k
    Pos++;
106
8.00k
  }
107
1.05k
  std::string_view Output = Rest.substr(0, Pos);
108
1.05k
  Rest.remove_prefix(Pos);
109
1.05k
  return Output;
110
1.05k
}
111
112
// plainname ::= <label> | '[constructor|method|static]' <label> ('.' <label>)?
113
13.2k
Expect<void> ExternName::parsePlainName() noexcept {
114
13.2k
  if (tryRead("[constructor]"sv)) {
115
58
    if (!isKebabString(Rest)) {
116
46
      spdlog::error(ErrCode::Value::ComponentNameNotKebab);
117
46
      spdlog::error("    Component name: label '{}' is not in kebab case"sv,
118
46
                    Rest);
119
46
      return Unexpect(ErrCode::Value::ComponentNameNotKebab);
120
46
    }
121
12
    NoTagName = Rest;
122
12
    NameDetail.Resource = Rest;
123
12
    NameKind = Kind::Constructor;
124
12
    return {};
125
58
  }
126
127
  // A '[method]' or '[static]' name needs a '.' between two kebab labels.
128
13.2k
  auto ReadResourceAndLabel = [this](std::string_view &Resource,
129
13.2k
                                     std::string_view &Label) -> Expect<void> {
130
135
    NoTagName = Rest;
131
135
    if (!readUntil('.', Resource)) {
132
4
      spdlog::error(ErrCode::Value::NameFailedToFindDot);
133
4
      spdlog::error("    Component name: failed to find `.` character"sv);
134
4
      return Unexpect(ErrCode::Value::NameFailedToFindDot);
135
4
    }
136
131
    if (!isKebabString(Resource)) {
137
44
      spdlog::error(ErrCode::Value::ComponentNameNotKebab);
138
44
      spdlog::error("    Component name: label '{}' is not in kebab case"sv,
139
44
                    Resource);
140
44
      return Unexpect(ErrCode::Value::ComponentNameNotKebab);
141
44
    }
142
87
    if (!isKebabString(Rest)) {
143
59
      spdlog::error(ErrCode::Value::ComponentNameNotKebab);
144
59
      spdlog::error("    Component name: label '{}' is not in kebab case"sv,
145
59
                    Rest);
146
59
      return Unexpect(ErrCode::Value::ComponentNameNotKebab);
147
59
    }
148
28
    Label = Rest;
149
28
    return {};
150
87
  };
151
152
13.2k
  if (tryRead("[method]"sv)) {
153
28
    std::string_view Resource, Label;
154
28
    EXPECTED_TRY(ReadResourceAndLabel(Resource, Label));
155
13
    NameDetail.Resource = Resource;
156
13
    NameDetail.Method = Label;
157
13
    NameKind = Kind::Method;
158
13
    return {};
159
28
  }
160
161
13.1k
  if (tryRead("[static]"sv)) {
162
107
    std::string_view Resource, Label;
163
107
    EXPECTED_TRY(ReadResourceAndLabel(Resource, Label));
164
15
    NameDetail.Resource = Resource;
165
15
    NameDetail.Method = Label;
166
15
    NameKind = Kind::Static;
167
15
    return {};
168
107
  }
169
170
13.0k
  if (!Rest.empty() && Rest[0] == '[') {
171
47
    spdlog::error(ErrCode::Value::ComponentInvalidName);
172
47
    spdlog::error("    Component name: unknown annotation"sv);
173
47
    return Unexpect(ErrCode::Value::ComponentInvalidName);
174
47
  }
175
176
13.0k
  if (!isKebabString(Rest)) {
177
210
    spdlog::error(ErrCode::Value::ComponentNameNotKebab);
178
210
    spdlog::error("    Component name: label '{}' is not in kebab case"sv,
179
210
                  Rest);
180
210
    return Unexpect(ErrCode::Value::ComponentNameNotKebab);
181
210
  }
182
12.8k
  NameKind = Kind::Label;
183
12.8k
  return {};
184
13.0k
}
185
186
// interfacename ::= <words> ':' <label> '/' <label> <interfaceversion>?
187
620
Expect<void> ExternName::parseInterfaceName() noexcept {
188
620
  size_t ColonPos = Rest.find(':');
189
620
  std::string_view Namespace = Rest.substr(0, ColonPos);
190
620
  Rest.remove_prefix(ColonPos + 1);
191
620
  EXPECTED_TRY(checkWordsLabel(Namespace, "namespace"sv));
192
193
550
  std::string_view Package = readLabelChars();
194
550
  EXPECTED_TRY(checkWordsLabel(Package, "package"sv));
195
196
  // Nested namespaces (`a:b:c/d`) are feature-gated; only a projection follows.
197
521
  if (Rest.empty() || Rest[0] != '/') {
198
17
    spdlog::error(ErrCode::Value::NameExpectedSlashAfterPackage);
199
17
    spdlog::error("    Component name: expected `/` after package name"sv);
200
17
    return Unexpect(ErrCode::Value::NameExpectedSlashAfterPackage);
201
17
  }
202
504
  Rest.remove_prefix(1);
203
204
504
  std::string_view Interface = readLabelChars();
205
504
  if (!isKebabString(Interface)) {
206
39
    spdlog::error(ErrCode::Value::ComponentNameNotKebab);
207
39
    spdlog::error("    Component name: label '{}' is not in kebab case"sv,
208
39
                  Interface);
209
39
    return Unexpect(ErrCode::Value::ComponentNameNotKebab);
210
39
  }
211
212
  // Nested projections (`a:b/c/d`) are feature-gated; only a version follows.
213
465
  if (!Rest.empty() && Rest[0] != '@') {
214
19
    spdlog::error(ErrCode::Value::NameTrailingCharacters);
215
19
    spdlog::error(
216
19
        "    Component name: trailing characters found after projection"sv);
217
19
    return Unexpect(ErrCode::Value::NameTrailingCharacters);
218
19
  }
219
220
446
  std::string_view Version;
221
446
  if (!Rest.empty()) {
222
335
    Rest.remove_prefix(1);
223
335
    Version = Rest;
224
335
    if (!isCanonVersion(Version)) {
225
293
      if (auto Res = scanSemver(Version); !Res) {
226
143
        spdlog::error(Res.error().getEnum());
227
143
        spdlog::error("    Component name: version '{}' is not valid"sv,
228
143
                      Version);
229
143
        return Unexpect(Res);
230
143
      }
231
293
    }
232
335
  }
233
234
303
  NameDetail.Namespace = Namespace;
235
303
  NameDetail.Package = Package;
236
303
  NameDetail.Interface = Interface;
237
303
  NameDetail.Version = Version;
238
303
  NameKind = Kind::InterfaceType;
239
303
  return {};
240
446
}
241
242
// words ::= <first-word> ( '-' <word> )*: a kebab label that is all lowercase.
243
Expect<void> ExternName::checkWordsLabel(std::string_view Label,
244
1.17k
                                         std::string_view What) const noexcept {
245
1.17k
  if (!isKebabString(Label)) {
246
88
    spdlog::error(ErrCode::Value::ComponentNameNotKebab);
247
88
    spdlog::error("    Component name: label '{}' is not in kebab case"sv,
248
88
                  Label);
249
88
    return Unexpect(ErrCode::Value::ComponentNameNotKebab);
250
88
  }
251
  // A kebab label already has the `<words>` shape, so only case is left.
252
2.66k
  for (char C : Label) {
253
2.66k
    if (isupper(static_cast<unsigned char>(C))) {
254
11
      spdlog::error(ErrCode::Value::ComponentPackageNameNotLowercase);
255
11
      spdlog::error("    Component name: {} '{}' is not lowercase"sv, What,
256
11
                    Label);
257
11
      return Unexpect(ErrCode::Value::ComponentPackageNameNotLowercase);
258
11
    }
259
2.66k
  }
260
1.07k
  return {};
261
1.08k
}
262
263
// semversuffix ::= [0-9A-Za-z.+-]*
264
Expect<void>
265
13
ExternName::checkVersionSuffix(std::string_view Suffix) const noexcept {
266
60
  for (char C : Suffix) {
267
60
    if (!isalnum(static_cast<unsigned char>(C)) && C != '.' && C != '+' &&
268
23
        C != '-') {
269
7
      spdlog::error(ErrCode::Value::ComponentVersionSuffixInvalid);
270
7
      spdlog::error("    `versionsuffix` `{}` is not a semver suffix"sv,
271
7
                    Suffix);
272
7
      return Unexpect(ErrCode::Value::ComponentVersionSuffixInvalid);
273
7
    }
274
60
  }
275
6
  if (NameKind != Kind::InterfaceType || NameDetail.Version.empty() ||
276
6
      !isCanonVersion(NameDetail.Version)) {
277
6
    spdlog::error(ErrCode::Value::ComponentVersionSuffixInvalid);
278
6
    spdlog::error("    `versionsuffix` needs a preceding canonical version"sv);
279
6
    return Unexpect(ErrCode::Value::ComponentVersionSuffixInvalid);
280
6
  }
281
0
  std::string Full(NameDetail.Version);
282
0
  Full.append(Suffix);
283
0
  if (!scanSemver(Full)) {
284
0
    spdlog::error(ErrCode::Value::ComponentVersionSuffixInvalid);
285
0
    spdlog::error("    `{}` and `versionsuffix` `{}` are not a valid semver"sv,
286
0
                  NameDetail.Version, Suffix);
287
0
    return Unexpect(ErrCode::Value::ComponentVersionSuffixInvalid);
288
0
  }
289
0
  return {};
290
0
}
291
292
// canonversion ::= [1-9] [0-9]* | '0.' [1-9] [0-9]* | '0.0.' [1-9] [0-9]*
293
335
bool ExternName::isCanonVersion(std::string_view V) const noexcept {
294
335
  if (V.substr(0, 4) == "0.0."sv) {
295
30
    V.remove_prefix(4);
296
305
  } else if (V.substr(0, 2) == "0."sv) {
297
49
    V.remove_prefix(2);
298
49
  }
299
335
  if (V.empty() || V[0] < '1' || V[0] > '9') {
300
50
    return false;
301
50
  }
302
750
  for (char C : V) {
303
750
    if (!isdigit(static_cast<unsigned char>(C))) {
304
243
      return false;
305
243
    }
306
750
  }
307
42
  return true;
308
285
}
309
310
// Scans MAJOR.MINOR.PATCH[-prerelease][+build] per semver.org 2.0, unlogged.
311
293
Expect<void> ExternName::scanSemver(std::string_view V) const noexcept {
312
293
  if (V.empty()) {
313
3
    return Unexpect(ErrCode::Value::NameEmptyString);
314
3
  }
315
316
979
  for (uint32_t I = 0; I < 3; I++) {
317
780
    if (I > 0) {
318
490
      if (V.empty()) {
319
9
        return Unexpect(ErrCode::Value::NameUnexpectedEnd);
320
9
      }
321
481
      if (V[0] != '.') {
322
31
        return Unexpect(ErrCode::Value::NameUnexpectedCharacter);
323
31
      }
324
450
      V.remove_prefix(1);
325
450
    }
326
740
    size_t Len = 0;
327
2.20k
    while (Len < V.size() && isdigit(static_cast<unsigned char>(V[Len]))) {
328
1.46k
      Len++;
329
1.46k
    }
330
740
    if (Len == 0) {
331
35
      return Unexpect(V.empty() ? ErrCode::Value::NameUnexpectedEnd
332
35
                                : ErrCode::Value::NameUnexpectedCharacter);
333
35
    }
334
705
    if (Len > 1 && V[0] == '0') {
335
16
      return Unexpect(ErrCode::Value::ComponentInvalidName);
336
16
    }
337
689
    V.remove_prefix(Len);
338
689
  }
339
340
199
  if (V.empty()) {
341
15
    return {};
342
15
  }
343
184
  if (V[0] != '-' && V[0] != '+') {
344
11
    return Unexpect(ErrCode::Value::NameUnexpectedCharacter);
345
11
  }
346
347
173
  if (V[0] == '-') {
348
154
    V.remove_prefix(1);
349
154
    size_t PlusPos = V.find('+');
350
154
    std::string_view PreRelease =
351
154
        (PlusPos == V.npos) ? V : V.substr(0, PlusPos);
352
154
    EXPECTED_TRY(scanSemverIdentifiers(PreRelease, true));
353
131
    if (PlusPos == V.npos) {
354
70
      return {};
355
70
    }
356
61
    V.remove_prefix(PlusPos);
357
61
  }
358
359
  // Here V starts with '+': scan the build metadata identifiers.
360
80
  V.remove_prefix(1);
361
80
  return scanSemverIdentifiers(V, false);
362
173
}
363
364
// Scans a dot-separated identifier list, rejecting leading zeros when asked.
365
Expect<void>
366
ExternName::scanSemverIdentifiers(std::string_view Idents,
367
234
                                  bool CheckLeadingZeros) const noexcept {
368
234
  size_t Start = 0;
369
506
  while (true) {
370
506
    size_t DotPos = Idents.find('.', Start);
371
506
    std::string_view Ident = (DotPos == Idents.npos)
372
506
                                 ? Idents.substr(Start)
373
506
                                 : Idents.substr(Start, DotPos - Start);
374
506
    if (Ident.empty()) {
375
10
      return Unexpect(ErrCode::Value::NameEmptyIdentifierSegment);
376
10
    }
377
496
    bool AllDigits = true;
378
1.91k
    for (char C : Ident) {
379
1.91k
      if (!isdigit(static_cast<unsigned char>(C))) {
380
865
        AllDigits = false;
381
865
        if (!isalnum(static_cast<unsigned char>(C)) && C != '-') {
382
25
          return Unexpect(ErrCode::Value::NameUnexpectedCharacter);
383
25
        }
384
865
      }
385
1.91k
    }
386
471
    if (CheckLeadingZeros && AllDigits && Ident.size() > 1 && Ident[0] == '0') {
387
3
      return Unexpect(ErrCode::Value::ComponentInvalidName);
388
3
    }
389
468
    if (DotPos == Idents.npos) {
390
196
      return {};
391
196
    }
392
272
    Start = DotPos + 1;
393
272
  }
394
234
}
395
396
} // namespace Component
397
} // namespace Validator
398
} // namespace WasmEdge