/src/WasmEdge/lib/validator/component_name.cpp
Line | Count | Source |
1 | | // SPDX-License-Identifier: Apache-2.0 |
2 | | // SPDX-FileCopyrightText: Copyright The WasmEdge Authors |
3 | | |
4 | | #include "validator/component_name.h" |
5 | | |
6 | | #include "spdlog/spdlog.h" |
7 | | |
8 | | #include <algorithm> |
9 | | #include <cctype> |
10 | | #include <string_view> |
11 | | |
12 | | namespace WasmEdge { |
13 | | namespace Validator { |
14 | | |
15 | | using namespace std::literals; |
16 | | |
17 | | // label ::= <first-fragment> ( '-' <fragment> )* |
18 | | // first-fragment ::= <first-word> | <first-acronym> |
19 | | // first-word ::= [a-z] [0-9a-z]* |
20 | | // first-acronym ::= [A-Z] [0-9A-Z]* |
21 | | // fragment ::= <word> | <acronym> |
22 | | // word ::= [0-9a-z]+ |
23 | | // acronym ::= [0-9A-Z]+ |
24 | 0 | bool isKebabString(std::string_view Input) { |
25 | 0 | bool IsFirstPart = true; |
26 | 0 | bool Uppercase = false; |
27 | 0 | bool Lowercase = false; |
28 | 0 | bool Digit = false; |
29 | |
|
30 | 0 | for (char C : Input) { |
31 | 0 | if (islower(C)) { |
32 | 0 | if (Uppercase) |
33 | 0 | return false; |
34 | 0 | Lowercase = true; |
35 | 0 | } else if (isupper(C)) { |
36 | 0 | if (Lowercase) |
37 | 0 | return false; |
38 | 0 | Uppercase = true; |
39 | 0 | } else if (isdigit(C)) { |
40 | 0 | if (IsFirstPart && !(Uppercase || Lowercase)) |
41 | 0 | return false; |
42 | 0 | Digit = true; |
43 | 0 | } else if (C == '-') { |
44 | 0 | if (Uppercase || Lowercase || Digit) { |
45 | 0 | IsFirstPart = false; |
46 | 0 | Uppercase = false; |
47 | 0 | Lowercase = false; |
48 | 0 | Digit = false; |
49 | 0 | } else { |
50 | 0 | return false; |
51 | 0 | } |
52 | 0 | } else { |
53 | 0 | return false; |
54 | 0 | } |
55 | 0 | } |
56 | | |
57 | 0 | return Input.size() > 0 && Input.back() != '-'; |
58 | 0 | } |
59 | | |
60 | | namespace { |
61 | | |
62 | | // words ::= <first-word> ( '-' <word> )* |
63 | | // first-word ::= [a-z] [0-9a-z]* |
64 | | // word ::= [0-9a-z]+ |
65 | 0 | bool isLowercaseKebabString(std::string_view Input) { |
66 | 0 | if (Input.empty() || !islower(Input[0])) |
67 | 0 | return false; |
68 | 0 | for (char C : Input) { |
69 | 0 | if (C != '-' && !islower(C) && !isdigit(C)) |
70 | 0 | return false; |
71 | 0 | } |
72 | 0 | return Input.back() != '-' && Input.find("--"sv) == Input.npos; |
73 | 0 | } |
74 | | |
75 | 0 | bool isEOF(std::string_view Input) { return Input.empty(); } |
76 | | |
77 | 0 | bool readUntil(std::string_view &Input, char Delim, std::string_view &Output) { |
78 | 0 | size_t Pos = Input.find(Delim); |
79 | 0 | if (Pos == Input.npos) { |
80 | 0 | return false; |
81 | 0 | } |
82 | | |
83 | 0 | Output = Input.substr(0, Pos); |
84 | 0 | Input.remove_prefix(Pos + 1); |
85 | 0 | return true; |
86 | 0 | } |
87 | | |
88 | 0 | bool tryRead(std::string_view Prefix, std::string_view &Name) { |
89 | 0 | if (Prefix.size() > Name.size()) |
90 | 0 | return false; |
91 | 0 | if (Prefix != Name.substr(0, Prefix.size())) |
92 | 0 | return false; |
93 | | |
94 | 0 | Name.remove_prefix(Prefix.size()); |
95 | 0 | return true; |
96 | 0 | } |
97 | | |
98 | 0 | bool tryReadKebab(std::string_view &Input, std::string_view &Output) { |
99 | 0 | size_t Pos = 0; |
100 | 0 | while (Pos < Input.size()) { |
101 | 0 | if (isalnum(Input[Pos]) || Input[Pos] == '-') { |
102 | 0 | Pos++; |
103 | 0 | } else { |
104 | 0 | break; |
105 | 0 | } |
106 | 0 | } |
107 | 0 | Output = Input.substr(0, Pos); |
108 | 0 | Input.remove_prefix(Pos); |
109 | 0 | return isKebabString(Output); |
110 | 0 | } |
111 | | |
112 | | // integrity-metadata = *WSP hash-with-options *(1*WSP hash-with-options) *WSP |
113 | | // hash-with-options = hash-expression *("?" option-expression) |
114 | | // hash-expression = hash-algorithm "-" base64-value |
115 | | // hash-algorithm = "sha256" / "sha384" / "sha512" |
116 | | // base64-value = *VCHAR (visible chars, no whitespace) |
117 | 0 | bool isIntegrityMetadata(std::string_view Input) { |
118 | 0 | while (!Input.empty() && Input.front() == ' ') |
119 | 0 | Input.remove_prefix(1); |
120 | 0 | while (!Input.empty() && Input.back() == ' ') |
121 | 0 | Input.remove_suffix(1); |
122 | 0 | if (Input.empty()) |
123 | 0 | return false; |
124 | | |
125 | 0 | bool HasToken = false; |
126 | 0 | while (!Input.empty()) { |
127 | 0 | while (!Input.empty() && Input.front() == ' ') |
128 | 0 | Input.remove_prefix(1); |
129 | 0 | if (Input.empty()) |
130 | 0 | break; |
131 | | |
132 | 0 | size_t TokenEnd = Input.find(' '); |
133 | 0 | std::string_view Token = |
134 | 0 | (TokenEnd == Input.npos) ? Input : Input.substr(0, TokenEnd); |
135 | 0 | Input = |
136 | 0 | (TokenEnd == Input.npos) ? std::string_view{} : Input.substr(TokenEnd); |
137 | |
|
138 | 0 | size_t OptPos = Token.find('?'); |
139 | 0 | std::string_view HashExpr = |
140 | 0 | (OptPos == Token.npos) ? Token : Token.substr(0, OptPos); |
141 | |
|
142 | 0 | bool ValidAlgo = false; |
143 | 0 | static constexpr std::string_view Algos[3] = {"sha256-", "sha384-", |
144 | 0 | "sha512-"}; |
145 | 0 | for (auto AlgoSV : Algos) { |
146 | 0 | if (HashExpr.size() > AlgoSV.size() && |
147 | 0 | HashExpr.substr(0, AlgoSV.size()) == AlgoSV) { |
148 | 0 | auto Value = HashExpr.substr(AlgoSV.size()); |
149 | 0 | if (std::all_of(Value.begin(), Value.end(), |
150 | 0 | [](char C) { return C >= 0x21 && C <= 0x7E; })) { |
151 | 0 | ValidAlgo = true; |
152 | 0 | } |
153 | 0 | break; |
154 | 0 | } |
155 | 0 | } |
156 | 0 | if (!ValidAlgo) |
157 | 0 | return false; |
158 | | |
159 | 0 | HasToken = true; |
160 | 0 | } |
161 | | |
162 | 0 | return HasToken; |
163 | 0 | } |
164 | | |
165 | | // Parses a non-negative integer without leading zeros. |
166 | | // Returns the end position, or npos on failure. |
167 | 0 | size_t parseNumeric(std::string_view V) { |
168 | 0 | if (V.empty()) |
169 | 0 | return std::string_view::npos; |
170 | 0 | if (V[0] == '0') { |
171 | 0 | return 1; |
172 | 0 | } |
173 | 0 | if (V[0] >= '1' && V[0] <= '9') { |
174 | 0 | size_t Pos = 1; |
175 | 0 | while (Pos < V.size() && isdigit(V[Pos])) |
176 | 0 | Pos++; |
177 | 0 | return Pos; |
178 | 0 | } |
179 | 0 | return std::string_view::npos; |
180 | 0 | } |
181 | | |
182 | | // canonversion ::= [1-9] [0-9]* |
183 | | // | '0.' [1-9] [0-9]* |
184 | | // | '0.0.' [1-9] [0-9]* |
185 | 0 | bool isCanonVersion(std::string_view V) { |
186 | 0 | if (V.empty()) |
187 | 0 | return false; |
188 | | |
189 | | // canonversion ::= [1-9] [0-9]* | '0.' [1-9] [0-9]* | '0.0.' [1-9] [0-9]* |
190 | 0 | for (int I = 0; I < 3; I++) { |
191 | 0 | if (V[0] >= '1' && V[0] <= '9') { |
192 | 0 | size_t End = parseNumeric(V); |
193 | 0 | return End == V.size(); |
194 | 0 | } |
195 | 0 | if (!tryRead("0."sv, V) || V.empty()) |
196 | 0 | return false; |
197 | 0 | } |
198 | | |
199 | 0 | return false; |
200 | 0 | } |
201 | | |
202 | | // Validates a dot-separated pre-release or build identifier segment. |
203 | | // Each identifier is [0-9A-Za-z-]+. |
204 | | // Numeric identifiers must not have leading zeros. |
205 | 0 | bool isPreReleaseOrBuild(std::string_view V, bool CheckLeadingZeros) { |
206 | 0 | if (V.empty()) |
207 | 0 | return false; |
208 | 0 | size_t Start = 0; |
209 | 0 | while (Start < V.size()) { |
210 | 0 | size_t DotPos = V.find('.', Start); |
211 | 0 | std::string_view Ident = |
212 | 0 | (DotPos == V.npos) ? V.substr(Start) : V.substr(Start, DotPos - Start); |
213 | 0 | if (Ident.empty()) |
214 | 0 | return false; |
215 | 0 | for (char C : Ident) { |
216 | 0 | if (!isalnum(C) && C != '-') |
217 | 0 | return false; |
218 | 0 | } |
219 | 0 | if (CheckLeadingZeros) { |
220 | 0 | bool AllDigits = std::all_of(Ident.begin(), Ident.end(), |
221 | 0 | [](char C) { return isdigit(C); }); |
222 | 0 | if (AllDigits && Ident.size() > 1 && Ident[0] == '0') |
223 | 0 | return false; |
224 | 0 | } |
225 | 0 | if (DotPos == V.npos) |
226 | 0 | break; |
227 | 0 | Start = DotPos + 1; |
228 | 0 | } |
229 | 0 | return true; |
230 | 0 | } |
231 | | |
232 | | // MAJOR.MINOR.PATCH[-prerelease][+build] per semver.org 2.0 |
233 | 0 | bool isValidSemver(std::string_view V) { |
234 | 0 | if (V.empty()) |
235 | 0 | return false; |
236 | | |
237 | | // Parse MAJOR.MINOR.PATCH |
238 | 0 | for (int I = 0; I < 3; I++) { |
239 | 0 | size_t End = parseNumeric(V); |
240 | 0 | if (End == std::string_view::npos) |
241 | 0 | return false; |
242 | 0 | if (I < 2) { |
243 | 0 | if (End >= V.size() || V[End] != '.') |
244 | 0 | return false; |
245 | 0 | V.remove_prefix(End + 1); |
246 | 0 | } else { |
247 | 0 | V.remove_prefix(End); |
248 | 0 | } |
249 | 0 | } |
250 | | |
251 | 0 | if (V.empty()) |
252 | 0 | return true; |
253 | | |
254 | 0 | if (V[0] == '-') { |
255 | 0 | V.remove_prefix(1); |
256 | 0 | size_t PlusPos = V.find('+'); |
257 | 0 | std::string_view PreRelease = |
258 | 0 | (PlusPos == V.npos) ? V : V.substr(0, PlusPos); |
259 | 0 | if (!isPreReleaseOrBuild(PreRelease, true)) |
260 | 0 | return false; |
261 | 0 | if (PlusPos == V.npos) |
262 | 0 | return true; |
263 | 0 | V.remove_prefix(PlusPos); |
264 | 0 | } |
265 | | |
266 | 0 | if (!V.empty() && V[0] == '+') { |
267 | 0 | V.remove_prefix(1); |
268 | 0 | return isPreReleaseOrBuild(V, false); |
269 | 0 | } |
270 | | |
271 | 0 | return V.empty(); |
272 | 0 | } |
273 | | |
274 | 0 | bool isVersion(std::string_view V) { |
275 | 0 | return isCanonVersion(V) || isValidSemver(V); |
276 | 0 | } |
277 | | |
278 | 0 | Unexpected<ErrCode> reportError(std::string_view Reason) { |
279 | 0 | spdlog::error(ErrCode::Value::ComponentInvalidName); |
280 | 0 | spdlog::error(" Component name: {}"sv, Reason); |
281 | 0 | return Unexpect(ErrCode::Value::ComponentInvalidName); |
282 | 0 | } |
283 | | |
284 | | // hashname ::= 'integrity=<' <integrity-metadata> '>' |
285 | | // Parses optional ',integrity=<...>' suffix from Next. |
286 | | // If Next is empty, returns true with empty Integrity. |
287 | | // On success, Next is consumed and Integrity is set. |
288 | | Expect<void> tryParseIntegritySuffix(std::string_view &Next, |
289 | 0 | std::string_view &Integrity) { |
290 | 0 | if (Next.empty()) { |
291 | 0 | Integrity = {}; |
292 | 0 | return {}; |
293 | 0 | } |
294 | 0 | if (!tryRead(",integrity=<"sv, Next)) |
295 | 0 | return reportError("expected ',integrity=<' after "sv); |
296 | 0 | std::string_view IntegrityData; |
297 | 0 | if (!readUntil(Next, '>', IntegrityData)) |
298 | 0 | return reportError("expected '>' closing integrity"sv); |
299 | 0 | if (!isIntegrityMetadata(IntegrityData)) |
300 | 0 | return reportError("invalid integrity metadata"sv); |
301 | 0 | if (!isEOF(Next)) |
302 | 0 | return reportError("unexpected trailing content after integrity"sv); |
303 | 0 | Integrity = IntegrityData; |
304 | 0 | return {}; |
305 | 0 | } |
306 | | |
307 | | // pkgpath ::= <namespace> <words> |
308 | | // Parses 'namespace:package' from Next, stopping at delimiters in StopChars. |
309 | | struct PkgPath { |
310 | | std::string_view Namespace; |
311 | | std::string_view Package; |
312 | | }; |
313 | | |
314 | | Expect<PkgPath> parsePkgPath(std::string_view &Next, |
315 | 0 | std::string_view StopChars) { |
316 | 0 | std::string_view Namespace; |
317 | 0 | if (!readUntil(Next, ':', Namespace)) |
318 | 0 | return reportError("expected ':' in namespace"sv); |
319 | 0 | if (!isLowercaseKebabString(Namespace)) |
320 | 0 | return reportError("invalid namespace"sv); |
321 | | |
322 | 0 | size_t PkgEnd = Next.find_first_of(StopChars); |
323 | 0 | if (PkgEnd == Next.npos) |
324 | 0 | return reportError("unterminated package name"sv); |
325 | 0 | std::string_view Package = Next.substr(0, PkgEnd); |
326 | 0 | Next.remove_prefix(PkgEnd); |
327 | 0 | if (!isLowercaseKebabString(Package)) |
328 | 0 | return reportError("invalid package name"sv); |
329 | | |
330 | 0 | return PkgPath{Namespace, Package}; |
331 | 0 | } |
332 | | |
333 | | } // anonymous namespace |
334 | | |
335 | | // exportname ::= <plainname> | <interfacename> |
336 | | // importname ::= <exportname> | <depname> | <urlname> | <hashname> |
337 | 0 | Expect<ComponentName> ComponentName::parse(std::string_view Name) { |
338 | 0 | ComponentName Result(Name); |
339 | 0 | auto Next = Name; |
340 | | |
341 | | // plainname ::= <label> |
342 | | // | '[constructor]' <label> |
343 | | // | '[method]' <label> '.' <label> |
344 | | // | '[static]' <label> '.' <label> |
345 | |
|
346 | 0 | if (tryRead("[constructor]"sv, Next)) { |
347 | 0 | if (!isKebabString(Next)) { |
348 | 0 | return reportError("invalid label after [constructor]"sv); |
349 | 0 | } |
350 | 0 | Result.Detail.emplace<ConstructorDetail>(ConstructorDetail{Next}); |
351 | 0 | Result.NoTagName = Next; |
352 | 0 | Result.Kind = ComponentNameKind::Constructor; |
353 | 0 | return Result; |
354 | 0 | } |
355 | | |
356 | 0 | auto tryReadResourceWithLabel = [&](std::string_view Tag, |
357 | 0 | std::string_view &Resource, |
358 | 0 | std::string_view &Label) -> bool { |
359 | 0 | auto Saved = Next; |
360 | 0 | if (!tryRead(Tag, Next)) { |
361 | 0 | return false; |
362 | 0 | } |
363 | 0 | auto TmpNoTagName = Next; |
364 | 0 | if (!readUntil(Next, '.', Resource)) { |
365 | 0 | Next = Saved; |
366 | 0 | return false; |
367 | 0 | } |
368 | 0 | if (!isKebabString(Resource) || !isKebabString(Next)) { |
369 | 0 | Next = Saved; |
370 | 0 | return false; |
371 | 0 | } |
372 | 0 | Result.NoTagName = TmpNoTagName; |
373 | 0 | Label = Next; |
374 | 0 | return true; |
375 | 0 | }; |
376 | |
|
377 | 0 | { |
378 | 0 | std::string_view Resource, Label; |
379 | 0 | if (tryReadResourceWithLabel("[method]"sv, Resource, Label)) { |
380 | 0 | Result.Detail.emplace<MethodDetail>(MethodDetail{Resource, Label}); |
381 | 0 | Result.Kind = ComponentNameKind::Method; |
382 | 0 | return Result; |
383 | 0 | } |
384 | 0 | } |
385 | | |
386 | 0 | { |
387 | 0 | std::string_view Resource, Label; |
388 | 0 | if (tryReadResourceWithLabel("[static]"sv, Resource, Label)) { |
389 | 0 | Result.Detail.emplace<StaticDetail>(StaticDetail{Resource, Label}); |
390 | 0 | Result.Kind = ComponentNameKind::Static; |
391 | 0 | return Result; |
392 | 0 | } |
393 | 0 | } |
394 | | |
395 | 0 | if (tryRead("[async]"sv, Next)) { |
396 | 0 | Result.NoTagName = Next; |
397 | 0 | return reportError("[async] not supported yet"sv); |
398 | 0 | } |
399 | | |
400 | 0 | if (tryRead("[async method]"sv, Next)) { |
401 | 0 | Result.NoTagName = Next; |
402 | 0 | return reportError("[async method] not supported yet"sv); |
403 | 0 | } |
404 | | |
405 | 0 | if (tryRead("[async static]"sv, Next)) { |
406 | 0 | Result.NoTagName = Next; |
407 | 0 | return reportError("[async static] not supported yet"sv); |
408 | 0 | } |
409 | | |
410 | 0 | if (Next.size() != 0 && Next[0] == '[') { |
411 | 0 | return reportError("unknown annotation"sv); |
412 | 0 | } |
413 | 0 | Result.NoTagName = Next; |
414 | | |
415 | | // depname ::= 'unlocked-dep=<' <pkgnamequery> '>' |
416 | | // | 'locked-dep=<' <pkgname> '>' ( ',' <hashname> )? |
417 | |
|
418 | 0 | if (tryRead("unlocked-dep="sv, Next)) { |
419 | 0 | if (!tryRead("<"sv, Next)) |
420 | 0 | return reportError("expected '<' after unlocked-dep="sv); |
421 | | |
422 | 0 | EXPECTED_TRY(auto Path, parsePkgPath(Next, "@>"sv)); |
423 | | |
424 | | // verrange ::= '@*' |
425 | | // | '@{' verlower '}' |
426 | | // | '@{' verupper '}' |
427 | | // | '@{' verlower ' ' verupper '}' |
428 | 0 | std::string_view VersionRange; |
429 | 0 | if (!Next.empty() && Next[0] == '@') { |
430 | 0 | auto VerStart = Next; |
431 | 0 | Next.remove_prefix(1); |
432 | 0 | if (Next.empty()) |
433 | 0 | return reportError( |
434 | 0 | "expected version range after '@' in unlocked-dep"sv); |
435 | | |
436 | 0 | if (Next[0] == '*') { |
437 | 0 | Next.remove_prefix(1); |
438 | 0 | } else if (Next[0] == '{') { |
439 | 0 | size_t ClosePos = Next.find('}'); |
440 | 0 | if (ClosePos == Next.npos) |
441 | 0 | return reportError("expected '}' in unlocked-dep version range"sv); |
442 | 0 | auto RangeBody = Next.substr(1, ClosePos - 1); |
443 | |
|
444 | 0 | auto ValidateRange = [](std::string_view Body) -> bool { |
445 | 0 | if (Body.empty()) |
446 | 0 | return false; |
447 | 0 | auto Remaining = Body; |
448 | |
|
449 | 0 | if (tryRead(">="sv, Remaining)) { |
450 | 0 | size_t SpacePos = Remaining.find(' '); |
451 | 0 | std::string_view Lower = (SpacePos == Remaining.npos) |
452 | 0 | ? Remaining |
453 | 0 | : Remaining.substr(0, SpacePos); |
454 | 0 | if (!isValidSemver(Lower)) |
455 | 0 | return false; |
456 | 0 | if (SpacePos == Remaining.npos) |
457 | 0 | return true; |
458 | 0 | Remaining.remove_prefix(SpacePos + 1); |
459 | 0 | if (!tryRead("<"sv, Remaining)) |
460 | 0 | return false; |
461 | 0 | return isValidSemver(Remaining); |
462 | 0 | } |
463 | | |
464 | 0 | if (tryRead("<"sv, Remaining)) { |
465 | 0 | return isValidSemver(Remaining); |
466 | 0 | } |
467 | | |
468 | 0 | return false; |
469 | 0 | }; |
470 | |
|
471 | 0 | if (!ValidateRange(RangeBody)) |
472 | 0 | return reportError("invalid version range in unlocked-dep"sv); |
473 | | |
474 | 0 | Next.remove_prefix(ClosePos + 1); |
475 | 0 | } else { |
476 | 0 | return reportError("expected '*' or '{' after '@' in unlocked-dep"sv); |
477 | 0 | } |
478 | 0 | VersionRange = VerStart.substr(0, VerStart.size() - Next.size()); |
479 | 0 | } |
480 | | |
481 | 0 | if (!tryRead(">"sv, Next)) |
482 | 0 | return reportError("expected '>' closing unlocked-dep"sv); |
483 | | |
484 | 0 | if (!isEOF(Next)) |
485 | 0 | return reportError("unexpected trailing content after unlocked-dep"sv); |
486 | | |
487 | 0 | Result.Detail.emplace<UnlockedDepDetail>( |
488 | 0 | UnlockedDepDetail{Path.Namespace, Path.Package, VersionRange}); |
489 | 0 | Result.Kind = ComponentNameKind::UnlockedDep; |
490 | 0 | return Result; |
491 | 0 | } |
492 | | |
493 | 0 | if (tryRead("locked-dep="sv, Next)) { |
494 | 0 | if (!tryRead("<"sv, Next)) |
495 | 0 | return reportError("expected '<' after locked-dep="sv); |
496 | | |
497 | 0 | EXPECTED_TRY(auto Path, parsePkgPath(Next, "@>"sv)); |
498 | |
|
499 | 0 | std::string_view Version; |
500 | 0 | if (!Next.empty() && Next[0] == '@') { |
501 | 0 | Next.remove_prefix(1); |
502 | 0 | size_t VerEnd = Next.find('>'); |
503 | 0 | if (VerEnd == Next.npos) |
504 | 0 | return reportError("expected '>' after version in locked-dep"sv); |
505 | 0 | Version = Next.substr(0, VerEnd); |
506 | 0 | Next.remove_prefix(VerEnd); |
507 | 0 | if (!isValidSemver(Version)) |
508 | 0 | return reportError("invalid semver in locked-dep"sv); |
509 | 0 | } |
510 | | |
511 | 0 | if (!tryRead(">"sv, Next)) |
512 | 0 | return reportError("expected '>' closing locked-dep"sv); |
513 | | |
514 | 0 | std::string_view Integrity; |
515 | 0 | EXPECTED_TRY(tryParseIntegritySuffix(Next, Integrity)); |
516 | | |
517 | 0 | Result.Detail.emplace<LockedDepDetail>( |
518 | 0 | LockedDepDetail{Path.Namespace, Path.Package, Version, Integrity}); |
519 | 0 | Result.Kind = ComponentNameKind::LockedDep; |
520 | 0 | return Result; |
521 | 0 | } |
522 | | |
523 | | // urlname ::= 'url=<' <nonbrackets> '>' (',' <hashname>)? |
524 | | // nonbrackets ::= [^<>]* |
525 | 0 | if (tryRead("url="sv, Next)) { |
526 | 0 | if (!tryRead("<"sv, Next)) |
527 | 0 | return reportError("expected '<' after url="sv); |
528 | | |
529 | 0 | size_t ClosePos = Next.find('>'); |
530 | 0 | if (ClosePos == Next.npos) |
531 | 0 | return reportError("expected '>' closing url"sv); |
532 | | |
533 | 0 | std::string_view UrlContent = Next.substr(0, ClosePos); |
534 | 0 | if (UrlContent.find('<') != UrlContent.npos) |
535 | 0 | return reportError("'<' not allowed inside url"sv); |
536 | 0 | Next.remove_prefix(ClosePos + 1); |
537 | |
|
538 | 0 | std::string_view Integrity; |
539 | 0 | EXPECTED_TRY(tryParseIntegritySuffix(Next, Integrity)); |
540 | | |
541 | 0 | Result.Detail.emplace<UrlDetail>(UrlDetail{UrlContent, Integrity}); |
542 | 0 | Result.Kind = ComponentNameKind::Url; |
543 | 0 | return Result; |
544 | 0 | } |
545 | | |
546 | | // hashname ::= 'integrity=<' <integrity-metadata> '>' |
547 | 0 | if (tryRead("integrity="sv, Next)) { |
548 | 0 | if (!tryRead("<"sv, Next)) |
549 | 0 | return reportError("expected '<' after integrity="sv); |
550 | 0 | std::string_view IntegrityData; |
551 | 0 | if (!readUntil(Next, '>', IntegrityData)) |
552 | 0 | return reportError("expected '>' closing integrity"sv); |
553 | 0 | if (!isIntegrityMetadata(IntegrityData)) |
554 | 0 | return reportError("invalid integrity metadata"sv); |
555 | 0 | if (!isEOF(Next)) |
556 | 0 | return reportError("unexpected trailing content after integrity"sv); |
557 | 0 | Result.Detail.emplace<IntegrityDetail>(IntegrityDetail{IntegrityData}); |
558 | 0 | Result.Kind = ComponentNameKind::Integrity; |
559 | 0 | return Result; |
560 | 0 | } |
561 | | |
562 | | // interfacename ::= <namespace> <label> <projection> <interfaceversion>? |
563 | | // namespace ::= <words> ':' |
564 | | // projection ::= '/' <label> |
565 | | // interfaceversion ::= '@' <valid semver> | '@' <canonversion> |
566 | 0 | { |
567 | 0 | std::string_view Namespace, Package, Interface, Version; |
568 | |
|
569 | 0 | int Counter = 0; |
570 | 0 | while (readUntil(Next, ':', Namespace)) { |
571 | 0 | Counter++; |
572 | 0 | if (!isLowercaseKebabString(Namespace)) { |
573 | 0 | return reportError("invalid namespace in interface name"sv); |
574 | 0 | } |
575 | 0 | } |
576 | 0 | if (Counter == 0) { |
577 | | // No ':' found — fall through to label parsing below. |
578 | 0 | goto ParseLabel; |
579 | 0 | } |
580 | 0 | if (Counter != 1) { |
581 | 0 | return reportError("nested namespaces not supported yet"sv); |
582 | 0 | } |
583 | | |
584 | | // interfacename ::= <namespace> <words> <projection> ... |
585 | 0 | if (!tryReadKebab(Next, Package) || !isLowercaseKebabString(Package)) { |
586 | 0 | return reportError("invalid package in interface name"sv); |
587 | 0 | } |
588 | | |
589 | 0 | Counter = 0; |
590 | 0 | while (!isEOF(Next) && Next[0] == '/') { |
591 | 0 | Next.remove_prefix(1); |
592 | 0 | Counter++; |
593 | 0 | if (!tryReadKebab(Next, Interface)) { |
594 | 0 | return reportError("invalid projection label in interface name"sv); |
595 | 0 | } |
596 | 0 | } |
597 | | |
598 | 0 | if (Counter == 0) { |
599 | 0 | return reportError("expected '/' projection in interface name"sv); |
600 | 0 | } |
601 | 0 | if (Counter != 1) { |
602 | 0 | return reportError("nested projections not supported yet"sv); |
603 | 0 | } |
604 | | |
605 | 0 | if (!isEOF(Next) && Next[0] == '@') { |
606 | 0 | Next.remove_prefix(1); |
607 | 0 | Version = Next; |
608 | 0 | if (!isVersion(Version)) { |
609 | 0 | return reportError("invalid version in interface name"sv); |
610 | 0 | } |
611 | 0 | } |
612 | | |
613 | 0 | Result.Detail.emplace<InterfaceDetail>( |
614 | 0 | InterfaceDetail{Namespace, Package, Interface, Version}); |
615 | 0 | Result.Kind = ComponentNameKind::InterfaceType; |
616 | 0 | return Result; |
617 | 0 | } |
618 | | |
619 | 0 | ParseLabel: |
620 | 0 | if (!isKebabString(Next)) { |
621 | 0 | return reportError("invalid label"sv); |
622 | 0 | } |
623 | 0 | Result.Detail.emplace<LabelDetail>(); |
624 | 0 | Result.Kind = ComponentNameKind::Label; |
625 | 0 | return Result; |
626 | 0 | } |
627 | | |
628 | | } // namespace Validator |
629 | | } // namespace WasmEdge |