Coverage Report

Created: 2026-11-12 06:35

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