MsiExecArguments.cpp (22353B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "Public/winget/MsiExecArguments.h" 5 #include "Public/AppInstallerErrors.h" 6 #include "Public/AppInstallerLogging.h" 7 #include "Public/AppInstallerStrings.h" 8 9 10 namespace AppInstaller::Msi 11 { 12 using namespace std::string_view_literals; 13 14 namespace 15 { 16 const char MsiExecQuietOption = 'q'; 17 const char MsiExecLogOption = 'l'; 18 19 // Description of how a long option is replaced by a short option. 20 struct TokenReplacement 21 { 22 TokenReplacement(std::string_view longOption, std::string_view shortOption) : LongOption(longOption), ShortOption({ shortOption }) {} 23 TokenReplacement(std::string_view longOption, std::vector<std::string_view>&& shortOption) : LongOption(longOption), ShortOption(std::move(shortOption)) {} 24 std::string_view LongOption; 25 std::vector<std::string_view> ShortOption; 26 }; 27 28 // Determines whether an argument token is a switch/option. 29 bool IsSwitch(std::string_view token) 30 { 31 THROW_HR_IF(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR, token.empty()); 32 return token[0] == '-' || token[0] == '/'; 33 } 34 35 // Parses the log mode and log file for the Log (/l) option. 36 // The option has a modifier specifying the log mode (what is logged) 37 // and a value specifying the log file. 38 // E.g. /l* log.txt, /lw warnings.txt 39 void ParseLogOption(std::string_view logModeString, std::string_view logFile, MsiParsedArguments& parsedArgs) 40 { 41 if (Utility::IsEmptyOrWhitespace(logFile)) 42 { 43 AICLI_LOG(Core, Error, << "MSI log file path cannot be empty"); 44 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 45 } 46 47 INSTALLLOGMODE logMode = {}; 48 INSTALLLOGATTRIBUTES logAttributes = {}; 49 50 // Note: These flags are mostly consecutive bits in the order given, except where indicated. 51 // Skipped flags are not mapped to a command line option. 52 std::map<char, INSTALLLOGMODE> ValidLogModes 53 { 54 { 'm', INSTALLLOGMODE_FATALEXIT }, 55 { 'e', INSTALLLOGMODE_ERROR }, 56 { 'w', INSTALLLOGMODE_WARNING }, 57 { 'u', INSTALLLOGMODE_USER }, 58 { 'i', INSTALLLOGMODE_INFO }, 59 // FILESINUSE 60 // RESOLVESOURCE 61 { 'o', INSTALLLOGMODE_OUTOFDISKSPACE }, 62 { 'a', INSTALLLOGMODE_ACTIONSTART }, 63 { 'r', INSTALLLOGMODE_ACTIONDATA }, 64 { 'p', INSTALLLOGMODE_PROPERTYDUMP }, 65 { 'c', INSTALLLOGMODE_COMMONDATA }, 66 { 'v', INSTALLLOGMODE_VERBOSE }, 67 { 'x', INSTALLLOGMODE_EXTRADEBUG }, 68 // LOGONLYONERROR 69 // LOGPERFORMANCE 70 }; 71 72 std::map<char, INSTALLLOGATTRIBUTES> ValidLogAttributes 73 { 74 { '+', INSTALLLOGATTRIBUTES_APPEND }, 75 { '!', INSTALLLOGATTRIBUTES_FLUSHEACHLINE }, 76 }; 77 78 bool isLogModeSet = false; 79 for (char c : logModeString) 80 { 81 // Log-all option 82 if (c == '*') 83 { 84 logMode |= AllLogMode; 85 isLogModeSet = true; 86 continue; 87 } 88 89 auto modeItr = ValidLogModes.find(c); 90 if (modeItr != ValidLogModes.end()) 91 { 92 logMode |= modeItr->second; 93 isLogModeSet = true; 94 continue; 95 } 96 97 auto attributeItr = ValidLogAttributes.find(c); 98 if (attributeItr != ValidLogAttributes.end()) 99 { 100 logAttributes |= attributeItr->second; 101 continue; 102 } 103 104 AICLI_LOG(Core, Error, << "Unknown msiexec log modifier: " << c); 105 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 106 } 107 108 if (!isLogModeSet) 109 { 110 logMode = DefaultLogMode; 111 } 112 113 parsedArgs.LogMode = logMode; 114 parsedArgs.LogAttributes = logAttributes; 115 parsedArgs.LogFile = Utility::ConvertToUTF16(logFile); 116 } 117 118 // Parses the modifier for the UI Level option (/q) 119 // The modifier starts with a base (b, f, n, r), followed by extra flags (+, -, !). 120 // E.g. /qn, /qb-! 121 void ParseQuietOption(std::string_view modifier, MsiParsedArguments& parsedArgs) 122 { 123 if (modifier.empty()) 124 { 125 // /q is treated as equivalent to /qn 126 modifier = "n"sv; 127 } 128 129 // Lower values in INSTALLUILEVEL work like a base enum (e.g. None=2, Basic=3) 130 // with higher values being modifying flags (e.g. HideCancel=0x20, ProgressOnly=0x40). 131 // Some steps depend on the base enum, so we keep it separate for easier checking. 132 INSTALLUILEVEL uiLevelBase = {}; 133 INSTALLUILEVEL uiLevelModifiers = {}; 134 135 // Parse the base level 136 switch (std::tolower(modifier[0])) 137 { 138 case 'f': 139 uiLevelBase = INSTALLUILEVEL_FULL; 140 break; 141 case 'r': 142 uiLevelBase = INSTALLUILEVEL_REDUCED; 143 break; 144 case 'b': 145 uiLevelBase = INSTALLUILEVEL_BASIC; 146 break; 147 case '+': 148 uiLevelBase = INSTALLUILEVEL_NONE; 149 uiLevelModifiers = INSTALLUILEVEL_ENDDIALOG; 150 break; 151 case 'n': 152 uiLevelBase = INSTALLUILEVEL_NONE; 153 break; 154 default: 155 AICLI_LOG(Core, Error, << "Invalid modifier for msiexec /q argument: " << modifier); 156 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 157 }; 158 159 // Parse the modifiers 160 for (size_t i = 1; i < modifier.size(); ++i) 161 { 162 const char c = modifier[i]; 163 164 if (c == '+') 165 { 166 WI_SetFlag(uiLevelModifiers, INSTALLUILEVEL_ENDDIALOG); 167 } 168 else if (c == '-') 169 { 170 if (uiLevelBase == INSTALLUILEVEL_BASIC) 171 { 172 WI_SetFlag(uiLevelModifiers, INSTALLUILEVEL_PROGRESSONLY); 173 } 174 else 175 { 176 AICLI_LOG(Core, Error, << "msiexec UI option Progress Only (-) is only valid with UI level Basic (b)"); 177 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 178 } 179 } 180 else if (c == '!') 181 { 182 if (uiLevelBase == INSTALLUILEVEL_BASIC) 183 { 184 WI_SetFlag(uiLevelModifiers, INSTALLUILEVEL_HIDECANCEL); 185 } 186 else 187 { 188 AICLI_LOG(Core, Error, << "msiexec UI option Hide Cancel (!) is only valid with UI level Basic (b)"); 189 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 190 } 191 } 192 } 193 194 // Only deviation from msiexec: 195 // When using UI Level None, allow showing the UAC prompt. 196 WI_SetFlagIf(uiLevelModifiers, INSTALLUILEVEL_UACONLY, uiLevelBase == INSTALLUILEVEL_NONE); 197 198 parsedArgs.UILevel = uiLevelBase | uiLevelModifiers; 199 } 200 201 bool IsWhiteSpace(char c) 202 { 203 return c == ' ' || c == '\t'; 204 } 205 206 // Gets the next token found in the arguments string, starting the search on the given position. 207 // If there are no more tokens, return empty. 208 // After finding the token, updates `start` to point to the next place we need to start the next token search. 209 std::string_view GetNextToken(std::string_view arguments, size_t& start) 210 { 211 // Eat leading whitespace 212 while (start < arguments.size() && IsWhiteSpace(arguments[start])) 213 { 214 ++start; 215 } 216 217 if (start >= arguments.size()) 218 { 219 // We reached the end 220 return {}; 221 } 222 223 size_t pos = start; 224 bool seekingSpaceSeparator = ('"' != arguments[pos]); 225 bool withinQuotes = false; 226 227 // Start looking from the next character 228 ++pos; 229 230 // Advance until we hit the end or the next separator 231 while (pos < arguments.size()) 232 { 233 bool isSpace = IsWhiteSpace(arguments[pos]); 234 bool isQuote = ('"' == arguments[pos]); 235 236 if (isSpace || isQuote) 237 { 238 // We've encountered one of the two separators we're interested in 239 if (seekingSpaceSeparator) 240 { 241 if (isQuote) 242 { 243 // We will ignore space characters enclosed between double quotes 244 withinQuotes = !withinQuotes; 245 } 246 else 247 { 248 // This is a space character. If it is between quotes we ignore it; 249 // otherwise it is a separator. 250 if (!withinQuotes) 251 { 252 break; 253 } 254 } 255 } 256 else 257 { 258 if (isQuote) 259 { 260 // we've got what we needed, it is OK to stop 261 break; 262 } 263 } 264 } 265 266 ++pos; 267 } 268 269 if (!seekingSpaceSeparator) 270 { 271 // We were looking for a terminating " character. 272 if (pos < arguments.size()) 273 { 274 // We move past the " character (it is OK for the end of the line 275 // to act as the matching " character in some cases) 276 ++pos; 277 } 278 } 279 280 auto result = arguments.substr(start, pos - start); 281 start = pos; 282 return result; 283 } 284 285 // Split the arguments string into tokens. Tokens are delimited by whitespace 286 // unless quoted. Each token represents an option (like /q), an argument 287 // for an option, or a property. 288 std::list<std::string> TokenizeMsiArguments(std::string_view arguments) 289 { 290 size_t start = 0; 291 std::list<std::string> result; 292 auto token = GetNextToken(arguments, start); 293 while (!token.empty()) 294 { 295 result.emplace_back(token); 296 token = GetNextToken(arguments, start); 297 } 298 299 return result; 300 } 301 302 // Parses a token that represents an argument to an option. 303 // If the value is unquoted, returns it as is. 304 // If the value is quoted, removes the quotes and replaces escaped characters. 305 std::string ParseValue(std::string_view valueToken) 306 { 307 if (valueToken.empty() || valueToken[0] != '"') 308 { 309 // Nothing to do for empty or unquoted tokens 310 return std::string{ valueToken }; 311 } 312 313 // Copy the string ignoring the quotes and replacing escaped characters. 314 // In quoted tokens, the back quote represents double quotes (` means ") 315 // and can be escaped with back slash (\` means `). 316 // Note that we accept quoted values with a missing closing quote (the end 317 // of string signals the end). 318 std::string result; 319 for (size_t i = 1; i < valueToken.size(); ++i) 320 { 321 if (valueToken[i] == '"') 322 { 323 // The tokenizer can leave several pairs of quotes in the token 324 // but they are not accepted in this case. We only accept the final 325 // closing quotes. 326 if (i + 1 == valueToken.size()) 327 { 328 break; 329 } 330 else 331 { 332 AICLI_LOG(Core, Error, << "Invalid msiexec argument: " << valueToken); 333 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 334 } 335 } 336 337 if (i + 1 < valueToken.size() && valueToken[i] == '\\' && valueToken[i + 1] == '`') 338 { 339 result += '`'; 340 ++i; 341 } 342 else if (valueToken[i] == '`') 343 { 344 result += '"'; 345 } 346 else 347 { 348 result += valueToken[i]; 349 } 350 } 351 352 return result; 353 } 354 355 // Validates that a token represents a property. 356 // This checks that the property has the form PropertyName=Value, 357 // with the value optionally quoted. 358 bool IsValidPropertyToken(std::string_view token) 359 { 360 THROW_HR_IF(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR, token.empty()); 361 362 if (token[0] != '%' && !IsCharAlphaNumericA(token[0])) 363 { 364 AICLI_LOG(Core, Error, << "Bad property for msiexec: " << token); 365 return false; 366 } 367 368 // Find the = separator at the end of the property name 369 size_t pos = 0; 370 while (pos < token.size() && !IsWhiteSpace(token[pos]) && token[pos] != '=') 371 { 372 ++pos; 373 } 374 375 if (pos == token.size() || token[pos] != '=') 376 { 377 AICLI_LOG(Core, Error, << "Expected property for call to msiexec, but couldn't find separator: " << token); 378 return false; 379 } 380 381 // Validate the property value. 382 // It should be completely enclosed in quotes, or not contain white space. 383 // If quoted, there can be pairs of consecutive quotes that work as escape sequences. 384 // We accept empty property values. 385 ++pos; 386 if (pos == token.size()) 387 { 388 // Empty value 389 return true; 390 } 391 392 // If quoted, we will only inspect the values between the quotes. 393 bool quoted = false; 394 size_t end = token.size(); 395 if (token[pos] == '"') 396 { 397 ++pos; 398 399 if (pos >= end || token.back() != '"') 400 { 401 AICLI_LOG(Core, Error, << "Badly quoted msiexec property: " << token); 402 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 403 } 404 405 --end; 406 quoted = true; 407 } 408 409 while (pos < end) 410 { 411 if (quoted) 412 { 413 // For quoted values, any internal quote must be followed by another one. 414 if (token[pos] == '"') 415 { 416 if (pos + 1 < end && token[pos + 1] == '"') 417 { 418 // Skip the two quotes 419 ++pos; 420 } 421 else 422 { 423 AICLI_LOG(Core, Error, << "Unexpected quotes in msiexec property arg: " << token); 424 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 425 } 426 } 427 } 428 else 429 { 430 // For unquoted values, we only check that there is no whitespace 431 if (IsWhiteSpace(token[pos])) 432 { 433 AICLI_LOG(Core, Error, << "Unexpected space in msiexec property arg: " << token); 434 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 435 } 436 } 437 438 ++pos; 439 } 440 441 return true; 442 } 443 444 // Replaces long options in the arguments (e.g. /quiet), by their short equivalents 445 // (e.g. /qn). The replacement is done in-place. 446 void ReplaceLongOptions(std::list<std::string>& tokens) 447 { 448 // We don't handle all possible options because we don't need to. 449 // Options not handled: 450 // /update 451 // /uninstall 452 // /package 453 // /help 454 const std::vector<TokenReplacement> Replacements 455 { 456 { "quiet"sv, "/qn"sv }, 457 { "passive"sv, { "/qb!-"sv, "REBOOTPROMPT=S"sv } }, 458 { "norestart"sv, "REBOOT=ReallySuppress"sv }, 459 { "forcerestart"sv, "REBOOT=Force"sv }, 460 { "promptrestart"sv, "REBOOTPROMPT=\"\""sv }, 461 { "log"sv, "/l*"sv }, 462 }; 463 464 auto itr = tokens.begin(); 465 while (itr != tokens.end()) 466 { 467 if (!IsSwitch(*itr)) 468 { 469 // We only need to replace switches. 470 ++itr; 471 continue; 472 } 473 474 // Find if there is a replacement for this option. 475 // We ignore the leading / or - when comparing. 476 auto option = std::string_view(*itr).substr(1); 477 auto replacementItr = std::find_if(Replacements.begin(), Replacements.end(), [&](const TokenReplacement& replacement) { return Utility::CaseInsensitiveEquals(replacement.LongOption, option); }); 478 if (replacementItr == Replacements.end()) 479 { 480 // There is no replacement for this switch; 481 ++itr; 482 continue; 483 } 484 485 // Add all the replacements tokens needed before this one, then delete the existing token. 486 tokens.insert(itr, replacementItr->ShortOption.begin(), replacementItr->ShortOption.end()); 487 488 // Delete the current token an move to the next one. 489 // We don't need to do anything more to the newly added tokens. 490 itr = tokens.erase(itr); 491 } 492 } 493 494 // Consumes the next argument token in the list. If the token is an option 495 // that takes an argument, also consumes it. After consuming the token(s), 496 // removes it from the list and updates the parsed arguments accordingly. 497 void ConsumeNextToken(std::list<std::string>& tokens, MsiParsedArguments& parsedArgs) 498 { 499 THROW_HR_IF(APPINSTALLER_CLI_ERROR_INTERNAL_ERROR, tokens.empty()); 500 501 auto token = std::move(tokens.front()); 502 tokens.pop_front(); 503 if (!IsSwitch(token)) 504 { 505 // Token is a property, i.e. NAME=value. Add it to the parsed args. 506 THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT, !IsValidPropertyToken(token)); 507 parsedArgs.Properties += L" " + Utility::ConvertToUTF16(token); 508 return; 509 } 510 511 // Token is an option. 512 if (token.size() <= 1) 513 { 514 AICLI_LOG(Core, Error, << "Invalid command line argument for msiexec: " << token); 515 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 516 } 517 518 char option = token[1]; 519 auto optionModifier = ParseValue(std::string_view(token).substr(2)); 520 521 // Options are case-insensitive 522 switch (std::tolower(option)) 523 { 524 case MsiExecQuietOption: 525 { 526 ParseQuietOption(optionModifier, parsedArgs); 527 break; 528 } 529 case MsiExecLogOption: 530 { 531 if (tokens.empty()) 532 { 533 // Log option must be followed by an option argument 534 AICLI_LOG(Core, Error, << "msiexec option " << token << " must be followed by a value"); 535 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 536 } 537 538 const auto optionValue = ParseValue(tokens.front()); 539 tokens.pop_front(); 540 541 ParseLogOption(optionModifier, optionValue, parsedArgs); 542 break; 543 } 544 default: 545 { 546 AICLI_LOG(Core, Error, << "Invalid option for msiexec: " << token); 547 THROW_HR(APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT); 548 } 549 } 550 } 551 } 552 553 MsiParsedArguments ParseMSIArguments(std::string_view arguments) 554 { 555 // Split the arguments into tokens, which we will process one by one. 556 auto argumentTokens = TokenizeMsiArguments(arguments); 557 558 // Replace long options so we can work only with short ones. 559 ReplaceLongOptions(argumentTokens); 560 561 // Process the arguments. 562 MsiParsedArguments result; 563 while (!argumentTokens.empty()) 564 { 565 ConsumeNextToken(argumentTokens, result); 566 } 567 568 return result; 569 } 570 }