#include "kernel_context.h" #include "text.h" #include #include #include #include #include namespace raven { namespace { constexpr size_t max_path = 260; // The text the searching functions look through may be a whole document: it is read to its end, however long. constexpr size_t whole_text = size_t(1) << 30; bool separator(char16_t character) { return character == u'\\' || character == u'/'; } // Index of the file name inside a path: after the last separator or drive colon. size_t file_name_start(std::u16string_view path) { size_t start = 0; // A separator or colon starts the name only when a name follows it: "C:" and "C:\" are their own file names, as Windows answers. for (size_t at = 0; at + 1 < path.size(); ++at) if ((separator(path[at]) || path[at] == u':') && !separator(path[at + 1])) start = at + 1; return start; } // The scheme of a URL: a letter, then letters, digits, '+', '-' or '.', before a colon. A single letter is a drive, not a scheme. size_t scheme_length(std::u16string_view text) { size_t at = 0; const auto letter = [](char16_t c) { return (c >= u'a' && c <= u'z') || (c >= u'A' && c <= u'Z'); }; while (at < text.size() && (letter(text[at]) || (at && ((text[at] >= u'0' && text[at] <= u'9') || text[at] == u'+' || text[at] == u'-' || text[at] == u'.')))) ++at; return at >= 2 && at < text.size() && text[at] == u':' ? at : 0; } // What UrlIs tells of a text: a URL, an opaque one (nothing hierarchical after the scheme), a file URL, a directory. bool url_is(std::u16string_view text, uint32_t kind) { const size_t scheme = scheme_length(text); switch (kind) { case 0: return scheme; case 1: return scheme && (scheme + 1 == text.size() || !separator(text[scheme + 1])); case 3: { std::u16string lower(text.substr(0, scheme)); std::transform(lower.begin(), lower.end(), lower.begin(), [](char16_t c) { return c >= u'A' && c <= u'Z' ? char16_t(c + 32) : c; }); return lower == u"file"; } case 5: return !text.empty() && separator(text.back()); default: return false; } } size_t extension_start(std::u16string_view path) { // The last dot after the last separator, a trailing one included. size_t dot = std::u16string_view::npos; for (size_t at = 0; at < path.size(); ++at) if (separator(path[at]) || (path[at] == u':' && at == 1)) dot = std::u16string_view::npos; else if (path[at] == u'.') dot = at; return dot == std::u16string_view::npos ? path.size() : dot; } // Length of the root part: "X:\", "\\server\share\" or a single leading separator. size_t root_length(std::u16string_view path) { if (path.size() >= 3 && path[1] == u':' && separator(path[2])) return 3; if (path.size() >= 2 && separator(path[0]) && separator(path[1])) { size_t at = 2, parts = 0; while (at < path.size() && parts < 2) { while (at < path.size() && !separator(path[at])) ++at; ++parts; if (at < path.size() && parts < 2) ++at; } if (at < path.size()) ++at; return at; } if (!path.empty() && separator(path[0])) return 1; return 0; } bool is_unc_root(std::u16string_view path) { return path.size() >= 2 && separator(path[0]) && separator(path[1]); } std::u16string canonical(std::u16string_view path) { const size_t root = root_length(path); std::u16string result(path.substr(0, root)); for (auto &character : result) if (character == u'/') character = u'\\'; std::vector parts; size_t at = root; const bool trailing = !path.empty() && separator(path.back()) && path.size() > root; while (at <= path.size()) { size_t end = at; while (end < path.size() && !separator(path[end])) ++end; const auto part = path.substr(at, end - at); if (part == u"..") { if (!parts.empty()) parts.pop_back(); } else if (!part.empty() && part != u".") { parts.emplace_back(part); } if (end >= path.size()) break; at = end + 1; } for (size_t index = 0; index < parts.size(); ++index) { if (!result.empty() && result.back() != u'\\') result.push_back(u'\\'); result += parts[index]; } if (trailing && !parts.empty()) result.push_back(u'\\'); if (result.empty()) result = u"\\"; return result; } std::u16string join(std::u16string_view directory, std::u16string_view file) { if (directory.empty()) return std::u16string(file); if (file.empty()) return std::u16string(directory); std::u16string result(directory); if (!separator(result.back())) result.push_back(u'\\'); result += separator(file.front()) ? file.substr(1) : file; return result; } bool absolute(std::u16string_view path) { return root_length(path) > 0; } bool match_one(std::u16string_view spec, std::u16string_view name) { size_t p = 0, n = 0, star = std::u16string_view::npos, mark = 0; while (n < name.size()) { if (p < spec.size() && (spec[p] == u'?' || equal_fold(spec.substr(p, 1), name.substr(n, 1)))) { ++p; ++n; } else if (p < spec.size() && spec[p] == u'*') { star = p++; mark = n; } else if (star != std::u16string_view::npos) { p = star + 1; n = ++mark; } else { return false; } } while (p < spec.size() && spec[p] == u'*') ++p; return p == spec.size(); } // HLS in the 0..240 range of shlwapi, from 8-bit RGB. void rgb_to_hls(uint32_t rgb, uint16_t &hue, uint16_t &luminance, uint16_t &saturation) { const int r = rgb & 255, g = (rgb >> 8) & 255, b = (rgb >> 16) & 255; const int maximum = std::max({r, g, b}), minimum = std::min({r, g, b}); const int sum = maximum + minimum, difference = maximum - minimum; luminance = uint16_t(((sum * 240) + 255) / 510); if (!difference) { saturation = 0; hue = 160; return; } saturation = uint16_t(luminance <= 120 ? (difference * 240 + sum / 2) / sum : (difference * 240 + (510 - sum) / 2) / (510 - sum)); const int rc = ((maximum - r) * 40 + difference / 2) / difference, gc = ((maximum - g) * 40 + difference / 2) / difference, bc = ((maximum - b) * 40 + difference / 2) / difference; int result = r == maximum ? bc - gc : g == maximum ? 80 + rc - bc : 160 + gc - rc; if (result < 0) result += 240; if (result > 240) result -= 240; hue = uint16_t(result); } int hue_component(int n1, int n2, int hue) { if (hue < 0) hue += 240; if (hue > 240) hue -= 240; if (hue < 40) return n1 + ((n2 - n1) * hue + 20) / 40; if (hue < 120) return n2; if (hue < 160) return n1 + ((n2 - n1) * (160 - hue) + 20) / 40; return n1; } uint32_t hls_to_rgb(int hue, int luminance, int saturation) { int r, g, b; if (!saturation) { r = g = b = luminance * 255 / 240; } else { const int m2 = luminance <= 120 ? (luminance * (240 + saturation) + 120) / 240 : luminance + saturation - (luminance * saturation + 120) / 240; const int m1 = 2 * luminance - m2; const auto scale = [](int value) { return std::clamp((value * 255 + 120) / 240, 0, 255); }; r = scale(hue_component(m1, m2, hue + 80)); g = scale(hue_component(m1, m2, hue)); b = scale(hue_component(m1, m2, hue - 80)); } return uint32_t(r) | uint32_t(g) << 8 | uint32_t(b) << 16; } std::u16string compact(std::u16string_view path, size_t capacity) { if (path.size() < capacity) return std::u16string(path); if (capacity < 4) return std::u16string(path.substr(0, capacity ? capacity - 1 : 0)); const size_t name = file_name_start(path); const auto file = path.substr(name); const std::u16string_view ellipsis = u"..."; if (file.size() + ellipsis.size() + 1 >= capacity) { std::u16string result(file.substr(0, capacity - 1 - ellipsis.size())); return result + std::u16string(ellipsis); } const size_t room = capacity - 1 - ellipsis.size() - file.size(); std::u16string result(path.substr(0, std::min(room, name))); return result + std::u16string(ellipsis) + std::u16string(file); } } void register_shlwapi_api(ApiRegistry ®istry, KernelContext &context) { auto &paths = context.paths; // Path functions edit a MAX_PATH buffer in place; this reads it, applies the edit and writes it back. const auto edit = [](ApiCall &call, const std::function &change) { auto text = call.wide(call[0], max_path * 4); const bool changed = change(text); call.put_wide(call[0], text, max_path * 4); call.ret(changed); }; // The system the shell reports is a Windows 10 Pro workstation; a 32-bit program also learns that it runs under WOW64, which installers // read before they turn to the 64-bit registry view and program folder. registry.add("IsOS", 1, [](ApiCall &call) { switch (call.u32(0)) { case 1: case 2: case 3: case 5: case 7: case 8: case 17: case 18: case 20: case 26: case 27: call.ret(1); break; case 30: call.ret(call.pointer_width() == 4); break; default: call.ret(0); } }); // Completion lists are drawn by the shell; an edit control without one still works, so enabling it succeeds without a list. registry.add("SHAutoComplete", 2, [](ApiCall &call) { call.ret(0); }); registry.add("PathRemoveFileSpecW", 1, [edit](ApiCall &call) { edit(call, [](std::u16string &path) { const size_t root = root_length(path); if (path.empty() || (path.size() == root && root)) return false; const auto last = path.find_last_of(u"\\/"); if (last == path.npos) { if (path.size() > 2 && path[1] == u':') { path.resize(2); return true; } path.clear(); return true; } if (is_unc_root(path) && last < root) { if (last + 1 == path.size() && path.size() > root - 1 && root == path.size()) return false; } if (last + 1 == path.size()) { path.pop_back(); return true; } if (last < root && root) path.resize(root); else path.resize(last); return true; }); }); registry.add("PathStripPathW", 1, [edit](ApiCall &call) { edit(call, [](std::u16string &path) { path.erase(0, file_name_start(path)); return true; }); }); registry.add("PathRemoveExtensionW", 1, [edit](ApiCall &call) { edit(call, [](std::u16string &path) { path.resize(extension_start(path)); return true; }); }); registry.add("PathAddExtensionW", 2, [](ApiCall &call) { auto path = call.wide(call[0], max_path * 4); const auto extension = call[1] ? call.wide(call[1]) : u".exe"; if (extension_start(path) != path.size()) { call.ret(0); return; } if (path.size() + extension.size() >= max_path) { call.ret(0); return; } path += extension; call.put_wide(call[0], path, max_path); call.ret(1); }); registry.add("PathAppendW", 2, [](ApiCall &call) { auto path = call.wide(call[0], max_path * 4); const auto more = call.wide(call[1]); auto joined = canonical(join(path, more)); if (joined.size() >= max_path) { call.ret(0); return; } call.put_wide(call[0], joined, max_path); call.ret(1); }); registry.add("PathCombineW", 3, [](ApiCall &call) { const auto directory = call[1] ? call.wide(call[1]) : std::u16string(); const auto file = call[2] ? call.wide(call[2]) : std::u16string(); const auto combined = absolute(file) && !(separator(file[0]) && !is_unc_root(file)) ? file : join(directory, file); const auto result = canonical(combined); if (result.size() >= max_path) { call.ret(0); return; } call.put_wide(call[0], result, max_path); call.ret(call[0]); }); registry.add("PathCanonicalizeW", 2, [](ApiCall &call) { const auto result = canonical(call.wide(call[1])); if (result.size() >= max_path) { call.fail(206); return; } call.put_wide(call[0], result, max_path); call.ret(1); }); registry.add("PathIsRelativeW", 1, [](ApiCall &call) { const auto path = call.wide(call[0]); call.ret(path.empty() || !(root_length(path) > 0)); }); // The path cut back to its root, "C:\" or "\\server\share\"; a path without one is emptied and fails. registry.add("PathStripToRootW", 1, [](ApiCall &call) { const auto path = call.wide(call[0]); const size_t root = root_length(path); call.put_wide(call[0], path.substr(0, root), root + 1); call.ret(root > 0); }); registry.add("PathStripToRootA", 1, [](ApiCall &call) { const auto path = call.narrow(call[0]); const size_t root = root_length(widen_ascii(path)); call.put_narrow(call[0], path.substr(0, root), root + 1); call.ret(root > 0); }); // %XX sequences become the characters they stand for; URL_DONT_UNESCAPE_EXTRA_INFO keeps what follows '?' or '#' as it is, and // URL_UNESCAPE_INPLACE writes over the URL itself. const auto unescape = [](std::u16string url, uint32_t flags) { std::u16string result; const auto digit = [](char16_t c) { return c >= u'0' && c <= u'9' ? c - u'0' : c >= u'a' && c <= u'f' ? c - u'a' + 10 : c >= u'A' && c <= u'F' ? c - u'A' + 10 : -1; }; bool rest = false; for (size_t at = 0; at < url.size(); ++at) { if ((flags & 0x02000000U) && (url[at] == u'?' || url[at] == u'#')) rest = true; if (!rest && url[at] == u'%' && at + 2 < url.size() + 0 && digit(url[at + 1]) >= 0 && digit(url[at + 2]) >= 0) { result.push_back(char16_t(digit(url[at + 1]) * 16 + digit(url[at + 2]))); at += 2; } else { result.push_back(url[at]); } } return result; }; const auto url_unescape = [unescape](ApiCall &call, bool wide) { const auto url = wide ? call.wide(call[0]) : widen_ascii(call.narrow(call[0])); const auto result = unescape(url, call.u32(3)); const auto store = [&](uint64_t address, size_t capacity) { if (wide) call.put_wide(address, result, capacity); else call.put_narrow(address, std::string(result.begin(), result.end()), capacity); }; if (call.u32(3) & 0x00100000U) { store(call[0], result.size() + 1); call.ret(0); return; } if (!call[1] || !call[2]) { call.ret(0x80070057U); return; } const uint32_t capacity = call.load(call[2]); if (capacity <= result.size()) { call.store(call[2], uint32_t(result.size() + 1)); call.ret(0x80004003U); return; } store(call[1], capacity); call.store(call[2], uint32_t(result.size())); call.ret(0); }; // A bare name is looked for as SearchPath finds programs; anything else becomes a full path. Both write at most the given length. registry.add("PathSearchAndQualifyW", 3, [®istry](ApiCall &call) { const auto path = call.wide(call[0]); if (path.find_first_of(u"\\/:") == std::u16string::npos) if (const auto search = registry.find("SearchPathW")) { ApiCall inner(call.process, call.environment, {0, call[0], 0, call[2], call[1], 0}); search->second(inner); if (inner.result && inner.result < call.u32(2)) { call.ret(1); return; } } const auto full = registry.find("GetFullPathNameW"); ApiCall inner(call.process, call.environment, {call[0], call[2], call[1], 0}); full->second(inner); call.ret(inner.result && inner.result < call.u32(2)); }); // A scheme for text that has none: a Windows path becomes a file URL (URL_APPLY_GUESSFILE), "www." and "ftp." names get their scheme // (URL_APPLY_GUESSSCHEME), anything else http (URL_APPLY_DEFAULT). Text that has a scheme already is left as it is (S_FALSE). const auto apply_scheme = [](std::u16string text, uint32_t flags) -> std::optional { if (scheme_length(text) && !(flags & 8)) return std::nullopt; const bool drive = text.size() >= 2 && text[1] == u':' && ((text[0] >= u'a' && text[0] <= u'z') || (text[0] >= u'A' && text[0] <= u'Z')); if ((flags & 4) && (drive || (text.size() >= 2 && separator(text[0]) && separator(text[1])))) { std::u16string url = drive ? u"file:///" : u"file:"; for (const auto character : text) url.push_back(character == u'\\' ? u'/' : character); return url; } if (flags & 2) { std::u16string lower(text.substr(0, 4)); for (auto &character : lower) if (character >= u'A' && character <= u'Z') character = char16_t(character + 32); if (lower == u"www.") return u"http://" + text; if (lower == u"ftp.") return u"ftp://" + text; } if (flags & 1) return u"http://" + text; return std::nullopt; }; const auto url_apply_scheme = [apply_scheme](ApiCall &call, bool wide) { const auto text = wide ? call.wide(call[0]) : widen_ascii(call.narrow(call[0])); const auto result = apply_scheme(text, call.u32(3)); if (!result) { call.ret(1); return; } const uint32_t capacity = call.load(call[2]); if (capacity <= result->size()) { call.store(call[2], uint32_t(result->size() + 1)); call.ret(0x80004003U); return; } if (wide) call.put_wide(call[1], *result, capacity); else call.put_narrow(call[1], std::string(result->begin(), result->end()), capacity); call.store(call[2], uint32_t(result->size())); call.ret(0); }; // The standard form of a URL. A Windows path becomes a file URL, "file:///C:/dir/name" with forward slashes, or with // URL_FILE_USE_PATHURL "file://C:\dir\name"; the scheme is written in lower case; "." and ".." steps of the path are resolved unless // URL_DONT_SIMPLIFY; URL_UNESCAPE decodes %XX sequences and URL_ESCAPE_SPACES_ONLY writes spaces as %20. const auto canonicalize = [unescape](std::u16string text, uint32_t flags) { const bool drive = text.size() >= 2 && text[1] == u':' && ((text[0] >= u'a' && text[0] <= u'z') || (text[0] >= u'A' && text[0] <= u'Z')); const bool unc = text.size() >= 2 && separator(text[0]) && separator(text[1]); const bool path_url = flags & 0x00010000U; std::u16string url; size_t path_start = 0; if (!scheme_length(text) && (drive || unc)) { url = path_url ? u"file://" : drive ? u"file:///" : u"file:"; path_start = url.size(); for (const auto character : text) url.push_back(path_url ? (character == u'/' ? u'\\' : character) : character == u'\\' ? u'/' : character); } else { url = text; const size_t scheme = scheme_length(url); for (size_t at = 0; at < scheme; ++at) if (url[at] >= u'A' && url[at] <= u'Z') url[at] = char16_t(url[at] + 32); path_start = scheme ? scheme + 1 : 0; while (path_start < url.size() && path_start < scheme + 3 && url[path_start] == u'/') ++path_start; if (scheme && url.compare(0, scheme, u"file") == 0 && path_url) { std::u16string path = url.substr(path_start); for (auto &character : path) if (character == u'/') character = u'\\'; url = u"file://" + path; path_start = 7; } } if (!(flags & 0x08000000U)) { const char16_t slash = path_url ? u'\\' : u'/'; const size_t end = std::min(url.size(), url.find_first_of(u"?#", path_start)); std::vector steps; size_t at = path_start; while (at <= end) { size_t next = url.find(slash, at); if (next == std::u16string::npos || next > end) next = end; const auto step = url.substr(at, next - at); if (step == u"..") { if (steps.size() > 1) steps.pop_back(); } else if (step != u".") { steps.push_back(step); } at = next + 1; } std::u16string path; for (size_t index = 0; index < steps.size(); ++index) { if (index) path.push_back(slash); path += steps[index]; } url = url.substr(0, path_start) + path + url.substr(end); } if (flags & 0x10000000U) url = unescape(url, flags); if (flags & 0x04000000U) { std::u16string escaped; for (const auto character : url) escaped += character == u' ' ? std::u16string(u"%20") : std::u16string(1, character); url = escaped; } return url; }; const auto url_canonicalize = [canonicalize](ApiCall &call, bool wide) { if (!call[0] || !call[1] || !call[2]) { call.ret(0x80070057U); return; } const auto text = wide ? call.wide(call[0]) : widen_ascii(call.narrow(call[0])); const auto result = canonicalize(text, call.u32(3)); const uint32_t capacity = call.load(call[2]); if (capacity <= result.size()) { call.store(call[2], uint32_t(result.size() + 1)); call.ret(0x80004003U); return; } if (wide) call.put_wide(call[1], result, capacity); else call.put_narrow(call[1], std::string(result.begin(), result.end()), capacity); call.store(call[2], uint32_t(result.size())); call.ret(0); }; // A relative URL joined to a base: one with a scheme of its own stands alone, one that starts with a slash replaces the base's path // after its host, anything else replaces the base's last step. The result is canonicalized with the same flags. const auto url_combine = [canonicalize](ApiCall &call, bool wide) { if (!call[0] || !call[1] || !call[3]) { call.ret(0x80070057U); return; } const auto text = [&](uint64_t address) { return wide ? call.wide(address) : widen_ascii(call.narrow(address)); }; const uint32_t flags = call.u32(4); const auto relative = text(call[1]); std::u16string joined; // A full Windows path, "C:\..." or "\\server\...", is a file URL of its own. const bool drive = relative.size() >= 2 && relative[1] == u':' && ((relative[0] >= u'a' && relative[0] <= u'z') || (relative[0] >= u'A' && relative[0] <= u'Z')); const bool unc = relative.size() >= 2 && separator(relative[0]) && separator(relative[1]); if (scheme_length(relative) || drive || unc) joined = relative; else { const auto base = canonicalize(text(call[0]), flags & ~0x14000000U); const char16_t slash = (flags & 0x00010000U) ? u'\\' : u'/'; size_t path = scheme_length(base); path = path ? path + 1 : 0; if (base.compare(path, 2, u"//") == 0) { const size_t host_end = base.find_first_of(u"/\\", path + 2); path = host_end == std::u16string::npos ? base.size() : host_end; if (base.compare(0, 8, u"file:///") == 0) path = base.find_first_of(u"/\\", 8) == std::u16string::npos ? base.size() : base.find_first_of(u"/\\", 8); } if (relative.empty()) joined = base; else if (separator(relative[0])) joined = base.substr(0, path) + relative; else { const size_t end = std::min(base.size(), base.find_first_of(u"?#")); const size_t last = base.find_last_of(u"/\\", end ? end - 1 : 0); joined = (last == std::u16string::npos || last < path ? base.substr(0, end) + slash : base.substr(0, last + 1)) + relative; } } const auto result = canonicalize(joined, flags); const uint32_t capacity = call.load(call[3]); if (!call[2] || capacity <= result.size()) { call.store(call[3], uint32_t(result.size() + 1)); call.ret(0x80004003U); return; } if (wide) call.put_wide(call[2], result, capacity); else call.put_narrow(call[2], std::string(result.begin(), result.end()), capacity); call.store(call[3], uint32_t(result.size())); call.ret(0); }; registry.add("UrlCombineW", 5, [url_combine](ApiCall &call) { url_combine(call, true); }); registry.add("UrlCombineA", 5, [url_combine](ApiCall &call) { url_combine(call, false); }); // Writes a result into a caller's buffer whose length in characters is behind a pointer, as the Url* functions do: too small a // buffer gets the length needed, terminator included, and E_POINTER. const auto put_url = [](ApiCall &call, bool wide, uint64_t buffer, uint64_t length, const std::u16string &result) { const uint32_t capacity = call.load(length); if (!buffer || capacity <= result.size()) { call.store(length, uint32_t(result.size() + 1)); call.ret(0x80004003U); return; } if (wide) call.put_wide(buffer, result, capacity); else call.put_narrow(buffer, std::string(result.begin(), result.end()), capacity); call.store(length, uint32_t(result.size())); call.ret(0); }; // Characters a URL cannot hold as they are become %XX: controls, spaces, non-ASCII and " # % < > [ \ ] ^ ` { | } after the scheme and // host. '%' itself only with URL_ESCAPE_PERCENT; URL_ESCAPE_SPACES_ONLY escapes nothing else; URL_ESCAPE_SEGMENT_ONLY takes the // whole text as one path segment, so '/', '?' and '#' are escaped too. What follows '?' or '#' is left alone otherwise. const auto url_escape = [put_url](ApiCall &call, bool wide) { if (!call[0] || !call[2]) { call.ret(0x80070057U); return; } const auto url = wide ? call.wide(call[0]) : widen_ascii(call.narrow(call[0])); const uint32_t flags = call.u32(3); const bool segment = flags & 0x00002000U, spaces = flags & 0x04000000U, percent = flags & 0x00001000U; size_t start = 0; if (!segment) { start = scheme_length(url); start = start ? start + 1 : 0; if (url.compare(start, 2, u"//") == 0) { const size_t host = url.find(u'/', start + 2); start = host == std::u16string::npos ? url.size() : host; } } std::u16string result = url.substr(0, start); bool rest = false; for (size_t at = start; at < url.size(); ++at) { const char16_t character = url[at]; if (!segment && (character == u'?' || character == u'#')) rest = true; const bool unsafe = spaces ? character == u' ' : character <= u' ' || character >= 0x7f || std::u16string_view(u"\"<>[\\]^`{|}").find(character) != std::u16string_view::npos || (percent && character == u'%') || (segment && std::u16string_view(u"/?#").find(character) != std::u16string_view::npos); if (rest || !unsafe || character > 0xff) { result.push_back(character); continue; } static constexpr char hex[] = "0123456789ABCDEF"; result += u'%'; result += char16_t(hex[character >> 4]); result += char16_t(hex[character & 15]); } if (flags & 0x00080000U) { call.put_wide(call[0], result, result.size() + 1); call.ret(0); return; } put_url(call, wide, call[1], call[2], result); }; // One part of a URL "scheme://user:password@host:port/path?query": URL_PART_SCHEME, _HOSTNAME, _USERNAME, _PASSWORD, _PORT or _QUERY. // URL_PARTFLAG_KEEPSCHEME puts the scheme and a colon before the part. A part the URL does not have is empty, with S_FALSE. const auto url_get_part = [put_url](ApiCall &call, bool wide) { if (!call[0] || !call[1] || !call[2] || call.u32(3) < 1 || call.u32(3) > 6) { call.ret(0x80070057U); return; } const auto url = wide ? call.wide(call[0]) : widen_ascii(call.narrow(call[0])); const size_t scheme = scheme_length(url); if (!scheme) { call.ret(0x80070057U); return; } std::u16string part; const uint32_t which = call.u32(3); if (which == 1) part = url.substr(0, scheme); else if (which == 6) { const size_t query = url.find(u'?'); if (query != std::u16string::npos) part = url.substr(query + 1, url.find(u'#', query) == std::u16string::npos ? std::u16string::npos : url.find(u'#', query) - query - 1); } else if (url.compare(scheme + 1, 2, u"//") == 0) { const size_t start = scheme + 3; const size_t end = std::min(url.size(), url.find_first_of(u"/\\?#", start)); const auto authority = url.substr(start, end - start); const size_t at = authority.rfind(u'@'); const auto credentials = at == std::u16string::npos ? std::u16string() : authority.substr(0, at); const auto host = at == std::u16string::npos ? authority : authority.substr(at + 1); const size_t colon = host.find(u':'), separator = credentials.find(u':'); if (which == 2) part = host.substr(0, colon); else if (which == 5 && colon != std::u16string::npos) part = host.substr(colon + 1); else if (which == 3) part = credentials.substr(0, separator); else if (which == 4 && separator != std::u16string::npos) part = credentials.substr(separator + 1); } if ((call.u32(4) & 1) && which != 1 && !part.empty()) part = url.substr(0, scheme) + u":" + part; put_url(call, wide, call[1], call[2], part); if (part.empty() && call.result == 0) call.ret(1); }; registry.add("UrlGetPartW", 5, [url_get_part](ApiCall &call) { url_get_part(call, true); }); registry.add("UrlGetPartA", 5, [url_get_part](ApiCall &call) { url_get_part(call, false); }); registry.add("UrlEscapeW", 4, [url_escape](ApiCall &call) { url_escape(call, true); }); registry.add("UrlEscapeA", 4, [url_escape](ApiCall &call) { url_escape(call, false); }); // The Windows path a file URL names: "file:///C:/dir/a%20b" is "C:\dir\a b", "file://server/share" is "\\server\share" and // "file://C:\dir" is "C:\dir". A URL of another scheme is not a path. const auto path_from_url = [unescape, put_url](ApiCall &call, bool wide) { if (!call[0] || !call[1] || !call[2]) { call.ret(0x80070057U); return; } const auto url = wide ? call.wide(call[0]) : widen_ascii(call.narrow(call[0])); std::u16string lower = url.substr(0, 5); for (auto &character : lower) if (character >= u'A' && character <= u'Z') character = char16_t(character + 32); if (lower != u"file:") { call.ret(0x80070057U); return; } std::u16string rest = unescape(url.substr(5), 0); for (auto &character : rest) if (character == u'/') character = u'\\'; size_t slashes = 0; while (slashes < rest.size() && rest[slashes] == u'\\') ++slashes; const auto body = rest.substr(slashes); const bool drive = body.size() >= 2 && (body[1] == u':' || body[1] == u'|'); std::u16string path; if (drive) { path = body; path[1] = u':'; } else if (slashes == 2 && body.compare(0, 10, u"localhost\\") == 0) { path = body.substr(10); } else if (slashes >= 2) { path = u"\\\\" + body; } else { path = rest; } put_url(call, wide, call[1], call[2], path); }; registry.add("PathCreateFromUrlW", 4, [path_from_url](ApiCall &call) { path_from_url(call, true); }); registry.add("PathCreateFromUrlA", 4, [path_from_url](ApiCall &call) { path_from_url(call, false); }); registry.add("UrlCanonicalizeW", 4, [url_canonicalize](ApiCall &call) { url_canonicalize(call, true); }); registry.add("UrlCanonicalizeA", 4, [url_canonicalize](ApiCall &call) { url_canonicalize(call, false); }); registry.add("UrlApplySchemeW", 4, [url_apply_scheme](ApiCall &call) { url_apply_scheme(call, true); }); registry.add("UrlApplySchemeA", 4, [url_apply_scheme](ApiCall &call) { url_apply_scheme(call, false); }); registry.add("UrlUnescapeW", 4, [url_unescape](ApiCall &call) { url_unescape(call, true); }); registry.add("UrlUnescapeA", 4, [url_unescape](ApiCall &call) { url_unescape(call, false); }); registry.add("UrlIsW", 2, [](ApiCall &call) { call.ret(call[0] && url_is(call.wide(call[0]), call.u32(1))); }); registry.add("UrlIsA", 2, [](ApiCall &call) { call.ret(call[0] && url_is(widen_ascii(call.narrow(call[0])), call.u32(1))); }); registry.add("PathIsURLW", 1, [](ApiCall &call) { call.ret(call[0] && url_is(call.wide(call[0]), 0)); }); registry.add("PathIsURLA", 1, [](ApiCall &call) { call.ret(call[0] && url_is(widen_ascii(call.narrow(call[0])), 0)); }); registry.add("PathIsRootW", 1, [](ApiCall &call) { const auto path = call.wide(call[0]); call.ret(!path.empty() && root_length(path) == path.size()); }); registry.add("PathIsUNCW", 1, [](ApiCall &call) { call.ret(is_unc_root(call.wide(call[0]))); }); // The part two paths share, compared without case and ending at a whole component: C:\win\a and c:\win\b share C:\win, two // paths on one drive share at least its root, and two UNC paths nothing unless their server is the same. const auto common_prefix = [](const std::u16string &left, const std::u16string &right) { const bool unc = left.starts_with(u"\\\\") && right.starts_with(u"\\\\"); size_t at = 0, common = 0; while (at < left.size() && at < right.size() && upper_case(left[at]) == upper_case(right[at])) { if (left[at] == u'\\' && !(unc && at < 2)) common = at == 2 && left[1] == u':' ? 3 : at; ++at; } const auto ends = [](const std::u16string &text, size_t at) { return at == text.size() || text[at] == u'\\'; }; if (ends(left, at) && ends(right, at)) common = at; if (unc && left.find(u'\\', 2) != std::u16string::npos && common < left.find(u'\\', 2)) common = 0; return common; }; registry.add("PathCommonPrefixW", 3, [common_prefix](ApiCall &call) { if (!call[0] || !call[1]) { call.ret(0); return; } const auto left = call.wide(call[0]); const size_t common = common_prefix(left, call.wide(call[1])); if (call[2]) call.put_wide(call[2], left.substr(0, common), 260); call.ret(common); }); // A prefix is a path the other one begins with, whole components and all. registry.add("PathIsPrefixW", 2, [common_prefix](ApiCall &call) { if (!call[0] || !call[1]) { call.ret(0); return; } const auto prefix = call.wide(call[0]); call.ret(!prefix.empty() && common_prefix(prefix, call.wide(call[1])) == prefix.size()); }); registry.add("PathIsNetworkPathW", 1, [](ApiCall &call) { call.ret(is_unc_root(call.wide(call[0]))); }); registry.add("PathIsFileSpecW", 1, [](ApiCall &call) { call.ret(call.wide(call[0]).find_first_of(u"\\/:") == std::u16string::npos); }); registry.add("PathGetDriveNumberW", 1, [](ApiCall &call) { const auto path = call.wide(call[0]); if (path.size() >= 2 && path[1] == u':' && ((path[0] >= u'a' && path[0] <= u'z') || (path[0] >= u'A' && path[0] <= u'Z'))) call.ret(uint32_t(upper_case(path[0]) - u'A')); else call.ret(uint32_t(-1)); }); registry.add("PathFindFileNameW", 1, [](ApiCall &call) { const auto path = call.wide(call[0]); call.ret(call[0] + file_name_start(path) * 2); }); registry.add("PathFindExtensionW", 1, [](ApiCall &call) { const auto path = call.wide(call[0]); call.ret(call[0] + extension_start(path) * 2); }); registry.add("PathSkipRootW", 1, [](ApiCall &call) { const auto path = call.wide(call[0]); const size_t root = root_length(path); call.ret(root ? call[0] + root * 2 : 0); }); // The PathCch forms report through an HRESULT: E_INVALIDARG for a missing argument or root, and the strsafe overflow code when the // result does not fit the buffer. registry.add("PathCchSkipRoot", 2, [](ApiCall &call) { const size_t root = call[0] && call[1] ? root_length(call.wide(call[0])) : 0; if (!root) { call.ret(0x80070057U); return; } call.store_pointer(call[1], call[0] + root * 2); call.ret(0); }); registry.add("PathCchCombineEx", 5, [](ApiCall &call) { if (!call[0] || !call[1] || call[1] > 32768 || (!call[2] && !call[3])) { call.ret(0x80070057U); return; } const auto directory = call[2] ? call.wide(call[2]) : std::u16string(); const auto file = call[3] ? call.wide(call[3]) : std::u16string(); const auto combined = absolute(file) && !(separator(file[0]) && !is_unc_root(file)) ? file : join(directory, file); const auto result = canonical(combined); if (result.size() >= call[1]) { call.store(call[0], 0); call.ret(0x8007007aU); return; } call.put_wide(call[0], result, call[1]); call.ret(0); }); // PathCchCanonicalize is the Ex form with no flags; both remove the . and .. parts the way PathCchCombineEx does. const auto path_cch_canonicalize = [](ApiCall &call) { if (!call[0] || !call[1] || call[1] > 32768 || !call[2]) { call.ret(0x80070057U); return; } const auto result = canonical(call.wide(call[2])); if (result.size() >= call[1]) { call.store(call[0], 0); call.ret(0x8007007aU); return; } call.put_wide(call[0], result, call[1]); call.ret(0); }; registry.add("PathCchCanonicalize", 3, path_cch_canonicalize); // PathCchRemoveFileSpec drops the last element and keeps the root; S_FALSE says there was nothing left to remove. registry.add("PathCchRemoveFileSpec", 2, [](ApiCall &call) { if (!call[0] || !call[1] || call[1] > 32768) { call.ret(0x80070057U); return; } auto path = call.wide(call[0], size_t(call[1])); const size_t root = root_length(path); if (path.size() <= root) { call.ret(1); return; } size_t end = path.size(); while (end > root && !separator(path[end - 1])) --end; path.resize(end > root ? end - 1 : root); call.put_wide(call[0], path, call[1]); call.ret(0); }); registry.add("PathCchCanonicalizeEx", 4, path_cch_canonicalize); registry.add("PathAddBackslashW", 1, [](ApiCall &call) { auto path = call.wide(call[0], max_path * 4); if (path.empty() || separator(path.back())) { call.ret(call[0] + path.size() * 2); return; } if (path.size() + 1 >= max_path) { call.ret(0); return; } path.push_back(u'\\'); call.put_wide(call[0], path, max_path); call.ret(call[0] + path.size() * 2); }); registry.add("PathRemoveBackslashW", 1, [](ApiCall &call) { auto path = call.wide(call[0], max_path * 4); if (path.size() > root_length(path) && !path.empty() && separator(path.back())) { path.pop_back(); call.put_wide(call[0], path, max_path); } call.ret(call[0] + path.size() * 2); }); registry.add("PathQuoteSpacesW", 1, [](ApiCall &call) { auto path = call.wide(call[0], max_path * 4); if (path.find(u' ') != path.npos && path.size() + 2 < max_path) { path = u"\"" + path + u"\""; call.put_wide(call[0], path, max_path); } }); registry.add("PathUnquoteSpacesW", 1, [](ApiCall &call) { auto path = call.wide(call[0], max_path * 4); if (path.size() >= 2 && path.front() == u'"' && path.back() == u'"') { path = path.substr(1, path.size() - 2); call.put_wide(call[0], path, max_path); } }); // The arguments of a command line: after the first space outside quotes, or the terminator when there is none. for (const bool narrow : {false, true}) registry.add(narrow ? "PathGetArgsA" : "PathGetArgsW", 1, [narrow](ApiCall &call) { if (!call[0]) { call.ret(0); return; } const size_t unit = narrow ? 1 : 2; const auto text = narrow ? widen_ascii(call.narrow(call[0], max_path * 8)) : call.wide(call[0], max_path * 8); bool quoted = false; for (size_t at = 0; at < text.size(); ++at) { if (text[at] == u'"') quoted = !quoted; else if (text[at] == u' ' && !quoted) { call.ret(call[0] + (at + 1) * unit); return; } } call.ret(call[0] + text.size() * unit); }); // The way from one path to another: up from what is left of the first once their shared folders are gone, then down the second. Paths // on different roots have none. The attributes say whether each end is a folder, whose whole name counts, or a file, whose name does not. registry.add("PathRelativePathToW", 5, [](ApiCall &call) { if (!call[0] || !call[1] || !call[3]) { call.ret(0); return; } const auto parts_of = [](std::u16string path, bool folder) { std::vector parts; size_t at = 0; while (at <= path.size()) { const size_t end = std::min(path.find_first_of(u"\\/", at), path.size()); if (end > at) parts.push_back(path.substr(at, end - at)); at = end + 1; } if (!folder && !parts.empty()) parts.pop_back(); return parts; }; const auto same_part = [](const std::u16string &left, const std::u16string &right) { return std::equal(left.begin(), left.end(), right.begin(), right.end(), [](char16_t a, char16_t b) { return (a >= u'A' && a <= u'Z' ? char16_t(a + 32) : a) == (b >= u'A' && b <= u'Z' ? char16_t(b + 32) : b); }); }; const auto from = parts_of(call.wide(call[1]), call.u32(2) & 0x10), to = parts_of(call.wide(call[3]), true); size_t shared = 0; while (shared < from.size() && shared < to.size() && same_part(from[shared], to[shared])) ++shared; if (!shared) { call.ret(0); return; } std::u16string out; for (size_t index = shared; index < from.size(); ++index) out += out.empty() ? u".." : u"\\.."; for (size_t index = shared; index < to.size(); ++index) out += (out.empty() ? u".\\" : u"\\") + to[index]; if (out.empty()) out = u"."; call.put_wide(call[0], out, max_path); call.ret(1); }); // The command line cut before its arguments, without the space that led to them. for (const bool narrow : {false, true}) registry.add(narrow ? "PathRemoveArgsA" : "PathRemoveArgsW", 1, [narrow](ApiCall &call) { if (call[0]) { const size_t unit = narrow ? 1 : 2; const auto text = narrow ? widen_ascii(call.narrow(call[0], max_path * 8)) : call.wide(call[0], max_path * 8); bool quoted = false; for (size_t at = 0; at < text.size(); ++at) { if (text[at] == u'"') quoted = !quoted; else if (text[at] == u' ' && !quoted) { call.write(call[0] + at * unit, std::vector(unit)); break; } } } call.ret(0); }); registry.add("PathMatchSpecW", 2, [](ApiCall &call) { const auto file = call.wide(call[0]); const auto spec = call.wide(call[1]); bool matched = false; size_t at = 0; while (at <= spec.size() && !matched) { const size_t end = std::min(spec.find(u';', at), spec.size()); auto part = std::u16string_view(spec).substr(at, end - at); while (!part.empty() && part.front() == u' ') part.remove_prefix(1); matched = !part.empty() && match_one(part, file); at = end + 1; } call.ret(matched); }); registry.add("PathCompactPathExW", 4, [](ApiCall &call) { const auto path = call.wide(call[1]); const size_t capacity = call.u32(2); if (!capacity) { call.ret(0); return; } const auto result = compact(path, capacity); call.put_wide(call[0], result, capacity); call.ret(1); }); registry.add("PathFileExistsW", 1, [&](ApiCall &call) { const auto resolved = paths.resolve(call.wide(call[0])); call.ret(resolved && resolved->exists); }); registry.add("PathIsDirectoryW", 1, [&](ApiCall &call) { const auto resolved = paths.resolve(call.wide(call[0])); struct stat metadata{}; call.ret(resolved && resolved->exists && !::stat(resolved->host.c_str(), &metadata) && S_ISDIR(metadata.st_mode) ? 0x10 : 0); }); registry.add("PathIsDirectoryEmptyW", 1, [&](ApiCall &call) { const auto resolved = paths.resolve(call.wide(call[0])); std::error_code error; call.ret(resolved && std::filesystem::is_directory(resolved->host, error) && std::filesystem::is_empty(resolved->host, error)); }); registry.add("StrCmpW", 2, [](ApiCall &call) { const auto a = call.wide(call[0]), b = call.wide(call[1]); call.ret(uint32_t(a < b ? -1 : a > b ? 1 : 0)); }); registry.add("StrCmpIW", 2, [](ApiCall &call) { call.ret(uint32_t(compare_fold(call.wide(call[0]), call.wide(call[1])))); }); registry.add("StrCmpNW", 3, [](ApiCall &call) { const size_t count = call.u32(2); const auto a = call.wide(call[0]).substr(0, count), b = call.wide(call[1]).substr(0, count); call.ret(uint32_t(a < b ? -1 : a > b ? 1 : 0)); }); registry.add("StrCmpNIW", 3, [](ApiCall &call) { const size_t count = call.u32(2); call.ret(uint32_t(compare_fold(call.wide(call[0]).substr(0, count), call.wide(call[1]).substr(0, count)))); }); registry.add("StrStrW", 2, [](ApiCall &call) { const auto text = call.wide(call[0], whole_text), needle = call.wide(call[1]); const auto found = text.find(needle); call.ret(found == text.npos ? 0 : call[0] + found * 2); }); registry.add("StrStrIW", 2, [](ApiCall &call) { const auto text = call.wide(call[0], whole_text), needle = call.wide(call[1]); for (size_t at = 0; at + needle.size() <= text.size(); ++at) if (equal_fold(std::u16string_view(text).substr(at, needle.size()), needle)) { call.ret(call[0] + at * 2); return; } call.ret(0); }); registry.add("StrChrW", 2, [](ApiCall &call) { const auto text = call.wide(call[0], whole_text); const auto found = text.find(char16_t(call[1])); call.ret(found == text.npos ? 0 : call[0] + found * 2); }); // The first character of the string that is one of the set's. registry.add("StrPBrkW", 2, [](ApiCall &call) { // Without a string or a set there is nothing to find. if (!call[0] || !call[1]) { call.ret(0); return; } const auto text = call.wide(call[0], whole_text), set = call.wide(call[1]); const auto found = text.find_first_of(set); call.ret(found == text.npos || set.empty() ? 0 : call[0] + found * 2); }); registry.add("StrRChrW", 3, [](ApiCall &call) { const auto text = call.wide(call[0], whole_text); const auto end = call[1] ? std::min((call[1] - call[0]) / 2, text.size()) : text.size(); const auto found = text.substr(0, end).rfind(char16_t(call[2])); call.ret(found == text.npos ? 0 : call[0] + found * 2); }); registry.add("StrToIntW", 1, [](ApiCall &call) { const auto text = call.wide(call[0]); size_t at = 0; bool negative = false; if (at < text.size() && text[at] == u'-') { negative = true; ++at; } int64_t value = 0; for (; at < text.size() && text[at] >= u'0' && text[at] <= u'9'; ++at) value = std::min(value * 10 + (text[at] - u'0'), 0x7fffffffLL + 1); call.ret(uint32_t(int32_t(negative ? -value : value))); }); // StrToIntExW and StrToInt64ExW: leading blanks, a sign, then decimal digits, or 0x and hexadecimal ones with STIF_SUPPORT_HEX. FALSE // when no digit follows; the value goes out at the width the function names. const auto number = [](ApiCall &call, size_t width) { const auto text = call.wide(call[0]); size_t at = 0; while (at < text.size() && (text[at] == u' ' || text[at] == u'\t')) ++at; bool negative = false; if (at < text.size() && (text[at] == u'-' || text[at] == u'+')) negative = text[at++] == u'-'; const bool hex = (call.u32(1) & 1) && at + 1 < text.size() && text[at] == u'0' && (text[at + 1] | 0x20) == u'x'; if (hex) at += 2; uint64_t value = 0; size_t digits = 0; for (; at < text.size(); ++at, ++digits) { const char16_t c = text[at], lower = char16_t(c | 0x20); const int digit = c >= u'0' && c <= u'9' ? c - u'0' : hex && lower >= u'a' && lower <= u'f' ? lower - u'a' + 10 : -1; if (digit < 0) break; value = value * (hex ? 16 : 10) + uint64_t(digit); } if (!digits || !call[2]) { call.ret(0); return; } const uint64_t result = negative ? uint64_t(0) - value : value; if (width == 8) call.store(call[2], result); else call.store(call[2], uint32_t(result)); call.ret(1); }; registry.add("StrToIntExW", 3, [number](ApiCall &call) { number(call, 4); }); registry.add("StrToInt64ExW", 3, [number](ApiCall &call) { number(call, 8); }); registry.add("StrTrimW", 2, [](ApiCall &call) { auto text = call.wide(call[0]); const auto trim = call.wide(call[1]); const auto first = text.find_first_not_of(trim); const bool changed = first != 0 || (!text.empty() && trim.find(text.back()) != trim.npos); if (first == text.npos) text.clear(); else text = text.substr(first, text.find_last_not_of(trim) - first + 1); call.put_wide(call[0], text, text.size() + 1); call.ret(changed); }); registry.add("StrCpyNW", 3, [](ApiCall &call) { const int32_t capacity = call.i32(2); const auto text = call.wide(call[1]); if (capacity > 0) call.put_wide(call[0], text.substr(0, size_t(capacity) - 1), size_t(capacity)); call.ret(call[0]); }); registry.add("StrNCatW", 3, [](ApiCall &call) { auto text = call.wide(call[0]); const int32_t capacity = call.i32(2); const auto more = call.wide(call[1]); if (capacity > int32_t(text.size() + 1)) { text += more.substr(0, size_t(capacity) - text.size() - 1); call.put_wide(call[0], text, text.size() + 1); } call.ret(call[0]); }); registry.add("StrDupW", 1, [&](ApiCall &call) { const auto text = call.wide(call[0]); const auto copy = context.allocator.allocate((text.size() + 1) * 2, false); if (!copy) { call.ret(0); return; } call.put_wide(copy, text, text.size() + 1); call.ret(copy); }); // The wide copy of a string in memory from the task allocator, which the caller frees with CoTaskMemFree. for (const bool wide : {false, true}) registry.add(wide ? "SHStrDupW" : "SHStrDupA", 2, [&context, wide](ApiCall &call) { if (!call[0] || !call[1]) { if (call[1]) call.store_pointer(call[1], 0); call.ret(0x80070057U); return; } const auto text = wide ? call.wide(call[0]) : widen_ascii(call.narrow(call[0])); const auto copy = context.allocator.allocate((text.size() + 1) * 2, false); call.store_pointer(call[1], copy); if (!copy) { call.ret(0x8007000eU); return; } call.put_wide(copy, text, text.size() + 1); call.ret(0); }); registry.add("StrFormatByteSizeW", 3, [](ApiCall &call) { const uint64_t value = call[0]; static constexpr const char *units[] = {" bytes", " KB", " MB", " GB", " TB", " PB", " EB"}; std::string text; if (value < 1024) { text = std::to_string(value) + units[0]; } else { double scaled = double(value); size_t unit = 0; while (scaled >= 1024 && unit < 6) { scaled /= 1024; ++unit; } // Windows shows three significant digits. char buffer[32]; const int decimals = scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2; std::snprintf(buffer, sizeof buffer, "%.*f", decimals, scaled); text = std::string(buffer) + units[unit]; } call.put_wide(call[1], widen_ascii(text), call.u32(2)); call.ret(call[1]); }); registry.add("ColorRGBToHLS", 4, [](ApiCall &call) { uint16_t hue, luminance, saturation; rgb_to_hls(call.u32(0), hue, luminance, saturation); if (call[1]) call.store(call[1], hue); if (call[2]) call.store(call[2], luminance); if (call[3]) call.store(call[3], saturation); }); registry.add("ColorHLSToRGB", 3, [](ApiCall &call) { call.ret(hls_to_rgb(int(call[0] & 0xffff), int(call[1] & 0xffff), int(call[2] & 0xffff))); }); registry.add("ColorAdjustLuma", 3, [](ApiCall &call) { const uint32_t color = call.u32(0); const int32_t amount = call.i32(1); if (!amount) { call.ret(color); return; } uint16_t hue, luminance, saturation; rgb_to_hls(color, hue, luminance, saturation); int value = luminance; if (call.u32(2)) value = value * (1000 + amount) / 1000; else value += amount * 240 / 1000; call.ret(hls_to_rgb(hue, std::clamp(value, 0, 240), saturation)); }); registry.add("AssocQueryStringW", 6, [](ApiCall &call) { call.ret(0x80070483U); }); } }