diff options
Diffstat (limited to 'toolkit/xre/CmdLineAndEnvUtils.h')
-rw-r--r-- | toolkit/xre/CmdLineAndEnvUtils.h | 731 |
1 files changed, 731 insertions, 0 deletions
diff --git a/toolkit/xre/CmdLineAndEnvUtils.h b/toolkit/xre/CmdLineAndEnvUtils.h new file mode 100644 index 0000000000..6d26c9b45c --- /dev/null +++ b/toolkit/xre/CmdLineAndEnvUtils.h @@ -0,0 +1,731 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_CmdLineAndEnvUtils_h +#define mozilla_CmdLineAndEnvUtils_h + +// NB: This code may be used outside of xul and thus must not depend on XPCOM + +#if defined(MOZILLA_INTERNAL_API) +# include "prenv.h" +# include "prprf.h" +# include <string.h> +#endif + +#if defined(XP_WIN) +# include "mozilla/UniquePtr.h" +# include "mozilla/Vector.h" +# include "mozilla/WinHeaderOnlyUtils.h" + +# include <wchar.h> +# include <windows.h> +#endif // defined(XP_WIN) + +#include "mozilla/Maybe.h" +#include "mozilla/MemoryChecking.h" +#include "mozilla/TypedEnumBits.h" + +#include <ctype.h> +#include <stdint.h> +#include <stdlib.h> + +#ifndef NS_NO_XPCOM +# include "nsIFile.h" +# include "mozilla/AlreadyAddRefed.h" +#endif + +// Undo X11/X.h's definition of None +#undef None + +namespace mozilla { + +enum ArgResult { + ARG_NONE = 0, + ARG_FOUND = 1, + ARG_BAD = 2 // you wanted a param, but there isn't one +}; + +template <typename CharT> +inline void RemoveArg(int& argc, CharT** argv) { + do { + *argv = *(argv + 1); + ++argv; + } while (*argv); + + --argc; +} + +namespace internal { + +#if 'a' == '\x61' +// Valid option characters must have the same representation in every locale +// (which is true for most of ASCII, barring \x5C and \x7E). +static inline constexpr bool isValidOptionCharacter(char c) { + // We specifically avoid the use of `islower` here; it's locale-dependent, and + // may return true for non-ASCII values in some locales. + return ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || c == '-'; +}; + +// Convert uppercase to lowercase, locale-insensitively. +static inline constexpr char toLowercase(char c) { + // We specifically avoid the use of `tolower` here; it's locale-dependent, and + // may output ASCII values for non-ASCII input (or vice versa) in some + // locales. + return ('A' <= c && c <= 'Z') ? char(c | ' ') : c; +}; + +// Convert a CharT to a char, ensuring that no CharT is mapped to any valid +// option character except the unique CharT naturally corresponding thereto. +template <typename CharT> +static inline constexpr char toNarrow(CharT c) { + // confirmed to compile down to nothing when `CharT` is `char` + return (c & static_cast<CharT>(0xff)) == c ? c : 0xff; +}; +#else +// The target system's character set isn't even ASCII-compatible. If you're +// porting Gecko to such a platform, you'll have to implement these yourself. +# error Character conversion functions not implemented for this platform. +#endif + +// Case-insensitively compare a string taken from the command-line (`mixedstr`) +// to the text of some known command-line option (`lowerstr`). +template <typename CharT> +static inline bool strimatch(const char* lowerstr, const CharT* mixedstr) { + while (*lowerstr) { + if (!*mixedstr) return false; // mixedstr is shorter + + // Non-ASCII strings may compare incorrectly depending on the user's locale. + // Some ASCII-safe characters are also dispermitted for semantic reasons + // and simplicity. + if (!isValidOptionCharacter(*lowerstr)) return false; + + if (toLowercase(toNarrow(*mixedstr)) != *lowerstr) { + return false; // no match + } + + ++lowerstr; + ++mixedstr; + } + + if (*mixedstr) return false; // lowerstr is shorter + + return true; +} + +// Given a command-line argument, return Nothing if it isn't structurally a +// command-line option, and Some(<the option text>) if it is. +template <typename CharT> +mozilla::Maybe<const CharT*> ReadAsOption(const CharT* str) { + if (!str) { + return Nothing(); + } + if (*str == '-') { + str++; + if (*str == '-') { + str++; + } + return Some(str); + } +#ifdef XP_WIN + if (*str == '/') { + return Some(str + 1); + } +#endif + return Nothing(); +} + +} // namespace internal + +using internal::strimatch; + +const wchar_t kCommandLineDelimiter[] = L" \t"; + +enum class CheckArgFlag : uint32_t { + None = 0, + // (1 << 0) Used to be CheckOSInt + RemoveArg = (1 << 1) // Remove the argument from the argv array. +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(CheckArgFlag) + +/** + * Check for a commandline flag. If the flag takes a parameter, the + * parameter is returned in aParam. Flags may be in the form -arg or + * --arg (or /arg on win32). + * + * @param aArgc The argc value. + * @param aArgv The original argv. + * @param aArg the parameter to check. Must be lowercase. + * @param aParam if non-null, the -arg <data> will be stored in this pointer. + * This is *not* allocated, but rather a pointer to the argv data. + * @param aFlags Flags @see CheckArgFlag + */ +template <typename CharT> +inline ArgResult CheckArg(int& aArgc, CharT** aArgv, const char* aArg, + const CharT** aParam = nullptr, + CheckArgFlag aFlags = CheckArgFlag::RemoveArg) { + using internal::ReadAsOption; + MOZ_ASSERT(aArgv && aArg); + + CharT** curarg = aArgv + 1; // skip argv[0] + ArgResult ar = ARG_NONE; + + while (*curarg) { + if (const auto arg = ReadAsOption(*curarg)) { + if (strimatch(aArg, arg.value())) { + if (aFlags & CheckArgFlag::RemoveArg) { + RemoveArg(aArgc, curarg); + } else { + ++curarg; + } + + if (!aParam) { + ar = ARG_FOUND; + break; + } + + if (*curarg) { + if (ReadAsOption(*curarg)) { + return ARG_BAD; + } + + *aParam = *curarg; + + if (aFlags & CheckArgFlag::RemoveArg) { + RemoveArg(aArgc, curarg); + } + + ar = ARG_FOUND; + break; + } + + return ARG_BAD; + } + } + + ++curarg; + } + + return ar; +} + +template <typename CharT> +inline ArgResult CheckArg(int& aArgc, CharT** aArgv, const char* aArg, + std::nullptr_t, + CheckArgFlag aFlags = CheckArgFlag::RemoveArg) { + return CheckArg<CharT>(aArgc, aArgv, aArg, + static_cast<const CharT**>(nullptr), aFlags); +} + +namespace internal { +// template <typename T> +// constexpr bool IsStringRange = +// std::convertible_to<std::ranges::range_value_t<T>, const char *>; + +template <typename CharT, typename ListT> +// requires IsStringRange<ListT> +static bool MatchesAnyOf(CharT const* unknown, ListT const& known) { + for (const char* k : known) { + if (strimatch(k, unknown)) { + return true; + } + } + return false; +} + +template <typename CharT, typename ReqContainerT, typename OptContainerT> +// requires IsStringRange<ReqContainerT> && IsStringRange<OptContainerT> +inline bool EnsureCommandlineSafeImpl(int aArgc, CharT** aArgv, + ReqContainerT const& requiredParams, + OptContainerT const& optionalParams) { + // We expect either no -osint, or the full commandline to be: + // + // app -osint [<optional-param>...] <required-param> <required-argument> + // + // Otherwise, we abort to avoid abuse of other command-line handlers from apps + // that do a poor job escaping links they give to the OS. + // + // Note that the above implies that optional parameters do not themselves take + // arguments. This is a security feature, to prevent the possible injection of + // additional parameters via such arguments. (See, e.g., bug 384384.) + + static constexpr const char* osintLit = "osint"; + + // If "-osint" (or the equivalent) is not present, then this is trivially + // satisfied. + if (CheckArg(aArgc, aArgv, osintLit, nullptr, CheckArgFlag::None) != + ARG_FOUND) { + return true; + } + + // There should be at least 4 items present: + // <app name> -osint <required param> <arg>. + if (aArgc < 4) { + return false; + } + + // The first parameter must be osint. + const auto arg1 = ReadAsOption(aArgv[1]); + if (!arg1) return false; + if (!strimatch(osintLit, arg1.value())) { + return false; + } + // Following this is any number of optional parameters, terminated by a + // required parameter. + int pos = 2; + while (true) { + if (pos >= aArgc) return false; + + auto const arg = ReadAsOption(aArgv[pos]); + if (!arg) return false; + + if (MatchesAnyOf(arg.value(), optionalParams)) { + ++pos; + continue; + } + + if (MatchesAnyOf(arg.value(), requiredParams)) { + ++pos; + break; + } + + return false; + } + + // There must be one argument remaining... + if (pos + 1 != aArgc) return false; + // ... which must not be another option. + if (ReadAsOption(aArgv[pos])) { + return false; + } + + // Nothing ill-formed was passed. + return true; +} + +// C (and so C++) disallows empty arrays. Rather than require callers to jump +// through hoops to specify an empty optional-argument list, allow either its +// omission or its specification as `nullptr`, and do the hoop-jumping here. +// +// No such facility is provided for requiredParams, which must have at least one +// entry. +template <typename CharT, typename ReqContainerT> +inline bool EnsureCommandlineSafeImpl(int aArgc, CharT** aArgv, + ReqContainerT const& requiredParams, + std::nullptr_t _ = nullptr) { + struct { + inline const char** begin() const { return nullptr; } + inline const char** end() const { return nullptr; } + } emptyContainer; + return EnsureCommandlineSafeImpl(aArgc, aArgv, requiredParams, + emptyContainer); +} +} // namespace internal + +template <typename CharT, typename ReqContainerT, + typename OptContainerT = std::nullptr_t> +inline void EnsureCommandlineSafe( + int aArgc, CharT** aArgv, ReqContainerT const& requiredParams, + OptContainerT const& optionalParams = nullptr) { + if (!internal::EnsureCommandlineSafeImpl(aArgc, aArgv, requiredParams, + optionalParams)) { + exit(127); + } +} + +#if defined(XP_WIN) +namespace internal { + +/** + * Attempt to copy the string `s` (considered as a command-line argument) into + * the buffer `d` with all necessary escaping and quoting. Returns the number of + * characters written. + * + * If `d` is NULL, doesn't actually write anything to `d`, and merely returns + * the number of characters that _would_ have been written. + * + * (This moderately-awkward conflation ensures that the pre-allocation counting + * step and post-allocation copying step use the same algorithm.) + */ +inline size_t CopyArgImpl_(wchar_t* d, const wchar_t* s) { + size_t len = 0; + + bool const actuallyCopy = d != nullptr; + auto const appendChar = [&](wchar_t c) { + if (actuallyCopy) { + *d++ = c; + } + len++; + }; + + bool hasDoubleQuote = wcschr(s, L'"') != nullptr; + // Only add doublequotes if... + bool addDoubleQuotes = + // ... the string is empty, or... + *s == '\0' || + // ... the string contains a space or a tab. + wcspbrk(s, kCommandLineDelimiter) != nullptr; + + if (addDoubleQuotes) { + appendChar('"'); + } + + if (hasDoubleQuote) { + size_t backslashes = 0; + while (*s) { + if (*s == '\\') { + ++backslashes; + } else { + if (*s == '"') { + // Escape the doublequote and all backslashes preceding the + // doublequote + for (size_t i = 0; i <= backslashes; ++i) { + appendChar('\\'); + } + } + + backslashes = 0; + } + + appendChar(*s); + ++s; + } + } else { + // optimization: just blit + auto const src_len = wcslen(s); + if (actuallyCopy) { + ::wcscpy(d, s); + d += src_len; + } + len += src_len; + } + + if (addDoubleQuotes) { + appendChar('"'); + } + + return len; +} + +/** + * Compute the space required for the serialized form of this argument. Includes + * any additional space needed for quotes and backslash-escapes. + */ +inline size_t ArgStrLen(const wchar_t* s) { return CopyArgImpl_(nullptr, s); } + +/** + * Copy string "s" to string "d", quoting the argument as appropriate and + * escaping doublequotes along with any backslashes that immediately precede + * doublequotes. + * The CRT parses this to retrieve the original argc/argv that we meant, + * see STDARGV.C in the MSVC CRT sources. + * + * @return the end of the string + */ +inline wchar_t* ArgToString(wchar_t* d, const wchar_t* s) { + return d + CopyArgImpl_(d, s); +} + +} // namespace internal + +/** + * Creates a command line from a list of arguments. + * + * @param argc Number of elements in |argv| + * @param argv Array of arguments + * @param aArgcExtra Number of elements in |aArgvExtra| + * @param aArgvExtra Optional array of arguments to be appended to the resulting + * command line after those provided by |argv|. + */ +inline UniquePtr<wchar_t[]> MakeCommandLine( + int argc, const wchar_t* const* argv, int aArgcExtra = 0, + const wchar_t* const* aArgvExtra = nullptr) { + int i; + size_t len = 0; + + // The + 1 for each argument reserves space for either a ' ' or the null + // terminator, depending on the position of the argument. + for (i = 0; i < argc; ++i) { + len += internal::ArgStrLen(argv[i]) + 1; + } + + for (i = 0; i < aArgcExtra; ++i) { + len += internal::ArgStrLen(aArgvExtra[i]) + 1; + } + + // Protect against callers that pass 0 arguments + if (len == 0) { + len = 1; + } + + auto s = MakeUnique<wchar_t[]>(len); + + int totalArgc = argc + aArgcExtra; + + wchar_t* c = s.get(); + for (i = 0; i < argc; ++i) { + c = internal::ArgToString(c, argv[i]); + if (i + 1 != totalArgc) { + *c = ' '; + ++c; + } + } + + for (i = 0; i < aArgcExtra; ++i) { + c = internal::ArgToString(c, aArgvExtra[i]); + if (i + 1 != aArgcExtra) { + *c = ' '; + ++c; + } + } + + *c = '\0'; + + return s; +} + +inline bool SetArgv0ToFullBinaryPath(wchar_t* aArgv[]) { + if (!aArgv) { + return false; + } + + UniquePtr<wchar_t[]> newArgv_0(GetFullBinaryPath()); + if (!newArgv_0) { + return false; + } + + // We intentionally leak newArgv_0 into argv[0] + aArgv[0] = newArgv_0.release(); + MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(aArgv[0]); + return true; +} + +# if defined(MOZILLA_INTERNAL_API) +// This class converts a command line string into an array of the arguments. +// It's basically the opposite of MakeCommandLine. However, the behavior is +// different from ::CommandLineToArgvW in several ways, such as escaping a +// backslash or quoting an argument containing whitespaces. This satisfies +// the examples at: +// https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args#results-of-parsing-command-lines +// https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85) +template <typename T> +class CommandLineParserWin final { + int mArgc; + T** mArgv; + + void Release() { + if (mArgv) { + while (mArgc) { + delete[] mArgv[--mArgc]; + } + delete[] mArgv; + mArgv = nullptr; + } + } + + public: + CommandLineParserWin() : mArgc(0), mArgv(nullptr) {} + ~CommandLineParserWin() { Release(); } + + CommandLineParserWin(const CommandLineParserWin&) = delete; + CommandLineParserWin(CommandLineParserWin&&) = delete; + CommandLineParserWin& operator=(const CommandLineParserWin&) = delete; + CommandLineParserWin& operator=(CommandLineParserWin&&) = delete; + + int Argc() const { return mArgc; } + const T* const* Argv() const { return mArgv; } + + // Returns the number of characters handled + int HandleCommandLine(const nsTSubstring<T>& aCmdLineString) { + Release(); + + if (aCmdLineString.IsEmpty()) { + return 0; + } + + int justCounting = 1; + // Flags, etc. + int init = 1; + int between, quoted, bSlashCount; + const T* p; + const T* const pEnd = aCmdLineString.EndReading(); + nsTAutoString<T> arg; + + // We loop if we've not finished the second pass through. + while (1) { + // Initialize if required. + if (init) { + p = aCmdLineString.BeginReading(); + between = 1; + mArgc = quoted = bSlashCount = 0; + + init = 0; + } + + const T charCurr = (p < pEnd) ? *p : 0; + const T charNext = (p + 1 < pEnd) ? *(p + 1) : 0; + + if (between) { + // We are traversing whitespace between args. + // Check for start of next arg. + if (charCurr != 0 && !wcschr(kCommandLineDelimiter, charCurr)) { + // Start of another arg. + between = 0; + arg.Truncate(); + switch (charCurr) { + case '\\': + // Count the backslash. + bSlashCount = 1; + break; + case '"': + // Remember we're inside quotes. + quoted = 1; + break; + default: + // Add character to arg. + arg += charCurr; + break; + } + } else { + // Another space between args, ignore it. + } + } else { + // We are processing the contents of an argument. + // Check for whitespace or end. + if (charCurr == 0 || + (!quoted && wcschr(kCommandLineDelimiter, charCurr))) { + // Process pending backslashes (interpret them + // literally since they're not followed by a "). + while (bSlashCount) { + arg += '\\'; + bSlashCount--; + } + // End current arg. + if (!justCounting) { + mArgv[mArgc] = new T[arg.Length() + 1]; + memcpy(mArgv[mArgc], arg.get(), (arg.Length() + 1) * sizeof(T)); + } + mArgc++; + // We're now between args. + between = 1; + } else { + // Still inside argument, process the character. + switch (charCurr) { + case '"': + // First, digest preceding backslashes (if any). + while (bSlashCount > 1) { + // Put one backsplash in arg for each pair. + arg += '\\'; + bSlashCount -= 2; + } + if (bSlashCount) { + // Quote is literal. + arg += '"'; + bSlashCount = 0; + } else { + // Quote starts or ends a quoted section. + if (quoted) { + // Check for special case of consecutive double + // quotes inside a quoted section. + if (charNext == '"') { + // This implies a literal double-quote. Fake that + // out by causing next double-quote to look as + // if it was preceded by a backslash. + bSlashCount = 1; + } else { + quoted = 0; + } + } else { + quoted = 1; + } + } + break; + case '\\': + // Add to count. + bSlashCount++; + break; + default: + // Accept any preceding backslashes literally. + while (bSlashCount) { + arg += '\\'; + bSlashCount--; + } + // Just add next char to the current arg. + arg += charCurr; + break; + } + } + } + + // Check for end of input. + if (charCurr) { + // Go to next character. + p++; + } else { + // If on first pass, go on to second. + if (justCounting) { + // Allocate argv array. + mArgv = new T*[mArgc]; + + // Start second pass + justCounting = 0; + init = 1; + } else { + // Quit. + break; + } + } + } + + return p - aCmdLineString.BeginReading(); + } +}; +# endif // defined(MOZILLA_INTERNAL_API) + +#endif // defined(XP_WIN) + +// SaveToEnv and EnvHasValue are only available on Windows or when +// MOZILLA_INTERNAL_API is defined +#if defined(MOZILLA_INTERNAL_API) || defined(XP_WIN) + +// Save literal putenv string to environment variable. +MOZ_NEVER_INLINE inline void SaveToEnv(const char* aEnvString) { +# if defined(MOZILLA_INTERNAL_API) + char* expr = strdup(aEnvString); + if (expr) { + PR_SetEnv(expr); + } + + // We intentionally leak |expr| here since it is required by PR_SetEnv. + MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(expr); +# elif defined(XP_WIN) + // This is the same as the NSPR implementation + // (Note that we don't need to do a strdup for this case; the CRT makes a + // copy) + _putenv(aEnvString); +# endif +} + +inline bool EnvHasValue(const char* aVarName) { +# if defined(MOZILLA_INTERNAL_API) + const char* val = PR_GetEnv(aVarName); + return val && *val; +# elif defined(XP_WIN) + // This is the same as the NSPR implementation + const char* val = getenv(aVarName); + return val && *val; +# endif +} + +#endif // end windows/internal_api-only definitions + +#ifndef NS_NO_XPCOM +already_AddRefed<nsIFile> GetFileFromEnv(const char* name); +#endif + +} // namespace mozilla + +#endif // mozilla_CmdLineAndEnvUtils_h |