mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-11-03 21:46:32 +00:00
503 lines
19 KiB
C++
503 lines
19 KiB
C++
#include "windows.h"
|
|
#include "nt/nls.h"
|
|
#include "nt/text.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <stdexcept>
|
|
#include <sys/mman.h>
|
|
|
|
namespace raven {
|
|
|
|
namespace {
|
|
|
|
uint64_t read_le(std::span<const uint8_t> bytes, size_t at, size_t width)
|
|
{
|
|
uint64_t value = 0;
|
|
for (size_t byte = 0; byte < width; ++byte)
|
|
value |= uint64_t(bytes[at + byte]) << (byte * 8);
|
|
return value;
|
|
}
|
|
|
|
void write_le(std::span<uint8_t> bytes, size_t at, uint64_t value, size_t width)
|
|
{
|
|
for (size_t byte = 0; byte < width; ++byte)
|
|
bytes[at + byte] = uint8_t(value >> (byte * 8));
|
|
}
|
|
|
|
// One printf-style conversion of Win32 wsprintf: flags, width, precision, size prefix and type.
|
|
struct Conversion {
|
|
bool left = false, zero = false, plus = false, space = false, alternate = false;
|
|
int64_t width = 0, precision = -1;
|
|
bool wide_size = false, narrow_size = false;
|
|
char type = 0;
|
|
bool width_argument = false, precision_argument = false;
|
|
};
|
|
|
|
std::optional<Conversion> parse_conversion(std::u16string_view format, size_t &at)
|
|
{
|
|
Conversion item;
|
|
for (; at < format.size(); ++at) {
|
|
const auto c = format[at];
|
|
if (c == u'-') item.left = true;
|
|
else if (c == u'0') item.zero = true;
|
|
else if (c == u'+') item.plus = true;
|
|
else if (c == u' ') item.space = true;
|
|
else if (c == u'#') item.alternate = true;
|
|
else break;
|
|
}
|
|
if (at < format.size() && format[at] == u'*') {
|
|
item.width_argument = true;
|
|
++at;
|
|
} else {
|
|
for (; at < format.size() && format[at] >= u'0' && format[at] <= u'9'; ++at)
|
|
item.width = std::min<int64_t>(item.width * 10 + (format[at] - u'0'), 1024);
|
|
}
|
|
if (at < format.size() && format[at] == u'.') {
|
|
++at;
|
|
item.precision = 0;
|
|
if (at < format.size() && format[at] == u'*') {
|
|
item.precision_argument = true;
|
|
++at;
|
|
} else {
|
|
for (; at < format.size() && format[at] >= u'0' && format[at] <= u'9'; ++at)
|
|
item.precision = std::min<int64_t>(item.precision * 10 + (format[at] - u'0'), 1024);
|
|
}
|
|
}
|
|
for (; at < format.size(); ++at) {
|
|
if (format[at] == u'l') item.wide_size = true;
|
|
else if (format[at] == u'h') item.narrow_size = true;
|
|
else break;
|
|
}
|
|
if (at >= format.size())
|
|
return std::nullopt;
|
|
const auto type = format[at++];
|
|
if (type > 127 || std::string_view("cCdiuxXsS").find(char(type)) == std::string_view::npos)
|
|
return std::nullopt;
|
|
item.type = char(type);
|
|
return item;
|
|
}
|
|
|
|
}
|
|
|
|
uint64_t NativeWindows::desktop_window()
|
|
{
|
|
if (desktop && objects.window(desktop))
|
|
return desktop;
|
|
// The class needs a non-zero procedure to register; the window itself carries none and is answered by the system.
|
|
DesktopClass definition{"#32769", 0, 1};
|
|
const auto id = objects.register_class(std::move(definition));
|
|
if (!id)
|
|
throw uint32_t(8);
|
|
const auto &screen = objects.nonclient_metrics();
|
|
DesktopWindow record{id, 0, {}, 0, 0, screen.screen_x, screen.screen_y};
|
|
record.style = 0x96000000;
|
|
record.visible = true;
|
|
desktop = objects.add(std::move(record));
|
|
if (!desktop)
|
|
throw uint32_t(8);
|
|
return desktop;
|
|
}
|
|
|
|
// Re-runs WM_NCCALCSIZE for the current outer rectangle and adopts the client rectangle it answers.
|
|
void NativeWindows::recalculate_client(uint64_t handle,int32_t previous_width,int32_t previous_height)
|
|
{
|
|
auto *window = objects.window(handle);
|
|
if (!window)
|
|
throw uint32_t(1400);
|
|
const auto scratch = process.map_memory(4096, PROT_READ | PROT_WRITE);
|
|
DesktopRect client{};
|
|
bool resized = false;
|
|
int32_t old_width = previous_width >= 0 ? previous_width : window->width, old_height = previous_height >= 0 ? previous_height : window->height;
|
|
try {
|
|
std::array<uint8_t, 16> bytes{};
|
|
const int32_t values[4] = {window->outer.left, window->outer.top, window->outer.right, window->outer.bottom};
|
|
for (size_t index = 0; index < 4; ++index)
|
|
write_le(bytes, index * 4, uint32_t(values[index]), 4);
|
|
process.write_memory(scratch, bytes);
|
|
send(handle, 0x83, 0, scratch);
|
|
window = objects.window(handle);
|
|
if (!window)
|
|
throw std::runtime_error("window was destroyed during frame calculation");
|
|
const auto answer = process.read_memory(scratch, 16);
|
|
client = {int32_t(read_le(answer, 0, 4)), int32_t(read_le(answer, 4, 4)), int32_t(read_le(answer, 8, 4)), int32_t(read_le(answer, 12, 4))};
|
|
if (std::getenv("RAVEN_TRACE_NC")) fprintf(stderr, "nc %llx outer %d,%d,%d,%d -> %d,%d,%d,%d\n", (unsigned long long)handle, values[0], values[1], values[2], values[3], client.left, client.top, client.right, client.bottom);
|
|
} catch (...) {
|
|
process.unmap_memory(scratch);
|
|
throw;
|
|
}
|
|
process.unmap_memory(scratch);
|
|
// A window too small for its frame has an empty client area, never a negative one.
|
|
client.right = std::max(client.right, client.left);
|
|
client.bottom = std::max(client.bottom, client.top);
|
|
window->x = client.left;
|
|
window->y = client.top;
|
|
window->width = client.right - client.left;
|
|
window->height = client.bottom - client.top;
|
|
resized = window->width != old_width || window->height != old_height;
|
|
objects.update_window_dcs(handle);
|
|
if (resized)
|
|
send(handle, 5, 0, uint16_t(window->width) | (uint64_t(uint16_t(window->height)) << 16));
|
|
if (objects.window(handle) && objects.window_visible(handle)) {
|
|
window = objects.window(handle);
|
|
objects.invalidate(handle, {0, 0, window->width, window->height}, true);
|
|
present(handle);
|
|
}
|
|
}
|
|
|
|
void NativeWindows::reshape(uint64_t handle, DesktopRect outer)
|
|
{
|
|
auto *window = objects.window(handle);
|
|
const bool moved = window->outer.left != outer.left || window->outer.top != outer.top;
|
|
const bool resized = window->outer.right - window->outer.left != outer.right - outer.left ||
|
|
window->outer.bottom - window->outer.top != outer.bottom - outer.top;
|
|
const int32_t client_width = window->width, client_height = window->height;
|
|
if (resized && window->bitmap) {
|
|
// The surface is outer-sized, so a new size needs a new surface. A window that carries a graphics driver's surface keeps its host
|
|
// window, because that surface stays attached to it; any other gets a new host window, which the compositor places afresh.
|
|
const bool visible = window->visible;
|
|
const bool kept = keep_host_window;
|
|
keep_host_window = keep_host_window || hosting_surface.contains(handle);
|
|
try { release_surface(handle); } catch (...) { keep_host_window = kept; throw; }
|
|
keep_host_window = kept;
|
|
window = objects.window(handle);
|
|
window->bitmap = 0;
|
|
window->dc = 0;
|
|
window->outer = outer;
|
|
window->x = outer.left;
|
|
window->y = outer.top;
|
|
window->width = outer.right - outer.left;
|
|
window->height = outer.bottom - outer.top;
|
|
recalculate_client(handle, client_width, client_height);
|
|
(void)visible;
|
|
} else {
|
|
window->outer = outer;
|
|
window->x = outer.left;
|
|
window->y = outer.top;
|
|
window->width = outer.right - outer.left;
|
|
window->height = outer.bottom - outer.top;
|
|
recalculate_client(handle, client_width, client_height);
|
|
}
|
|
window = objects.window(handle);
|
|
if (window && moved)
|
|
send(handle, 3, 0, uint16_t(window->x) | (uint64_t(uint16_t(window->y)) << 16));
|
|
}
|
|
|
|
// Queues WM_TIMER for every expired timer; messages already waiting for the same timer are not duplicated.
|
|
void NativeWindows::post_due_timers()
|
|
{
|
|
const auto now = std::chrono::steady_clock::now();
|
|
for (auto &timer : timers) {
|
|
if (timer.due > now)
|
|
continue;
|
|
const uint32_t kind = timer.system ? 0x118 : 0x113;
|
|
const bool pending = std::any_of(messages.begin(), messages.end(), [&](const Message &message) {
|
|
return message.message == kind && message.window == timer.window && message.wparam == timer.id;
|
|
});
|
|
if (!pending && messages.size() < 10000)
|
|
messages.push_back({timer.window, timer.id, timer.procedure, kind, uint32_t(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count()), cursor_x, cursor_y});
|
|
timer.due = now + std::chrono::milliseconds(timer.interval);
|
|
}
|
|
}
|
|
|
|
bool NativeWindows::system_dispatch(std::string_view name)
|
|
{
|
|
const bool ansi = name == "wsprintfA";
|
|
const bool format = name == "wsprintfW" || ansi;
|
|
if (name != "GetDesktopWindow" && name != "PtInRect" && name != "SetRect" && name != "TranslateAcceleratorW" && name != "SetMenu" &&
|
|
name != "GetMenuItemRect" && name != "CheckMenuItem" && name != "SetCapture" &&
|
|
name != "ReleaseCapture" && name != "SetTimer" && name != "SetCoalescableTimer" && name != "KillTimer" && !format)
|
|
return false;
|
|
uint64_t result = 0;
|
|
try {
|
|
if (name == "GetDesktopWindow") {
|
|
process.api_arguments(0);
|
|
result = desktop_window();
|
|
} else if (name == "PtInRect") {
|
|
const auto args = process.api_arguments(3);
|
|
const auto bytes = process.read_memory(args[0], 16);
|
|
const auto left = int32_t(read_le(bytes, 0, 4)), top = int32_t(read_le(bytes, 4, 4));
|
|
const auto right = int32_t(read_le(bytes, 8, 4)), bottom = int32_t(read_le(bytes, 12, 4));
|
|
// A 32-bit caller pushes the two coordinates; a 64-bit caller passes the POINT packed in one register.
|
|
const bool packed = process.mode() != NativeMode::x86;
|
|
const auto x = int32_t(uint32_t(args[1])), y = packed ? int32_t(uint32_t(args[1] >> 32)) : int32_t(uint32_t(args[2]));
|
|
result = x >= left && x < right && y >= top && y < bottom;
|
|
} else if (name == "TranslateAcceleratorW") {
|
|
const auto args = process.api_arguments(3);
|
|
const auto *table = objects.accelerators(args[1]);
|
|
if (!table || !objects.window(args[0]))
|
|
throw uint32_t(1400);
|
|
const bool narrow = process.mode() == NativeMode::x86;
|
|
const auto bytes = process.read_memory(args[2], narrow ? 28 : 48);
|
|
const auto message = uint32_t(read_le(bytes, narrow ? 4 : 8, 4));
|
|
const auto wparam = read_le(bytes, narrow ? 8 : 16, narrow ? 4 : 8);
|
|
const auto state = modifier_state();
|
|
for (const auto &entry : *table) {
|
|
const bool virtual_key = entry.flags & 1;
|
|
bool match = false;
|
|
if (virtual_key && (message == 0x100 || message == 0x104))
|
|
match = wparam == entry.key && bool(entry.flags & 4) == state.shift && bool(entry.flags & 8) == state.control &&
|
|
bool(entry.flags & 0x10) == state.alt;
|
|
else if (!virtual_key && message == 0x102 && !state.alt)
|
|
match = wparam == entry.key;
|
|
if (match) {
|
|
send(args[0], 0x111, uint64_t(entry.command) | (uint64_t(1) << 16), 0);
|
|
result = 1;
|
|
break;
|
|
}
|
|
}
|
|
} else if (name == "SetMenu") {
|
|
const auto args = process.api_arguments(2);
|
|
auto *window = objects.window(args[0]);
|
|
if (!window)
|
|
throw uint32_t(1400);
|
|
if (window->parent || (args[1] && !objects.menu(args[1])))
|
|
throw uint32_t(args[1] && !objects.menu(args[1]) ? 1401 : 120);
|
|
window->menu = args[1];
|
|
recalculate_client(args[0]);
|
|
result = 1;
|
|
} else if (name == "GetMenuItemRect") {
|
|
const auto args = process.api_arguments(4);
|
|
const auto *window = objects.window(args[0]);
|
|
if (!window)
|
|
throw uint32_t(1400);
|
|
// Only the menu bar of its own window has a layout; popup menus are not laid out yet.
|
|
if (args[1] != window->menu || !window->menu)
|
|
throw uint32_t(120);
|
|
const auto rects = menu_bar_items(*window);
|
|
if (uint32_t(args[2]) >= rects.size())
|
|
throw uint32_t(87);
|
|
const auto &rect = rects[uint32_t(args[2])];
|
|
std::array<uint8_t, 16> bytes{};
|
|
const int32_t values[4] = {window->outer.left + rect.left, window->outer.top + rect.top, window->outer.left + rect.right,
|
|
window->outer.top + rect.bottom};
|
|
for (size_t index = 0; index < 4; ++index)
|
|
write_le(bytes, index * 4, uint32_t(values[index]), 4);
|
|
process.write_memory(args[3], bytes);
|
|
result = 1;
|
|
} else if (name == "CheckMenuItem") {
|
|
const auto args = process.api_arguments(3);
|
|
auto *menu = objects.edit_menu(args[0]);
|
|
if (!menu)
|
|
throw uint32_t(1401);
|
|
const bool by_position = uint32_t(args[2]) & 0x400;
|
|
DesktopMenuItem *found = nullptr;
|
|
const auto search = [&](auto &&self, DesktopMenu &level) -> void {
|
|
if (by_position) {
|
|
if (!found && uint32_t(args[1]) < level.items.size())
|
|
found = &level.items[uint32_t(args[1])];
|
|
return;
|
|
}
|
|
for (auto &item : level.items) {
|
|
if (!item.submenu && item.id == uint32_t(args[1])) {
|
|
found = &item;
|
|
return;
|
|
}
|
|
if (item.submenu)
|
|
if (auto *child = objects.edit_menu(item.submenu)) {
|
|
self(self, *child);
|
|
if (found)
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
search(search, *menu);
|
|
if (!found) {
|
|
result = UINT32_MAX;
|
|
} else {
|
|
result = (found->state & 8) ? 8 : 0;
|
|
found->state = (found->state & ~8U) | ((uint32_t(args[2]) & 8) ? 8U : 0U);
|
|
}
|
|
} else if (name == "SetCapture") {
|
|
const auto args = process.api_arguments(1);
|
|
if (!objects.window(args[0]))
|
|
throw uint32_t(1400);
|
|
result = capture;
|
|
capture = args[0];
|
|
if (result && result != args[0] && objects.window(result))
|
|
send(result, 0x215, 0, args[0]);
|
|
else if (result && !objects.window(result))
|
|
result = 0;
|
|
} else if (name == "ReleaseCapture") {
|
|
process.api_arguments(0);
|
|
if (capture) {
|
|
const auto previous = capture;
|
|
capture = 0;
|
|
if (objects.window(previous))
|
|
send(previous, 0x215, 0, 0);
|
|
}
|
|
result = 1;
|
|
} else if (name == "SetTimer" || name == "SetCoalescableTimer") {
|
|
// The coalescing tolerance is only a hint, so both entry points share one timer.
|
|
const auto args = process.api_arguments(name == "SetTimer" ? 4 : 5);
|
|
const auto interval = std::clamp<uint32_t>(uint32_t(args[2]), 10, 0x7fffffff);
|
|
if (args[0] && !objects.window(args[0]))
|
|
throw uint32_t(1400);
|
|
uint64_t id = args[1];
|
|
if (!args[0]) {
|
|
// A thread timer without a window gets a fresh system identifier.
|
|
id = next_timer_id++;
|
|
} else if (!id) {
|
|
throw uint32_t(87);
|
|
}
|
|
if (timers.size() >= 1024)
|
|
throw uint32_t(8);
|
|
std::erase_if(timers, [&](const Timer &timer) { return timer.window == args[0] && timer.id == id; });
|
|
timers.push_back({args[0], id, args[3], interval, std::chrono::steady_clock::now() + std::chrono::milliseconds(interval)});
|
|
result = id;
|
|
} else if (name == "KillTimer") {
|
|
const auto args = process.api_arguments(2);
|
|
const auto before = timers.size();
|
|
std::erase_if(timers, [&](const Timer &timer) { return timer.window == args[0] && timer.id == args[1]; });
|
|
if (before == timers.size())
|
|
throw uint32_t(1814);
|
|
result = 1;
|
|
} else if (name == "SetRect") {
|
|
const auto args = process.api_arguments(5);
|
|
std::array<uint8_t, 16> bytes{};
|
|
for (size_t index = 0; index < 4; ++index)
|
|
write_le(bytes, index * 4, uint32_t(args[1 + index]), 4);
|
|
process.write_memory(args[0], bytes);
|
|
result = 1;
|
|
} else {
|
|
// cdecl variadic: count the consumed words from the format string before reading them off the stack.
|
|
const auto head = process.api_arguments(2);
|
|
std::u16string pattern;
|
|
if (ansi) {
|
|
std::string narrow;
|
|
for (size_t index = 0; index < 4096; ++index) {
|
|
const auto byte = process.read_memory(head[1] + index, 1)[0];
|
|
if (!byte)
|
|
break;
|
|
narrow.push_back(char(byte));
|
|
}
|
|
pattern = from_ansi(narrow, 1252);
|
|
} else {
|
|
pattern = window_text(head[1]);
|
|
}
|
|
size_t words = 0;
|
|
for (size_t at = 0; at < pattern.size();) {
|
|
if (pattern[at++] != u'%')
|
|
continue;
|
|
if (at < pattern.size() && pattern[at] == u'%') {
|
|
++at;
|
|
continue;
|
|
}
|
|
const auto item = parse_conversion(pattern, at);
|
|
if (!item)
|
|
break;
|
|
words += 1 + item->width_argument + item->precision_argument;
|
|
}
|
|
if (words > 64)
|
|
throw uint32_t(87);
|
|
const auto args = process.api_arguments(2 + words);
|
|
size_t next = 2;
|
|
const auto take = [&] { return args[next++]; };
|
|
std::u16string output;
|
|
for (size_t at = 0; at < pattern.size();) {
|
|
const auto c = pattern[at++];
|
|
if (c != u'%') {
|
|
output.push_back(c);
|
|
continue;
|
|
}
|
|
if (at < pattern.size() && pattern[at] == u'%') {
|
|
output.push_back(u'%');
|
|
++at;
|
|
continue;
|
|
}
|
|
auto item = parse_conversion(pattern, at);
|
|
if (!item)
|
|
break;
|
|
if (item->width_argument) {
|
|
const auto value = int32_t(uint32_t(take()));
|
|
item->left |= value < 0;
|
|
item->width = std::min<int64_t>(std::abs(int64_t(value)), 1024);
|
|
}
|
|
if (item->precision_argument) {
|
|
const auto value = int32_t(uint32_t(take()));
|
|
item->precision = value < 0 ? -1 : std::min<int64_t>(value, 1024);
|
|
}
|
|
const auto value = take();
|
|
std::u16string body;
|
|
if (item->type == 'c' || item->type == 'C') {
|
|
if (ansi && item->type == 'c')
|
|
body = from_ansi(std::string(1, char(uint8_t(value))), 1252);
|
|
else
|
|
body.push_back(char16_t(uint16_t(value)));
|
|
} else if (item->type == 's' || item->type == 'S') {
|
|
// %s takes the string width of the function (UTF-16); %S, %hs and %ls swap it.
|
|
bool wide = (item->type == 's') != ansi;
|
|
if (item->narrow_size) wide = false;
|
|
if (item->wide_size) wide = true;
|
|
if (!value) {
|
|
body = u"(null)";
|
|
} else {
|
|
for (size_t index = 0; index < 4096; ++index) {
|
|
const auto bytes = process.read_memory(value + index * (wide ? 2 : 1), wide ? 2 : 1);
|
|
const char16_t character = wide ? char16_t(bytes[0] | bytes[1] << 8) : char16_t(bytes[0]);
|
|
if (!character || (item->precision >= 0 && int64_t(index) >= item->precision))
|
|
break;
|
|
body.push_back(character);
|
|
}
|
|
}
|
|
} else {
|
|
const bool signed_type = item->type == 'd' || item->type == 'i';
|
|
uint32_t raw = uint32_t(value);
|
|
if (item->narrow_size)
|
|
raw = signed_type ? uint32_t(int32_t(int16_t(raw))) : uint32_t(uint16_t(raw));
|
|
const bool negative = signed_type && int32_t(raw) < 0;
|
|
uint64_t magnitude = signed_type ? (negative ? uint64_t(-int64_t(int32_t(raw))) : raw) : raw;
|
|
std::string digits;
|
|
const bool hexadecimal = item->type == 'x' || item->type == 'X';
|
|
do {
|
|
const auto digit = unsigned(magnitude % (hexadecimal ? 16 : 10));
|
|
digits.push_back(char((digit < 10 ? '0' : (item->type == 'X' ? 'A' : 'a') - 10) + digit));
|
|
magnitude /= hexadecimal ? 16 : 10;
|
|
} while (magnitude);
|
|
while (item->precision >= 0 && int64_t(digits.size()) < item->precision)
|
|
digits.push_back('0');
|
|
std::string prefix;
|
|
if (negative) prefix = "-";
|
|
else if (signed_type && item->plus) prefix = "+";
|
|
else if (signed_type && item->space) prefix = " ";
|
|
if (hexadecimal && item->alternate && raw) prefix = item->type == 'X' ? "0X" : "0x";
|
|
if (item->zero && !item->left && item->precision < 0)
|
|
while (int64_t(prefix.size() + digits.size()) < item->width)
|
|
digits.push_back('0');
|
|
std::reverse(digits.begin(), digits.end());
|
|
for (const char character : prefix + digits)
|
|
body.push_back(char16_t(character));
|
|
}
|
|
while (int64_t(body.size()) < item->width)
|
|
body.insert(item->left ? body.end() : body.begin(), u' ');
|
|
output += body;
|
|
}
|
|
if (output.size() > 1023)
|
|
output.resize(1023);
|
|
std::vector<uint8_t> bytes;
|
|
if (ansi) {
|
|
const auto narrow = to_ansi(output, 1252);
|
|
bytes.assign(narrow.begin(), narrow.end());
|
|
bytes.push_back(0);
|
|
result = narrow.size();
|
|
} else {
|
|
bytes.resize((output.size() + 1) * 2);
|
|
for (size_t index = 0; index < output.size(); ++index)
|
|
write_le(bytes, index * 2, output[index], 2);
|
|
result = output.size();
|
|
}
|
|
process.write_memory(head[0], bytes);
|
|
}
|
|
} catch (uint32_t error) { environment.set_last_error(error); }
|
|
catch (const std::runtime_error &failure) {
|
|
const std::string_view message = failure.what();
|
|
if (message.find("readable") == message.npos && message.find("writable") == message.npos) throw;
|
|
environment.set_last_error(998);
|
|
result = 0;
|
|
}
|
|
process.return_api(result);
|
|
return true;
|
|
}
|
|
|
|
}
|