winget-cli

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

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 }