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)