| 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::c_reclaimedSpacePrecision; |
| 8 | using wsl::windows::common::string::Ellipsis; |
| 9 | using wsl::windows::common::string::FormatHumanReadableSize; |
| 10 | using wsl::windows::common::string::ParseStorageSize; |
| 11 | using wsl::windows::common::string::StorageSizeUnit; |
| 12 | |
| 13 | namespace { |
| 14 | |
| 15 | struct StorageSizeFormatCase |
| 16 | { |
| 17 | uint64_t Bytes; |
| 18 | StorageSizeUnit Unit; |
| 19 | uint32_t Precision; |
| 20 | std::wstring Expected; |
| 21 | }; |
| 22 | |
| 23 | struct StorageSizeTextRoundTripCase |
| 24 | { |
| 25 | std::wstring Text; |
| 26 | StorageSizeUnit Unit; |
| 27 | uint32_t Precision; |
| 28 | }; |
| 29 | |
| 30 | void VerifyDockerStorageSize(const std::string& Input, StorageSizeUnit Unit, std::optional<uint64_t> Expected) |
| 31 | { |
| 32 | const auto wideInput = wsl::shared::string::MultiByteToWide(Input); |
| 33 | VERIFY_ARE_EQUAL(Expected, ParseStorageSize(wideInput, Unit)); |
| 34 | } |
| 35 | |
| 36 | std::vector<std::string> DockerSuffixes(char Unit) |
| 37 | { |
| 38 | const auto UpperUnit = static_cast<char>(std::toupper(static_cast<unsigned char>(Unit))); |
| 39 | return { |
| 40 | {Unit}, |
| 41 | {UpperUnit}, |
| 42 | {Unit, 'b'}, |
| 43 | {Unit, 'B'}, |
| 44 | {UpperUnit, 'b'}, |
| 45 | {UpperUnit, 'B'}, |
| 46 | {Unit, 'i', 'b'}, |
| 47 | {Unit, 'i', 'B'}, |
| 48 | {Unit, 'I', 'b'}, |
| 49 | {Unit, 'I', 'B'}, |
| 50 | {UpperUnit, 'i', 'b'}, |
| 51 | {UpperUnit, 'i', 'B'}, |
| 52 | {UpperUnit, 'I', 'b'}, |
| 53 | {UpperUnit, 'I', 'B'}, |
| 54 | }; |
| 55 | } |
| 56 | |
| 57 | void VerifyDockerStorageUnits(StorageSizeUnit Unit, uint64_t Base) |
| 58 | { |
| 59 | uint64_t Factor = Base; |
| 60 | for (const auto UnitName : {'k', 'm', 'g', 't', 'p'}) |
| 61 | { |
| 62 | for (const auto& Suffix : DockerSuffixes(UnitName)) |
| 63 | { |
| 64 | VerifyDockerStorageSize("32" + Suffix, Unit, 32 * Factor); |
| 65 | } |
| 66 | |
| 67 | Factor *= Base; |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | } // namespace |
| 72 | |
| 73 | namespace StringUnitTests { |
| 74 | class StringUnitTests |
| 75 | { |
| 76 | WSL_TEST_CLASS(StringUnitTests) |
| 77 | |
| 78 | TEST_METHOD(FormatUtf8StringAsWideString) |
| 79 | { |
| 80 | const std::string input{"安装依赖"}; |
| 81 | const auto expected = wsl::shared::string::MultiByteToWide(input); |
| 82 | |
| 83 | VERIFY_ARE_EQUAL(expected, std::format(L"{}", input)); |
| 84 | } |
| 85 | |
| 86 | TEST_METHOD(ParseMemorySize_LegacyForms) |
| 87 | { |
| 88 | const std::vector<std::pair<LPCSTR, std::optional<uint64_t>>> TestCases{ |
| 89 | {"0", 0}, |
| 90 | {"1", 1}, |
| 91 | {" 1", 1}, |
| 92 | {"1B", 1}, |
| 93 | {"1K", 1024}, |
| 94 | {"1KB", 1024}, |
| 95 | {"2M", 2 * 1024 * 1024}, |
| 96 | {"100MB", 100 * 1024 * 1024}, |
| 97 | {"9G", 9 * 1024ULL * 1024ULL * 1024ULL}, |
| 98 | {"44GB", 44 * 1024ULL * 1024ULL * 1024ULL}, |
| 99 | {"1TB", 1ULL << 40}, |
| 100 | {"2T", 2ULL << 40}, |
| 101 | {"1 B", std::nullopt}, |
| 102 | {nullptr, std::nullopt}, |
| 103 | {"", std::nullopt}, |
| 104 | {"foo", std::nullopt}}; |
| 105 | |
| 106 | for (const auto& [Input, Expected] : TestCases) |
| 107 | { |
| 108 | VERIFY_ARE_EQUAL(Expected, wsl::shared::string::ParseMemorySize(Input)); |
| 109 | |
| 110 | const auto wideInput = wsl::shared::string::MultiByteToWide(Input); |
| 111 | VERIFY_ARE_EQUAL(Expected, wsl::shared::string::ParseMemorySize(wideInput.c_str())); |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | TEST_METHOD(ParseStorageSize_DockerDecimalUnits) |
| 116 | { |
| 117 | VerifyDockerStorageUnits(StorageSizeUnit::Decimal, 1000); |
| 118 | } |
| 119 | |
| 120 | TEST_METHOD(ParseStorageSize_DockerBinaryUnits) |
| 121 | { |
| 122 | VerifyDockerStorageUnits(StorageSizeUnit::Binary, 1024); |
| 123 | } |
| 124 | |
| 125 | TEST_METHOD(ParseStorageSize_DockerNumericForms) |
| 126 | { |
| 127 | for (const auto Unit : {StorageSizeUnit::Decimal, StorageSizeUnit::Binary}) |
| 128 | { |
| 129 | VerifyDockerStorageSize("0", Unit, 0); |
| 130 | VerifyDockerStorageSize("0b", Unit, 0); |
| 131 | VerifyDockerStorageSize("0B", Unit, 0); |
| 132 | VerifyDockerStorageSize("0 B", Unit, 0); |
| 133 | VerifyDockerStorageSize("32", Unit, 32); |
| 134 | VerifyDockerStorageSize("32b", Unit, 32); |
| 135 | VerifyDockerStorageSize("32B", Unit, 32); |
| 136 | VerifyDockerStorageSize("32.5 B", Unit, 32); |
| 137 | VerifyDockerStorageSize("0.", Unit, 0); |
| 138 | VerifyDockerStorageSize("0. ", Unit, 0); |
| 139 | VerifyDockerStorageSize("0.b", Unit, 0); |
| 140 | VerifyDockerStorageSize("0.B", Unit, 0); |
| 141 | VerifyDockerStorageSize("-0", Unit, 0); |
| 142 | VerifyDockerStorageSize("-0b", Unit, 0); |
| 143 | VerifyDockerStorageSize("-0B", Unit, 0); |
| 144 | VerifyDockerStorageSize("-0 b", Unit, 0); |
| 145 | VerifyDockerStorageSize("-0 B", Unit, 0); |
| 146 | VerifyDockerStorageSize("+32K", Unit, 32 * (Unit == StorageSizeUnit::Decimal ? 1000 : 1024)); |
| 147 | VerifyDockerStorageSize("1e3K", Unit, 1000 * (Unit == StorageSizeUnit::Decimal ? 1000 : 1024)); |
| 148 | VerifyDockerStorageSize("32.", Unit, 32); |
| 149 | VerifyDockerStorageSize("32.b", Unit, 32); |
| 150 | VerifyDockerStorageSize("32.B", Unit, 32); |
| 151 | VerifyDockerStorageSize("32. b", Unit, 32); |
| 152 | VerifyDockerStorageSize("32. B", Unit, 32); |
| 153 | VerifyDockerStorageSize("9007199254740991", Unit, 9'007'199'254'740'991); |
| 154 | VerifyDockerStorageSize("9007199254740992", Unit, 9'007'199'254'740'992); |
| 155 | VerifyDockerStorageSize("9007199254740993", Unit, 9'007'199'254'740'993); |
| 156 | VerifyDockerStorageSize("9223372036854775806", Unit, 9'223'372'036'854'775'806); |
| 157 | VerifyDockerStorageSize("9223372036854775807", Unit, 9'223'372'036'854'775'807); |
| 158 | VerifyDockerStorageSize("9223372036854775808", Unit, 9'223'372'036'854'775'808ULL); |
| 159 | VerifyDockerStorageSize("18446744073709551615", Unit, std::numeric_limits<uint64_t>::max()); |
| 160 | } |
| 161 | |
| 162 | VerifyDockerStorageSize("32.5kB", StorageSizeUnit::Decimal, 32'500); |
| 163 | VerifyDockerStorageSize("32.5 kB", StorageSizeUnit::Decimal, 32'500); |
| 164 | VerifyDockerStorageSize("0.3 K", StorageSizeUnit::Decimal, 300); |
| 165 | VerifyDockerStorageSize(".3kB", StorageSizeUnit::Decimal, 300); |
| 166 | VerifyDockerStorageSize("32.3 mb", StorageSizeUnit::Binary, 33'869'004); |
| 167 | VerifyDockerStorageSize("0.3MB", StorageSizeUnit::Binary, 314'572); |
| 168 | VerifyDockerStorageSize("18446744073709551K", StorageSizeUnit::Decimal, 18'446'744'073'709'551'000ULL); |
| 169 | VerifyDockerStorageSize("18446744073709552K", StorageSizeUnit::Decimal, std::nullopt); |
| 170 | VerifyDockerStorageSize("18014398509481983K", StorageSizeUnit::Binary, 18'446'744'073'709'550'592ULL); |
| 171 | VerifyDockerStorageSize("18014398509481984K", StorageSizeUnit::Binary, std::nullopt); |
| 172 | } |
| 173 | |
| 174 | TEST_METHOD(ParseStorageSize_DockerInvalidForms) |
| 175 | { |
| 176 | const std::vector<std::string> InvalidSizes{ |
| 177 | "", "hello", |
| 178 | ".", ". ", |
| 179 | " ", " ", |
| 180 | " .", " . ", |
| 181 | " 0", " 0b", |
| 182 | " 0B", " 0 B", |
| 183 | "0b ", "0B ", |
| 184 | "0 B ", "-32", |
| 185 | "-32b", "-32B", |
| 186 | "-32 b", "-32 B", |
| 187 | "32b.", "32B.", |
| 188 | "32 b.", "32 B.", |
| 189 | "32 bb", "32 BB", |
| 190 | "32 b b", "32 B B", |
| 191 | "32 b", "32 B", |
| 192 | " 32 ", "32m b", |
| 193 | "32bm", "1E", |
| 194 | "1EB", "1EiB", |
| 195 | "1e309", "18446744073709551616", |
| 196 | }; |
| 197 | |
| 198 | for (const auto Unit : {StorageSizeUnit::Decimal, StorageSizeUnit::Binary}) |
| 199 | { |
| 200 | for (const auto& Input : InvalidSizes) |
| 201 | { |
| 202 | VerifyDockerStorageSize(Input, Unit, std::nullopt); |
| 203 | } |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | // Memory sizes are rendered in binary units with four significant digits and no space, matching |
| 208 | // docker's units.BytesSize. |
| 209 | TEST_METHOD(FormatHumanReadableSize_SupportsBinaryUnits) |
| 210 | { |
| 211 | const std::vector<StorageSizeFormatCase> TestCases{ |
| 212 | {0, StorageSizeUnit::Binary, 4, L"0B"}, |
| 213 | {1'023, StorageSizeUnit::Binary, 4, L"1023B"}, |
| 214 | {1'024, StorageSizeUnit::Binary, 4, L"1KiB"}, |
| 215 | {1'536, StorageSizeUnit::Binary, 4, L"1.5KiB"}, |
| 216 | {44'000, StorageSizeUnit::Binary, 4, L"42.97KiB"}, |
| 217 | {1'610'612'736, StorageSizeUnit::Binary, 4, L"1.5GiB"}, |
| 218 | {8ULL << 30, StorageSizeUnit::Binary, 4, L"8GiB"}, |
| 219 | {1ULL << 40, StorageSizeUnit::Binary, 4, L"1TiB"}, |
| 220 | {1ULL << 50, StorageSizeUnit::Binary, 4, L"1PiB"}, |
| 221 | {1'536, StorageSizeUnit::Binary, 3, L"1.5KiB"}, |
| 222 | {1'000, StorageSizeUnit::Decimal, 4, L"1kB"}, |
| 223 | {1'610'612'736, StorageSizeUnit::Decimal, 4, L"1.611GB"}, |
| 224 | }; |
| 225 | |
| 226 | for (const auto& TestCase : TestCases) |
| 227 | { |
| 228 | VERIFY_ARE_EQUAL(TestCase.Expected, FormatHumanReadableSize(TestCase.Bytes, TestCase.Precision, TestCase.Unit)); |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | // Image sizes are rendered with three significant digits, base 1000, no space, and "kB" rather |
| 233 | // than "KB". |
| 234 | TEST_METHOD(FormatHumanReadableSize_MatchesImageSizePrecision) |
| 235 | { |
| 236 | const std::vector<std::pair<uint64_t, std::wstring>> TestCases{ |
| 237 | {0, L"0B"}, |
| 238 | {999, L"999B"}, |
| 239 | {1'000, L"1kB"}, |
| 240 | {1'500, L"1.5kB"}, |
| 241 | {7'050'000, L"7.05MB"}, |
| 242 | {119'856'765, L"120MB"}, |
| 243 | {1'090'000'000, L"1.09GB"}, |
| 244 | {1'000'000'000'000ULL, L"1TB"}, |
| 245 | }; |
| 246 | |
| 247 | for (const auto& [bytes, expected] : TestCases) |
| 248 | { |
| 249 | VERIFY_ARE_EQUAL(expected, FormatHumanReadableSize(bytes)); |
| 250 | } |
| 251 | |
| 252 | // Three significant digits switch to exponent form just below the next unit, matching Go's %g. |
| 253 | VERIFY_ARE_EQUAL(std::wstring{L"1e+03MB"}, FormatHumanReadableSize(999'900'000)); |
| 254 | } |
| 255 | |
| 256 | TEST_METHOD(FormatHumanReadableSize_SupportsReclaimedSpacePrecision) |
| 257 | { |
| 258 | const std::vector<std::pair<uint64_t, std::wstring>> TestCases{ |
| 259 | {0, L"0B"}, |
| 260 | {999, L"999B"}, |
| 261 | {12'288, L"12.29kB"}, |
| 262 | {119'856'765, L"119.9MB"}, |
| 263 | {1'090'000'000, L"1.09GB"}, |
| 264 | }; |
| 265 | |
| 266 | for (const auto& [bytes, expected] : TestCases) |
| 267 | { |
| 268 | VERIFY_ARE_EQUAL(expected, FormatHumanReadableSize(bytes, c_reclaimedSpacePrecision)); |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | // Docker shortens display values with formatter.Ellipsis, which measures terminal columns rather than |
| 273 | // characters so East Asian wide and fullwidth code points count double. |
| 274 | TEST_METHOD(Ellipsis_NarrowCharacters_AreCountedAsOneColumn) |
| 275 | { |
| 276 | VERIFY_ARE_EQUAL(std::wstring{L""}, Ellipsis(L"", 20)); |
| 277 | VERIFY_ARE_EQUAL(std::wstring{L"sleep 3600"}, Ellipsis(L"sleep 3600", 20)); |
| 278 | VERIFY_ARE_EQUAL(std::wstring{L"12345678901234567890"}, Ellipsis(L"12345678901234567890", 20)); |
| 279 | VERIFY_ARE_EQUAL(std::wstring{L"1234567890123456789\u2026"}, Ellipsis(L"123456789012345678901", 20)); |
| 280 | VERIFY_ARE_EQUAL(std::wstring(19, L'\u00E0') + L"\u2026", Ellipsis(std::wstring(21, L'\u00E0'), 20)); |
| 281 | } |
| 282 | |
| 283 | TEST_METHOD(Ellipsis_WideCharacters_AreCountedAsTwoColumns) |
| 284 | { |
| 285 | // Ten wide characters fill the twenty columns exactly, so an eleventh forces the value to be shortened |
| 286 | // to the nine characters that leave room for the ellipsis. |
| 287 | VERIFY_ARE_EQUAL(std::wstring(10, L'\u65E5'), Ellipsis(std::wstring(10, L'\u65E5'), 20)); |
| 288 | VERIFY_ARE_EQUAL(std::wstring(9, L'\u65E5') + L"\u2026", Ellipsis(std::wstring(11, L'\u65E5'), 20)); |
| 289 | VERIFY_ARE_EQUAL(std::wstring(9, L'\uFF21') + L"\u2026", Ellipsis(std::wstring(11, L'\uFF21'), 20)); |
| 290 | VERIFY_ARE_EQUAL(std::wstring{L"ab"} + std::wstring(8, L'\u65E5') + L"\u2026", Ellipsis(L"ab" + std::wstring(10, L'\u65E5'), 20)); |
| 291 | } |
| 292 | |
| 293 | TEST_METHOD(Ellipsis_SurrogatePairs_AreNotSplit) |
| 294 | { |
| 295 | // Emoji are wide and encoded as surrogate pairs, so both the column count and the code unit boundary |
| 296 | // have to be honored. |
| 297 | const std::wstring emoji{L"\U0001F600"}; |
| 298 | std::wstring ten; |
| 299 | for (size_t index = 0; index < 10; ++index) |
| 300 | { |
| 301 | ten += emoji; |
| 302 | } |
| 303 | |
| 304 | VERIFY_ARE_EQUAL(ten, Ellipsis(ten, 20)); |
| 305 | VERIFY_ARE_EQUAL(ten.substr(0, 18) + L"\u2026", Ellipsis(ten + emoji, 20)); |
| 306 | } |
| 307 | |
| 308 | TEST_METHOD(Ellipsis_SmallWidths_MatchDocker) |
| 309 | { |
| 310 | VERIFY_ARE_EQUAL(std::wstring{L""}, Ellipsis(L"abc", 0)); |
| 311 | |
| 312 | // A width of one has no room for both content and an ellipsis, so the leading code point is kept even |
| 313 | // when it is wider than the limit. |
| 314 | VERIFY_ARE_EQUAL(std::wstring{L"a"}, Ellipsis(L"abc", 1)); |
| 315 | VERIFY_ARE_EQUAL(std::wstring{L"\u65E5"}, Ellipsis(L"\u65E5\u65E5", 1)); |
| 316 | VERIFY_ARE_EQUAL(std::wstring{L"\U0001F600"}, Ellipsis(L"\U0001F600\U0001F600", 1)); |
| 317 | |
| 318 | // A leading wide character leaves no room for the ellipsis at a width of two, and docker returns the |
| 319 | // value untouched in that case. |
| 320 | VERIFY_ARE_EQUAL(std::wstring{L"\u65E5\u65E5"}, Ellipsis(L"\u65E5\u65E5", 2)); |
| 321 | VERIFY_ARE_EQUAL(std::wstring{L"a\u2026"}, Ellipsis(L"abc", 2)); |
| 322 | VERIFY_ARE_EQUAL(std::wstring{L"\u65E5\u2026"}, Ellipsis(L"\u65E5\u65E5", 3)); |
| 323 | } |
| 324 | |
| 325 | TEST_METHOD(StorageSize_BytesToTextRoundTrips) |
| 326 | { |
| 327 | // The parser accepts suffixes up to peta, matching docker's unit map, so the round trip is |
| 328 | // only defined below one exabyte. |
| 329 | const auto VerifyRoundTrip = [](uint64_t Bytes, StorageSizeUnit Unit, uint32_t Precision) { |
| 330 | const auto text = FormatHumanReadableSize(Bytes, Precision, Unit); |
| 331 | VERIFY_ARE_EQUAL(std::optional<uint64_t>{Bytes}, ParseStorageSize(text, Unit)); |
| 332 | }; |
| 333 | |
| 334 | VerifyRoundTrip(0, StorageSizeUnit::Decimal, 3); |
| 335 | VerifyRoundTrip(32, StorageSizeUnit::Decimal, 3); |
| 336 | VerifyRoundTrip(1'500, StorageSizeUnit::Decimal, 3); |
| 337 | VerifyRoundTrip(1'536, StorageSizeUnit::Binary, 3); |
| 338 | VerifyRoundTrip(1'250'000'000'000ULL, StorageSizeUnit::Decimal, 4); |
| 339 | VerifyRoundTrip(1ULL << 50, StorageSizeUnit::Binary, 4); |
| 340 | |
| 341 | uint64_t decimalFactor = 1'000; |
| 342 | uint64_t binaryFactor = 1'024; |
| 343 | for (size_t index = 0; index < 5; ++index) |
| 344 | { |
| 345 | VerifyRoundTrip(32 * decimalFactor, StorageSizeUnit::Decimal, 3); |
| 346 | VerifyRoundTrip(32 * binaryFactor, StorageSizeUnit::Binary, 3); |
| 347 | decimalFactor *= 1'000; |
| 348 | binaryFactor *= 1'024; |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | TEST_METHOD(StorageSize_TextToBytesRoundTrips) |
| 353 | { |
| 354 | const std::vector<StorageSizeTextRoundTripCase> TestCases{ |
| 355 | {L"0B", StorageSizeUnit::Decimal, 3}, |
| 356 | {L"32B", StorageSizeUnit::Decimal, 3}, |
| 357 | {L"32kB", StorageSizeUnit::Decimal, 3}, |
| 358 | {L"32.5MB", StorageSizeUnit::Decimal, 3}, |
| 359 | {L"1GB", StorageSizeUnit::Decimal, 3}, |
| 360 | {L"1.25TB", StorageSizeUnit::Decimal, 3}, |
| 361 | {L"1PB", StorageSizeUnit::Decimal, 3}, |
| 362 | {L"32KiB", StorageSizeUnit::Binary, 3}, |
| 363 | {L"1.5MiB", StorageSizeUnit::Binary, 3}, |
| 364 | {L"1GiB", StorageSizeUnit::Binary, 3}, |
| 365 | {L"1.25TiB", StorageSizeUnit::Binary, 3}, |
| 366 | {L"1PiB", StorageSizeUnit::Binary, 3}, |
| 367 | }; |
| 368 | |
| 369 | for (const auto& TestCase : TestCases) |
| 370 | { |
| 371 | const auto bytes = ParseStorageSize(TestCase.Text, TestCase.Unit); |
| 372 | VERIFY_IS_TRUE(bytes.has_value()); |
| 373 | |
| 374 | const auto text = FormatHumanReadableSize(bytes.value(), TestCase.Precision, TestCase.Unit); |
| 375 | VERIFY_ARE_EQUAL(TestCase.Text, text); |
| 376 | VERIFY_ARE_EQUAL(bytes, ParseStorageSize(text, TestCase.Unit)); |
| 377 | } |
| 378 | } |
| 379 | }; |
| 380 | } // namespace StringUnitTests |