/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 |