master
cpp 801 lines 29.4 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 SpecParsing.cpp
8
9 Abstract:
10
11 Parsers that turn delimited command-line spec strings (e.g. --secret,
12 --ulimit, --label, --filter) into structured values. These share the
13 SplitKeyValue helper for consistent key/value splitting.
14
15 --*/
16
17 #include "precomp.h"
18 #include "SpecParsing.h"
19 #include "ArgumentValidation.h"
20 #include "Exceptions.h"
21 #include "ImageService.h"
22 #include "JsonUtils.h"
23 #include "Localization.h"
24 #include <algorithm>
25 #include <charconv>
26 #include <chrono>
27 #include <cmath>
28 #include <filesystem>
29 #include <format>
30 #include <limits>
31 #include <optional>
32 #include <unordered_map>
33 #include <wslc.h>
34
35 using namespace wsl::windows::common;
36 using namespace wsl::shared;
37 using namespace wsl::shared::string;
38
39 namespace wsl::windows::wslc::validation {
40
41 KeyValueSplit SplitKeyValue(const std::wstring& value, wchar_t separator)
42 {
43 const auto pos = value.find(separator);
44 if (pos == std::wstring::npos)
45 {
46 return {value, std::wstring{}, false};
47 }
48
49 return {value.substr(0, pos), value.substr(pos + 1), true};
50 }
51
52 services::BuildSecret ParseSecretSpec(const std::wstring& spec)
53 {
54 std::wstring id;
55 std::wstring type;
56 std::wstring envName;
57 std::wstring srcPath;
58
59 // Docker parity: buildx parses --secret as a single CSV record (go-csvvalue), so a quoted field
60 // may contain commas (e.g. a 'src=' path). Malformed quoting is rejected like any other bad spec.
61 const auto parts = SplitCsvFields(spec);
62 if (!parts.has_value())
63 {
64 throw ArgumentException(Localization::MessageWslcSecretInvalidSpec(spec, L"malformed quoting"));
65 }
66
67 for (const auto& part : *parts)
68 {
69 const auto kv = SplitKeyValue(part);
70 if (!kv.HadSeparator || kv.Key.empty())
71 {
72 throw ArgumentException(
73 Localization::MessageWslcSecretInvalidSpec(spec, L"expected key=value pairs separated by ','"));
74 }
75 const auto& key = kv.Key;
76 const auto& value = kv.Value;
77
78 if (key == L"id")
79 {
80 id = value;
81 }
82 else if (key == L"type")
83 {
84 type = value;
85 }
86 else if (key == L"env")
87 {
88 envName = value;
89 }
90 else if (key == L"src" || key == L"source")
91 {
92 srcPath = value;
93 }
94 else
95 {
96 throw ArgumentException(Localization::MessageWslcSecretInvalidSpec(spec, std::format(L"unsupported key '{}'", key)));
97 }
98 }
99
100 if (id.empty())
101 {
102 throw ArgumentException(Localization::MessageWslcSecretInvalidSpec(spec, L"'id=' is required"));
103 }
104
105 // Docker parity: 'id' may not start with '-' because that would be interpreted as a command-line option.
106 if (id[0] == L'-')
107 {
108 throw ArgumentException(Localization::MessageWslcSecretInvalidSpec(spec, L"'id' may not start with '-'"));
109 }
110
111 // The id is forwarded into docker's comma/'='-delimited --secret spec, so reject any character
112 // that could break out of the id= field and inject additional options (e.g. ",src=/etc/passwd").
113 for (auto ch : id)
114 {
115 const bool allowed = (ch >= L'a' && ch <= L'z') || (ch >= L'A' && ch <= L'Z') || (ch >= L'0' && ch <= L'9') ||
116 ch == L'_' || ch == L'-' || ch == L'.';
117 if (!allowed)
118 {
119 throw ArgumentException(
120 Localization::MessageWslcSecretInvalidSpec(spec, L"'id' may only contain letters, digits, '_', '-' or '.'"));
121 }
122 }
123
124 if (!type.empty() && type != L"file" && type != L"env")
125 {
126 throw ArgumentException(Localization::MessageWslcSecretInvalidSpec(spec, std::format(L"unsupported secret type '{}'", type)));
127 }
128
129 // Docker parity: 'type=file' names a source file, so it requires 'src='. Without it we would
130 // otherwise fall through to reading an environment variable, silently contradicting the type.
131 if (type == L"file" && srcPath.empty())
132 {
133 throw ArgumentException(Localization::MessageWslcSecretInvalidSpec(spec, L"'type=file' requires 'src='"));
134 }
135
136 // Docker parity: with 'type=env', a bare 'src=' names the environment variable to read (rather
137 // than a file path), unless an explicit 'env=' was also given.
138 if (type == L"env" && envName.empty() && !srcPath.empty())
139 {
140 envName = std::move(srcPath);
141 srcPath.clear();
142 }
143
144 if (!envName.empty() && !srcPath.empty())
145 {
146 // Docker parity: 'env=' and 'src=' are not mutually exclusive; when both are given the
147 // environment variable wins and the file path is ignored.
148 srcPath.clear();
149 }
150 if (envName.empty() && srcPath.empty())
151 {
152 // Docker parity: with neither 'env=' nor 'src=', the secret value is read from the host
153 // environment variable whose name matches the id. Unlike an explicit 'env=', that variable
154 // must be set - Docker errors when the id-named variable is undefined.
155 envName = id;
156 if (!wsl::windows::common::wslutil::ReadEnvironmentVariable(envName.c_str()).has_value())
157 {
158 throw ArgumentException(
159 Localization::MessageWslcSecretInvalidSpec(spec, std::format(L"environment variable '{}' is not set", envName)));
160 }
161 }
162
163 if (!srcPath.empty())
164 {
165 std::error_code ec;
166 // Normalize to an absolute path (the service requires one to mount the file's directory) but do
167 // not verify the file exists or is a regular file here: that would be a TOCTOU race with the
168 // build, and the file may only be reachable from the service's context. Let the service/BuildKit
169 // reject an unmountable or unreadable file instead. GetCanonicalPath resolves a relative path
170 // against the current directory, collapses '..', and resolves symlinks for the portion of the
171 // path that exists; it succeeds for a missing file but still reports genuine errors.
172 auto absPath = wsl::windows::common::filesystem::GetCanonicalPath(srcPath, ec);
173 if (ec.value() != 0)
174 {
175 throw ArgumentException(
176 Localization::MessageWslcSecretInvalidSpec(spec, std::format(L"could not resolve source path: {}", srcPath)));
177 }
178
179 // Forward the resolved path rather than the bytes: the server mounts the file's parent directory
180 // into the build VM read-only and references the file in place with docker's --secret src=, so
181 // the secret is never copied off its original (possibly EFS-encrypted) location while still
182 // delivering arbitrary binary content byte-for-byte - matching Docker's type=file semantics.
183 return services::BuildSecret{
184 .Id = std::move(id),
185 .SourcePath = absPath.wstring(),
186 };
187 }
188
189 // Docker parity: a referenced environment variable that is unset (or set but empty) yields an
190 // empty secret value rather than an error. ReadEnvironmentVariable returns nullopt for an
191 // undefined variable, which we collapse to an empty value.
192 const std::wstring value = wsl::windows::common::wslutil::ReadEnvironmentVariable(envName.c_str()).value_or(std::wstring{});
193
194 // The env value is delivered as UTF-8 bytes, matching how the guest exposes it at /run/secrets/<id>.
195 auto valueBytes = wsl::windows::common::string::WideToMultiByte(value);
196 return services::BuildSecret{
197 .Id = std::move(id),
198 .Value = std::vector<BYTE>(valueBytes.begin(), valueBytes.end()),
199 };
200 }
201
202 services::BuildOutput ParseOutputSpec(const std::wstring& spec)
203 {
204 // Mirrors `docker buildx build --output`. A bare token is shorthand for a destination; otherwise
205 // the spec is a single CSV record of key=value pairs where 'type'/'dest' are structural and every
206 // other key is forwarded verbatim to buildx as an exporter attribute.
207 if (spec.empty())
208 {
209 throw ArgumentException(Localization::MessageWslcOutputInvalidSpec(spec, L"the value may not be empty"));
210 }
211
212 // buildx parses the spec as one CSV record (go-csvvalue / encoding/csv): fields are comma
213 // separated, a field may be double-quoted, "" inside a quoted field is a literal quote, and a
214 // comma inside quotes is part of the value. This lets a value such as an annotation contain commas.
215 const auto fields = SplitCsvFields(spec);
216 if (!fields.has_value())
217 {
218 throw ArgumentException(Localization::MessageWslcOutputInvalidSpec(spec, L"malformed quoting"));
219 }
220
221 // Keys are ASCII and matched case-insensitively, and buildx TrimSpace's each key so " dest=x" after
222 // a comma is accepted; values are left untouched. AsciiToLower/TrimAscii live in stringshared.h.
223
224 // Shorthand: a single field that is exactly the input and does not start with "type=" names the
225 // destination. Matching Docker, '-' streams a tarball to stdout ('type=tar,dest=-'); anything else
226 // is the local (directory) exporter ('type=local,dest=<path>'), which is not supported.
227 if (fields->size() == 1 && fields->front() == spec && spec.compare(0, 5, L"type=") != 0)
228 {
229 if (fields->front() == L"-")
230 {
231 return services::BuildOutput{.Type = L"tar", .Dest = L"-"};
232 }
233
234 throw ArgumentException(Localization::MessageWslcOutputInvalidSpec(
235 spec,
236 L"directory exporters are not supported; write a single file with 'dest=<file>' (type=tar/oci/docker) "
237 L"or stream a tarball to stdout with 'type=tar,dest=-'"));
238 }
239
240 services::BuildOutput output;
241 std::wstring rawType;
242 bool hasType = false;
243
244 for (const auto& field : *fields)
245 {
246 // buildx splits each field on the FIRST '=' and requires two parts; the value is not trimmed.
247 const auto pos = field.find(L'=');
248 if (pos == std::wstring::npos)
249 {
250 throw ArgumentException(
251 Localization::MessageWslcOutputInvalidSpec(spec, L"expected key=value pairs separated by ','"));
252 }
253
254 const auto key = AsciiToLower(TrimAscii(std::wstring_view(field).substr(0, pos)));
255 auto value = field.substr(pos + 1);
256 if (key.empty())
257 {
258 throw ArgumentException(
259 Localization::MessageWslcOutputInvalidSpec(spec, L"expected key=value pairs separated by ','"));
260 }
261
262 if (key == L"type")
263 {
264 rawType = value;
265 output.Type = AsciiToLower(std::wstring_view(value));
266 hasType = true;
267 }
268 else if (key == L"dest")
269 {
270 output.Dest = std::move(value);
271 }
272 else
273 {
274 // Remaining attributes (name, push, compression, tar, annotations, ...) are matched
275 // case-insensitively by buildx, so their keys are already lowercased above.
276 output.Attributes[key] = std::move(value);
277 }
278 }
279
280 if (!hasType || output.Type.empty())
281 {
282 throw ArgumentException(Localization::MessageWslcOutputInvalidSpec(spec, L"type is required"));
283 }
284
285 // buildx forwards the type to buildkit and only rejects it there; we route the exporter ourselves,
286 // so an unroutable type has to be rejected up front. Every real exporter is in this list.
287 const bool supportedType = output.Type == L"local" || output.Type == L"tar" || output.Type == L"oci" ||
288 output.Type == L"docker" || output.Type == L"image" || output.Type == L"registry" ||
289 output.Type == L"cacheonly";
290 if (!supportedType)
291 {
292 throw ArgumentException(Localization::MessageWslcOutputInvalidSpec(spec, std::format(L"unsupported output type '{}'", rawType)));
293 }
294
295 // The 'tar' attribute selects a single tarball vs. an OCI layout directory for oci/docker, and it
296 // drives file-vs-directory routing here. buildx parses it with Go's ParseBool and errors on
297 // anything else, so reject an invalid value up front rather than failing confusingly in the VM.
298 if (output.Type == L"oci" || output.Type == L"docker")
299 {
300 const auto tarIt = output.Attributes.find(L"tar");
301 if (tarIt != output.Attributes.end() && !ParseBool(tarIt->second.c_str(), true).has_value())
302 {
303 throw ArgumentException(
304 Localization::MessageWslcOutputInvalidSpec(spec, std::format(L"invalid boolean value '{}' for 'tar'", tarIt->second)));
305 }
306 }
307
308 // Destination resolution, mirroring `docker buildx build --output`:
309 if (OutputIsDirectory(output))
310 {
311 // Directory exporters (local, or oci/docker with tar=false) write a Linux directory tree, which
312 // cannot be materialized faithfully on a Windows destination, so they are not supported. Point
313 // users at the single-file exporters instead.
314 throw ArgumentException(Localization::MessageWslcOutputInvalidSpec(
315 spec,
316 L"directory exporters are not supported; write a single file with 'dest=<file>' (type=tar/oci/docker) "
317 L"or stream a tarball to stdout with 'type=tar,dest=-'"));
318 }
319
320 if (output.Type == L"tar" || output.Type == L"oci")
321 {
322 // Single-tarball exporters stream to stdout when no destination is given (buildx default).
323 if (output.Dest.empty())
324 {
325 output.Dest = L"-";
326 }
327 }
328 // docker: no dest -> load into the VM image store (leave empty); dest='-' streams a tarball to
329 // stdout; a path writes a file. image/registry/cacheonly run in the VM and ignore 'dest'.
330
331 return output;
332 }
333
334 bool OutputStreamsToClient(const services::BuildOutput& output)
335 {
336 if (output.Type == L"local" || output.Type == L"tar" || output.Type == L"oci")
337 {
338 return true;
339 }
340
341 if (output.Type == L"docker")
342 {
343 // An omitted destination loads the image into the store in the VM; any destination (a file or
344 // stdout '-') is produced in the VM and streamed back to the client.
345 return !output.Dest.empty();
346 }
347
348 // image / registry / cacheonly run entirely in the build VM.
349 return false;
350 }
351
352 bool OutputIsDirectory(const services::BuildOutput& output)
353 {
354 if (output.Type == L"local")
355 {
356 return true;
357 }
358
359 if (output.Type == L"oci" || output.Type == L"docker")
360 {
361 // oci/docker default to a single tarball but export an OCI layout directory when tar is false.
362 const auto it = output.Attributes.find(L"tar");
363 if (it != output.Attributes.end())
364 {
365 const auto tar = ParseBool(it->second.c_str(), true);
366 return tar.has_value() && !tar.value();
367 }
368 }
369
370 return false;
371 }
372
373 std::wstring FormatOutputSpec(const services::BuildOutput& output)
374 {
375 // buildx consumes the same CSV key=value form we parsed, so we round-trip the parsed struct back
376 // into a canonical spec. This is what actually reaches `docker build --output <spec>`. Each token
377 // is CSV-escaped so a value containing a comma or quote survives the trip.
378 std::vector<std::wstring> fields;
379 fields.push_back(std::format(L"type={}", output.Type));
380 if (!output.Dest.empty())
381 {
382 fields.push_back(std::format(L"dest={}", output.Dest));
383 }
384
385 for (const auto& [key, value] : output.Attributes)
386 {
387 fields.push_back(std::format(L"{}={}", key, value));
388 }
389
390 return JoinCsvFields(fields);
391 }
392
393 std::tuple<std::string, int64_t, int64_t> ParseUlimit(const std::wstring& input, const std::wstring& argName)
394 {
395 // Accepts <name>=<soft>[:<hard>]; if hard is omitted hard = soft. -1 means unlimited.
396 const auto nameValue = SplitKeyValue(input);
397 if (!nameValue.HadSeparator || nameValue.Key.empty())
398 {
399 throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
400 }
401
402 const std::wstring& valuesPart = nameValue.Value;
403 const auto colonPos = valuesPart.find(L':');
404
405 auto parseLimit = [&](const std::wstring& limitStr) -> int64_t {
406 if (limitStr.empty())
407 {
408 throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
409 }
410
411 try
412 {
413 return GetIntegerFromString<int64_t>(limitStr, argName, [](int64_t v) { return v >= -1; });
414 }
415 catch (const ArgumentException&)
416 {
417 // Re-throw with the ulimit-specific error message so the user sees the full input.
418 throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
419 }
420 };
421
422 const int64_t soft = parseLimit(colonPos == std::wstring::npos ? valuesPart : valuesPart.substr(0, colonPos));
423 const int64_t hard = colonPos == std::wstring::npos ? soft : parseLimit(valuesPart.substr(colonPos + 1));
424
425 // This rejects "-1:1024" and "-1:<finite>" while allowing "<finite>:-1", "-1:-1", and "-1".
426 const bool invalidRange = (soft == -1) ? (hard != -1) : (hard != -1 && hard < soft);
427 if (invalidRange)
428 {
429 throw ArgumentException(Localization::WSLCCLI_InvalidUlimitError(argName, input));
430 }
431
432 return {WideToMultiByte(nameValue.Key), soft, hard};
433 }
434
435 std::pair<std::string, std::string> ParseLabel(const std::wstring& value)
436 {
437 const auto kv = SplitKeyValue(value);
438 auto key = WideToMultiByte(kv.Key);
439 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::WSLCCLI_LabelKeyEmptyError(), key.empty());
440 return {std::move(key), WideToMultiByte(kv.Value)};
441 }
442
443 std::pair<std::string, std::string> ParseDriverOption(const std::wstring& value)
444 {
445 const auto kv = SplitKeyValue(value);
446 return {WideToMultiByte(kv.Key), WideToMultiByte(kv.Value)};
447 }
448
449 std::pair<std::string, std::string> ParseFilter(const std::wstring& value)
450 {
451 const auto kv = SplitKeyValue(value);
452 if (!kv.HadSeparator)
453 {
454 throw ArgumentException(Localization::WSLCCLI_InvalidFilterError(value));
455 }
456
457 return {WideToMultiByte(kv.Key), WideToMultiByte(kv.Value)};
458 }
459
460 ParsedNetworkArgument ParseNetworkArgument(std::wstring_view value, const std::wstring& argName)
461 {
462 ParsedNetworkArgument result;
463
464 auto parseOptions = [&](std::wstring_view options, bool requireName) {
465 bool parsedName = false;
466 for (const auto part : SplitPreserveEmpty(options, L','))
467 {
468 const auto separator = part.find(L'=');
469 if (separator == std::wstring_view::npos || separator == 0)
470 {
471 throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(argName, std::wstring{part}));
472 }
473
474 const auto key = part.substr(0, separator);
475 const auto optionValue = part.substr(separator + 1);
476 if (key == L"name")
477 {
478 if (IsEmptyOrWhitespace(optionValue))
479 {
480 throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName));
481 }
482
483 if (parsedName)
484 {
485 throw ArgumentException(Localization::WSLCCLI_NetworkDuplicateNameError(argName));
486 }
487
488 parsedName = true;
489 result.Name = WideToMultiByte(std::wstring{optionValue});
490 }
491 else if (key == L"alias")
492 {
493 if (IsEmptyOrWhitespace(optionValue))
494 {
495 throw ArgumentException(Localization::WSLCCLI_NetworkAliasEmptyError(argName));
496 }
497
498 result.Aliases.emplace_back(WideToMultiByte(std::wstring{optionValue}));
499 }
500 else
501 {
502 throw ArgumentException(Localization::WSLCCLI_NetworkUnsupportedOptionError(argName, std::wstring{key}));
503 }
504 }
505
506 if (requireName && !parsedName)
507 {
508 throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName));
509 }
510 };
511
512 if (value.find(L'=') != std::wstring_view::npos)
513 {
514 parseOptions(value, true);
515 }
516 else
517 {
518 if (IsEmptyOrWhitespace(value))
519 {
520 throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName));
521 }
522
523 result.Name = WideToMultiByte(std::wstring{value});
524 }
525
526 if (result.Name.empty())
527 {
528 throw ArgumentException(Localization::WSLCCLI_NetworkEmptyError(argName));
529 }
530
531 return result;
532 }
533
534 // Map of signal names to WSLCSignal enum values
535 static const std::unordered_map<std::wstring, WSLCSignal> SignalMap = {
536 {L"SIGHUP", WSLCSignalSIGHUP}, {L"SIGINT", WSLCSignalSIGINT}, {L"SIGQUIT", WSLCSignalSIGQUIT},
537 {L"SIGILL", WSLCSignalSIGILL}, {L"SIGTRAP", WSLCSignalSIGTRAP}, {L"SIGABRT", WSLCSignalSIGABRT},
538 {L"SIGIOT", WSLCSignalSIGIOT}, {L"SIGBUS", WSLCSignalSIGBUS}, {L"SIGFPE", WSLCSignalSIGFPE},
539 {L"SIGKILL", WSLCSignalSIGKILL}, {L"SIGUSR1", WSLCSignalSIGUSR1}, {L"SIGSEGV", WSLCSignalSIGSEGV},
540 {L"SIGUSR2", WSLCSignalSIGUSR2}, {L"SIGPIPE", WSLCSignalSIGPIPE}, {L"SIGALRM", WSLCSignalSIGALRM},
541 {L"SIGTERM", WSLCSignalSIGTERM}, {L"SIGTKFLT", WSLCSignalSIGTKFLT}, {L"SIGCHLD", WSLCSignalSIGCHLD},
542 {L"SIGCONT", WSLCSignalSIGCONT}, {L"SIGSTOP", WSLCSignalSIGSTOP}, {L"SIGTSTP", WSLCSignalSIGTSTP},
543 {L"SIGTTIN", WSLCSignalSIGTTIN}, {L"SIGTTOU", WSLCSignalSIGTTOU}, {L"SIGURG", WSLCSignalSIGURG},
544 {L"SIGXCPU", WSLCSignalSIGXCPU}, {L"SIGXFSZ", WSLCSignalSIGXFSZ}, {L"SIGVTALRM", WSLCSignalSIGVTALRM},
545 {L"SIGPROF", WSLCSignalSIGPROF}, {L"SIGWINCH", WSLCSignalSIGWINCH}, {L"SIGIO", WSLCSignalSIGIO},
546 {L"SIGPOLL", WSLCSignalSIGPOLL}, {L"SIGPWR", WSLCSignalSIGPWR}, {L"SIGSYS", WSLCSignalSIGSYS},
547 };
548
549 // Convert string to WSLCSignal enum - accepts either signal name (e.g., "SIGKILL") or number (e.g., "9")
550 WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring& argName)
551 {
552 constexpr int MIN_SIGNAL = WSLCSignalSIGHUP;
553 constexpr int MAX_SIGNAL = WSLCSignalSIGSYS;
554 constexpr std::wstring_view sigPrefix = L"SIG";
555
556 // Normalize input: ensure it has "SIG" prefix for map lookup
557 std::wstring normalizedInput;
558 if (IsEqual(input.substr(0, sigPrefix.size()), sigPrefix, true))
559 {
560 normalizedInput = input;
561 }
562 else
563 {
564 normalizedInput = std::wstring(sigPrefix) + input;
565 }
566
567 for (const auto& [signalName, signalValue] : SignalMap)
568 {
569 if (IsEqual(normalizedInput, signalName, true))
570 {
571 return signalValue;
572 }
573 }
574
575 // User may have input an integer representation instead.
576 int signalValue{};
577 try
578 {
579 signalValue = GetIntegerFromString<int>(input, argName);
580 }
581 // If it fails to be converted give a better user message than just the integer conversion
582 // failure since we also know it failed to be found in the map.
583 catch (ArgumentException)
584 {
585 throw ArgumentException(Localization::WSLCCLI_InvalidSignalError(argName, input));
586 }
587
588 if (signalValue < MIN_SIGNAL || signalValue > MAX_SIGNAL)
589 {
590 throw ArgumentException(Localization::WSLCCLI_SignalOutOfRangeError(argName, input, MIN_SIGNAL, MAX_SIGNAL));
591 }
592
593 return static_cast<WSLCSignal>(signalValue);
594 }
595
596 LONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName)
597 {
598 std::string narrowValue = wsl::windows::common::string::WideToMultiByte(value);
599
600 // Try integer (Unix epoch seconds) first
601 LONGLONG intValue{};
602 const char* begin = narrowValue.c_str();
603 const char* end = begin + narrowValue.size();
604 auto result = std::from_chars(begin, end, intValue);
605 if (result.ec == std::errc() && result.ptr == end)
606 {
607 return intValue;
608 }
609
610 if (const auto duration = wsl::windows::common::timestamp::TryParseDuration(narrowValue); duration.has_value())
611 {
612 // Apply the duration at full precision and truncate once, so that a sub-second value keeps its sign.
613 const auto target = std::chrono::system_clock::now() - duration.value();
614
615 return std::chrono::floor<std::chrono::seconds>(target.time_since_epoch()).count();
616 }
617
618 try
619 {
620 return wsl::windows::common::timestamp::Rfc3339ToEpoch(wsl::windows::common::timestamp::ExpandToRfc3339(narrowValue));
621 }
622 // Name the offending argument rather than surfacing the raw parse failure.
623 catch (...)
624 {
625 throw ArgumentException(Localization::WSLCCLI_InvalidTimestampArgumentError(argName, value));
626 }
627 }
628
629 models::FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring& argName)
630 {
631 // Single source of truth for the accepted format values. It drives both parsing and the error
632 // message's supported-values list, so adding a type here updates both automatically.
633 static constexpr std::pair<std::wstring_view, models::FormatType> c_formatTypes[] = {
634 {L"json", models::FormatType::Json},
635 {L"table", models::FormatType::Table},
636 };
637
638 for (const auto& [name, type] : c_formatTypes)
639 {
640 if (IsEqual(input, name))
641 {
642 return type;
643 }
644 }
645
646 std::wstring supportedValues;
647 for (const auto& formatType : c_formatTypes)
648 {
649 if (!supportedValues.empty())
650 {
651 supportedValues += L", ";
652 }
653
654 supportedValues += formatType.first;
655 }
656
657 throw ArgumentException(Localization::WSLCCLI_InvalidFormatValueError(argName, input, supportedValues));
658 }
659
660 int GetInspectJsonIndentFromString(const std::wstring& input, const std::wstring& argName)
661 {
662 if (!IsEqual(input, L"json"))
663 {
664 constexpr std::wstring_view supportedValues = L"json";
665 throw ArgumentException(Localization::WSLCCLI_InvalidFormatValueError(argName, input, supportedValues));
666 }
667
668 return wsl::shared::c_jsonCompactIndent;
669 }
670
671 models::PullPolicy GetPullPolicyFromString(const std::wstring& input, const std::wstring& argName)
672 {
673 static constexpr std::pair<std::wstring_view, models::PullPolicy> c_pullPolicies[] = {
674 {L"always", models::PullPolicy::Always},
675 {L"missing", models::PullPolicy::Missing},
676 {L"never", models::PullPolicy::Never},
677 };
678
679 for (const auto& [name, policy] : c_pullPolicies)
680 {
681 if (IsEqual(input, name))
682 {
683 return policy;
684 }
685 }
686
687 std::wstring supportedValues;
688 for (const auto& pullPolicy : c_pullPolicies)
689 {
690 if (!supportedValues.empty())
691 {
692 supportedValues += L", ";
693 }
694
695 supportedValues += pullPolicy.first;
696 }
697
698 throw ArgumentException(Localization::WSLCCLI_InvalidPullPolicyError(argName, input, supportedValues));
699 }
700
701 models::ProgressMode GetProgressModeFromString(const std::wstring& input, const std::wstring& argName)
702 {
703 static constexpr std::pair<std::wstring_view, models::ProgressMode> c_progressModes[] = {
704 {L"auto", models::ProgressMode::Auto},
705 {L"tty", models::ProgressMode::Tty},
706 {L"plain", models::ProgressMode::Plain},
707 {L"quiet", models::ProgressMode::Quiet},
708 };
709
710 for (const auto& [name, mode] : c_progressModes)
711 {
712 if (IsEqual(input, name))
713 {
714 return mode;
715 }
716 }
717
718 std::wstring supportedValues;
719 for (const auto& progressMode : c_progressModes)
720 {
721 if (!supportedValues.empty())
722 {
723 supportedValues += L", ";
724 }
725
726 supportedValues += progressMode.first;
727 }
728
729 throw ArgumentException(Localization::WSLCCLI_InvalidProgressTypeError(argName, input, supportedValues));
730 }
731
732 models::InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName)
733 {
734 if (IsEqual(input, L"image"))
735 {
736 return models::InspectType::Image;
737 }
738 else if (IsEqual(input, L"container"))
739 {
740 return models::InspectType::Container;
741 }
742 else if (IsEqual(input, L"network"))
743 {
744 return models::InspectType::Network;
745 }
746 else if (IsEqual(input, L"volume"))
747 {
748 return models::InspectType::Volume;
749 }
750 else
751 {
752 constexpr std::wstring_view supportedValues = L"image, container, network, volume";
753 throw ArgumentException(Localization::WSLCCLI_InvalidInspectError(argName, input, supportedValues));
754 }
755 }
756
757 int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& argName)
758 {
759 const auto bytes =
760 wsl::windows::common::string::ParseStorageSize(std::wstring_view{input}, wsl::windows::common::string::StorageSizeUnit::Binary);
761 if (!bytes.has_value() || bytes.value() > static_cast<uint64_t>(std::numeric_limits<int64_t>::max()))
762 {
763 throw ArgumentException(Localization::WSLCCLI_InvalidMemorySizeError(argName, input));
764 }
765
766 return static_cast<int64_t>(bytes.value());
767 }
768
769 int64_t GetDurationNanosFromString(const std::wstring& input, const std::wstring& argName)
770 {
771 const std::string narrow = WideToMultiByte(input);
772 const auto parsed = wsl::windows::common::timestamp::TryParseDuration(narrow);
773
774 if (!parsed.has_value() || parsed.value() < std::chrono::nanoseconds::zero())
775 {
776 throw ArgumentException(Localization::WSLCCLI_InvalidDurationError(argName, input));
777 }
778
779 return parsed.value().count();
780 }
781
782 int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName)
783 {
784 constexpr double NanosPerCpu = 1'000'000'000.0;
785 constexpr double MaxCpus = static_cast<double>(std::numeric_limits<int64_t>::max()) / NanosPerCpu;
786
787 const std::string narrow = WideToMultiByte(input);
788 const char* begin = narrow.c_str();
789 const char* end = begin + narrow.size();
790
791 double cpus{};
792 const auto result = std::from_chars(begin, end, cpus, std::chars_format::fixed);
793 if (result.ec != std::errc() || result.ptr != end || cpus <= 0.0 || cpus > MaxCpus)
794 {
795 throw ArgumentException(Localization::WSLCCLI_InvalidCpusError(argName, input));
796 }
797
798 return static_cast<int64_t>(cpus * NanosPerCpu);
799 }
800
801 } // namespace wsl::windows::wslc::validation