NameNormalization.cpp (25395B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "Public/winget/NameNormalization.h" 5 #include "Public/AppInstallerStrings.h" 6 #include "Public/winget/Regex.h" 7 8 9 namespace AppInstaller::Utility 10 { 11 namespace 12 { 13 struct InterimNameNormalizationResult 14 { 15 std::wstring Name; 16 Architecture Architecture = Architecture::Unknown; 17 std::wstring Locale; 18 }; 19 20 struct InterimPublisherNormalizationResult 21 { 22 std::wstring Publisher; 23 }; 24 25 // To maintain consistency, changes that result in different output must be done in a new version. 26 // This can potentially be ignored (if thought through) when the changes will only increase the 27 // number of matches being made, with no impact to existing matches. For instance, removing an 28 // arbitrary new processor architecture from names would hopefully only affect existing packages 29 // that were not matching properly. Fixing a bug that was causing bad strings to be produced would 30 // be impactful, and thus should likely result in a new iteration. 31 class NormalizationInitial : public details::INameNormalizer 32 { 33 static std::wstring PrepareForValidation(std::string_view value) 34 { 35 std::wstring result = Utility::Normalize(ConvertToUTF16(value)); 36 Trim(result); 37 size_t atPos = result.find(L"@@", 3); 38 if (atPos != std::wstring::npos) 39 { 40 result = result.substr(0, atPos); 41 } 42 return result; 43 } 44 45 // If the string is wrapped with some character groups, remove them. 46 // Returns true if string was wrapped; false if not. 47 static bool Unwrap(std::wstring& value) 48 { 49 if (value.length() >= 2) 50 { 51 bool unwrap = false; 52 53 switch (value[0]) 54 { 55 case L'"': 56 unwrap = value.back() == L'"'; 57 break; 58 59 case L'(': 60 unwrap = value.back() == L')'; 61 break; 62 } 63 64 if (unwrap) 65 { 66 value = value.substr(1, value.length() - 2); 67 return true; 68 } 69 } 70 71 return false; 72 } 73 74 // Removes all matches from the input string. 75 static bool Remove(const Regex::Expression& re, std::wstring& input) 76 { 77 std::wstring output = re.Replace(input, {}); 78 bool result = (output != input); 79 input = std::move(output); 80 return result; 81 } 82 83 // Removes the architecture and returns the value, if any 84 Architecture RemoveArchitecture(std::wstring& value) const 85 { 86 Architecture result = Architecture::Unknown; 87 88 // Must detect this first because "32/64 bit" is a superstring of "64 bit" 89 if (Remove(Architecture32Or64Bit, value)) 90 { 91 // If the program is 32 and 64 bit in the same installer, leave as unknown. 92 } 93 // Must detect 64 bit before 32 bit because of "x86-64" being a superstring of "x86" 94 else if (Remove(ArchitectureX64, value) || Remove(Architecture64Bit, value)) 95 { 96 result = Architecture::X64; 97 } 98 else if (Remove(ArchitectureX32, value) || Remove(Architecture32Bit, value)) 99 { 100 result = Architecture::X86; 101 } 102 103 return result; 104 } 105 106 // Removes all matches for the given regular expressions 107 static bool RemoveAll(const std::vector<Regex::Expression*>& regexes, std::wstring& value) 108 { 109 bool result = false; 110 111 for (const auto& re : regexes) 112 { 113 result = Remove(*re, value) || result; 114 } 115 116 return result; 117 } 118 119 // Removes all locales and returns the common value, if any 120 std::wstring RemoveLocale(std::wstring& value) const 121 { 122 bool localeFound = false; 123 std::wstring result; 124 125 std::wstring newValue; 126 auto newValueInserter = std::back_inserter(newValue); 127 128 Locale.ForEach(value, 129 [&](bool isMatch, std::wstring_view text) 130 { 131 bool copy = !isMatch; 132 133 if (isMatch) 134 { 135 std::wstring foldedText = ConvertToUTF16(FoldCase(text)); 136 137 // Ensure that the value is in the locale list 138 auto bound = std::lower_bound(Locales.begin(), Locales.end(), foldedText); 139 140 if (bound == Locales.end() || *bound != foldedText) 141 { 142 // Match was not a locale in our list, so copy it out 143 copy = true; 144 } 145 else if (!localeFound) 146 { 147 // First/only match, just extract the value 148 result = foldedText; 149 localeFound = true; 150 } 151 else if (!result.empty()) 152 { 153 // For some reason, there are multiple locales listed. 154 // See if they have anything in common. 155 if (result != foldedText) 156 { 157 // Not completely the same (expected), see if they are at least the same language 158 result.erase(result.find(L'-')); 159 foldedText.erase(foldedText.find(L'-')); 160 161 if (result != foldedText) 162 { 163 // Not the same language, abandon having a locale and just clean them 164 result.clear(); 165 } 166 } 167 } 168 } 169 170 if (copy) 171 { 172 std::copy(text.begin(), text.end(), newValueInserter); 173 } 174 175 return true; 176 }); 177 178 value = std::move(newValue); 179 180 return result; 181 } 182 183 // Splits the string based on the regex matches, excluding empty/whitespace strings 184 // and any values found in the exclusions. 185 static std::vector<std::wstring> Split(const Regex::Expression& re, const std::wstring& value, const std::vector<std::wstring>& exclusions, bool stopOnExclusion = false) 186 { 187 std::vector<std::wstring> result; 188 189 re.ForEach(value, 190 [&](bool, std::wstring_view text) 191 { 192 if (IsEmptyOrWhitespace(text)) 193 { 194 return true; 195 } 196 197 // Do not stop for an exclusion if it is the first word found 198 if (!result.empty()) 199 { 200 std::wstring foldedText = ConvertToUTF16(FoldCase(text)); 201 202 auto bound = std::lower_bound(exclusions.begin(), exclusions.end(), foldedText); 203 204 if (bound != exclusions.end() && *bound == foldedText) 205 { 206 return !stopOnExclusion; 207 } 208 } 209 210 result.emplace_back(std::wstring{ text }); 211 return true; 212 }); 213 214 return result; 215 } 216 217 // Joins all of the given strings into a single value 218 static std::wstring Join(const std::vector<std::wstring>& values, const std::wstring& separator = {}) 219 { 220 std::wstring result; 221 222 bool isFirst = true; 223 for (const auto& v : values) 224 { 225 if (isFirst) 226 { 227 isFirst = false; 228 } 229 else 230 { 231 result += separator; 232 } 233 234 result += v; 235 } 236 237 return result; 238 } 239 240 static constexpr Regex::Options reOptions = Regex::Options::CaseInsensitive; 241 242 // Architecture 243 Regex::Expression ArchitectureX32{ R"((?<=^|[^\p{L}\p{Nd}])(X32|X86)(?=\P{Nd}|$)(?:\sEDITION)?)", reOptions }; 244 Regex::Expression ArchitectureX64{ R"((?<=^|[^\p{L}\p{Nd}])(X64|AMD64|X86([\p{Pd}\p{Pc}]64))(?=\P{Nd}|$)(?:\sEDITION)?)", reOptions }; 245 Regex::Expression Architecture32Bit{ R"((?<=^|[^\p{L}\p{Nd}])(32[\p{Pd}\p{Pc}\p{Z}]?BIT)S?(?:\sEDITION)?)", reOptions }; 246 Regex::Expression Architecture64Bit{ R"((?<=^|[^\p{L}\p{Nd}])(64[\p{Pd}\p{Pc}\p{Z}]?BIT)S?(?:\sEDITION)?)", reOptions }; 247 Regex::Expression Architecture32Or64Bit{ R"((?<=^|[^\p{L}\p{Nd}])((64[\\\/]32|32[\\\/]64)[\p{Pd}\p{Pc}\p{Z}]?BIT)S?(?:\sEDITION)?)", reOptions }; 248 249 // Locale 250 Regex::Expression Locale{ R"((?<![A-Z])((?:\p{Lu}{2,3}(-(CANS|CYRL|LATN|MONG))?-\p{Lu}{2})(?![A-Z])(?:-VALENCIA)?))", reOptions }; 251 252 // Specifically for SAP Business Objects programs 253 Regex::Expression SAPPackage{ R"(^(?:[\p{Lu}\p{Nd}]+[\._])+[\p{Lu}\p{Nd}]+(?:-(?:\p{Nd}+\.)+\p{Nd}+)(?:-(?:\p{Lu}{2}(?:_\p{Lu}{2})?|CORE))(?:-(?:\p{Lu}{2}|\p{Nd}{2}))$)", reOptions }; 254 255 // Extract KB numbers from their parens to preserve them 256 Regex::Expression KBNumbers{ R"(\((KB\d+)\))", reOptions }; 257 258 Regex::Expression NonLettersAndDigits{ R"([^\p{L}\p{Nd}])", reOptions }; 259 Regex::Expression NonLetterDigitOrSpace{ R"([^\p{L}\p{Nd}\s])", reOptions }; 260 Regex::Expression URIProtocol{ R"((?<!\p{L})(?:http[s]?|ftp):\/\/)", reOptions }; // remove protocol from URIs 261 262 Regex::Expression VersionDelimited{ R"(((?<!\p{L})(?:V|VER|VERSI(?:O|Ó)N|VERSÃO|VERSIE|WERSJA|BUILD|RELEASE|RC|SP)\P{L}?)?\p{Nd}+([\p{Po}\p{Pd}\p{Pc}]\p{Nd}?(RC|B|A|R|SP|K)?\p{Nd}+)+([\p{Po}\p{Pd}\p{Pc}]?[\p{L}\p{Nd}]+)*)", reOptions }; 263 Regex::Expression Version{ R"((FOR\s)?(?<!\p{L})(?:P|V|R|VER|VERSI(?:O|Ó)N|VERSÃO|VERSIE|WERSJA|BUILD|RELEASE|RC|SP)(?:\P{L}|\P{L}\p{L})?(\p{Nd}|\.\p{Nd})+(?:RC|B|A|R|V|SP)?\p{Nd}?)", reOptions }; 264 Regex::Expression VersionLetter{ R"((?<!\p{L})(?:(?:V|VER|VERSI(?:O|Ó)N|VERSÃO|VERSIE|WERSJA|BUILD|RELEASE|RC|SP)\P{L})?\p{Lu}\p{Nd}+(?:[\p{Po}\p{Pd}\p{Pc}]\p{Nd}+)+)", reOptions }; 265 Regex::Expression NonNestedBracket{ R"(\([^\(\)]*\)|\[[^\[\]]*\])", reOptions }; // remove things in parentheses, if there aren't parentheses nested inside 266 Regex::Expression BracketEnclosed{ R"((?:\p{Ps}.*\p{Pe}|".*"))", reOptions }; // Impossible to properly handle nested parens with regex 267 Regex::Expression LeadingSymbols{ R"(^[^\p{L}\p{Nd}]+)", reOptions }; // remove symbols at the beginning 268 Regex::Expression TrailingNonLetters{ R"(\P{L}+$)", reOptions }; // remove non-letters at the end 269 Regex::Expression PrefixParens{ R"(^\(.*?\))", reOptions }; // remove things in parentheses at the front of program names 270 Regex::Expression EmptyParens{ R"((\(\s*\)|\[\s*\]|"\s*"))", reOptions }; // remove appearances of (), [], and "", with any number of spaces within 271 Regex::Expression EN{ R"(\sEN\s*$)", reOptions }; // remove appearances of EN (represents English language) at the ends of program names 272 Regex::Expression TrailingSymbols{ R"([^\p{L}\p{Nd}]+$)", reOptions }; // remove all non-letter/numbers at the end 273 Regex::Expression FilePath{ R"(((INSTALLED\sAT|IN)\s)?[CDEF]:\\(.+?\\)*[^\s]*\\?)", reOptions }; // remove file paths 274 Regex::Expression FilePathGHS{ R"(\(CHANGE #\d{1,2} TO [CDEF]:\\(.+?\\)*[^\s]*\\?\))", reOptions }; // remove file paths in certain Green Hills Software program names 275 Regex::Expression FilePathParens{ R"(\([CDEF]:\\(.+?\\)*[^\s]*\\?\))", reOptions }; // remove file paths within parentheses 276 Regex::Expression FilePathQuotes{ R"("[CDEF]:\\(.+?\\)*[^\s]*\\?")", reOptions }; // remove file paths within quotes 277 Regex::Expression Roblox{ R"((?<=^ROBLOX\s(PLAYER|STUDIO))(\sFOR\s.*))", reOptions }; // for Roblox programs 278 Regex::Expression Bomgar{ R"((?<=^BOMGAR\s(JUMP CLIENT|(ACCESS|REPRESENTATIVE) CONSOLE|BUTTON)|^EMBEDDED CALLBACK)(\s.*))", reOptions }; // for Bomgar programs 279 Regex::Expression AcronymSeparators{ R"((?:(?<=^\p{L})|(?<=\P{L}\p{L}))(\.|\/)(?=\p{L}(?:\P{L}|$)))", reOptions }; 280 Regex::Expression NonLetters{ R"((?<=^|\s)[^\p{L}]+(?=\s|$))", reOptions }; // remove all non-letters not attached to 281 Regex::Expression ProgramNameSplit{ R"([^\p{L}\p{Nd}\+\&])", reOptions }; // used to separate 'words' in program names 282 Regex::Expression PublisherNameSplit{ R"([^\p{L}\p{Nd}])", reOptions }; // used to separate 'words' in publisher names 283 284 const std::vector<Regex::Expression*> ProgramNameRegexes 285 { 286 &Roblox, 287 &Bomgar, 288 &PrefixParens, 289 &EmptyParens, 290 &FilePathGHS, 291 &FilePathParens, 292 &FilePathQuotes, 293 &FilePath, 294 &VersionLetter, 295 &VersionDelimited, 296 &Version, 297 &EN, 298 &NonNestedBracket, 299 &BracketEnclosed, 300 &URIProtocol, 301 &LeadingSymbols, 302 &TrailingSymbols 303 }; 304 305 const std::vector<Regex::Expression*> PublisherNameRegexes 306 { 307 &VersionDelimited, 308 &Version, 309 &NonNestedBracket, 310 &BracketEnclosed, 311 &URIProtocol, 312 &NonLetters, 313 &TrailingNonLetters, 314 &AcronymSeparators 315 }; 316 317 // Add values here but use Locales in code. 318 const std::vector<std::wstring_view> LocaleViews 319 { 320 L"AF-ZA", L"AM-ET", L"AR-AE", L"AR-BH", L"AR-DZ", L"AR-EG", L"AR-IQ", L"AR-JO", L"AR-KW", L"AR-LB", L"AR-LY", 321 L"AR-MA", L"ARN-CL", L"AR-OM", L"AR-QA", L"AR-SA", L"AR-SY", L"AR-TN", L"AR-YE", L"AS-IN", L"BA-RU", L"BE-BY", 322 L"BG-BG", L"BN-BD", L"BN-IN", L"BO-CN", L"BR-FR", L"CA-ES", L"CA-ES-VALENCIA", 323 L"CO-FR", L"CS-CZ", L"CY-GB", L"DA-DK", L"DE-AT", 324 L"DE-CH", L"DE-DE", L"DE-LI", L"DE-LU", L"DSB-DE", L"DV-MV", L"EL-GR", L"EN-AU", L"EN-BZ", L"EN-CA", L"EN-GB", 325 L"EN-IE", L"EN-IN", L"EN-JM", L"EN-MY", L"EN-NZ", L"EN-PH", L"EN-SG", L"EN-TT", L"EN-US", L"EN-ZA", L"EN-ZW", 326 L"ES-AR", L"ES-BO", L"ES-CL", L"ES-CO", L"ES-CR", L"ES-DO", L"ES-EC", L"ES-ES", L"ES-GT", L"ES-HN", L"ES-MX", 327 L"ES-NI", L"ES-PA", L"ES-PE", L"ES-PR", L"ES-PY", L"ES-SV", L"ES-US", L"ES-UY", L"ES-VE", L"ET-EE", L"EU-ES", 328 L"FA-IR", L"FI-FI", L"FIL-PH", L"FO-FO", L"FR-BE", L"FR-CA", L"FR-CH", L"FR-FR", L"FR-LU", L"FR-MC", L"FY-NL", 329 L"GA-IE", L"GD-DB", L"GL-ES", L"GSW-FR", L"GU-IN", L"HE-IL", L"HI-IN", L"HR-BA", L"HR-HR", L"HSB-DE", L"HU-HU", 330 L"HY-AM", L"ID-ID", L"IG-NG", L"II-CN", L"IS-IS", L"IT-CH", L"IT-IT", L"JA-JP", L"KA-GE", L"KK-KZ", L"KL-GL", 331 L"KM-KH", L"KN-IN", L"KOK-IN", L"KO-KR", L"KY-KG", L"LB-LU", L"LO-LA", L"LT-LT", L"LV-LV", L"MI-NZ", L"MK-MK", 332 L"ML-IN", L"MN-MN", L"MOH-CA", L"MR-IN", L"MS-BN", L"MS-MY", L"MT-MT", L"NB-NO", L"NE-NP", L"NL-BE", L"NL-NL", 333 L"NN-NO", L"NSO-ZA", L"OC-FR", L"OR-IN", L"PA-IN", L"PL-PL", L"PRS-AF", L"PS-AF", L"PT-BR", L"PT-PT", L"QUT-GT", 334 L"QUZ-BO", L"QUZ-EC", L"QUZ-PE", L"RM-CH", L"RO-RO", L"RU-RU", L"RW-RW", L"SAH-RU", L"SA-IN", L"SE-FI", L"SE-NO", 335 L"SE-SE", L"SI-LK", L"SK-SK", L"SL-SI", L"SMA-NO", L"SMA-SE", L"SMJ-NO", L"SMJ-SE", L"SMN-FI", L"SMS-FI", L"SQ-AL", 336 L"SV-FI", L"SV-SE", L"SW-KE", L"SYR-SY", L"TA-IN", L"TE-IN", L"TH-TH", L"TK-TM", L"TN-ZA", L"TR-TR", L"TT-RU", 337 L"UG-CN", L"UK-UA", L"UR-PK", L"VI-VN", L"WO-SN", L"XH-ZA", L"YO-NG", L"ZH-CN", L"ZH-HK", L"ZH-MO", L"ZH-SG", 338 L"ZH-TW", L"ZU-ZA", L"AZ-CYRL-AZ", L"AZ-LATN-AZ", L"BS-CYRL-BA", L"BS-LATN-BA", L"HA-LATN-NG", L"IU-CANS-CA", 339 L"IU-LATN-CA", L"MN-MONG-CN", L"SR-CYRL-BA", L"SR-CYRL-CS", L"SR-CYRL-ME", L"SR-CYRL-RS", L"SR-LATN-BA", 340 L"SR-LATN-CS", L"SR-LATN-ME", L"SR-LATN-RS", L"TG-CYRL-TJ", L"TZM-LATN-DZ", L"UZ-CYRL-UZ", L"UZ-LATN-UZ", 341 }; 342 343 // The folded and sorted version of LocaleViews. 344 const std::vector<std::wstring> Locales; 345 346 // Add values here but use LegalEntitySuffixes in code. 347 const std::vector<std::wstring_view> LegalEntitySuffixViews 348 { 349 // Acronyms 350 L"AB", L"AD", L"AG", L"APS", L"AS", L"ASA", L"BV", L"CO", L"CV", L"DOO", L"eV", L"GES", L"GESMBH", L"GMBH", L"INC", L"KG", 351 L"KS", L"PS", L"LLC", L"LP", L"LTD", L"LTDA", L"MBH", L"NV", L"PLC", L"SL", L"PTY", L"PVT", L"SA", L"SARL", 352 L"SC", L"SCA", L"SL", L"SP", L"SPA", L"SRL", L"SRO", 353 354 // Words 355 L"COMPANY", L"CORP", L"CORPORATION", L"HOLDING", L"HOLDINGS", L"INCORPORATED", L"LIMITED", L"SUBSIDIARY" 356 }; 357 358 // The folded and sorted version of LocaleViews. 359 const std::vector<std::wstring> LegalEntitySuffixes; 360 361 const bool PreserveWhiteSpace; 362 363 static std::vector<std::wstring> FoldAndSort(const std::vector<std::wstring_view>& input) 364 { 365 std::vector<std::wstring> result; 366 std::transform(input.begin(), input.end(), std::back_inserter(result), [](const std::wstring_view wsv) { return Utility::ConvertToUTF16(Utility::FoldCase(wsv)); }); 367 std::sort(result.begin(), result.end()); 368 return result; 369 } 370 371 InterimNameNormalizationResult NormalizeNameInternal(std::string_view name) const 372 { 373 InterimNameNormalizationResult result; 374 result.Name = PrepareForValidation(name); 375 while (Unwrap(result.Name)); // remove wrappers 376 377 // handle (large majority of) SAP Business Object programs 378 if (SAPPackage.IsMatch(result.Name)) 379 { 380 return result; 381 } 382 383 result.Architecture = RemoveArchitecture(result.Name); 384 result.Locale = RemoveLocale(result.Name); 385 386 // Extract KB numbers from their parens and preserve them 387 result.Name = KBNumbers.Replace(result.Name, L"$1"); 388 389 // Repeatedly remove matches for the regexes to create the minimum name 390 while (RemoveAll(ProgramNameRegexes, result.Name)); 391 392 auto tokens = Split(ProgramNameSplit, result.Name, LegalEntitySuffixes); 393 394 // Re-join the tokens and drop all undesired characters 395 if (PreserveWhiteSpace) 396 { 397 result.Name = Join(tokens, L" "); 398 Remove(NonLetterDigitOrSpace, result.Name); 399 } 400 else 401 { 402 result.Name = Join(tokens); 403 Remove(NonLettersAndDigits, result.Name); 404 } 405 406 return result; 407 } 408 409 InterimPublisherNormalizationResult NormalizePublisherInternal(std::string_view publisher) const 410 { 411 InterimPublisherNormalizationResult result; 412 413 result.Publisher = PrepareForValidation(publisher); 414 while (Unwrap(result.Publisher)); // remove wrappers 415 416 while (RemoveAll(PublisherNameRegexes, result.Publisher)); 417 418 auto tokens = Split(PublisherNameSplit, result.Publisher, LegalEntitySuffixes, true); 419 420 // Re-join the tokens and drop all undesired characters 421 if (PreserveWhiteSpace) 422 { 423 result.Publisher = Join(tokens, L" "); 424 Remove(NonLetterDigitOrSpace, result.Publisher); 425 } 426 else 427 { 428 result.Publisher = Join(tokens); 429 Remove(NonLettersAndDigits, result.Publisher); 430 } 431 432 return result; 433 } 434 435 public: 436 NormalizationInitial(bool preserveWhiteSpace) : Locales(FoldAndSort(LocaleViews)), LegalEntitySuffixes(FoldAndSort(LegalEntitySuffixViews)), PreserveWhiteSpace(preserveWhiteSpace) 437 { 438 } 439 440 NormalizedName Normalize(std::string_view name, std::string_view publisher) const override 441 { 442 InterimNameNormalizationResult nameResult = NormalizeNameInternal(name); 443 InterimPublisherNormalizationResult pubResult = NormalizePublisherInternal(publisher); 444 445 NormalizedName result; 446 result.Name(ConvertToUTF8(nameResult.Name)); 447 result.Architecture(nameResult.Architecture); 448 result.Locale(ConvertToUTF8(nameResult.Locale)); 449 result.Publisher(ConvertToUTF8(pubResult.Publisher)); 450 451 return result; 452 } 453 454 NormalizedName NormalizeName(std::string_view name) const override 455 { 456 InterimNameNormalizationResult nameResult = NormalizeNameInternal(name); 457 458 NormalizedName result; 459 result.Name(ConvertToUTF8(nameResult.Name)); 460 result.Architecture(nameResult.Architecture); 461 result.Locale(ConvertToUTF8(nameResult.Locale)); 462 463 return result; 464 } 465 466 std::string NormalizePublisher(std::string_view publisher) const override 467 { 468 InterimPublisherNormalizationResult pubResult = NormalizePublisherInternal(publisher); 469 470 return ConvertToUTF8(pubResult.Publisher); 471 } 472 }; 473 } 474 475 NameNormalizer::NameNormalizer(NormalizationVersion version) 476 { 477 switch (version) 478 { 479 case AppInstaller::Utility::NormalizationVersion::Initial: 480 m_normalizer = std::make_unique<NormalizationInitial>(false); 481 break; 482 case AppInstaller::Utility::NormalizationVersion::InitialPreserveWhiteSpace: 483 m_normalizer = std::make_unique<NormalizationInitial>(true); 484 break; 485 default: 486 THROW_HR(E_INVALIDARG); 487 } 488 } 489 490 NormalizedName NameNormalizer::Normalize(std::string_view name, std::string_view publisher) const 491 { 492 return m_normalizer->Normalize(name, publisher); 493 } 494 495 NormalizedName NameNormalizer::NormalizeName(std::string_view name) const 496 { 497 return m_normalizer->NormalizeName(name); 498 } 499 500 std::string NameNormalizer::NormalizePublisher(std::string_view publisher) const 501 { 502 return m_normalizer->NormalizePublisher(publisher); 503 } 504 505 std::string NormalizedName::GetNormalizedName(NormalizationField fieldsToInclude) const 506 { 507 std::string result = Name(); 508 509 if (WI_IsFlagSet(fieldsToInclude, NormalizationField::Architecture) && m_arch != Utility::Architecture::Unknown) 510 { 511 result += '(' + std::string(Utility::ToString(m_arch)) + ')'; 512 } 513 514 return result; 515 } 516 517 NormalizationField NormalizedName::GetNormalizedFields() const 518 { 519 NormalizationField result = NormalizationField::None; 520 521 if (m_arch != Utility::Architecture::Unknown) 522 { 523 result |= NormalizationField::Architecture; 524 } 525 526 return result; 527 } 528 }