winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

Strings.cpp (14564B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 #include "pch.h"
      4 #include "TestCommon.h"
      5 #include <AppInstallerStrings.h>
      6 #include <AppInstallerSHA256.h>
      7 #include <ExecutionReporter.h>
      8 
      9 using namespace std::string_view_literals;
     10 using namespace AppInstaller::Utility;
     11 using namespace AppInstaller::Utility::literals;
     12 
     13 TEST_CASE("UTF8Length", "[strings]")
     14 {
     15     REQUIRE(UTF8Length("") == 0);
     16     REQUIRE(UTF8Length("a") == 1);
     17     REQUIRE(UTF8Length(" a b c ") == 7);
     18     REQUIRE(UTF8Length("K\xC3\xA4se") == 4); // "Käse"
     19     REQUIRE(UTF8Length("bye\xE2\x80\xA6") == 4); // "bye…"
     20     REQUIRE(UTF8Length("\xf0\x9f\xa6\x86") == 1); // [duck emoji]
     21     REQUIRE(UTF8Length("\xf0\x9d\x85\xa0\xf0\x9d\x85\xa0") == 2); // [8th note][8th note]
     22 }
     23 
     24 TEST_CASE("UTF8Substring", "[strings]")
     25 {    
     26     REQUIRE(UTF8Substring("", 0, 0) == "");
     27     REQUIRE(UTF8Substring("abcd", 0, 4) == "abcd");
     28     REQUIRE(UTF8Substring("abcd", 0, 5) == "abcd");
     29     REQUIRE(UTF8Substring("abcd", 0, 2) == "ab");
     30     REQUIRE(UTF8Substring("abcd", 1, 0) == "");
     31     REQUIRE(UTF8Substring("abcd", 1, 1) == "b");
     32     REQUIRE(UTF8Substring("abcd", 1, 3) == "bcd");
     33     REQUIRE(UTF8Substring("abcd", 4, 0) == "");
     34 
     35     const char* s = "\xf0\x9f\xa6\x86s like \xf0\x9f\x8c\x8a"; // [duck emoji]s like [wave emoji]
     36     REQUIRE(UTF8Substring(s, 0, 9) == "\xf0\x9f\xa6\x86s like \xf0\x9f\x8c\x8a");
     37     REQUIRE(UTF8Substring(s, 0, 1) == "\xf0\x9f\xa6\x86");
     38     REQUIRE(UTF8Substring(s, 0, 2) == "\xf0\x9f\xa6\x86s");
     39     REQUIRE(UTF8Substring(s, 1, 7) == "s like ");
     40     REQUIRE(UTF8Substring(s, 1, 8) == "s like \xf0\x9f\x8c\x8a");
     41 }
     42 
     43 TEST_CASE("UTF8ColumnWidth", "[strings]")
     44 {
     45     REQUIRE(UTF8ColumnWidth("") == 0);
     46     REQUIRE(UTF8ColumnWidth("a") == 1);
     47     REQUIRE(UTF8ColumnWidth(" a b c ") == 7);
     48     REQUIRE(UTF8ColumnWidth("K\xC3\xA4se") == 4); // "Käse"
     49     REQUIRE(UTF8ColumnWidth("bye\xE2\x80\xA6") == 4); // "bye…"
     50     REQUIRE(UTF8ColumnWidth("fi\xEF\xAC\x81") == 3); // "fi[fi]" [fi] is not decoupled
     51     REQUIRE(UTF8ColumnWidth("\xf0\x9f\xa6\x86") == 2); // [duck emoji]
     52     REQUIRE(UTF8ColumnWidth("\xf0\x9d\x85\xa0\xf0\x9d\x85\xa0") == 2); // [8th note][8th note]
     53     REQUIRE(UTF8ColumnWidth("\xe6\xb5\x8b\xe8\xaf\x95") == 4); // 测试
     54     REQUIRE(UTF8ColumnWidth("te\xe6\xb5\x8bs\xe8\xaf\x95t") == 8); // te测s试t
     55 }
     56 
     57 TEST_CASE("UTF8TrimRightToColumnWidth", "[strings]")
     58 {
     59     size_t actualWidth;
     60     REQUIRE((UTF8TrimRightToColumnWidth("", 0, actualWidth) == "" && actualWidth == 0));
     61     REQUIRE((UTF8TrimRightToColumnWidth("abcd", 4, actualWidth) == "abcd" && actualWidth == 4));
     62     REQUIRE((UTF8TrimRightToColumnWidth("abcd", 5, actualWidth) == "abcd" && actualWidth == 4));
     63     REQUIRE((UTF8TrimRightToColumnWidth("abcd", 2, actualWidth) == "ab" && actualWidth == 2));
     64 
     65     NormalizedString s{ "te\xe6\xb5\x8bs\xe8\xaf\x95t" }; // // te测s试t
     66     REQUIRE((UTF8TrimRightToColumnWidth(s, 0, actualWidth) == "" && actualWidth == 0));
     67     REQUIRE((UTF8TrimRightToColumnWidth(s, 2, actualWidth) == "te" && actualWidth == 2));
     68     REQUIRE((UTF8TrimRightToColumnWidth(s, 3, actualWidth) == "te" && actualWidth == 2));
     69     REQUIRE((UTF8TrimRightToColumnWidth(s, 4, actualWidth) == "te\xe6\xb5\x8b" && actualWidth == 4));
     70     REQUIRE((UTF8TrimRightToColumnWidth(s, 8, actualWidth) == "te\xe6\xb5\x8bs\xe8\xaf\x95t" && actualWidth == 8));
     71     REQUIRE((UTF8TrimRightToColumnWidth(s, 10, actualWidth) == "te\xe6\xb5\x8bs\xe8\xaf\x95t" && actualWidth == 8));
     72 }
     73 
     74 TEST_CASE("Normalize", "[strings]")
     75 {
     76     REQUIRE(Normalize("test") == "test");
     77 
     78     // A + combining Dieresis => single A with umlaut char
     79     REQUIRE(Normalize(L"\x41\x308") == L"\xC4");
     80     // This will stop working in C++20, sigh.
     81     REQUIRE(Normalize(u8"\x41\x308") == u8"\xC4");
     82 
     83     // Ligature fi => f + i
     84     REQUIRE(Normalize(u8"\xFB01") == u8"fi");
     85 }
     86 
     87 TEST_CASE("NormalizedString", "[strings]")
     88 {
     89     REQUIRE(NormalizedString("test") == "test");
     90     std::string input = "test";
     91     REQUIRE(NormalizedString(input) == input);
     92 
     93     // A + combining Dieresis => single A with umlaut char
     94     REQUIRE(NormalizedString(std::wstring_view(L"\x41\x308")) == u8"\xC4");
     95     // This will stop working in C++20, sigh.
     96     input = u8"\x41\x308";
     97     REQUIRE(NormalizedString(input) == u8"\xC4");
     98 
     99     // Ligature fi => f + i
    100     std::string_view input2 = u8"\xFB01";
    101     REQUIRE(NormalizedString(input2) == u8"fi");
    102 
    103     // Embedded null
    104     std::string_view input3{ "Test\0Case", 9 };
    105     REQUIRE(NormalizedString(input3) == "Test Case");
    106 }
    107 
    108 TEST_CASE("Trim", "[strings]")
    109 {
    110     std::string str;
    111     REQUIRE(Trim(str.assign("")) == "");
    112     REQUIRE(Trim(str.assign(" ")) == "");
    113     REQUIRE(Trim(str.assign(" \t ")) == "");
    114     REQUIRE(Trim(str.assign(" a")) == "a");
    115     REQUIRE(Trim(str.assign("bght ")) == "bght");
    116     REQUIRE(Trim(str.assign("\tStuff\f")) == "Stuff");
    117     REQUIRE(Trim(str.assign("Multiple words")) == "Multiple words");
    118     REQUIRE(Trim(str.assign("         Multiple words")) == "Multiple words");
    119     REQUIRE(Trim(str.assign("Much after is taken \f\n\r\t\v\v\t\r\n\f ")) == "Much after is taken");
    120 }
    121 
    122 TEST_CASE("CaseInsensitiveStartsWith", "[strings]")
    123 {
    124     REQUIRE(CaseInsensitiveStartsWith("startswith", "starts"));
    125     REQUIRE(CaseInsensitiveStartsWith("startswith", "STAR"));
    126     REQUIRE(CaseInsensitiveStartsWith("startswith", "startSWITH"));
    127     REQUIRE(CaseInsensitiveStartsWith("startswith", ""));
    128 
    129     REQUIRE(!CaseInsensitiveStartsWith("starts", "startswith"));
    130     REQUIRE(!CaseInsensitiveStartsWith("", "nuffing"));
    131     REQUIRE(!CaseInsensitiveStartsWith("withstarts", "starts"));
    132     REQUIRE(!CaseInsensitiveStartsWith(" starts", "starts"));
    133 }
    134 
    135 TEST_CASE("FoldCase", "[strings]")
    136 {
    137     REQUIRE(FoldCase(""sv) == FoldCase(""sv));
    138     REQUIRE(FoldCase("foldcase"sv) == FoldCase("FOLDCASE"sv));
    139     REQUIRE(FoldCase(u8"f\xF6ldcase"sv) == FoldCase(u8"F\xD6LDCASE"sv));
    140     REQUIRE(FoldCase(u8"foldc\x430se"sv) == FoldCase(u8"FOLDC\x410SE"sv));
    141 }
    142 
    143 TEST_CASE("ExpandEnvironmentVariables", "[strings]")
    144 {
    145     wchar_t buffer[MAX_PATH];
    146     GetTempPathW(ARRAYSIZE(buffer), buffer);
    147 
    148     std::wstring tempPath = buffer;
    149     if (!tempPath.empty() && tempPath.back() == '\\')
    150     {
    151         tempPath.resize(tempPath.size() - 1);
    152     }
    153 
    154     REQUIRE(ExpandEnvironmentVariables(L"%TEMP%") == tempPath);
    155 }
    156 
    157 TEST_CASE("PathOutput", "[strings]")
    158 {
    159     std::string original = "\xe6\xb5\x8b\xe8\xaf\x95";
    160     std::filesystem::path path = ConvertToUTF16(original);
    161     AICLI_LOG(Test, Info, << path);
    162 
    163     std::istringstream in;
    164     std::ostringstream out;
    165     AppInstaller::CLI::Execution::Reporter reporter{ out, in };
    166 
    167     reporter.Info() << path;
    168 
    169     std::string output = out.str();
    170     REQUIRE(output.substr(output.size() - original.size()) == original);
    171 }
    172 
    173 TEST_CASE("ReplaceWhileCopying", "[strings]")
    174 {
    175     REQUIRE(ReplaceWhileCopying(L"A red apple", L"red", L"green") == L"A green apple");
    176     REQUIRE(ReplaceWhileCopying(L"A red, red apple", L"red", L"green") == L"A green, green apple");
    177     REQUIRE(ReplaceWhileCopying(L"A red, red apple", L"ed", L"ad") == L"A rad, rad apple");
    178     REQUIRE(ReplaceWhileCopying(L"A red apple", L"p", L"f") == L"A red affle");
    179     REQUIRE(ReplaceWhileCopying(L"A red apple", L"", L"green") == L"A red apple");
    180 }
    181 
    182 TEST_CASE("MakeSuitablePathPart", "[strings]")
    183 {
    184     REQUIRE(MakeSuitablePathPart("A\\B") == "A_B");
    185     REQUIRE(MakeSuitablePathPart("A\\B/") == "A_B_");
    186     REQUIRE(MakeSuitablePathPart("*AB") == "_AB");
    187     REQUIRE(MakeSuitablePathPart(u8"f*\xF6*ldcase") == u8"f_\xF6_ldcase");
    188     REQUIRE(MakeSuitablePathPart(".") == "_");
    189     REQUIRE(MakeSuitablePathPart("..") == "._");
    190     REQUIRE(MakeSuitablePathPart(std::string(300, ' ')) == SHA256::ConvertToString(SHA256::ComputeHash(std::string(300, ' '))));
    191     REQUIRE_THROWS_HR(MakeSuitablePathPart("COM1"), E_INVALIDARG);
    192     REQUIRE_THROWS_HR(MakeSuitablePathPart("NUL.txt"), E_INVALIDARG);
    193 }
    194 
    195 TEST_CASE("GetFileNameFromURI", "[strings]")
    196 {
    197     REQUIRE(GetFileNameFromURI("https://github.com/microsoft/winget-cli/pull/1722").u8string() == "1722");
    198     REQUIRE(GetFileNameFromURI("https://github.com/microsoft/winget-cli/README.md").u8string() == "README.md");
    199     REQUIRE(GetFileNameFromURI("https://microsoft.com/").u8string() == "");
    200 }
    201 
    202 void ValidateSplitFileName(std::string_view uri, std::string_view base, std::string_view fileName)
    203 {
    204     auto split = SplitFileNameFromURI(uri);
    205     REQUIRE(split.first == base);
    206     REQUIRE(split.second.u8string() == fileName);
    207 }
    208 
    209 TEST_CASE("SplitFileNameFromURI", "[strings]")
    210 {
    211     ValidateSplitFileName("https://github.com/microsoft/winget-cli/pull/1722", "https://github.com/microsoft/winget-cli/pull/", "1722");
    212     ValidateSplitFileName("https://github.com/microsoft/winget-cli/README.md", "https://github.com/microsoft/winget-cli/", "README.md");
    213     ValidateSplitFileName("https://microsoft.com/", "https://microsoft.com/", "");
    214 }
    215 
    216 TEST_CASE("SplitIntoWords", "[strings]")
    217 {
    218     REQUIRE(SplitIntoWords("A B") == std::vector<std::string>{ "A", "B" });
    219     REQUIRE(SplitIntoWords("Some-Thing") == std::vector<std::string>{ "Some", "Thing" });
    220 
    221     // 私のテスト = "My test" according to an online translator
    222     // Split as "私" "の" "テスト"
    223     REQUIRE(SplitIntoWords("\xe7\xa7\x81\xe3\x81\xae\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88") == std::vector<std::string>{ "\xe7\xa7\x81", "\xe3\x81\xae", "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" });
    224 }
    225 
    226 TEST_CASE("ReplaceEmbeddedNullCharacters", "[strings]")
    227 {
    228     std::string test = "Test Parts";
    229     test[4] = '\0';
    230     ReplaceEmbeddedNullCharacters(test);
    231     REQUIRE(test == "Test Parts");
    232 }
    233 
    234 TEST_CASE("HexStrings", "[strings]")
    235 {
    236     std::vector<uint8_t> buffer{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
    237     std::string value = "000102030405060708090a0b0c0d0e0f";
    238 
    239     REQUIRE(value == ConvertToHexString(buffer));
    240     REQUIRE(std::equal(buffer.begin(), buffer.end(), ParseFromHexString(value).begin()));
    241 }
    242 
    243 TEST_CASE("Join", "[strings]")
    244 {
    245     std::vector<LocIndString> list_0{ };
    246     std::vector<LocIndString> list_1{ "A"_lis };
    247     std::vector<LocIndString> list_2{ "A"_lis, "B"_lis };
    248 
    249     REQUIRE(""_lis == Join(", "_liv, list_0));
    250     REQUIRE("A"_lis == Join(", "_liv, list_1));
    251     REQUIRE("A, B"_lis == Join(", "_liv, list_2));
    252     REQUIRE("AB"_lis == Join(""_liv, list_2));
    253 }
    254 
    255 TEST_CASE("Format", "[strings]")
    256 {
    257     REQUIRE("First Second" == Format("{0} {1}", "First", "Second"));
    258     REQUIRE("First Second" == Format("{1} {0}", "Second", "First"));
    259     REQUIRE("First Second" == Format("{0} {1}", "First", "Second", "(Extra", "Input", "Ignored)"));
    260     REQUIRE("First Second First Second" == Format("{0} {1} {0} {1}", "First", "Second"));
    261 
    262     // Note: C++20 std::format will throw an exception for this test case
    263     REQUIRE("First {1}" == Format("{0} {1}", "First"));
    264 }
    265 
    266 TEST_CASE("SplitIntoLines", "[strings]")
    267 {
    268     REQUIRE(SplitIntoLines("Boring test") == std::vector<std::string>{ "Boring test" });
    269     REQUIRE(SplitIntoLines(
    270         "I'm Luffy! The Man Who Will Become the Pirate King!\r-Monkey D. Luffy") == std::vector<std::string>{ "I'm Luffy! The Man Who Will Become the Pirate King!", "-Monkey D. Luffy" });
    271     REQUIRE(SplitIntoLines(
    272         "I want live!\n-Nico Robin") == std::vector<std::string>{ "I want live!", "-Nico Robin" });
    273     REQUIRE(SplitIntoLines(
    274         "You want my treasure?\rYou can have it!\nI left everything I gathered in one place!\r\nYou just have to find it!")
    275         == std::vector<std::string>{ "You want my treasure?", "You can have it!", "I left everything I gathered in one place!", "You just have to find it!" });
    276 }
    277 
    278 TEST_CASE("SplitWithSeparator", "[strings]")
    279 {
    280     std::vector<std::string> test1 = Split("first;second;third", ';');
    281     REQUIRE(test1.size() == 3);
    282     REQUIRE(test1[0] == "first");
    283     REQUIRE(test1[1] == "second");
    284     REQUIRE(test1[2] == "third");
    285 
    286     std::vector<std::string> test2 = Split("two  spaces", ' ');
    287     REQUIRE(test2.size() == 3);
    288     REQUIRE(test2[0] == "two");
    289     REQUIRE(test2[1] == "");
    290     REQUIRE(test2[2] == "spaces");
    291 
    292     std::vector<std::string> test3 = Split("test", '.');
    293     REQUIRE(test3.size() == 1);
    294     REQUIRE(test3[0] == "test");
    295 
    296     std::vector<std::string> test4 = Split(" trim |    spaces ", '|', true);
    297     REQUIRE(test4.size() == 2);
    298     REQUIRE(test4[0] == "trim");
    299     REQUIRE(test4[1] == "spaces");
    300 }
    301 
    302 TEST_CASE("ConvertGuid", "[strings]")
    303 {
    304     std::string validGuidString = "{4d1e55b2-f16f-11cf-88cb-001111000030}";
    305     GUID guid = { 0x4d1e55b2, 0xf16f, 0x11cf, 0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 };
    306 
    307     REQUIRE(CaseInsensitiveEquals(ConvertGuidToString(guid), validGuidString));
    308 }
    309 
    310 TEST_CASE("FindControlCodeToConvert", "[strings]")
    311 {
    312     REQUIRE(FindControlCodeToConvert("No codes") == std::string::npos);
    313     REQUIRE(FindControlCodeToConvert("Allowed codes: \t\r\n") == std::string::npos);
    314     REQUIRE(FindControlCodeToConvert("\x1bSkipped code", 1) == std::string::npos);
    315 
    316     REQUIRE(FindControlCodeToConvert("\x1bUnskipped code") == 0);
    317     REQUIRE(FindControlCodeToConvert("Escape code: \x1b") == 13);
    318 
    319     std::string_view allCodes{ "\x0\x1\x2\x3\x4\x5\x6\x7\x8\xb\xc\xe\xf\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"sv };
    320     for (size_t i = 0; i < allCodes.length(); ++i)
    321     {
    322         REQUIRE(FindControlCodeToConvert(allCodes, i) == i);
    323     }
    324 }
    325 
    326 TEST_CASE("ConvertControlCodesToPictures", "[strings]")
    327 {
    328     REQUIRE(ConvertControlCodesToPictures("No codes") == "No codes");
    329     REQUIRE(ConvertControlCodesToPictures("Allowed codes: \t\r\n") == "Allowed codes: \t\r\n");
    330 
    331     REQUIRE(ConvertControlCodesToPictures("\x1b Code First") == ConvertToUTF8(L"\x241b Code First"));
    332     REQUIRE(ConvertControlCodesToPictures("Escape code: \x1b") == ConvertToUTF8(L"Escape code: \x241b"));
    333 
    334     std::string_view allCodes{ "\x0\x1\x2\x3\x4\x5\x6\x7\x8\xb\xc\xe\xf\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"sv };
    335     std::wstring_view allPictures{ L"\x2400\x2401\x2402\x2403\x2404\x2405\x2406\x2407\x2408\x240b\x240c\x240e\x240f\x2410\x2411\x2412\x2413\x2414\x2415\x2416\x2417\x2418\x2419\x241a\x241b\x241c\x241d\x241e\x241f\x2421"sv };
    336 
    337     REQUIRE(ConvertControlCodesToPictures(allCodes) == ConvertToUTF8(allPictures));
    338 }