terminal/src/types/utils.cpp

562 lines
19 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "inc/utils.hpp"
#include "inc/colorTable.hpp"
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
}
// Function Description:
// - Creates a String representation of a guid, in the format
// "{12345678-ABCD-EF12-3456-7890ABCDEF12}"
// Arguments:
// - guid: the GUID to create the string for
// Return Value:
// - a string representation of the GUID. On failure, throws E_INVALIDARG.
std::wstring Utils::GuidToString(const GUID guid)
{
return wil::str_printf<std::wstring>(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]);
}
// Method Description:
// - Parses a GUID from a string representation of the GUID. Throws an exception
// if it fails to parse the GUID. See documentation of IIDFromString for
// details.
// Arguments:
// - wstr: a string representation of the GUID to parse
// Return Value:
// - A GUID if the string could successfully be parsed. On failure, throws the
// failing HRESULT.
GUID Utils::GuidFromString(_Null_terminated_ const wchar_t* str)
{
GUID result;
THROW_IF_FAILED(IIDFromString(str, &result));
return result;
}
// 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)
{
std::stringstream ss;
ss << "#" << std::uppercase << std::setfill('0') << std::hex;
// Force the compiler to promote from byte to int. Without it, the
// stringstream will try to write the components as chars
ss << std::setw(2) << static_cast<int>(color.r);
ss << std::setw(2) << static_cast<int>(color.g);
ss << std::setw(2) << static_cast<int>(color.b);
return ss.str();
}
// 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() != 7 && str.size() != 4);
THROW_HR_IF(E_INVALIDARG, str.at(0) != '#');
std::string rStr;
std::string gStr;
std::string bStr;
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));
}
else
{
rStr = std::string(&str.at(1), 2);
gStr = std::string(&str.at(3), 2);
bStr = std::string(&str.at(5), 2);
}
const BYTE r = gsl::narrow_cast<BYTE>(std::stoul(rStr, nullptr, 16));
const BYTE g = gsl::narrow_cast<BYTE>(std::stoul(gStr, nullptr, 16));
const BYTE b = gsl::narrow_cast<BYTE>(std::stoul(bStr, nullptr, 16));
return til::color{ r, g, b };
}
// 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
{
bool foundXParseColorSpec = false;
bool foundValidColorSpec = false;
bool 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++)
{
bool 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 wchar_t 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:
// - 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;
bool 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 wchar_t 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 wchar_t 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;
}
// 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 gsl::span<const gsl::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<gsl::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);
}