mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-19 04:31:10 +00:00
1467 lines
54 KiB
C++
1467 lines
54 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
#include "inc/utils.hpp"
|
|
|
|
#include <til/string.h>
|
|
#include <wil/token_helpers.h>
|
|
|
|
#include "inc/colorTable.hpp"
|
|
|
|
#include <icu.h>
|
|
|
|
using namespace Microsoft::Console;
|
|
|
|
// Routine Description:
|
|
// - Determines if a character is a valid number character, 0-9.
|
|
// Arguments:
|
|
// - wch - Character to check.
|
|
// Return Value:
|
|
// - True if it is. False if it isn't.
|
|
static constexpr bool _isNumber(const wchar_t wch) noexcept
|
|
{
|
|
return wch >= L'0' && wch <= L'9'; // 0x30 - 0x39
|
|
}
|
|
|
|
GSL_SUPPRESS(bounds)
|
|
static std::wstring guidToStringCommon(const GUID& guid, size_t offset, size_t length)
|
|
{
|
|
// This is just like StringFromGUID2 but with lowercase hexadecimal.
|
|
wchar_t buffer[39];
|
|
swprintf_s(&buffer[0], 39, L"{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
|
|
return { &buffer[offset], length };
|
|
}
|
|
|
|
// Creates a string from the given GUID in the format "{12345678-abcd-ef12-3456-7890abcdef12}".
|
|
std::wstring Utils::GuidToString(const GUID& guid)
|
|
{
|
|
return guidToStringCommon(guid, 0, 38);
|
|
}
|
|
|
|
// Creates a string from the given GUID in the format "12345678-abcd-ef12-3456-7890abcdef12".
|
|
std::wstring Utils::GuidToPlainString(const GUID& guid)
|
|
{
|
|
return guidToStringCommon(guid, 1, 36);
|
|
}
|
|
|
|
// Creates a GUID from a string in the format "{12345678-abcd-ef12-3456-7890abcdef12}".
|
|
// Throws if the conversion failed.
|
|
GUID Utils::GuidFromString(_Null_terminated_ const wchar_t* str)
|
|
{
|
|
GUID result;
|
|
THROW_IF_FAILED(IIDFromString(str, &result));
|
|
return result;
|
|
}
|
|
|
|
// Creates a GUID from a string in the format "12345678-abcd-ef12-3456-7890abcdef12".
|
|
// Throws if the conversion failed.
|
|
//
|
|
// Side-note: An interesting quirk of this method is that the given string doesn't need to be null-terminated.
|
|
// This method could be combined with GuidFromString() so that it also doesn't require null-termination.
|
|
GSL_SUPPRESS(bounds)
|
|
GUID Utils::GuidFromPlainString(_Null_terminated_ const wchar_t* str)
|
|
{
|
|
// Add "{}" brackets around our string, as required by IIDFromString().
|
|
wchar_t buffer[39];
|
|
buffer[0] = L'{';
|
|
// This wcscpy_s() copies 36 characters and 1 terminating null.
|
|
// The latter forces us to call this method before filling buffer[37] with '}'.
|
|
THROW_HR_IF(CO_E_CLASSSTRING, wcscpy_s(&buffer[1], 37, str));
|
|
buffer[37] = L'}';
|
|
buffer[38] = L'\0';
|
|
|
|
return GuidFromString(&buffer[0]);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Creates a GUID, but not via an out parameter.
|
|
// Return Value:
|
|
// - A GUID if there's enough randomness; otherwise, an exception.
|
|
GUID Utils::CreateGuid()
|
|
{
|
|
GUID result{};
|
|
THROW_IF_FAILED(::CoCreateGuid(&result));
|
|
return result;
|
|
}
|
|
|
|
// Function Description:
|
|
// - Creates a String representation of a color, in the format "#RRGGBB"
|
|
// Arguments:
|
|
// - color: the COLORREF to create the string for
|
|
// Return Value:
|
|
// - a string representation of the color
|
|
std::string Utils::ColorToHexString(const til::color color)
|
|
{
|
|
return fmt::format(FMT_COMPILE("#{:02X}{:02X}{:02X}"), color.r, color.g, color.b);
|
|
}
|
|
|
|
// Function Description:
|
|
// - Parses a color from a string. The string should be in the format "#RRGGBB" or "#RGB"
|
|
// Arguments:
|
|
// - str: a string representation of the COLORREF to parse
|
|
// Return Value:
|
|
// - A COLORREF if the string could successfully be parsed. If the string is not
|
|
// the correct format, throws E_INVALIDARG
|
|
til::color Utils::ColorFromHexString(const std::string_view str)
|
|
{
|
|
THROW_HR_IF(E_INVALIDARG, str.size() != 9 && str.size() != 7 && str.size() != 4);
|
|
THROW_HR_IF(E_INVALIDARG, str.at(0) != '#');
|
|
|
|
std::string rStr;
|
|
std::string gStr;
|
|
std::string bStr;
|
|
std::string aStr;
|
|
|
|
if (str.size() == 4)
|
|
{
|
|
rStr = std::string(2, str.at(1));
|
|
gStr = std::string(2, str.at(2));
|
|
bStr = std::string(2, str.at(3));
|
|
aStr = "ff";
|
|
}
|
|
else if (str.size() == 7)
|
|
{
|
|
rStr = std::string(&str.at(1), 2);
|
|
gStr = std::string(&str.at(3), 2);
|
|
bStr = std::string(&str.at(5), 2);
|
|
aStr = "ff";
|
|
}
|
|
else if (str.size() == 9)
|
|
{
|
|
// #rrggbbaa
|
|
rStr = std::string(&str.at(1), 2);
|
|
gStr = std::string(&str.at(3), 2);
|
|
bStr = std::string(&str.at(5), 2);
|
|
aStr = std::string(&str.at(7), 2);
|
|
}
|
|
|
|
const auto r = gsl::narrow_cast<BYTE>(std::stoul(rStr, nullptr, 16));
|
|
const auto g = gsl::narrow_cast<BYTE>(std::stoul(gStr, nullptr, 16));
|
|
const auto b = gsl::narrow_cast<BYTE>(std::stoul(bStr, nullptr, 16));
|
|
const auto a = gsl::narrow_cast<BYTE>(std::stoul(aStr, nullptr, 16));
|
|
|
|
return til::color{ r, g, b, a };
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Given a color string, attempts to parse the color.
|
|
// The color are specified by name or RGB specification as per XParseColor.
|
|
// Arguments:
|
|
// - string - The string containing the color spec string to parse.
|
|
// Return Value:
|
|
// - An optional color which contains value if a color was successfully parsed
|
|
std::optional<til::color> Utils::ColorFromXTermColor(const std::wstring_view string) noexcept
|
|
{
|
|
auto color = ColorFromXParseColorSpec(string);
|
|
if (!color.has_value())
|
|
{
|
|
// Try again, but use the app color name parser
|
|
color = ColorFromXOrgAppColorName(string);
|
|
}
|
|
|
|
return color;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Given a color spec string, attempts to parse the color that's encoded.
|
|
//
|
|
// Based on the XParseColor documentation, the supported specs currently are the following:
|
|
// spec1: a color in the following format:
|
|
// "rgb:<red>/<green>/<blue>"
|
|
// spec2: a color in the following format:
|
|
// "#<red><green><blue>"
|
|
//
|
|
// In both specs, <color> is a value contains up to 4 hex digits, upper or lower case.
|
|
// Arguments:
|
|
// - string - The string containing the color spec string to parse.
|
|
// Return Value:
|
|
// - An optional color which contains value if a color was successfully parsed
|
|
std::optional<til::color> Utils::ColorFromXParseColorSpec(const std::wstring_view string) noexcept
|
|
try
|
|
{
|
|
auto foundXParseColorSpec = false;
|
|
auto foundValidColorSpec = false;
|
|
|
|
auto isSharpSignFormat = false;
|
|
size_t rgbHexDigitCount = 0;
|
|
std::array<unsigned int, 3> colorValues = { 0 };
|
|
std::array<unsigned int, 3> parameterValues = { 0 };
|
|
const auto stringSize = string.size();
|
|
|
|
// First we look for "rgb:"
|
|
// Other colorspaces are theoretically possible, but we don't support them.
|
|
auto curr = string.cbegin();
|
|
if (stringSize > 4)
|
|
{
|
|
auto prefix = std::wstring(string.substr(0, 4));
|
|
|
|
// The "rgb:" indicator should be case-insensitive. To prevent possible issues under
|
|
// different locales, transform only ASCII range latin characters.
|
|
std::transform(prefix.begin(), prefix.end(), prefix.begin(), [](const auto x) {
|
|
return x >= L'A' && x <= L'Z' ? static_cast<wchar_t>(std::towlower(x)) : x;
|
|
});
|
|
|
|
if (prefix.compare(L"rgb:") == 0)
|
|
{
|
|
// If all the components have the same digit count, we can have one of the following formats:
|
|
// 9 "rgb:h/h/h"
|
|
// 12 "rgb:hh/hh/hh"
|
|
// 15 "rgb:hhh/hhh/hhh"
|
|
// 18 "rgb:hhhh/hhhh/hhhh"
|
|
// Note that the component sizes aren't required to be the same.
|
|
// Anything in between is also valid, e.g. "rgb:h/hh/h" and "rgb:h/hh/hhh".
|
|
// Any fewer cannot be valid, and any more will be too many. Return early in this case.
|
|
if (stringSize < 9 || stringSize > 18)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
foundXParseColorSpec = true;
|
|
|
|
std::advance(curr, 4);
|
|
}
|
|
}
|
|
|
|
// Try the sharp sign format.
|
|
if (!foundXParseColorSpec && stringSize > 1)
|
|
{
|
|
if (til::at(string, 0) == L'#')
|
|
{
|
|
// We can have one of the following formats:
|
|
// 4 "#hhh"
|
|
// 7 "#hhhhhh"
|
|
// 10 "#hhhhhhhhh"
|
|
// 13 "#hhhhhhhhhhhh"
|
|
// Any other cases will be invalid. Return early in this case.
|
|
if (!(stringSize == 4 || stringSize == 7 || stringSize == 10 || stringSize == 13))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
isSharpSignFormat = true;
|
|
foundXParseColorSpec = true;
|
|
rgbHexDigitCount = (stringSize - 1) / 3;
|
|
|
|
std::advance(curr, 1);
|
|
}
|
|
}
|
|
|
|
// No valid spec is found. Return early.
|
|
if (!foundXParseColorSpec)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Try to parse the actual color value of each component.
|
|
for (size_t component = 0; component < 3; component++)
|
|
{
|
|
auto foundColor = false;
|
|
auto& parameterValue = til::at(parameterValues, component);
|
|
// For "sharp sign" format, the rgbHexDigitCount is known.
|
|
// For "rgb:" format, colorspecs are up to hhhh/hhhh/hhhh, for 1-4 h's
|
|
const auto iteration = isSharpSignFormat ? rgbHexDigitCount : 4;
|
|
for (size_t i = 0; i < iteration && curr < string.cend(); i++)
|
|
{
|
|
const auto wch = *curr++;
|
|
|
|
parameterValue *= 16;
|
|
unsigned int intVal = 0;
|
|
const auto ret = HexToUint(wch, intVal);
|
|
if (!ret)
|
|
{
|
|
// Encountered something weird oh no
|
|
return std::nullopt;
|
|
}
|
|
|
|
parameterValue += intVal;
|
|
|
|
if (isSharpSignFormat)
|
|
{
|
|
// If we get this far, any number can be seen as a valid part
|
|
// of this component.
|
|
foundColor = true;
|
|
|
|
if (i >= rgbHexDigitCount)
|
|
{
|
|
// Successfully parsed this component. Start the next one.
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Record the hex digit count of the current component.
|
|
rgbHexDigitCount = i + 1;
|
|
|
|
// If this is the first 2 component...
|
|
if (component < 2 && curr < string.cend() && *curr == L'/')
|
|
{
|
|
// ...and we have successfully parsed this component, we need
|
|
// to skip the delimiter before starting the next one.
|
|
curr++;
|
|
foundColor = true;
|
|
break;
|
|
}
|
|
// Or we have reached the end of the string...
|
|
else if (curr >= string.cend())
|
|
{
|
|
// ...meaning that this is the last component. We're not going to
|
|
// see any delimiter. We can just break out.
|
|
foundColor = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!foundColor)
|
|
{
|
|
// Indicates there was some error parsing color.
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Calculate the actual color value based on the hex digit count.
|
|
auto& colorValue = til::at(colorValues, component);
|
|
const auto scaleMultiplier = isSharpSignFormat ? 0x10 : 0x11;
|
|
const auto scaleDivisor = scaleMultiplier << 8 >> 4 * (4 - rgbHexDigitCount);
|
|
colorValue = parameterValue * scaleMultiplier / scaleDivisor;
|
|
}
|
|
|
|
if (curr >= string.cend())
|
|
{
|
|
// We're at the end of the string and we have successfully parsed the color.
|
|
foundValidColorSpec = true;
|
|
}
|
|
|
|
// Only if we find a valid colorspec can we pass it out successfully.
|
|
if (foundValidColorSpec)
|
|
{
|
|
return til::color(LOBYTE(til::at(colorValues, 0)),
|
|
LOBYTE(til::at(colorValues, 1)),
|
|
LOBYTE(til::at(colorValues, 2)));
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_CAUGHT_EXCEPTION();
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Constructs a til::color value from RGB percentage components.
|
|
// Arguments:
|
|
// - r - The red component of the color (0-100%).
|
|
// - g - The green component of the color (0-100%).
|
|
// - b - The blue component of the color (0-100%).
|
|
// Return Value:
|
|
// - The color defined by the given components.
|
|
til::color Utils::ColorFromRGB100(const int r, const int g, const int b) noexcept
|
|
{
|
|
// The color class is expecting components in the range 0 to 255,
|
|
// so we need to scale our percentage values by 255/100. We can
|
|
// optimise this conversion with a pre-created lookup table.
|
|
static constexpr auto scale100To255 = [] {
|
|
std::array<uint8_t, 101> lut{};
|
|
for (size_t i = 0; i < std::size(lut); i++)
|
|
{
|
|
lut.at(i) = gsl::narrow_cast<uint8_t>((i * 255 + 50) / 100);
|
|
}
|
|
return lut;
|
|
}();
|
|
|
|
const auto red = til::at(scale100To255, std::min<unsigned>(r, 100u));
|
|
const auto green = til::at(scale100To255, std::min<unsigned>(g, 100u));
|
|
const auto blue = til::at(scale100To255, std::min<unsigned>(b, 100u));
|
|
return { red, green, blue };
|
|
}
|
|
|
|
// Function Description:
|
|
// - Returns the RGB percentage components of a given til::color value.
|
|
// Arguments:
|
|
// - color: the color being queried
|
|
// Return Value:
|
|
// - a tuple containing the three components
|
|
std::tuple<int, int, int> Utils::ColorToRGB100(const til::color color) noexcept
|
|
{
|
|
// The color class components are in the range 0 to 255, so we
|
|
// need to scale them by 100/255 to obtain percentage values. We
|
|
// can optimise this conversion with a pre-created lookup table.
|
|
static constexpr auto scale255To100 = [] {
|
|
std::array<int8_t, 256> lut{};
|
|
for (size_t i = 0; i < std::size(lut); i++)
|
|
{
|
|
lut.at(i) = gsl::narrow_cast<uint8_t>((i * 100 + 128) / 255);
|
|
}
|
|
return lut;
|
|
}();
|
|
|
|
const auto red = til::at(scale255To100, color.r);
|
|
const auto green = til::at(scale255To100, color.g);
|
|
const auto blue = til::at(scale255To100, color.b);
|
|
return { red, green, blue };
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Constructs a til::color value from HLS components.
|
|
// Arguments:
|
|
// - h - The hue component of the color (0-360°).
|
|
// - l - The luminosity component of the color (0-100%).
|
|
// - s - The saturation component of the color (0-100%).
|
|
// Return Value:
|
|
// - The color defined by the given components.
|
|
til::color Utils::ColorFromHLS(const int h, const int l, const int s) noexcept
|
|
{
|
|
const auto hue = h % 360;
|
|
const auto lum = gsl::narrow_cast<float>(std::min(l, 100));
|
|
const auto sat = gsl::narrow_cast<float>(std::min(s, 100));
|
|
|
|
// This calculation is based on the HSL to RGB algorithm described in
|
|
// Wikipedia: https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB
|
|
// We start by calculating the chroma value, and the point along the bottom
|
|
// faces of the RGB cube with the same hue and chroma as our color (x).
|
|
const auto chroma = (50.f - abs(lum - 50.f)) * sat / 50.f;
|
|
const auto x = chroma * (60 - abs(hue % 120 - 60)) / 60.f;
|
|
|
|
// We'll also need an offset added to each component to match lightness.
|
|
const auto lightness = lum - chroma / 2.0f;
|
|
|
|
// We use the chroma value for the brightest component, x for the second
|
|
// brightest, and 0 for the last. The values are scaled by 255/100 to get
|
|
// them in the range 0 to 255, as required by the color class.
|
|
constexpr auto scale = 255.f / 100.f;
|
|
const auto comp1 = gsl::narrow_cast<uint8_t>((chroma + lightness) * scale + 0.5f);
|
|
const auto comp2 = gsl::narrow_cast<uint8_t>((x + lightness) * scale + 0.5f);
|
|
const auto comp3 = gsl::narrow_cast<uint8_t>((0 + lightness) * scale + 0.5f);
|
|
|
|
// Finally we order the components based on the given hue. But note that the
|
|
// DEC terminals used a different mapping for hue than is typical for modern
|
|
// color models. Blue is at 0°, red is at 120°, and green is at 240°.
|
|
// See DEC STD 070, ReGIS Graphics Extension, § 8.6.2.2.2, Color by Value.
|
|
if (hue < 60)
|
|
return { comp2, comp3, comp1 }; // blue to magenta
|
|
else if (hue < 120)
|
|
return { comp1, comp3, comp2 }; // magenta to red
|
|
else if (hue < 180)
|
|
return { comp1, comp2, comp3 }; // red to yellow
|
|
else if (hue < 240)
|
|
return { comp2, comp1, comp3 }; // yellow to green
|
|
else if (hue < 300)
|
|
return { comp3, comp1, comp2 }; // green to cyan
|
|
else
|
|
return { comp3, comp2, comp1 }; // cyan to blue
|
|
}
|
|
|
|
// Function Description:
|
|
// - Returns the HLS components of a given til::color value.
|
|
// Arguments:
|
|
// - color: the color being queried
|
|
// Return Value:
|
|
// - a tuple containing the three components
|
|
std::tuple<int, int, int> Utils::ColorToHLS(const til::color color) noexcept
|
|
{
|
|
const auto red = color.r / 255.f;
|
|
const auto green = color.g / 255.f;
|
|
const auto blue = color.b / 255.f;
|
|
|
|
// This calculation is based on the RGB to HSL algorithm described in
|
|
// Wikipedia: https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
|
|
// We start by calculating the maximum and minimum component values.
|
|
const auto maxComp = std::max({ red, green, blue });
|
|
const auto minComp = std::min({ red, green, blue });
|
|
|
|
// The chroma value is the range of those components.
|
|
const auto chroma = maxComp - minComp;
|
|
|
|
// And the luma is the middle of the range. But we're actually calculating
|
|
// double that value here to save on a division.
|
|
const auto luma2 = (maxComp + minComp);
|
|
|
|
// The saturation is half the chroma value divided by min(luma, 1-luma),
|
|
// but since the luma is already doubled, we can use the chroma as is.
|
|
const auto divisor = std::min(luma2, 2.f - luma2);
|
|
const auto sat = divisor > 0 ? chroma / divisor : 0.f;
|
|
|
|
// Finally we calculate the hue, which is represented by the angle of a
|
|
// vector to a point in a color hexagon with blue, magenta, red, yellow,
|
|
// green, and cyan at its corners. As noted above, the DEC standard has
|
|
// blue at 0°, red at 120°, and green at 240°, which is slightly different
|
|
// from the way that hue is typically mapped in modern color models.
|
|
auto hue = 0.f;
|
|
if (chroma != 0)
|
|
{
|
|
if (maxComp == red)
|
|
hue = (green - blue) / chroma + 2.f; // magenta to yellow
|
|
else if (maxComp == green)
|
|
hue = (blue - red) / chroma + 4.f; // yellow to cyan
|
|
else if (maxComp == blue)
|
|
hue = (red - green) / chroma + 6.f; // cyan to magenta
|
|
}
|
|
|
|
// The hue value calculated above is essentially a fractional offset from the
|
|
// six hexagon corners, so it has to be scaled by 60 to get the angle value.
|
|
// Luma and saturation are percentages so must be scaled by 100, but our luma
|
|
// value is already doubled, so only needs to be scaled by 50.
|
|
const auto h = static_cast<int>(hue * 60.f + 0.5f) % 360;
|
|
const auto l = static_cast<int>(luma2 * 50.f + 0.5f);
|
|
const auto s = static_cast<int>(sat * 100.f + 0.5f);
|
|
return { h, l, s };
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Converts a hex character to its equivalent integer value.
|
|
// Arguments:
|
|
// - wch - Character to convert.
|
|
// - value - receives the int value of the char
|
|
// Return Value:
|
|
// - true iff the character is a hex character.
|
|
bool Utils::HexToUint(const wchar_t wch,
|
|
unsigned int& value) noexcept
|
|
{
|
|
value = 0;
|
|
auto success = false;
|
|
if (wch >= L'0' && wch <= L'9')
|
|
{
|
|
value = wch - L'0';
|
|
success = true;
|
|
}
|
|
else if (wch >= L'A' && wch <= L'F')
|
|
{
|
|
value = (wch - L'A') + 10;
|
|
success = true;
|
|
}
|
|
else if (wch >= L'a' && wch <= L'f')
|
|
{
|
|
value = (wch - L'a') + 10;
|
|
success = true;
|
|
}
|
|
return success;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Converts a number string to its equivalent unsigned integer value.
|
|
// Arguments:
|
|
// - wstr - String to convert.
|
|
// - value - receives the int value of the string
|
|
// Return Value:
|
|
// - true iff the string is a unsigned integer string.
|
|
bool Utils::StringToUint(const std::wstring_view wstr,
|
|
unsigned int& value)
|
|
{
|
|
if (wstr.size() < 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
unsigned int result = 0;
|
|
size_t current = 0;
|
|
while (current < wstr.size())
|
|
{
|
|
const auto wch = wstr.at(current);
|
|
if (_isNumber(wch))
|
|
{
|
|
result *= 10;
|
|
result += wch - L'0';
|
|
|
|
++current;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
value = result;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Split a string into different parts using the delimiter provided.
|
|
// Arguments:
|
|
// - wstr - String to split.
|
|
// - delimiter - delimiter to use.
|
|
// Return Value:
|
|
// - a vector containing the result string parts.
|
|
std::vector<std::wstring_view> Utils::SplitString(const std::wstring_view wstr,
|
|
const wchar_t delimiter) noexcept
|
|
try
|
|
{
|
|
std::vector<std::wstring_view> result;
|
|
size_t current = 0;
|
|
while (current < wstr.size())
|
|
{
|
|
const auto nextDelimiter = wstr.find(delimiter, current);
|
|
if (nextDelimiter == std::wstring::npos)
|
|
{
|
|
result.push_back(wstr.substr(current));
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
const auto length = nextDelimiter - current;
|
|
result.push_back(wstr.substr(current, length));
|
|
// Skip this part and the delimiter. Start the next one
|
|
current += length + 1;
|
|
// The next index is larger than string size, which means the string
|
|
// is in the format of "part1;part2;" (assuming use ';' as delimiter).
|
|
// Add the last part which is an empty string.
|
|
if (current >= wstr.size())
|
|
{
|
|
result.push_back(L"");
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_CAUGHT_EXCEPTION();
|
|
return {};
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Pre-process text pasted (presumably from the clipboard) with provided option.
|
|
// Arguments:
|
|
// - wstr - String to process.
|
|
// - option - option to use.
|
|
// Return Value:
|
|
// - The result string.
|
|
std::wstring Utils::FilterStringForPaste(const std::wstring_view wstr, const FilterOption option)
|
|
{
|
|
std::wstring filtered;
|
|
filtered.reserve(wstr.length());
|
|
|
|
const auto isControlCode = [](wchar_t c) {
|
|
if (c >= L'\x20' && c < L'\x7f')
|
|
{
|
|
// Printable ASCII characters.
|
|
return false;
|
|
}
|
|
|
|
if (c > L'\x9f')
|
|
{
|
|
// Not a control code.
|
|
return false;
|
|
}
|
|
|
|
// All C0 & C1 control codes will be removed except HT(0x09), LF(0x0a) and CR(0x0d).
|
|
return c != L'\x09' && c != L'\x0a' && c != L'\x0d';
|
|
};
|
|
|
|
std::wstring::size_type pos = 0;
|
|
std::wstring::size_type begin = 0;
|
|
|
|
while (pos < wstr.size())
|
|
{
|
|
const auto c = til::at(wstr, pos);
|
|
|
|
if (WI_IsFlagSet(option, FilterOption::CarriageReturnNewline) && c == L'\n')
|
|
{
|
|
// copy up to but not including the \n
|
|
filtered.append(wstr.cbegin() + begin, wstr.cbegin() + pos);
|
|
if (!(pos > 0 && (til::at(wstr, pos - 1) == L'\r')))
|
|
{
|
|
// there was no \r before the \n we did not copy,
|
|
// so append our own \r (this effectively replaces the \n
|
|
// with a \r)
|
|
filtered.push_back(L'\r');
|
|
}
|
|
++pos;
|
|
begin = pos;
|
|
}
|
|
else if (WI_IsFlagSet(option, FilterOption::ControlCodes) && isControlCode(c))
|
|
{
|
|
// copy up to but not including the control code
|
|
filtered.append(wstr.cbegin() + begin, wstr.cbegin() + pos);
|
|
++pos;
|
|
begin = pos;
|
|
}
|
|
else
|
|
{
|
|
++pos;
|
|
}
|
|
}
|
|
|
|
// If we entered the while loop even once, begin would be non-zero
|
|
// (because we set begin = pos right after incrementing pos)
|
|
// So, if begin is still zero at this point it means we never found a newline
|
|
// and we can just write the original string
|
|
if (begin == 0)
|
|
{
|
|
return std::wstring{ wstr };
|
|
}
|
|
else
|
|
{
|
|
filtered.append(wstr.cbegin() + begin, wstr.cend());
|
|
// we may have removed some characters, so we may not need as much space
|
|
// as we reserved earlier
|
|
filtered.shrink_to_fit();
|
|
return filtered;
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Shorthand check if a handle value is null or invalid.
|
|
// Arguments:
|
|
// - Handle
|
|
// Return Value:
|
|
// - True if non zero and not set to invalid magic value. False otherwise.
|
|
bool Utils::IsValidHandle(const HANDLE handle) noexcept
|
|
{
|
|
return handle != nullptr && handle != INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
#define FileModeInformation (FILE_INFORMATION_CLASS)16
|
|
|
|
#define FILE_PIPE_BYTE_STREAM_TYPE 0x00000000
|
|
#define FILE_PIPE_BYTE_STREAM_MODE 0x00000000
|
|
#define FILE_PIPE_QUEUE_OPERATION 0x00000000
|
|
|
|
typedef struct _FILE_MODE_INFORMATION
|
|
{
|
|
ULONG Mode;
|
|
} FILE_MODE_INFORMATION, *PFILE_MODE_INFORMATION;
|
|
|
|
extern "C" NTSTATUS NTAPI NtQueryInformationFile(
|
|
HANDLE FileHandle,
|
|
PIO_STATUS_BLOCK IoStatusBlock,
|
|
PVOID FileInformation,
|
|
ULONG Length,
|
|
FILE_INFORMATION_CLASS FileInformationClass);
|
|
|
|
extern "C" NTSTATUS NTAPI NtCreateNamedPipeFile(
|
|
PHANDLE FileHandle,
|
|
ULONG DesiredAccess,
|
|
POBJECT_ATTRIBUTES ObjectAttributes,
|
|
PIO_STATUS_BLOCK IoStatusBlock,
|
|
ULONG ShareAccess,
|
|
ULONG CreateDisposition,
|
|
ULONG CreateOptions,
|
|
ULONG NamedPipeType,
|
|
ULONG ReadMode,
|
|
ULONG CompletionMode,
|
|
ULONG MaximumInstances,
|
|
ULONG InboundQuota,
|
|
ULONG OutboundQuota,
|
|
PLARGE_INTEGER DefaultTimeout);
|
|
|
|
bool Utils::HandleWantsOverlappedIo(HANDLE handle) noexcept
|
|
{
|
|
IO_STATUS_BLOCK statusBlock;
|
|
FILE_MODE_INFORMATION modeInfo;
|
|
const auto status = NtQueryInformationFile(handle, &statusBlock, &modeInfo, sizeof(modeInfo), FileModeInformation);
|
|
return status == 0 && WI_AreAllFlagsClear(modeInfo.Mode, FILE_SYNCHRONOUS_IO_ALERT | FILE_SYNCHRONOUS_IO_NONALERT);
|
|
}
|
|
|
|
// Creates an anonymous pipe. Behaves like PIPE_ACCESS_INBOUND,
|
|
// meaning the .server is for reading and the .client is for writing.
|
|
Utils::Pipe Utils::CreatePipe(DWORD bufferSize)
|
|
{
|
|
wil::unique_hfile rx, tx;
|
|
THROW_IF_WIN32_BOOL_FALSE(::CreatePipe(rx.addressof(), tx.addressof(), nullptr, bufferSize));
|
|
return { std::move(rx), std::move(tx) };
|
|
}
|
|
|
|
// Creates an overlapped anonymous pipe. openMode should be either:
|
|
// * PIPE_ACCESS_INBOUND
|
|
// * PIPE_ACCESS_OUTBOUND
|
|
// * PIPE_ACCESS_DUPLEX
|
|
//
|
|
// I know, I know. MSDN infamously says
|
|
// > Asynchronous (overlapped) read and write operations are not supported by anonymous pipes.
|
|
// but that's a lie. The only reason they're not supported is because the Win32
|
|
// API doesn't have a parameter where you could pass FILE_FLAG_OVERLAPPED!
|
|
// So, we'll simply use the underlying NT APIs instead.
|
|
//
|
|
// Most code on the internet suggests creating named pipes with a random name,
|
|
// but usually conveniently forgets to mention that named pipes require strict ACLs.
|
|
// https://stackoverflow.com/q/60645 for instance contains a lot of poor advice.
|
|
// Anonymous pipes also cannot be discovered via NtQueryDirectoryFile inside the NPFS driver,
|
|
// whereas running a tool like Sysinternals' PipeList will return all those semi-named pipes.
|
|
//
|
|
// The code below contains comments to create unidirectional pipes.
|
|
Utils::Pipe Utils::CreateOverlappedPipe(DWORD openMode, DWORD bufferSize)
|
|
{
|
|
LARGE_INTEGER timeout = { .QuadPart = -10'0000'0000 }; // 1 second
|
|
UNICODE_STRING emptyPath{};
|
|
IO_STATUS_BLOCK statusBlock;
|
|
OBJECT_ATTRIBUTES objectAttributes{
|
|
.Length = sizeof(OBJECT_ATTRIBUTES),
|
|
.ObjectName = &emptyPath,
|
|
.Attributes = OBJ_CASE_INSENSITIVE,
|
|
};
|
|
DWORD serverDesiredAccess = 0;
|
|
DWORD clientDesiredAccess = 0;
|
|
DWORD serverShareAccess = 0;
|
|
DWORD clientShareAccess = 0;
|
|
|
|
switch (openMode)
|
|
{
|
|
case PIPE_ACCESS_INBOUND:
|
|
serverDesiredAccess = SYNCHRONIZE | GENERIC_READ | FILE_WRITE_ATTRIBUTES;
|
|
clientDesiredAccess = SYNCHRONIZE | GENERIC_WRITE | FILE_READ_ATTRIBUTES;
|
|
serverShareAccess = FILE_SHARE_WRITE;
|
|
clientShareAccess = FILE_SHARE_READ;
|
|
break;
|
|
case PIPE_ACCESS_OUTBOUND:
|
|
serverDesiredAccess = SYNCHRONIZE | GENERIC_WRITE | FILE_READ_ATTRIBUTES;
|
|
clientDesiredAccess = SYNCHRONIZE | GENERIC_READ | FILE_WRITE_ATTRIBUTES;
|
|
serverShareAccess = FILE_SHARE_READ;
|
|
clientShareAccess = FILE_SHARE_WRITE;
|
|
break;
|
|
case PIPE_ACCESS_DUPLEX:
|
|
serverDesiredAccess = SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE;
|
|
clientDesiredAccess = SYNCHRONIZE | GENERIC_READ | GENERIC_WRITE;
|
|
serverShareAccess = FILE_SHARE_READ | FILE_SHARE_WRITE;
|
|
clientShareAccess = FILE_SHARE_READ | FILE_SHARE_WRITE;
|
|
break;
|
|
default:
|
|
THROW_HR(E_UNEXPECTED);
|
|
}
|
|
|
|
// Cache a handle to the pipe driver.
|
|
static const auto pipeDirectory = []() {
|
|
UNICODE_STRING path = RTL_CONSTANT_STRING(L"\\Device\\NamedPipe\\");
|
|
|
|
OBJECT_ATTRIBUTES objectAttributes{
|
|
.Length = sizeof(OBJECT_ATTRIBUTES),
|
|
.ObjectName = &path,
|
|
};
|
|
|
|
wil::unique_hfile dir;
|
|
IO_STATUS_BLOCK statusBlock;
|
|
THROW_IF_NTSTATUS_FAILED(NtCreateFile(
|
|
/* FileHandle */ dir.addressof(),
|
|
/* DesiredAccess */ SYNCHRONIZE | GENERIC_READ,
|
|
/* ObjectAttributes */ &objectAttributes,
|
|
/* IoStatusBlock */ &statusBlock,
|
|
/* AllocationSize */ nullptr,
|
|
/* FileAttributes */ 0,
|
|
/* ShareAccess */ FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
/* CreateDisposition */ FILE_OPEN,
|
|
/* CreateOptions */ FILE_SYNCHRONOUS_IO_NONALERT,
|
|
/* EaBuffer */ nullptr,
|
|
/* EaLength */ 0));
|
|
|
|
return dir;
|
|
}();
|
|
|
|
wil::unique_hfile server;
|
|
objectAttributes.RootDirectory = pipeDirectory.get();
|
|
THROW_IF_NTSTATUS_FAILED(NtCreateNamedPipeFile(
|
|
/* FileHandle */ server.addressof(),
|
|
/* DesiredAccess */ serverDesiredAccess,
|
|
/* ObjectAttributes */ &objectAttributes,
|
|
/* IoStatusBlock */ &statusBlock,
|
|
/* ShareAccess */ serverShareAccess,
|
|
/* CreateDisposition */ FILE_CREATE,
|
|
/* CreateOptions */ 0, // would be FILE_SYNCHRONOUS_IO_NONALERT for a synchronous pipe
|
|
/* NamedPipeType */ FILE_PIPE_BYTE_STREAM_TYPE,
|
|
/* ReadMode */ FILE_PIPE_BYTE_STREAM_MODE,
|
|
/* CompletionMode */ FILE_PIPE_QUEUE_OPERATION, // would be FILE_PIPE_COMPLETE_OPERATION for PIPE_NOWAIT
|
|
/* MaximumInstances */ 1,
|
|
/* InboundQuota */ bufferSize,
|
|
/* OutboundQuota */ bufferSize,
|
|
/* DefaultTimeout */ &timeout));
|
|
|
|
wil::unique_hfile client;
|
|
objectAttributes.RootDirectory = server.get();
|
|
THROW_IF_NTSTATUS_FAILED(NtCreateFile(
|
|
/* FileHandle */ client.addressof(),
|
|
/* DesiredAccess */ clientDesiredAccess,
|
|
/* ObjectAttributes */ &objectAttributes,
|
|
/* IoStatusBlock */ &statusBlock,
|
|
/* AllocationSize */ nullptr,
|
|
/* FileAttributes */ 0,
|
|
/* ShareAccess */ clientShareAccess,
|
|
/* CreateDisposition */ FILE_OPEN,
|
|
/* CreateOptions */ FILE_NON_DIRECTORY_FILE, // would include FILE_SYNCHRONOUS_IO_NONALERT for a synchronous pipe
|
|
/* EaBuffer */ nullptr,
|
|
/* EaLength */ 0));
|
|
|
|
return { std::move(server), std::move(client) };
|
|
}
|
|
|
|
// GetOverlappedResult() for professionals! Only for single-threaded use.
|
|
//
|
|
// GetOverlappedResult() used to have a neat optimization where it would only call WaitForSingleObject() if the state was STATUS_PENDING.
|
|
// That got removed in Windows 7, because people kept starting a read/write on one thread and called GetOverlappedResult() on another.
|
|
// When the OS sets Internal from STATUS_PENDING to 0 (= done) and then flags the hEvent, that doesn't happen atomically.
|
|
// This results in a race condition if a OVERLAPPED is used across threads.
|
|
HRESULT Utils::GetOverlappedResultSameThread(const OVERLAPPED* overlapped, DWORD* bytesTransferred) noexcept
|
|
{
|
|
assert(overlapped != nullptr);
|
|
assert(overlapped->hEvent != nullptr);
|
|
assert(bytesTransferred != nullptr);
|
|
|
|
__assume(overlapped != nullptr);
|
|
__assume(overlapped->hEvent != nullptr);
|
|
__assume(bytesTransferred != nullptr);
|
|
|
|
if (overlapped->Internal == STATUS_PENDING)
|
|
{
|
|
if (WaitForSingleObjectEx(overlapped->hEvent, INFINITE, FALSE) != WAIT_OBJECT_0)
|
|
{
|
|
return HRESULT_FROM_WIN32(GetLastError());
|
|
}
|
|
}
|
|
|
|
// Assuming no multi-threading as per the function contract and
|
|
// now that we ensured that hEvent is set (= read/write done),
|
|
// we can safely read whatever want because nothing will set these concurrently.
|
|
*bytesTransferred = gsl::narrow_cast<DWORD>(overlapped->InternalHigh);
|
|
return HRESULT_FROM_NT(overlapped->Internal);
|
|
}
|
|
|
|
// Function Description:
|
|
// - Generate a Version 5 UUID (specified in RFC4122 4.3)
|
|
// v5 UUIDs are stable given the same namespace and "name".
|
|
// Arguments:
|
|
// - namespaceGuid: The GUID of the v5 UUID namespace, which provides both
|
|
// a seed and a tacit agreement that all UUIDs generated
|
|
// with it will follow the same data format.
|
|
// - name: Bytes comprising the name (in a namespace-specific format)
|
|
// Return Value:
|
|
// - a new stable v5 UUID
|
|
GUID Utils::CreateV5Uuid(const GUID& namespaceGuid, const std::span<const std::byte> name)
|
|
{
|
|
// v5 uuid generation happens over values in network byte order, so let's enforce that
|
|
auto correctEndianNamespaceGuid{ EndianSwap(namespaceGuid) };
|
|
|
|
wil::unique_bcrypt_hash hash;
|
|
THROW_IF_NTSTATUS_FAILED(BCryptCreateHash(BCRYPT_SHA1_ALG_HANDLE, &hash, nullptr, 0, nullptr, 0, 0));
|
|
|
|
// According to N4713 8.2.1.11 [basic.lval], accessing the bytes underlying an object
|
|
// through unsigned char or char pointer *is defined*.
|
|
THROW_IF_NTSTATUS_FAILED(BCryptHashData(hash.get(), reinterpret_cast<PUCHAR>(&correctEndianNamespaceGuid), sizeof(GUID), 0));
|
|
// BCryptHashData is ill-specified in that it leaves off "const" qualification for pbInput
|
|
THROW_IF_NTSTATUS_FAILED(BCryptHashData(hash.get(), reinterpret_cast<PUCHAR>(const_cast<std::byte*>(name.data())), gsl::narrow<ULONG>(name.size()), 0));
|
|
|
|
std::array<uint8_t, 20> buffer;
|
|
THROW_IF_NTSTATUS_FAILED(BCryptFinishHash(hash.get(), buffer.data(), gsl::narrow<ULONG>(buffer.size()), 0));
|
|
|
|
buffer.at(6) = (buffer.at(6) & 0x0F) | 0x50; // set the uuid version to 5
|
|
buffer.at(8) = (buffer.at(8) & 0x3F) | 0x80; // set the variant to 2 (RFC4122)
|
|
|
|
// We're using memcpy here pursuant to N4713 6.7.2/3 [basic.types],
|
|
// "...the underlying bytes making up the object can be copied into an array
|
|
// of char or unsigned char...array is copied back into the object..."
|
|
// std::copy may compile down to ::memcpy for these types, but using it might
|
|
// contravene the standard and nobody's got time for that.
|
|
GUID newGuid{ 0 };
|
|
::memcpy_s(&newGuid, sizeof(GUID), buffer.data(), sizeof(GUID));
|
|
return EndianSwap(newGuid);
|
|
}
|
|
|
|
// * Elevated users cannot use the modern drag drop experience. This is
|
|
// specifically normal users running the Terminal as admin
|
|
// * The Default Administrator, who does not have a split token, CAN drag drop
|
|
// perfectly fine. So in that case, we want to return false.
|
|
// * This has to be kept separate from IsRunningElevated, which is exclusively
|
|
// used for "is this instance running as admin".
|
|
bool Utils::CanUwpDragDrop()
|
|
{
|
|
// There's a lot of wacky double negatives here so that the logic is
|
|
// basically the same as IsRunningElevated, but the end result semantically
|
|
// makes sense as "CanDragDrop".
|
|
static auto isDragDropBroken = []() {
|
|
try
|
|
{
|
|
wil::unique_handle processToken{ GetCurrentProcessToken() };
|
|
const auto elevationType = wil::get_token_information<TOKEN_ELEVATION_TYPE>(processToken.get());
|
|
const auto elevationState = wil::get_token_information<TOKEN_ELEVATION>(processToken.get());
|
|
if (elevationType == TokenElevationTypeDefault && elevationState.TokenIsElevated)
|
|
{
|
|
// In this case, the user has UAC entirely disabled. This is sort of
|
|
// weird, we treat this like the user isn't an admin at all. There's no
|
|
// separation of powers, so the things we normally want to gate on
|
|
// "having special powers" doesn't apply.
|
|
//
|
|
// See GH#7754, GH#11096
|
|
return false;
|
|
// drag drop is _not_ broken -> they _can_ drag drop
|
|
}
|
|
|
|
// If they are running admin, they cannot drag drop.
|
|
return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS);
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_CAUGHT_EXCEPTION();
|
|
// This failed? That's very peculiar indeed. Let's err on the side
|
|
// of "drag drop is broken", just in case.
|
|
return true;
|
|
}
|
|
}();
|
|
|
|
return !isDragDropBroken;
|
|
}
|
|
|
|
// See CanUwpDragDrop, GH#13928 for why this is different.
|
|
bool Utils::IsRunningElevated()
|
|
{
|
|
static auto isElevated = []() {
|
|
try
|
|
{
|
|
return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS);
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_CAUGHT_EXCEPTION();
|
|
return false;
|
|
}
|
|
}();
|
|
return isElevated;
|
|
}
|
|
|
|
// Checks whether the command line starts with wsl(.exe) as its executable.
|
|
// This is intentionally permissive: ANY wsl.exe is treated as a WSL profile.
|
|
// The only goal is to avoid false positives like "cmd /c wsl ..." where wsl
|
|
// appears as an argument rather than the executable.
|
|
static bool _isWslExe(
|
|
std::wstring_view commandLine,
|
|
std::wstring_view& outExePath,
|
|
std::wstring_view& outArguments)
|
|
{
|
|
if (commandLine.size() < 3)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Shared by both branches: split arguments after the exe, skipping one optional space.
|
|
const auto splitArgs = [&](size_t pos) noexcept {
|
|
if (pos < commandLine.size() && til::at(commandLine, pos) == L' ')
|
|
{
|
|
pos++;
|
|
}
|
|
outArguments = til::safe_slice_abs(commandLine, pos, std::wstring_view::npos);
|
|
return true;
|
|
};
|
|
|
|
// Quoted: the executable path extends from the first to the second double-quote.
|
|
if (commandLine.front() == L'"')
|
|
{
|
|
const auto close = commandLine.find(L'"', 1);
|
|
if (close == std::wstring_view::npos)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const auto path = commandLine.substr(1, close - 1);
|
|
const auto sep = path.find_last_of(L"\\/");
|
|
const auto name = sep == std::wstring_view::npos ? path : path.substr(sep + 1);
|
|
if (!til::equals_insensitive_ascii(name, L"wsl") &&
|
|
!til::equals_insensitive_ascii(name, L"wsl.exe"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
outExePath = path;
|
|
return splitArgs(close + 1);
|
|
}
|
|
|
|
const auto beg = commandLine.begin();
|
|
const auto end = commandLine.end();
|
|
auto it = beg;
|
|
|
|
// Unquoted: find "wsl" as a filename component preceded by '\', '/' or start-of-string,
|
|
// optionally followed by ".exe", then ' ' or end-of-string.
|
|
for (;;)
|
|
{
|
|
static constexpr auto needle = L"wsl";
|
|
it = std::search(
|
|
it,
|
|
end,
|
|
needle,
|
|
needle + 3,
|
|
[](wchar_t c1, wchar_t c2) noexcept {
|
|
return til::tolower_ascii(c1) == c2;
|
|
});
|
|
if (it == end)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Ensure that no matter what, we advance at least 1 char per iteration.
|
|
++it;
|
|
|
|
// Verify if it's "/wsl" and not some freestanding word.
|
|
if ((it - beg) >= 2 && til::at(it, -2) != L'\\' && til::at(it, -2) != L'/')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip past "wsl" (we have already advanced by 1 above).
|
|
it += 2;
|
|
|
|
// Consume an optional ".exe" suffix.
|
|
const auto hasExeSuffix = (end - it) >= 4 && til::equals_insensitive_ascii(std::wstring_view{ &*it, 4 }, L".exe");
|
|
if (hasExeSuffix)
|
|
{
|
|
it += 4;
|
|
}
|
|
|
|
// Ensure that wsl.exe is followed by either whitespace or the end of the string.
|
|
// (Aka: It's its own word.)
|
|
if (it != end && *it != L' ')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const std::wstring_view candidate{ beg, it };
|
|
|
|
// For paths with spaces (e.g. "C:\Program Files\WSL\wsl.exe ..."), the
|
|
// boundary between exe and arguments is ambiguous. Verify the file exists.
|
|
if (candidate.find(L' ') != std::wstring_view::npos)
|
|
{
|
|
std::wstring path;
|
|
path.reserve(candidate.size() + 4);
|
|
path.append(candidate);
|
|
if (!hasExeSuffix)
|
|
{
|
|
path.append(L".exe");
|
|
}
|
|
|
|
const auto attrs = GetFileAttributesW(path.c_str());
|
|
if (attrs == INVALID_FILE_ATTRIBUTES || (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
outExePath = candidate;
|
|
return splitArgs(it - beg);
|
|
}
|
|
}
|
|
|
|
// Checks whether the given hostname refers to the local machine.
|
|
// WSL uses GetComputerNameExA(ComputerNamePhysicalDnsHostname) to produce
|
|
// the hostname in OSC 7 URIs (see WSL's GetLinuxHostName/CleanHostname).
|
|
static bool _isLocalHost(std::wstring_view hostname)
|
|
{
|
|
if (til::equals_insensitive_ascii(hostname, L"localhost"))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
static const auto cachedHostname = []() {
|
|
wchar_t buf[64];
|
|
DWORD len = ARRAYSIZE(buf);
|
|
if (!GetComputerNameExW(ComputerNamePhysicalDnsHostname, &buf[0], &len))
|
|
{
|
|
len = 0;
|
|
}
|
|
return std::wstring{ &buf[0], static_cast<size_t>(len) };
|
|
}();
|
|
return til::equals_insensitive_ascii(hostname, cachedHostname);
|
|
}
|
|
|
|
static std::wstring _mangleStartingDirectoryForWSL(std::wstring_view startingDirectory)
|
|
{
|
|
std::wstring dir{ startingDirectory };
|
|
|
|
if (til::starts_with(dir, L"//wsl$") || til::starts_with(dir, L"//wsl.localhost"))
|
|
{
|
|
// GH#11994: `wsl --cd` treats forward-slash paths as linux-relative.
|
|
// We routinely see these two paths being used, but they're actually
|
|
// meant as Windows paths, so we convert them to \\ here.
|
|
std::ranges::replace(dir, L'/', L'\\');
|
|
return dir;
|
|
}
|
|
|
|
if (!til::starts_with(dir, LR"(\\)"))
|
|
{
|
|
// Any other paths, primarily C:\..., etc., are not our concern.
|
|
return dir;
|
|
}
|
|
|
|
if (til::starts_with(dir, LR"(\\wsl$)") || til::starts_with(dir, LR"(\\wsl.localhost)"))
|
|
{
|
|
// Some users have configured shells in WSL to emit OSC 9;9 paths with wslpath.
|
|
// Do nothing with them (pass them through with --cd.)
|
|
return dir;
|
|
}
|
|
|
|
// WSL shells use OSC 7 which uses file URIs. We turn those into UNC paths for
|
|
// storage and here we turn them back (\\hostname\bar\baz --> /bar/baz).
|
|
// Only do this for UNC paths whose hostname refers to the local machine.
|
|
// Our OSC 7 parser already rejects lexically invalid paths (via til::is_legal_path).
|
|
const auto slash = dir.find_first_of(L"\\/", 2);
|
|
const auto hostname = til::safe_slice_abs(dir, 2, slash);
|
|
|
|
if (!_isLocalHost(hostname))
|
|
{
|
|
// Leave (most likely) genuine UNC SMB paths alone.
|
|
return dir;
|
|
}
|
|
|
|
// Extract the path component and convert it to forward slashes (= UNIX path).
|
|
dir.erase(0, std::min(slash, dir.size()));
|
|
std::ranges::replace(dir, L'\\', L'/');
|
|
return dir;
|
|
}
|
|
|
|
// Function Description:
|
|
// - Promotes a starting directory provided to a WSL invocation to a commandline argument.
|
|
// This is necessary because WSL has some modicum of support for linux-side directories (!) which
|
|
// CreateProcess never will.
|
|
std::tuple<std::wstring, std::wstring> Utils::MangleStartingDirectoryForWSL(std::wstring_view commandLine,
|
|
std::wstring_view startingDirectory)
|
|
{
|
|
// Returns true if arguments contain a bare ~ (end-of-string or followed by space).
|
|
// A bare ~ conflicts with --cd; ~/path is fine (e.g. wsl -d Debian ~/blah.sh).
|
|
const auto hasBareTilde = [](std::wstring_view args) {
|
|
const auto t = args.find(L'~');
|
|
return t != std::wstring_view::npos &&
|
|
(t + 1 == args.size() || til::at(args, t + 1) == L' ');
|
|
};
|
|
|
|
std::wstring_view exePath;
|
|
std::wstring_view arguments;
|
|
std::wstring newCmd;
|
|
std::wstring newDir;
|
|
|
|
if (!startingDirectory.empty() &&
|
|
_isWslExe(commandLine, exePath, arguments) &&
|
|
arguments.find(L"--cd") == std::wstring_view::npos &&
|
|
!hasBareTilde(arguments))
|
|
{
|
|
const auto dir = _mangleStartingDirectoryForWSL(startingDirectory);
|
|
newCmd = fmt::format(FMT_COMPILE(LR"("{}" --cd "{}" {})"), exePath, dir, arguments);
|
|
}
|
|
else
|
|
{
|
|
// GH#12353: ~ is never a valid Windows path. We can only accept it
|
|
// when the exe is wsl.exe. So, we mangle it to %USERPROFILE% here.
|
|
newCmd = commandLine;
|
|
newDir = startingDirectory == L"~" ? wil::GetEnvironmentVariableW<std::wstring>(L"USERPROFILE") : std::wstring{ startingDirectory };
|
|
}
|
|
|
|
return { std::move(newCmd), std::move(newDir) };
|
|
}
|
|
|
|
std::wstring_view Utils::TrimPaste(std::wstring_view textView) noexcept
|
|
{
|
|
const auto lastNonSpace = textView.find_last_not_of(L"\t\n\v\f\r ");
|
|
const auto firstNewline = textView.find_first_of(L"\n\v\f\r");
|
|
|
|
const bool isOnlyWhitespace = lastNonSpace == textView.npos;
|
|
const bool isMultiline = firstNewline < lastNonSpace;
|
|
|
|
if (isOnlyWhitespace)
|
|
{
|
|
// Text is all white space, nothing to paste
|
|
return L"";
|
|
}
|
|
|
|
if (isMultiline)
|
|
{
|
|
// In this case, the user totally wanted to paste multiple lines of text,
|
|
// and that likely includes the trailing newline.
|
|
// DON'T trim it in this case.
|
|
return textView;
|
|
}
|
|
|
|
return textView.substr(0, lastNonSpace + 1);
|
|
}
|
|
|
|
// Disable vectorization-unfriendly warnings.
|
|
#pragma warning(push)
|
|
#pragma warning(disable : 26429) // Symbol '...' is never tested for nullness, it can be marked as not_null (f.23).
|
|
#pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1).
|
|
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
|
|
#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1).
|
|
|
|
// Returns true for C0 characters and C1 [single-character] CSI.
|
|
constexpr bool isActionableFromGround(const wchar_t wch) noexcept
|
|
{
|
|
// This is equivalent to:
|
|
// return (wch <= 0x1f) || (wch >= 0x7f && wch <= 0x9f);
|
|
// It's written like this to get MSVC to emit optimal assembly for findActionableFromGround.
|
|
// It lacks the ability to turn boolean operators into binary operations and also happens
|
|
// to fail to optimize the printable-ASCII range check into a subtraction & comparison.
|
|
return (wch <= 0x1f) | (static_cast<wchar_t>(wch - 0x7f) <= 0x20);
|
|
}
|
|
|
|
const wchar_t* Utils::FindActionableControlCharacter(const wchar_t* beg, const size_t len) noexcept
|
|
{
|
|
auto it = beg;
|
|
|
|
// The following vectorized code replicates isActionableFromGround which is equivalent to:
|
|
// (wch <= 0x1f) || (wch >= 0x7f && wch <= 0x9f)
|
|
// or rather its more machine friendly equivalent:
|
|
// (wch <= 0x1f) | ((wch - 0x7f) <= 0x20)
|
|
#if defined(TIL_SSE_INTRINSICS)
|
|
|
|
for (const auto end = beg + (len & ~size_t{ 7 }); it < end; it += 8)
|
|
{
|
|
const auto wch = _mm_loadu_si128(reinterpret_cast<const __m128i*>(it));
|
|
const auto z = _mm_setzero_si128();
|
|
|
|
// Dealing with unsigned numbers in SSE2 is annoying because it has poor support for that.
|
|
// We'll use subtractions with saturation ("SubS") to work around that. A check like
|
|
// a < b can be implemented as "max(0, a - b) == 0" and "max(0, a - b)" is what "SubS" is.
|
|
|
|
// Check for (wch < 0x20)
|
|
auto a = _mm_subs_epu16(wch, _mm_set1_epi16(0x1f));
|
|
// Check for "((wch - 0x7f) <= 0x20)" by adding 0x10000-0x7f, which overflows to a
|
|
// negative number if "wch >= 0x7f" and then subtracting 0x9f-0x7f with saturation to an
|
|
// unsigned number (= can't go lower than 0), which results in all numbers up to 0x9f to be 0.
|
|
auto b = _mm_subs_epu16(_mm_add_epi16(wch, _mm_set1_epi16(static_cast<short>(0xff81))), _mm_set1_epi16(0x20));
|
|
a = _mm_cmpeq_epi16(a, z);
|
|
b = _mm_cmpeq_epi16(b, z);
|
|
|
|
const auto c = _mm_or_si128(a, b);
|
|
const auto mask = _mm_movemask_epi8(c);
|
|
|
|
if (mask)
|
|
{
|
|
unsigned long offset;
|
|
_BitScanForward(&offset, mask);
|
|
it += offset / 2;
|
|
return it;
|
|
}
|
|
}
|
|
|
|
#elif defined(TIL_ARM_NEON_INTRINSICS)
|
|
|
|
uint64_t mask;
|
|
|
|
for (const auto end = beg + (len & ~size_t{ 7 });;)
|
|
{
|
|
if (it >= end)
|
|
{
|
|
goto plainSearch;
|
|
}
|
|
|
|
const auto wch = vld1q_u16(it);
|
|
const auto a = vcleq_u16(wch, vdupq_n_u16(0x1f));
|
|
const auto b = vcleq_u16(vsubq_u16(wch, vdupq_n_u16(0x7f)), vdupq_n_u16(0x20));
|
|
const auto c = vorrq_u16(a, b);
|
|
|
|
mask = vgetq_lane_u64(c, 0);
|
|
if (mask)
|
|
{
|
|
break;
|
|
}
|
|
it += 4;
|
|
|
|
mask = vgetq_lane_u64(c, 1);
|
|
if (mask)
|
|
{
|
|
break;
|
|
}
|
|
it += 4;
|
|
}
|
|
|
|
unsigned long offset;
|
|
_BitScanForward64(&offset, mask);
|
|
it += offset / 16;
|
|
return it;
|
|
|
|
plainSearch:
|
|
|
|
#endif
|
|
|
|
#pragma loop(no_vector)
|
|
for (const auto end = beg + len; it < end && !isActionableFromGround(*it); ++it)
|
|
{
|
|
}
|
|
|
|
return it;
|
|
}
|
|
|
|
// Returns true if it's a valid path to a directory.
|
|
bool Utils::IsValidDirectory(const wchar_t* path) noexcept
|
|
{
|
|
if (path == nullptr || *path == L'\0')
|
|
{
|
|
return false;
|
|
}
|
|
|
|
WIN32_FILE_ATTRIBUTE_DATA data;
|
|
const auto ok = GetFileAttributesExW(path, GetFileExInfoStandard, &data);
|
|
return ok && (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
}
|
|
|
|
#pragma warning(pop)
|
|
|
|
std::wstring Utils::EvaluateStartingDirectory(
|
|
std::wstring_view currentDirectory,
|
|
std::wstring_view startingDirectory)
|
|
{
|
|
std::wstring resultPath{ startingDirectory };
|
|
|
|
// We only want to resolve the new WD against the CWD if it doesn't look
|
|
// like a Linux path (see GH#592)
|
|
|
|
// Append only if it DOESN'T look like a linux-y path. A linux-y path starts
|
|
// with `~` or `/`.
|
|
const bool looksLikeLinux =
|
|
resultPath.size() >= 1 &&
|
|
(til::at(resultPath, 0) == L'~' || til::at(resultPath, 0) == L'/');
|
|
|
|
if (!looksLikeLinux)
|
|
{
|
|
std::filesystem::path cwd{ currentDirectory };
|
|
cwd /= startingDirectory;
|
|
resultPath = cwd.wstring();
|
|
}
|
|
return resultPath;
|
|
}
|
|
|
|
bool Utils::IsWindows11() noexcept
|
|
{
|
|
static const bool isWindows11 = []() noexcept {
|
|
OSVERSIONINFOEXW osver{};
|
|
osver.dwOSVersionInfoSize = sizeof(osver);
|
|
osver.dwBuildNumber = 22000;
|
|
|
|
DWORDLONG dwlConditionMask = 0;
|
|
VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL);
|
|
|
|
if (VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE)
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}();
|
|
return isWindows11;
|
|
}
|
|
|
|
bool Utils::IsLikelyToBeEmojiOrSymbolIcon(std::wstring_view text) noexcept
|
|
{
|
|
if (text.size() == 1 && !IS_HIGH_SURROGATE(til::at(text, 0)))
|
|
{
|
|
// If it's a single code unit, it's definitely either zero or one grapheme clusters.
|
|
// If it turns out to be illegal Unicode, we don't really care.
|
|
return true;
|
|
}
|
|
|
|
if (text.size() >= 2 && til::at(text, 0) <= 0x7F && til::at(text, 1) <= 0x7F)
|
|
{
|
|
// Two adjacent ASCII characters (as seen in most file paths) aren't a single
|
|
// grapheme cluster.
|
|
return false;
|
|
}
|
|
|
|
// Use ICU to determine whether text is composed of a single grapheme cluster.
|
|
int32_t off{ 0 };
|
|
UErrorCode status{ U_ZERO_ERROR };
|
|
|
|
#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1).
|
|
const auto b{ ubrk_open(UBRK_CHARACTER,
|
|
nullptr,
|
|
reinterpret_cast<const UChar*>(text.data()),
|
|
gsl::narrow_cast<int32_t>(text.size()),
|
|
&status) };
|
|
if (status <= U_ZERO_ERROR)
|
|
{
|
|
off = ubrk_next(b);
|
|
ubrk_close(b);
|
|
}
|
|
return off == gsl::narrow_cast<int32_t>(text.size());
|
|
}
|