CLI: Fix shm-size and all text-to-byte size conversion and align them all to Docker (#41360)

David Bennett committed Aug 17, 2026 at 14:16 UTC 1ef0817a0465b9ad7458f155bec0a2c877462e6d
18 files changed +516 -105
src/shared/inc/stringshared.h
-24
@@ -1053,30 +1053,6 @@ struct CaseInsensitiveCompare
1053 }
1054 };
1055
1056 -inline std::wstring FormatBytes(uint64_t bytes)
1057 -{
1058 - constexpr double c_kB = 1000.0;
1059 - constexpr double c_MB = 1000.0 * 1000.0;
1060 - constexpr double c_GB = 1000.0 * 1000.0 * 1000.0;
1061 -
1062 - if (bytes >= static_cast<uint64_t>(c_GB))
1063 - {
1064 - return std::format(L"{:.2f} GB", bytes / c_GB);
1065 - }
1066 - else if (bytes >= static_cast<uint64_t>(c_MB))
1067 - {
1068 - return std::format(L"{:.2f} MB", bytes / c_MB);
1069 - }
1070 - else if (bytes >= static_cast<uint64_t>(c_kB))
1071 - {
1072 - return std::format(L"{:.2f} KB", bytes / c_kB);
1073 - }
1074 - else
1075 - {
1076 - return std::format(L"{} B", bytes);
1077 - }
1078 -}
1079 -
1056 template <typename TChar>
1057 inline std::basic_string<TChar> Trim(const std::basic_string<TChar>& input)
1058 {
src/windows/common/WSLCUserSettings.cpp
+11 -3
@@ -72,9 +72,17 @@ namespace details {
72
73 std::optional<uint32_t> ParseSettingsMemoryValue(const std::string& value)
74 {
75 - auto parsed = wsl::shared::string::ParseMemorySize(value.c_str());
76 - auto converted = parsed.has_value() ? *parsed / _1MB : 0; // To Mb, and anything less than 1Mb is considered invalid.
77 - return converted > 0 ? std::optional{static_cast<uint32_t>(converted)} : std::nullopt;
75 +
76 + // WSLC settings accept leading whitespace for compatibility with existing settings files.
77 + const auto wideValue = MultiByteToWide(value);
78 + const auto parsed = ParseStorageSize(StripLeadingWhitespace(wideValue), StorageSizeUnit::Binary);
79 + const auto converted = parsed.has_value() ? *parsed / _1MB : 0;
80 + if (converted == 0 || converted > std::numeric_limits<uint32_t>::max())
81 + {
82 + return std::nullopt;
83 + }
84 +
85 + return static_cast<uint32_t>(converted);
86 }
87
88 #define WSLC_VALIDATE_SETTING(_setting_) \
src/windows/common/string.cpp
+114
@@ -13,6 +13,9 @@ Abstract:
13 --*/
14
15 #include "precomp.h"
16 +#include <charconv>
17 +#include <cmath>
18 +#include <limits>
19
20 std::vector<std::string> wsl::windows::common::string::InitializeStringSet(_In_count_(BufferSize) LPCSTR Buffer, _In_ SIZE_T BufferSize)
21 {
@@ -286,6 +289,117 @@ std::string wsl::windows::common::string::WideToMultiByte(_In_ std::wstring_view
289 return WideToMultiByte(Source.data(), Source.length());
290 }
291
292 +std::optional<uint64_t> wsl::windows::common::string::ParseStorageSize(std::wstring_view String, StorageSizeUnit Unit)
293 +{
294 + std::wstring_view number;
295 + std::wstring_view suffix;
296 + const auto space = String.find(L' ');
297 + if (space != std::wstring_view::npos)
298 + {
299 + number = String.substr(0, space);
300 + suffix = String.substr(space + 1);
301 + }
302 + else
303 + {
304 + const auto numberEnd = String.find_last_of(L"0123456789.");
305 + if (numberEnd == std::wstring_view::npos)
306 + {
307 + return {};
308 + }
309 +
310 + number = String.substr(0, numberEnd + 1);
311 + suffix = String.substr(numberEnd + 1);
312 + }
313 +
314 + auto narrowNumber = WideToMultiByte(number);
315 + if (!narrowNumber.empty() && narrowNumber.front() == '+')
316 + {
317 + narrowNumber.erase(0, 1);
318 + }
319 +
320 + uint64_t multiplier = 1;
321 + if (!suffix.empty())
322 + {
323 + auto normalizedSuffix = wsl::shared::string::AsciiToLower(suffix);
324 + if (normalizedSuffix != L"b")
325 + {
326 + if (normalizedSuffix.size() > 3 || (normalizedSuffix.size() == 2 && normalizedSuffix[1] != L'b') ||
327 + (normalizedSuffix.size() == 3 && normalizedSuffix.substr(1) != L"ib"))
328 + {
329 + return {};
330 + }
331 +
332 + constexpr std::wstring_view c_memoryUnits = L"kmgtp";
333 + const auto unitIndex = c_memoryUnits.find(normalizedSuffix[0]);
334 + if (unitIndex == std::wstring_view::npos)
335 + {
336 + return {};
337 + }
338 +
339 + const uint64_t base = Unit == StorageSizeUnit::Decimal ? 1000 : 1024;
340 + for (size_t index = 0; index <= unitIndex; ++index)
341 + {
342 + multiplier *= base;
343 + }
344 + }
345 + }
346 +
347 + if (!narrowNumber.empty() && narrowNumber.find_first_not_of("0123456789") == std::string::npos)
348 + {
349 + uint64_t value{};
350 + const auto result = std::from_chars(narrowNumber.data(), narrowNumber.data() + narrowNumber.size(), value);
351 + if (result.ec != std::errc() || result.ptr != narrowNumber.data() + narrowNumber.size() ||
352 + value > std::numeric_limits<uint64_t>::max() / multiplier)
353 + {
354 + return {};
355 + }
356 +
357 + return value * multiplier;
358 + }
359 +
360 + // Fractional and exponent forms require floating-point parsing and may lose precision above 2^53.
361 + double value{};
362 + const auto result = std::from_chars(narrowNumber.data(), narrowNumber.data() + narrowNumber.size(), value, std::chars_format::general);
363 + if (result.ec != std::errc() || result.ptr != narrowNumber.data() + narrowNumber.size() || !std::isfinite(value) || value < 0)
364 + {
365 + return {};
366 + }
367 +
368 + const double bytes = value * static_cast<double>(multiplier);
369 + if (!std::isfinite(bytes) || bytes >= static_cast<double>(std::numeric_limits<uint64_t>::max()))
370 + {
371 + return {};
372 + }
373 +
374 + return static_cast<uint64_t>(bytes);
375 +}
376 +
377 +std::wstring wsl::windows::common::string::FormatStorageSize(uint64_t Bytes, StorageSizeUnit Unit, uint32_t DecimalPlaces, bool IncludeSpace)
378 +{
379 + constexpr size_t c_unitCount = 6;
380 + constexpr std::array<std::wstring_view, c_unitCount> c_decimalUnits{L"B", L"KB", L"MB", L"GB", L"TB", L"PB"};
381 + constexpr std::array<std::wstring_view, c_unitCount> c_binaryUnits{L"B", L"KiB", L"MiB", L"GiB", L"TiB", L"PiB"};
382 +
383 + const double base = Unit == StorageSizeUnit::Decimal ? 1000.0 : 1024.0;
384 + const auto& units = Unit == StorageSizeUnit::Decimal ? c_decimalUnits : c_binaryUnits;
385 +
386 + double value = static_cast<double>(Bytes);
387 + size_t unitIndex = 0;
388 + while (value >= base && unitIndex + 1 < c_unitCount)
389 + {
390 + value /= base;
391 + ++unitIndex;
392 + }
393 +
394 + const auto formattedValue = unitIndex == 0 ? std::to_wstring(Bytes) : std::format(L"{:.{}f}", value, DecimalPlaces);
395 + return std::format(L"{}{}{}", formattedValue, IncludeSpace ? L" " : L"", units[unitIndex]);
396 +}
397 +
398 +std::wstring wsl::windows::common::string::FormatBytes(uint64_t Bytes)
399 +{
400 + return FormatStorageSize(Bytes, StorageSizeUnit::Decimal, 2, true);
401 +}
402 +
403 std::wstring wsl::windows::common::string::TruncateId(_In_ std::wstring_view id, bool shortenLength)
404 {
405 return TruncateIdImpl(id, shortenLength);
src/windows/common/string.hpp
+12
@@ -23,6 +23,18 @@ using SOCKADDR_INET = union _SOCKADDR_INET;
23
24 namespace wsl::windows::common::string {
25
26 +enum class StorageSizeUnit
27 +{
28 + Decimal,
29 + Binary
30 +};
31 +
32 +std::optional<uint64_t> ParseStorageSize(std::wstring_view String, StorageSizeUnit Unit);
33 +
34 +std::wstring FormatStorageSize(uint64_t Bytes, StorageSizeUnit Unit, uint32_t DecimalPlaces, bool IncludeSpace = false);
35 +
36 +std::wstring FormatBytes(uint64_t Bytes);
37 +
38 std::vector<std::string> InitializeStringSet(_In_count_(BufferSize) LPCSTR Buffer, _In_ SIZE_T BufferSize);
39
40 bool IsPathComponentEqual(const std::wstring_view String1, const std::wstring_view String2);
src/windows/wslc/arguments/SpecParsing.cpp
+4 -3
@@ -814,13 +814,14 @@ models::InspectType GetInspectTypeFromString(const std::wstring& input, const st
814
815 int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& argName)
816 {
817 - auto parsed = wsl::shared::string::ParseMemorySize(input.c_str());
818 - if (!parsed.has_value())
817 + const auto bytes =
818 + wsl::windows::common::string::ParseStorageSize(std::wstring_view{input}, wsl::windows::common::string::StorageSizeUnit::Binary);
819 + if (!bytes.has_value() || bytes.value() > static_cast<uint64_t>(std::numeric_limits<int64_t>::max()))
820 {
821 throw ArgumentException(Localization::WSLCCLI_InvalidMemorySizeError(argName, input));
822 }
823
823 - return static_cast<int64_t>(parsed.value());
824 + return static_cast<int64_t>(bytes.value());
825 }
826
827 // Parses duration string into nanoseconds.
src/windows/wslc/arguments/SpecParsing.h
+1 -1
@@ -104,7 +104,7 @@ models::ProgressMode GetProgressModeFromString(const std::wstring& input, const
104 // Parses an inspect target ("image"/"container"/"network"/"volume") into an InspectType.
105 models::InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName);
106
107 -// Parses a memory size (e.g. "512m", "1g") into a byte count.
107 +// Parses a Docker-style memory size (e.g. "512m", "1.5g") into a byte count.
108 int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& argName = {});
109
110 // Parses a Go-style duration (e.g. "1.5h", "500ms") into nanoseconds.
src/windows/wslc/services/ImageProgressCallback.cpp
+4 -3
@@ -20,6 +20,7 @@ Abstract:
20 namespace wsl::windows::wslc::services {
21 using namespace wsl::shared;
22 using namespace wsl::windows::common::vt;
23 +using wsl::windows::common::string::FormatBytes;
24
25 auto ImageProgressCallback::MoveToLine(int line)
26 {
@@ -132,18 +133,18 @@ std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id,
133
134 // Docker's reported total is an estimate of the compressed layer size, so the actual bytes
135 // transferred can exceed it. Drop the total in that case to avoid displaying a count over 100%.
135 - auto progress = wsl::shared::string::FormatBytes(current);
136 + auto progress = FormatBytes(current);
137
138 if (current <= total)
139 {
139 - progress += std::format(L"/{}", wsl::shared::string::FormatBytes(total));
140 + progress += std::format(L"/{}", FormatBytes(total));
141 }
142
143 line = std::format(L"{}: {} [{}] {}", safeId, safeStatus, bar, progress);
144 }
145 else if (current != 0)
146 {
146 - line = std::format(L"{}: {} {}", safeId, safeStatus, wsl::shared::string::FormatBytes(current));
147 + line = std::format(L"{}: {} {}", safeId, safeStatus, FormatBytes(current));
148 }
149 else
150 {
src/windows/wslc/tasks/ContainerTasks.cpp
+10 -31
@@ -34,35 +34,12 @@ using namespace wsl::windows::common::wslutil;
34 using namespace wsl::windows::wslc::execution;
35 using namespace wsl::windows::wslc::models;
36 using namespace wsl::windows::wslc::services;
37 +using wsl::windows::common::string::FormatBytes;
38 +using wsl::windows::common::string::FormatStorageSize;
39 +using wsl::windows::common::string::StorageSizeUnit;
40
41 namespace {
42
40 -std::string FormatBytes(uint64_t bytes)
41 -{
42 - constexpr uint64_t c_kib = 1024;
43 - constexpr uint64_t c_mib = 1024 * c_kib;
44 - constexpr uint64_t c_gib = 1024 * c_mib;
45 -
46 - if (bytes >= c_gib)
47 - {
48 - return std::format("{:.2f} GiB", static_cast<double>(bytes) / static_cast<double>(c_gib));
49 - }
50 - else if (bytes >= c_mib)
51 - {
52 - return std::format("{:.2f} MiB", static_cast<double>(bytes) / static_cast<double>(c_mib));
53 - }
54 - else if (bytes >= c_kib)
55 - {
56 - return std::format("{:.2f} KiB", static_cast<double>(bytes) / static_cast<double>(c_kib));
57 - }
58 - else
59 - {
60 - // Bytes are always whole numbers, so decimal places are intentionally omitted here.
61 - // This matches the behaviour of `docker stats`.
62 - return std::format("{} B", bytes);
63 - }
64 -}
65 -
43 nlohmann::json ComputeContainerStatsJson(const wsl::windows::common::docker_schema::ContainerStats& stats)
44 {
45 // Calculate CPU %
@@ -120,15 +97,18 @@ nlohmann::json ComputeContainerStatsJson(const wsl::windows::common::docker_sche
97 }
98
99 const auto& containerName = stats.name.empty() ? stats.id : stats.name;
100 + const auto formatBinaryBytes = [](uint64_t bytes) {
101 + return WideToMultiByte(FormatStorageSize(bytes, StorageSizeUnit::Binary, 2, true));
102 + };
103
104 return {
105 {"ID", stats.id},
106 {"Name", containerName},
107 {"CPUPerc", std::format("{:.2f}%", cpuPercent)},
128 - {"MemUsage", std::format("{} / {}", FormatBytes(stats.memory_stats.usage), FormatBytes(stats.memory_stats.limit))},
108 + {"MemUsage", std::format("{} / {}", formatBinaryBytes(stats.memory_stats.usage), formatBinaryBytes(stats.memory_stats.limit))},
109 {"MemPerc", std::format("{:.2f}%", memPercent)},
130 - {"NetIO", std::format("{} / {}", FormatBytes(netRxBytes), FormatBytes(netTxBytes))},
131 - {"BlockIO", std::format("{} / {}", FormatBytes(blkReadBytes), FormatBytes(blkWriteBytes))},
110 + {"NetIO", std::format("{} / {}", formatBinaryBytes(netRxBytes), formatBinaryBytes(netTxBytes))},
111 + {"BlockIO", std::format("{} / {}", formatBinaryBytes(blkReadBytes), formatBinaryBytes(blkWriteBytes))},
112 {"PIDs", stats.pids_stats.current},
113 };
114 }
@@ -1082,7 +1062,6 @@ void PruneContainers(CLIExecutionContext& context)
1062 }
1063
1064 context.Terminal.Output(L"\n");
1085 - context.Terminal.Output(
1086 - L"{}\n", Localization::WSLCCLI_ContainerPruneSpaceReclaimedBytes(wsl::shared::string::FormatBytes(result.SpaceReclaimed)));
1065 + context.Terminal.Output(L"{}\n", Localization::WSLCCLI_ContainerPruneSpaceReclaimedBytes(FormatBytes(result.SpaceReclaimed)));
1066 }
1067 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/ImageTasks.cpp
+2 -2
@@ -32,6 +32,7 @@ using namespace wsl::windows::common::wslutil;
32 using namespace wsl::windows::wslc::execution;
33 using namespace wsl::windows::wslc::models;
34 using namespace wsl::windows::wslc::services;
35 +using wsl::windows::common::string::FormatBytes;
36
37 namespace wsl::windows::wslc::task {
38
@@ -405,7 +406,6 @@ void PruneImages(CLIExecutionContext& context)
406 }
407
408 context.Terminal.Output(L"\n");
408 - context.Terminal.Output(
409 - L"{}\n", Localization::WSLCCLI_ImagePruneSpaceReclaimedBytes(wsl::shared::string::FormatBytes(result.SpaceReclaimed)));
409 + context.Terminal.Output(L"{}\n", Localization::WSLCCLI_ImagePruneSpaceReclaimedBytes(FormatBytes(result.SpaceReclaimed)));
410 }
411 } // namespace wsl::windows::wslc::task
src/windows/wslc/tasks/VolumeTasks.cpp
+2 -1
@@ -27,6 +27,7 @@ using namespace wsl::windows::common::wslutil;
27 using namespace wsl::windows::wslc::execution;
28 using namespace wsl::windows::wslc::models;
29 using namespace wsl::windows::wslc::services;
30 +using wsl::windows::common::string::FormatBytes;
31
32 namespace wsl::windows::wslc::task {
33
@@ -215,6 +216,6 @@ void PruneVolumes(CLIExecutionContext& context)
216 }
217
218 context.Terminal.Output(L"\n");
218 - context.Terminal.Output(L"{}\n", Localization::WSLCCLI_VolumePruneSpaceReclaimed(wsl::shared::string::FormatBytes(result.SpaceReclaimed)));
219 + context.Terminal.Output(L"{}\n", Localization::WSLCCLI_VolumePruneSpaceReclaimed(FormatBytes(result.SpaceReclaimed)));
220 }
221 } // namespace wsl::windows::wslc::task
src/windows/wslcsession/WSLCSession.cpp
+3 -2
@@ -30,6 +30,7 @@ using io::MultiHandleWait;
30 using io::OverlappedIOHandle;
31 using io::WriteHandle;
32 using wsl::shared::Localization;
33 +using wsl::windows::common::string::FormatBytes;
34 using wsl::windows::service::wslc::UserCOMCallback;
35 using wsl::windows::service::wslc::UserHandle;
36 using wsl::windows::service::wslc::WSLCExecutionContext;
@@ -1431,8 +1432,8 @@ try
1432 {
1433 auto currentBytes = static_cast<ULONGLONG>(std::max<int64_t>(entry.current, 0));
1434 auto totalBytes = static_cast<ULONGLONG>(std::max<int64_t>(entry.total, 0));
1434 - auto current = wsl::shared::string::FormatBytes(currentBytes);
1435 - auto total = wsl::shared::string::FormatBytes(totalBytes);
1435 + auto current = FormatBytes(currentBytes);
1436 + auto total = FormatBytes(totalBytes);
1437 reportProgress(std::format("{}{} {} / {}", logPrefix(it->second), entry.id, current, total), entry.id.c_str(), currentBytes, totalBytes);
1438 }
1439 else if (reportedSteps.insert(entry.id).second)
test/windows/CMakeLists.txt
+1
@@ -6,6 +6,7 @@ set(SOURCES
6 Plan9Tests.cpp
7 DrvFsTests.cpp
8 FilesystemUnitTests.cpp
9 + StringUnitTests.cpp
10 Common.cpp
11 PluginTests.cpp
12 PolicyTests.cpp
test/windows/SimpleTests.cpp
-27
@@ -223,33 +223,6 @@ class SimpleTests
223 VERIFY_ARE_EQUAL(expected, wsl::shared::string::ParseBool(wideString.c_str(), true));
224 }
225
226 - // Test wsl::shared::string::ParseMemoryString
227 - const std::vector<std::pair<LPCSTR, std::optional<uint64_t>>> testCases{
228 - {"0", 0},
229 - {"1", 1},
230 - {" 1", 1},
231 - {"1B", 1},
232 - {"1K", 1024},
233 - {"1KB", 1024},
234 - {"2M", 2 * 1024 * 1024},
235 - {"100MB", 100 * 1024 * 1024},
236 - {"9G", 9 * 1024ULL * 1024ULL * 1024ULL},
237 - {"44GB", 44 * 1024ULL * 1024ULL * 1024ULL},
238 - {"1TB", 1ULL << 40},
239 - {"2T", 2ULL << 40},
240 - {"1 B", std::nullopt},
241 - {nullptr, std::nullopt},
242 - {"", std::nullopt},
243 - {"foo", std::nullopt}};
244 -
245 - for (const auto& [input, expected] : testCases)
246 - {
247 - VERIFY_ARE_EQUAL(wsl::shared::string::ParseMemorySize(input), expected);
248 -
249 - const auto wideInput = wsl::shared::string::MultiByteToWide(input);
250 - VERIFY_ARE_EQUAL(wsl::shared::string::ParseMemorySize(wideInput.c_str()), expected);
251 - }
252 -
226 // Test wsl::shared::string GUID helpers
227 const GUID guid = {0x1234567a, 0x1234, 0x5678, {0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78}};
228 const std::string guidString = "{1234567a-1234-5678-1234-567812345678}";
test/windows/StringUnitTests.cpp new
+283
@@ -0,0 +1,283 @@
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#include "precomp.h"
4 +#include "Common.h"
5 +#include "string.hpp"
6 +
7 +using wsl::windows::common::string::FormatBytes;
8 +using wsl::windows::common::string::FormatStorageSize;
9 +using wsl::windows::common::string::ParseStorageSize;
10 +using wsl::windows::common::string::StorageSizeUnit;
11 +
12 +namespace {
13 +
14 +struct StorageSizeFormatCase
15 +{
16 + uint64_t Bytes;
17 + StorageSizeUnit Unit;
18 + uint32_t DecimalPlaces;
19 + bool IncludeSpace;
20 + std::wstring Expected;
21 +};
22 +
23 +struct StorageSizeTextRoundTripCase
24 +{
25 + std::wstring Text;
26 + StorageSizeUnit Unit;
27 + uint32_t DecimalPlaces;
28 + bool IncludeSpace;
29 +};
30 +
31 +void VerifyDockerStorageSize(const std::string& Input, StorageSizeUnit Unit, std::optional<uint64_t> Expected)
32 +{
33 + const auto wideInput = wsl::shared::string::MultiByteToWide(Input);
34 + VERIFY_ARE_EQUAL(Expected, ParseStorageSize(wideInput, Unit));
35 +}
36 +
37 +std::vector<std::string> DockerSuffixes(char Unit)
38 +{
39 + const auto UpperUnit = static_cast<char>(std::toupper(static_cast<unsigned char>(Unit)));
40 + return {
41 + {Unit},
42 + {UpperUnit},
43 + {Unit, 'b'},
44 + {Unit, 'B'},
45 + {UpperUnit, 'b'},
46 + {UpperUnit, 'B'},
47 + {Unit, 'i', 'b'},
48 + {Unit, 'i', 'B'},
49 + {Unit, 'I', 'b'},
50 + {Unit, 'I', 'B'},
51 + {UpperUnit, 'i', 'b'},
52 + {UpperUnit, 'i', 'B'},
53 + {UpperUnit, 'I', 'b'},
54 + {UpperUnit, 'I', 'B'},
55 + };
56 +}
57 +
58 +void VerifyDockerStorageUnits(StorageSizeUnit Unit, uint64_t Base)
59 +{
60 + uint64_t Factor = Base;
61 + for (const auto UnitName : {'k', 'm', 'g', 't', 'p'})
62 + {
63 + for (const auto& Suffix : DockerSuffixes(UnitName))
64 + {
65 + VerifyDockerStorageSize("32" + Suffix, Unit, 32 * Factor);
66 + }
67 +
68 + Factor *= Base;
69 + }
70 +}
71 +
72 +} // namespace
73 +
74 +namespace StringUnitTests {
75 +class StringUnitTests
76 +{
77 + WSL_TEST_CLASS(StringUnitTests)
78 +
79 + TEST_METHOD(ParseMemorySize_LegacyForms)
80 + {
81 + const std::vector<std::pair<LPCSTR, std::optional<uint64_t>>> TestCases{
82 + {"0", 0},
83 + {"1", 1},
84 + {" 1", 1},
85 + {"1B", 1},
86 + {"1K", 1024},
87 + {"1KB", 1024},
88 + {"2M", 2 * 1024 * 1024},
89 + {"100MB", 100 * 1024 * 1024},
90 + {"9G", 9 * 1024ULL * 1024ULL * 1024ULL},
91 + {"44GB", 44 * 1024ULL * 1024ULL * 1024ULL},
92 + {"1TB", 1ULL << 40},
93 + {"2T", 2ULL << 40},
94 + {"1 B", std::nullopt},
95 + {nullptr, std::nullopt},
96 + {"", std::nullopt},
97 + {"foo", std::nullopt}};
98 +
99 + for (const auto& [Input, Expected] : TestCases)
100 + {
101 + VERIFY_ARE_EQUAL(Expected, wsl::shared::string::ParseMemorySize(Input));
102 +
103 + const auto wideInput = wsl::shared::string::MultiByteToWide(Input);
104 + VERIFY_ARE_EQUAL(Expected, wsl::shared::string::ParseMemorySize(wideInput.c_str()));
105 + }
106 + }
107 +
108 + TEST_METHOD(ParseStorageSize_DockerDecimalUnits)
109 + {
110 + VerifyDockerStorageUnits(StorageSizeUnit::Decimal, 1000);
111 + }
112 +
113 + TEST_METHOD(ParseStorageSize_DockerBinaryUnits)
114 + {
115 + VerifyDockerStorageUnits(StorageSizeUnit::Binary, 1024);
116 + }
117 +
118 + TEST_METHOD(ParseStorageSize_DockerNumericForms)
119 + {
120 + for (const auto Unit : {StorageSizeUnit::Decimal, StorageSizeUnit::Binary})
121 + {
122 + VerifyDockerStorageSize("0", Unit, 0);
123 + VerifyDockerStorageSize("0b", Unit, 0);
124 + VerifyDockerStorageSize("0B", Unit, 0);
125 + VerifyDockerStorageSize("0 B", Unit, 0);
126 + VerifyDockerStorageSize("32", Unit, 32);
127 + VerifyDockerStorageSize("32b", Unit, 32);
128 + VerifyDockerStorageSize("32B", Unit, 32);
129 + VerifyDockerStorageSize("32.5 B", Unit, 32);
130 + VerifyDockerStorageSize("0.", Unit, 0);
131 + VerifyDockerStorageSize("0. ", Unit, 0);
132 + VerifyDockerStorageSize("0.b", Unit, 0);
133 + VerifyDockerStorageSize("0.B", Unit, 0);
134 + VerifyDockerStorageSize("-0", Unit, 0);
135 + VerifyDockerStorageSize("-0b", Unit, 0);
136 + VerifyDockerStorageSize("-0B", Unit, 0);
137 + VerifyDockerStorageSize("-0 b", Unit, 0);
138 + VerifyDockerStorageSize("-0 B", Unit, 0);
139 + VerifyDockerStorageSize("+32K", Unit, 32 * (Unit == StorageSizeUnit::Decimal ? 1000 : 1024));
140 + VerifyDockerStorageSize("1e3K", Unit, 1000 * (Unit == StorageSizeUnit::Decimal ? 1000 : 1024));
141 + VerifyDockerStorageSize("32.", Unit, 32);
142 + VerifyDockerStorageSize("32.b", Unit, 32);
143 + VerifyDockerStorageSize("32.B", Unit, 32);
144 + VerifyDockerStorageSize("32. b", Unit, 32);
145 + VerifyDockerStorageSize("32. B", Unit, 32);
146 + VerifyDockerStorageSize("9007199254740991", Unit, 9'007'199'254'740'991);
147 + VerifyDockerStorageSize("9007199254740992", Unit, 9'007'199'254'740'992);
148 + VerifyDockerStorageSize("9007199254740993", Unit, 9'007'199'254'740'993);
149 + VerifyDockerStorageSize("9223372036854775806", Unit, 9'223'372'036'854'775'806);
150 + VerifyDockerStorageSize("9223372036854775807", Unit, 9'223'372'036'854'775'807);
151 + VerifyDockerStorageSize("9223372036854775808", Unit, 9'223'372'036'854'775'808ULL);
152 + VerifyDockerStorageSize("18446744073709551615", Unit, std::numeric_limits<uint64_t>::max());
153 + }
154 +
155 + VerifyDockerStorageSize("32.5kB", StorageSizeUnit::Decimal, 32'500);
156 + VerifyDockerStorageSize("32.5 kB", StorageSizeUnit::Decimal, 32'500);
157 + VerifyDockerStorageSize("0.3 K", StorageSizeUnit::Decimal, 300);
158 + VerifyDockerStorageSize(".3kB", StorageSizeUnit::Decimal, 300);
159 + VerifyDockerStorageSize("32.3 mb", StorageSizeUnit::Binary, 33'869'004);
160 + VerifyDockerStorageSize("0.3MB", StorageSizeUnit::Binary, 314'572);
161 + VerifyDockerStorageSize("18446744073709551K", StorageSizeUnit::Decimal, 18'446'744'073'709'551'000ULL);
162 + VerifyDockerStorageSize("18446744073709552K", StorageSizeUnit::Decimal, std::nullopt);
163 + VerifyDockerStorageSize("18014398509481983K", StorageSizeUnit::Binary, 18'446'744'073'709'550'592ULL);
164 + VerifyDockerStorageSize("18014398509481984K", StorageSizeUnit::Binary, std::nullopt);
165 + }
166 +
167 + TEST_METHOD(ParseStorageSize_DockerInvalidForms)
168 + {
169 + const std::vector<std::string> InvalidSizes{
170 + "", "hello",
171 + ".", ". ",
172 + " ", " ",
173 + " .", " . ",
174 + " 0", " 0b",
175 + " 0B", " 0 B",
176 + "0b ", "0B ",
177 + "0 B ", "-32",
178 + "-32b", "-32B",
179 + "-32 b", "-32 B",
180 + "32b.", "32B.",
181 + "32 b.", "32 B.",
182 + "32 bb", "32 BB",
183 + "32 b b", "32 B B",
184 + "32 b", "32 B",
185 + " 32 ", "32m b",
186 + "32bm", "1E",
187 + "1EB", "1EiB",
188 + "1e309", "18446744073709551616",
189 + };
190 +
191 + for (const auto Unit : {StorageSizeUnit::Decimal, StorageSizeUnit::Binary})
192 + {
193 + for (const auto& Input : InvalidSizes)
194 + {
195 + VerifyDockerStorageSize(Input, Unit, std::nullopt);
196 + }
197 + }
198 + }
199 +
200 + TEST_METHOD(FormatStorageSize_UsesRequestedPrecision)
201 + {
202 + const std::vector<StorageSizeFormatCase> TestCases{
203 + {0, StorageSizeUnit::Decimal, 0, false, L"0B"},
204 + {999, StorageSizeUnit::Decimal, 2, true, L"999 B"},
205 + {1'000, StorageSizeUnit::Decimal, 0, false, L"1KB"},
206 + {119'856'765, StorageSizeUnit::Decimal, 0, false, L"120MB"},
207 + {119'856'765, StorageSizeUnit::Decimal, 1, false, L"119.9MB"},
208 + {119'856'765, StorageSizeUnit::Decimal, 2, false, L"119.86MB"},
209 + {1'000'000'000'000ULL, StorageSizeUnit::Decimal, 2, false, L"1.00TB"},
210 + {1'000'000'000'000'000ULL, StorageSizeUnit::Decimal, 2, false, L"1.00PB"},
211 + {1'000'000'000'000'000'000ULL, StorageSizeUnit::Decimal, 2, false, L"1000.00PB"},
212 + {1'023, StorageSizeUnit::Binary, 2, true, L"1023 B"},
213 + {1'024, StorageSizeUnit::Binary, 0, false, L"1KiB"},
214 + {1'536, StorageSizeUnit::Binary, 1, false, L"1.5KiB"},
215 + {1'610'612'736, StorageSizeUnit::Binary, 0, false, L"2GiB"},
216 + {1'610'612'736, StorageSizeUnit::Binary, 1, false, L"1.5GiB"},
217 + {1ULL << 40, StorageSizeUnit::Binary, 2, false, L"1.00TiB"},
218 + {1ULL << 50, StorageSizeUnit::Binary, 2, false, L"1.00PiB"},
219 + {1ULL << 60, StorageSizeUnit::Binary, 2, false, L"1024.00PiB"},
220 + };
221 +
222 + for (const auto& TestCase : TestCases)
223 + {
224 + VERIFY_ARE_EQUAL(TestCase.Expected, FormatStorageSize(TestCase.Bytes, TestCase.Unit, TestCase.DecimalPlaces, TestCase.IncludeSpace));
225 + }
226 +
227 + VERIFY_ARE_EQUAL(std::wstring{L"119.86 MB"}, FormatBytes(119'856'765));
228 + }
229 +
230 + TEST_METHOD(StorageSize_BytesToTextRoundTrips)
231 + {
232 + const auto VerifyRoundTrip = [](uint64_t Bytes, StorageSizeUnit Unit, uint32_t DecimalPlaces, bool IncludeSpace = false) {
233 + const auto text = FormatStorageSize(Bytes, Unit, DecimalPlaces, IncludeSpace);
234 + VERIFY_ARE_EQUAL(std::optional<uint64_t>{Bytes}, ParseStorageSize(text, Unit));
235 + };
236 +
237 + VerifyRoundTrip(0, StorageSizeUnit::Decimal, 0);
238 + VerifyRoundTrip(32, StorageSizeUnit::Decimal, 0, true);
239 + VerifyRoundTrip(1'500, StorageSizeUnit::Decimal, 1);
240 + VerifyRoundTrip(1'536, StorageSizeUnit::Binary, 1);
241 + VerifyRoundTrip(1'000'000'000'000'000'000ULL, StorageSizeUnit::Decimal, 2);
242 + VerifyRoundTrip(1ULL << 60, StorageSizeUnit::Binary, 2);
243 +
244 + uint64_t decimalFactor = 1'000;
245 + uint64_t binaryFactor = 1'024;
246 + for (size_t index = 0; index < 5; ++index)
247 + {
248 + VerifyRoundTrip(32 * decimalFactor, StorageSizeUnit::Decimal, 0);
249 + VerifyRoundTrip(32 * binaryFactor, StorageSizeUnit::Binary, 0, true);
250 + decimalFactor *= 1'000;
251 + binaryFactor *= 1'024;
252 + }
253 + }
254 +
255 + TEST_METHOD(StorageSize_TextToBytesRoundTrips)
256 + {
257 + const std::vector<StorageSizeTextRoundTripCase> TestCases{
258 + {L"0B", StorageSizeUnit::Decimal, 0, false},
259 + {L"32 B", StorageSizeUnit::Decimal, 0, true},
260 + {L"32KB", StorageSizeUnit::Decimal, 0, false},
261 + {L"32.5MB", StorageSizeUnit::Decimal, 1, false},
262 + {L"1GB", StorageSizeUnit::Decimal, 0, false},
263 + {L"1.25TB", StorageSizeUnit::Decimal, 2, false},
264 + {L"1PB", StorageSizeUnit::Decimal, 0, false},
265 + {L"32KiB", StorageSizeUnit::Binary, 0, false},
266 + {L"1.5MiB", StorageSizeUnit::Binary, 1, false},
267 + {L"1GiB", StorageSizeUnit::Binary, 0, false},
268 + {L"1.25 TiB", StorageSizeUnit::Binary, 2, true},
269 + {L"1PiB", StorageSizeUnit::Binary, 0, false},
270 + };
271 +
272 + for (const auto& TestCase : TestCases)
273 + {
274 + const auto bytes = ParseStorageSize(TestCase.Text, TestCase.Unit);
275 + VERIFY_IS_TRUE(bytes.has_value());
276 +
277 + const auto text = FormatStorageSize(bytes.value(), TestCase.Unit, TestCase.DecimalPlaces, TestCase.IncludeSpace);
278 + VERIFY_ARE_EQUAL(TestCase.Text, text);
279 + VERIFY_ARE_EQUAL(bytes, ParseStorageSize(text, TestCase.Unit));
280 + }
281 + }
282 +};
283 +} // namespace StringUnitTests
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
+10 -1
@@ -181,6 +181,15 @@ class WSLCCLIArgumentUnitTests
181 VERIFY_THROWS(validation::GetProgressModeFromString(L"TTY"), ArgumentException); // Case-sensitive: only lowercase accepted
182 VERIFY_THROWS(validation::GetProgressModeFromString(L"fancy"), ArgumentException);
183
184 + // Verify Docker-style memory size conversion.
185 + VERIFY_ARE_EQUAL(static_cast<int64_t>(1'610'612'736), validation::GetMemorySizeFromString(L"1.5G"));
186 + VERIFY_ARE_EQUAL(static_cast<int64_t>(314'572), validation::GetMemorySizeFromString(L"0.3MiB"));
187 + VERIFY_ARE_EQUAL(static_cast<int64_t>(32), validation::GetMemorySizeFromString(L"32.3"));
188 + VERIFY_ARE_EQUAL(static_cast<int64_t>(9'007'199'254'740'993), validation::GetMemorySizeFromString(L"9007199254740993"));
189 + VERIFY_ARE_EQUAL(std::numeric_limits<int64_t>::max(), validation::GetMemorySizeFromString(L"9223372036854775807"));
190 + VERIFY_THROWS(validation::GetMemorySizeFromString(L"-1.5G"), ArgumentException);
191 + VERIFY_THROWS(validation::GetMemorySizeFromString(L"9223372036854775808"), ArgumentException);
192 +
193 // Verify GPU device argument
194 VERIFY_NO_THROW(validation::ValidateGpus({L"all"}, L"gpusArg"));
195 VERIFY_THROWS(validation::ValidateGpus({L"none"}, L"gpusArg"), ArgumentException);
@@ -391,7 +400,7 @@ class WSLCCLIArgumentUnitTests
400
401 // string -> int64_t (memory sizes). The cached value matches the converter's result.
402 VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::Memory>(L"512M"), validation::GetMemorySizeFromString(L"512M"));
394 - VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::ShmSize>(L"64M"), validation::GetMemorySizeFromString(L"64M"));
403 + VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::ShmSize>(L"1.5G"), static_cast<int64_t>(1'610'612'736));
404
405 // string -> int64_t (durations, in nanoseconds)
406 VERIFY_ARE_EQUAL(ValidateAndGetCached<ArgType::HealthInterval>(L"30s"), validation::GetDurationNanosFromString(L"30s"));
test/windows/wslc/WSLCCLISettingsUnitTests.cpp
+49
@@ -153,6 +153,22 @@ class WSLCCLISettingsUnitTests
153 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::File), static_cast<int>(s.Get<Setting::CredentialStore>()));
154 }
155
156 + TEST_METHOD(LoadSettings_StorageSizeFormats_YieldExpectedValues)
157 + {
158 + auto dir = UniqueTempDir();
159 + WriteFile(
160 + dir / L"settings.yaml",
161 + "session:\n"
162 + " memorySize: \" 1.5GiB\"\n"
163 + " maxStorageSize: 20.5GB\n");
164 +
165 + UserSettingsTest s{dir};
166 +
167 + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
168 + VERIFY_ARE_EQUAL(1536u, s.Get<Setting::SessionMemoryMb>());
169 + VERIFY_ARE_EQUAL(20992u, s.Get<Setting::SessionStorageSizeMb>());
170 + }
171 +
172 // An empty settings file is valid YAML (null document) but not a mapping;
173 // a structure warning is emitted and all settings use defaults.
174 TEST_METHOD(LoadSettings_EmptySettings_WarnsInvalidStructure)
@@ -236,6 +252,39 @@ class WSLCCLISettingsUnitTests
252 s.GetWarnings().front().Message);
253 }
254
255 + TEST_METHOD(Validation_MemoryMb_Limits_AreEnforced)
256 + {
257 + struct TestCase
258 + {
259 + std::string Value;
260 + uint32_t Expected;
261 + bool IsValid;
262 + };
263 +
264 + const std::vector<TestCase> TestCases{
265 + {"0.5MiB", 0, false},
266 + {"4294967295MiB", std::numeric_limits<uint32_t>::max(), true},
267 + {"4294967296MiB", 0, false},
268 + };
269 +
270 + for (const auto& TestCase : TestCases)
271 + {
272 + auto dir = UniqueTempDir();
273 + WriteFile(dir / L"settings.yaml", std::format("session:\n memorySize: {}\n", TestCase.Value));
274 +
275 + UserSettingsTest s{dir};
276 +
277 + VERIFY_ARE_EQUAL(TestCase.Expected, s.Get<Setting::SessionMemoryMb>());
278 + VERIFY_ARE_EQUAL(TestCase.IsValid ? 0u : 1u, s.GetWarnings().size());
279 + if (!TestCase.IsValid)
280 + {
281 + VERIFY_ARE_EQUAL(
282 + Loc::WSLCUserSettings_Warning_InvalidValue(L"session.memorySize", s.SettingsFilePath().wstring(), 2),
283 + s.GetWarnings().front().Message);
284 + }
285 + }
286 + }
287 +
288 // maxStorageSize: 0 must be rejected; the default is used.
289 TEST_METHOD(Validation_StorageSizeMb_Zero_UsesDefaultAndWarns)
290 {
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+7 -4
@@ -885,13 +885,16 @@ class WSLCE2EContainerCreateTests
885
886 WSLC_TEST_METHOD(WSLCE2E_Container_Create_ShmSize)
887 {
888 - auto result = RunWslc(
889 - std::format(L"container create --shm-size 128M --name {} {} df -h /dev/shm", WslcContainerName, DebianImage.NameAndTag()));
888 + auto cleanup = wil::scope_exit([&] { EnsureContainerDoesNotExist(WslcContainerName); });
889 +
890 + auto result = RunWslc(std::format(
891 + L"container create --shm-size 1.5G --name {} {} sh -c \"df -B1 /dev/shm --output=size | sed 1d\"",
892 + WslcContainerName,
893 + DebianImage.NameAndTag()));
894 result.Verify({.Stderr = L"", .ExitCode = 0});
895
896 result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
893 - result.Verify({.Stderr = L"", .ExitCode = 0});
894 - VERIFY_IS_TRUE(result.Stdout->find(L"128M") != std::wstring::npos);
897 + result.Verify({.Stdout = L"1610612736\n", .Stderr = L"", .ExitCode = 0});
898 }
899
900 WSLC_TEST_METHOD(WSLCE2E_Container_Create_ShmSize_Invalid)
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+3 -3
@@ -1183,9 +1183,9 @@ class WSLCE2EContainerRunTests
1183
1184 WSLC_TEST_METHOD(WSLCE2E_Container_Run_ShmSize)
1185 {
1186 - auto result = RunWslc(std::format(L"container run --rm --shm-size 128M {} df -h /dev/shm", DebianImage.NameAndTag()));
1187 - result.Verify({.Stderr = L"", .ExitCode = 0});
1188 - VERIFY_IS_TRUE(result.Stdout->find(L"128M") != std::wstring::npos);
1186 + auto result = RunWslc(std::format(
1187 + L"container run --rm --shm-size 1.5G {} sh -c \"df -B1 /dev/shm --output=size | sed 1d\"", DebianImage.NameAndTag()));
1188 + result.Verify({.Stdout = L"1610612736\n", .Stderr = L"", .ExitCode = 0});
1189 }
1190
1191 WSLC_TEST_METHOD(WSLCE2E_Container_Run_ShmSize_Invalid)