Files
terminal/src/types/utils.cpp
Leonard Hecker d22203fa0a Missed one
2026-04-17 21:38:08 +02:00

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());
}