More fixes discovered based on Deedra's work.

This commit is contained in:
Storm Dragon committed 2026-10-23 16:08:58 -04:00
1 parent de31e5b513
commit 17052c5147
22 files changed
+561 -76

No files matched your search

+8 -2
View File
@@ -99,6 +99,11 @@ if(BUILD_TESTING)
target_compile_features(msaa-states-test PRIVATE cxx_std_20)
target_compile_options(msaa-states-test PRIVATE -Wall -Wextra)
add_wine_test(msaa-states 35 "${CMAKE_CURRENT_BINARY_DIR}/msaa-states-test.exe")
add_executable(text-limits-test tests/text_limits_test.cpp reader_events.cpp)
target_compile_features(text-limits-test PRIVATE cxx_std_20)
target_compile_options(text-limits-test PRIVATE -Wall -Wextra)
target_link_libraries(text-limits-test PRIVATE PkgConfig::GIO user32)
add_wine_test(text-limits 35 dbus-run-session -- "${CMAKE_CURRENT_BINARY_DIR}/text-limits-test.exe")
add_executable(review-capture-test tests/review_capture_test.cpp)
target_compile_features(review-capture-test PRIVATE cxx_std_20)
target_compile_options(review-capture-test PRIVATE -Wall -Wextra)
@@ -120,14 +125,15 @@ if(BUILD_TESTING)
add_custom_target(hook-safety-fixture${arch} ALL DEPENDS "${hook_fixture}")
set(review_fixture "${CMAKE_CURRENT_BINARY_DIR}/review-fixture${arch}.exe")
add_custom_command(OUTPUT "${review_fixture}"
COMMAND "${fixture_compiler}" -std=c11 -Wall -Wextra -mwindows
COMMAND "${fixture_compiler}" -std=c11 -Wall -Wextra -mwindows -Wl,--stack,1048576
-o "${review_fixture}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/review_fixture.c" -luser32 -lcomctl32
DEPENDS tests/review_fixture.c VERBATIM)
DEPENDS tests/review_fixture.c edit_hook.h VERBATIM)
add_custom_target(review-fixture${arch} ALL DEPENDS "${review_fixture}")
add_dependencies(review-capture-test review-fixture${arch})
add_wine_test(review-capture${arch} 35
"${CMAKE_CURRENT_BINARY_DIR}/review-capture-test.exe" --fixture "${review_fixture}")
endforeach()
add_dependencies(review-capture-test wine-access-menu wine-access-menu32)
add_wine_test(hook-safety 100 python3
"${CMAKE_CURRENT_SOURCE_DIR}/tests/hook_safety_test.py" "${CMAKE_CURRENT_BINARY_DIR}")
endif()
+26
View File
@@ -34,6 +34,32 @@ imports remain valid across reader restarts. Close and reopen applications after
updating Wine Access to load the new hooks. `--no-dialog-reader` disables these
hooks along with standard control reading.
Long-running text processing is bounded at the helper boundary. MSAA properties
retain at most 4096 UTF-16 code units each, without splitting a surrogate pair.
The reader queue accepts at most 128 events, 128 KiB per serialized event, and
1 MiB of serialized pending events. When full it drops new feedback and reports
the condition once. NVDA/Tolk speech, braille and SSML requests longer than
65536 UTF-16 code units return `ERROR_INVALID_PARAMETER` before UTF-8 conversion.
These limits apply per request, not to the lifetime number of processed lines.
Window review reads large standard Edit controls through a native hook query,
which avoids sending their text buffers through Wine's message-hook stack.
Review can attach the foreground UI thread's hook to an output editor without
moving keyboard focus there.
The existing aggregate source limit of one million UTF-16 code units still
applies. Editors without the updated hook use a conservative 16384-unit fallback;
larger sources report `limited` until an updated hook is attached. Restart both
the helper and the affected Wine application after upgrading. Programmatic Edit
replacements defer snapshot refresh until an editor action needs it, background
updates do not copy the document, and snapshot buffers reuse bounded capacity.
Native list, tree and tab queries retain target-process buffers until their
completion callbacks arrive. A late query prevents additional remote allocations
on that reader thread, while ordinary event handling continues. Failed reads are
retried instead of being cached as complete. These protections do not establish
the cause of a reported whole-machine lockup or bound a reader's downstream
speech backlog.
The executable is named `wine-access.exe`. Its launcher, Winelib sidecar,
and native hooks share a reader-independent installation directory.
@@ -1,6 +1,6 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=wine-access-git
pkgver=r58.gdbf3ceb
pkgver=r60.gde31e5b
pkgrel=1
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for screen readers'
arch=('x86_64')
+75 -15
View File
@@ -19,6 +19,7 @@ typedef struct EditState {
BOOL history_key, history_replaced;
WCHAR pending_high;
DWORD pending_caret;
DWORD capacity;
} EditState;
static void selection(HWND window, DWORD *start, DWORD *end) {
@@ -37,22 +38,61 @@ static LRESULT query_text(HWND window, UINT message, WPARAM wp, LPARAM lp) {
}
static void snapshot(HWND window, EditState *state) {
LRESULT length = query_text(window, WM_GETTEXTLENGTH, 0, 0);
state->length = length >= 0 && (ULONGLONG)length <= MAXDWORD ? (DWORD)length : 0;
if (length < 0 || (ULONGLONG)length > MAX_SNAPSHOT_UNITS) goto discard;
if (state->capacity <= state->length) {
DWORD capacity = state->length + 1;
// Grow geometrically rather than allocating another transcript on every
// appended line. Never retain more than the original snapshot budget.
if (capacity < 4096) capacity = 4096;
if (capacity < state->capacity * 2) capacity = state->capacity * 2;
if (capacity > MAX_SNAPSHOT_UNITS + 1) capacity = MAX_SNAPSHOT_UNITS + 1;
WCHAR *text = realloc(state->text, (size_t)capacity * sizeof(WCHAR));
if (!text) goto discard;
state->text = text;
state->capacity = capacity;
}
LRESULT copied = query_text(window, WM_GETTEXT, state->length + 1, (LPARAM)state->text);
if (copied < 0 || (ULONGLONG)copied > state->length) goto discard;
state->length = copied;
state->text[state->length] = 0;
return;
discard:
free(state->text);
state->text = NULL;
state->length = query_text(window, WM_GETTEXTLENGTH, 0, 0);
if (state->length <= MAX_SNAPSHOT_UNITS) {
state->text = calloc((size_t)state->length + 1, sizeof(WCHAR));
if (state->text) {
LRESULT copied = query_text(window, WM_GETTEXT, state->length + 1, (LPARAM)state->text);
if (copied >= 0 && (ULONGLONG)copied <= state->length) {
state->length = copied;
} else {
// A failed query must never become a length used to index this buffer.
free(state->text);
state->text = NULL;
}
}
state->capacity = 0;
}
static LRESULT read_review_text(HWND window, EditState *state, WineAccessEditText *request) {
if (!request || !request->capacity || request->capacity > MAX_SNAPSHOT_UNITS + 1 ||
(GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD)) return 0;
state->busy = TRUE;
LRESULT length = query_text(window, WM_GETTEXTLENGTH, 0, 0);
LRESULT copied = -1;
if (!state->destroyed && !(GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD) && length >= 0) {
if ((ULONGLONG)length >= request->capacity) request->status = 2;
else copied = query_text(window, WM_GETTEXT, request->capacity, (LPARAM)request->text);
}
if (state->destroyed) {
free(state->text); free(state);
return 0;
}
BOOL password = (GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD) != 0;
if (password) {
// A provider can enable protection during the busy text query, bypassing
// handle_edit's ordinary password-change cleanup.
free(state->text); state->text = NULL; state->capacity = 0;
state->known = FALSE; state->pending_high = 0;
}
if (copied == length && copied >= 0 && (ULONGLONG)copied < request->capacity &&
!password) {
request->copied = copied;
request->text[copied] = 0;
request->status = 1;
}
state->busy = FALSE;
return request->status;
}
static void copy_range(WCHAR *output, const WCHAR *text, DWORD start, DWORD end) {
@@ -126,6 +166,20 @@ static __attribute__((noinline)) LRESULT handle_edit(HWND window, UINT message,
state->history_key = state->history_replaced = FALSE;
BOOL history_replacement = message == WM_SETTEXT && state->history_key;
// Programmatic output updates have no editor action to announce. Rebuild
// lazily on navigation/typing/focus, not on every incoming transcript line.
// Pretranslated command-history replacements still need immediate feedback.
BOOL programmatic = message == EM_REPLACESEL || message == EM_SETHANDLE ||
message == EM_FMTLINES || (message == WM_SETTEXT && !history_replacement);
if ((!focus && GetFocus() != window) || (changing && programmatic)) {
if (GetFocus() != window) {
free(state->text); state->text = NULL; state->capacity = 0;
}
state->known = FALSE;
state->pending_high = 0;
return DefSubclassProc(window, message, wp, lp);
}
state->busy = TRUE;
WCHAR pending_high = state->pending_high;
DWORD pending_caret = state->pending_caret;
@@ -159,7 +213,7 @@ static __attribute__((noinline)) LRESULT handle_edit(HWND window, UINT message,
selection(window, &start, &end);
password = (GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD) != 0;
if (password) {
free(state->text); state->text = NULL; state->known = FALSE;
free(state->text); state->text = NULL; state->capacity = 0; state->known = FALSE;
if (focus) notify_text(window, WINE_ACCESS_TEXT_PASSWORD, L"", start, end);
state->busy = FALSE;
return result;
@@ -265,11 +319,14 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
if (message == WM_NCDESTROY) {
RemoveWindowSubclass(window, edit_proc, id);
RemovePropW(window, EDIT_STATE_PROPERTY);
RemovePropW(window, EDIT_REVIEW_PROPERTY);
state->destroyed = TRUE;
if (!state->busy) { free(state->text); free(state); }
return DefSubclassProc(window, message, wp, lp);
}
if (state->busy) return DefSubclassProc(window, message, wp, lp);
if (message == RegisterWindowMessageW(EDIT_REVIEW_MESSAGE))
return read_review_text(window, state, (WineAccessEditText *)lp);
if (message == WM_KILLFOCUS)
state->history_key = state->history_replaced = FALSE;
BOOL relevant = FALSE;
@@ -313,7 +370,10 @@ static BOOL watch_edit(HWND window, EditState **edit_state) {
if (!SetWindowSubclass(window, edit_proc, EDIT_SUBCLASS_ID, (DWORD_PTR)state)) {
free(state); return FALSE;
}
if (!SetPropW(window, EDIT_STATE_PROPERTY, state)) {
if (!SetPropW(window, EDIT_STATE_PROPERTY, state) ||
!SetPropW(window, EDIT_REVIEW_PROPERTY, (HANDLE)1)) {
RemovePropW(window, EDIT_STATE_PROPERTY);
RemovePropW(window, EDIT_REVIEW_PROPERTY);
RemoveWindowSubclass(window, edit_proc, EDIT_SUBCLASS_ID);
free(state); return FALSE;
}
+8
View File
@@ -3,6 +3,14 @@
#include <windows.h>
#define EDIT_STATE_PROPERTY L"CthulhuWineEditState"
#define EDIT_FOCUS_MESSAGE L"CthulhuWineEditFocus"
#define EDIT_REVIEW_MESSAGE L"CthulhuWineEditReviewTextV1"
#define EDIT_REVIEW_PROPERTY L"CthulhuWineEditReviewV1"
// Fixed-width layout shared with 32-bit applications. The allocation lives
// in the application's address space until the message has really completed.
typedef struct WineAccessEditText {
DWORD capacity, copied, status; // status: 0 unavailable, 1 complete, 2 limited
WCHAR text[1];
} WineAccessEditText;
BOOL wine_access_watch_edit(HWND window);
BOOL wine_access_watch_scintilla(HWND window);
void wine_access_scintilla_input(HWND window, const MSG *message);
+9 -3
View File
@@ -60,8 +60,10 @@ int list_view_index(HWND window, int *count) {
ListViewRow read_list_view(HWND window) {
ListViewRow row;
row.index = list_view_index(window, &row.count);
if (row.index < 0 || row.index >= row.count)
if (row.index < 0 || row.index >= row.count) {
row.available = row.count == 0;
return row;
}
DWORD_PTR state = 0;
if (query(window, LVM_GETITEMSTATE, row.index, LVIS_SELECTED, &state))
row.selected = state & LVIS_SELECTED;
@@ -77,6 +79,7 @@ ListViewRow read_list_view(HWND window) {
}
// Keep provider work and the protocol payload bounded even for huge tables.
if (columnCount > 8) columnCount = 8;
bool complete = true;
for (DWORD_PTR column = 0; column < columnCount; ++column) {
BYTE buffer[bufferSize] = {};
const auto textAddress = remote.address(textOffset);
@@ -92,9 +95,11 @@ ListViewRow read_list_view(HWND window) {
DWORD_PTR result = 0;
wchar_t text[textCapacity] = {};
if (!remote.write(buffer, sizeof(buffer)) ||
!query(window, LVM_GETITEMTEXTW, row.index, static_cast<LPARAM>(remote.address()), &result) ||
!remote.read(text, sizeof(text), textOffset))
!remote.query(window, LVM_GETITEMTEXTW, row.index, &result) ||
!remote.read(text, sizeof(text), textOffset)) {
complete = false;
break;
}
text[textCapacity - 1] = L'\0';
if (!text[0]) continue;
char utf8[textCapacity * 4] = {};
@@ -103,6 +108,7 @@ ListViewRow read_list_view(HWND window) {
if (!row.text.empty()) row.text += ", ";
row.text += utf8;
}
row.available = complete;
return row;
}
+1
View File
@@ -4,6 +4,7 @@
#include <string>
struct ListViewRow {
bool available = false;
std::string text;
int index = -1;
int count = 0;
+15 -7
View File
@@ -16,6 +16,7 @@
#include "bootstrapper_controls.h"
#include "bootstrapper_hook.h"
#include "version.h"
#include "text_limits.h"
#include <commctrl.h>
#include <gio/gio.h>
@@ -411,11 +412,13 @@ ControlDescription describe_standard_control(HWND window) {
if (!control.states.empty()) control.states += ", ";
control.states += row.checked ? "checked" : "not checked";
}
lastListControl = window;
lastListIndex = row.index;
lastListCount = row.count;
lastListSelected = row.selected;
lastListChecked = row.checked;
if (row.available) {
lastListControl = window;
lastListIndex = row.index;
lastListCount = row.count;
lastListSelected = row.selected;
lastListChecked = row.checked;
} else lastListControl = nullptr; // Retry after a late native reply.
return control;
}
@@ -426,8 +429,10 @@ ControlDescription describe_standard_control(HWND window) {
control.states = item.states;
control.position = item.position;
control.count = item.count;
lastNativeControl = window;
lastNativeItem = item.key;
if (item.available) {
lastNativeControl = window;
lastNativeItem = item.key;
} else lastNativeControl = nullptr;
return control;
}
@@ -1067,6 +1072,7 @@ extern "C" error_status_t __stdcall testIfRunning(void) {
return reader_is_available() ? ERROR_SUCCESS : ERROR_SERVICE_NOT_ACTIVE;
}
extern "C" error_status_t __stdcall speakText(const wchar_t *text) {
if (!controller_text_fits(text)) return ERROR_INVALID_PARAMETER;
const std::string utf8 = to_utf8(text);
// Tolk calls cancelSpeech separately when its interrupt argument is true.
return call_boolean("SpeakText", g_variant_new("(sb)", utf8.c_str(), FALSE));
@@ -1075,6 +1081,7 @@ extern "C" error_status_t __stdcall cancelSpeech(void) {
return call_boolean("CancelSpeech", nullptr);
}
extern "C" error_status_t __stdcall brailleMessage(const wchar_t *text) {
if (!controller_text_fits(text)) return ERROR_INVALID_PARAMETER;
const std::string utf8 = to_utf8(text);
return call_boolean("BrailleMessage", g_variant_new("(s)", utf8.c_str()));
}
@@ -1086,6 +1093,7 @@ extern "C" error_status_t __stdcall getProcessId(ULONG *pid) {
}
extern "C" error_status_t __stdcall speakSsml(const wchar_t *ssml, SYMBOL_LEVEL,
SPEECH_PRIORITY, boolean) {
if (!controller_text_fits(ssml)) return ERROR_INVALID_PARAMETER;
const std::string utf8 = to_utf8(ssml);
GError *error = nullptr;
GVariant *reply = g_dbus_connection_call_sync(
+3 -1
View File
@@ -69,7 +69,9 @@ __declspec(dllexport) LRESULT CALLBACK WineAccessMenuHook(int code, WPARAM wp,
if (code >= 0) {
wine_access_watch_notifications();
const CWPSTRUCT *message = (const CWPSTRUCT *)lp;
if (message->message == WM_SETFOCUS) wine_access_watch_edit(message->hwnd);
if (message->message == WM_SETFOCUS ||
(message->message >= 0xc000 && message->message == RegisterWindowMessageW(EDIT_REVIEW_MESSAGE)))
wine_access_watch_edit(message->hwnd);
if (message->message == WM_MENUSELECT || message->message == WM_EXITMENULOOP)
handle_menu(message);
}
+5 -9
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "msaa_events.h"
#include "msaa_states.h"
#include "text_limits.h"
#include <oleacc.h>
#include <condition_variable>
#include <mutex>
@@ -13,19 +14,13 @@ namespace {
// Bound pending work and favor current focus if an application floods events.
constexpr size_t capacity = 32;
std::string text(BSTR value) {
if (!value) return {};
int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value, -1, nullptr, 0, nullptr, nullptr);
if (size <= 1) return {};
std::string result(size, '\0');
WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value, -1, result.data(), size, nullptr, nullptr);
result.resize(size - 1);
return result;
return bounded_accessible_text(value, max_accessible_units);
}
std::string property(IAccessible *object,
HRESULT (STDMETHODCALLTYPE IAccessible::*getter)(VARIANT, BSTR *), VARIANT child) {
BSTR value = nullptr;
HRESULT hr = (object->*getter)(child, &value);
auto result = SUCCEEDED(hr) ? text(value) : std::string{};
auto result = SUCCEEDED(hr) ? bounded_accessible_text(value, value ? SysStringLen(value) : 0) : std::string{};
SysFreeString(value);
return result;
}
@@ -79,7 +74,8 @@ struct MsaaEventReader::State {
WCHAR label[128]{};
if (GetRoleTextW(roleId, label, 128)) result.role = text(label);
else result.role = std::to_string(roleId);
} else if (role.vt == VT_BSTR) result.role = text(role.bstrVal);
} else if (role.vt == VT_BSTR)
result.role = bounded_accessible_text(role.bstrVal, role.bstrVal ? SysStringLen(role.bstrVal) : 0);
}
VariantClear(&role);
if (!current(event)) return result;
+4 -3
View File
@@ -72,7 +72,7 @@ NativeItem read_native_item(HWND window) {
item.role = tab ? "page tab" : "tree";
const auto key = native_item_key(window);
item.key = key;
if (!key.item) return item;
if (!key.item) { item.available = true; return item; }
native_control::RemoteBuffer remote(window, bufferSize);
if (!remote) return item;
const bool wow64 = remote.is32Bit();
@@ -95,9 +95,10 @@ NativeItem read_native_item(HWND window) {
}
DWORD_PTR result = 0;
if (remote.write(buffer, sizeof(buffer)) &&
query(window, tab ? TCM_GETITEMW : TVM_GETITEMW, tab ? key.item - 1 : 0,
static_cast<LPARAM>(remote.address()), &result) && result &&
remote.query(window, tab ? TCM_GETITEMW : TVM_GETITEMW, tab ? key.item - 1 : 0,
&result) && result &&
remote.read(buffer, sizeof(buffer))) {
item.available = true;
wchar_t text[textCapacity] = {};
memcpy(text, buffer + textOffset, sizeof(text));
text[textCapacity - 1] = L'\0';
+1
View File
@@ -11,6 +11,7 @@ struct NativeItemKey {
bool operator==(const NativeItemKey &) const = default;
};
struct NativeItem {
bool available = false;
NativeItemKey key;
std::string text;
std::string role;
+59 -18
View File
@@ -3,6 +3,7 @@
#include <windows.h>
#include <cstddef>
#include <cstdint>
#include <memory>
namespace native_control {
inline bool query(HWND window, UINT message, WPARAM wp, LPARAM lp, DWORD_PTR *result) {
@@ -13,46 +14,86 @@ inline bool query(HWND window, UINT message, WPARAM wp, LPARAM lp, DWORD_PTR *re
// Common-control messages above WM_USER do not marshal embedded pointers.
// Own the target allocation and process handle together, including failures.
class RemoteBuffer {
struct Allocation {
HANDLE process = nullptr;
void *memory = nullptr;
BOOL wow64 = FALSE;
bool pending = false;
LRESULT result = 0;
~Allocation() {
if (memory) VirtualFreeEx(process, memory, 0, MEM_RELEASE);
if (process) CloseHandle(process);
}
};
inline static thread_local std::weak_ptr<Allocation> outstanding_;
static void CALLBACK completed(HWND, UINT, ULONG_PTR data, LRESULT result) {
std::unique_ptr<std::shared_ptr<Allocation>> retained(
reinterpret_cast<std::shared_ptr<Allocation> *>(data));
(*retained)->result = result;
(*retained)->pending = false;
}
public:
RemoteBuffer(HWND window, size_t size) {
// One late application request may retain its allocation. Do not create
// another on every polling tick while that application is unresponsive.
if (auto previous = outstanding_.lock(); previous && previous->pending) return;
allocation_ = std::make_shared<Allocation>();
DWORD processId = 0;
GetWindowThreadProcessId(window, &processId);
process_ = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ |
allocation_->process = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ |
PROCESS_VM_WRITE | PROCESS_QUERY_LIMITED_INFORMATION,
FALSE, processId);
if (!process_ || !IsWow64Process(process_, &wow64_)) return;
memory_ = VirtualAllocEx(process_, nullptr, size,
if (!allocation_->process || !IsWow64Process(allocation_->process, &allocation_->wow64)) return;
allocation_->memory = VirtualAllocEx(allocation_->process, nullptr, size,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Every byte of the allocation must be addressable by the target.
if (memory_ && wow64_ &&
if (allocation_->memory && allocation_->wow64 &&
(address() > UINT32_MAX || size - 1 > UINT32_MAX - address())) {
VirtualFreeEx(process_, memory_, 0, MEM_RELEASE);
memory_ = nullptr;
VirtualFreeEx(allocation_->process, allocation_->memory, 0, MEM_RELEASE);
allocation_->memory = nullptr;
}
}
~RemoteBuffer() {
if (memory_) VirtualFreeEx(process_, memory_, 0, MEM_RELEASE);
if (process_) CloseHandle(process_);
}
RemoteBuffer(const RemoteBuffer &) = delete;
RemoteBuffer &operator=(const RemoteBuffer &) = delete;
explicit operator bool() const { return memory_ != nullptr; }
bool is32Bit() const { return wow64_ != FALSE; }
explicit operator bool() const { return allocation_ && allocation_->memory; }
bool is32Bit() const { return allocation_->wow64 != FALSE; }
uintptr_t address(size_t offset = 0) const {
return reinterpret_cast<uintptr_t>(memory_) + offset;
return reinterpret_cast<uintptr_t>(allocation_->memory) + offset;
}
bool write(const void *buffer, size_t size) const {
return WriteProcessMemory(process_, memory_, buffer, size, nullptr) != 0;
return !allocation_->pending && WriteProcessMemory(allocation_->process, allocation_->memory, buffer, size, nullptr) != 0;
}
bool read(void *buffer, size_t size, size_t offset = 0) const {
return ReadProcessMemory(process_, reinterpret_cast<const void *>(address(offset)),
return !allocation_->pending && ReadProcessMemory(allocation_->process, reinterpret_cast<const void *>(address(offset)),
buffer, size, nullptr) != 0;
}
bool query(HWND window, UINT message, WPARAM wp, DWORD_PTR *result) {
if (!*this || allocation_->pending) return false;
allocation_->pending = true;
outstanding_ = allocation_;
auto retained = new std::shared_ptr<Allocation>(allocation_);
if (!SendMessageCallbackW(window, message, wp, static_cast<LPARAM>(address()),
completed, reinterpret_cast<ULONG_PTR>(retained))) {
allocation_->pending = false;
delete retained;
return false;
}
const ULONGLONG deadline = GetTickCount64() + 100;
while (allocation_->pending && GetTickCount64() < deadline) {
// Dispatch sent-message completions without consuming queued UI work.
// Reentrant polls cannot allocate while outstanding_ remains pending.
MSG message{};
PeekMessageW(&message, nullptr, 0, 0, PM_NOREMOVE);
if (allocation_->pending) Sleep(1);
}
if (allocation_->pending) return false;
*result = static_cast<DWORD_PTR>(allocation_->result);
return true;
}
private:
HANDLE process_ = nullptr;
void *memory_ = nullptr;
BOOL wow64_ = FALSE;
std::shared_ptr<Allocation> allocation_;
};
} // namespace native_control
+9 -2
View File
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "reader_events.h"
#include "text_limits.h"
#include <condition_variable>
#include <cstdio>
#include <deque>
@@ -21,11 +22,13 @@ struct ReaderEventQueue::State {
std::mutex mutex;
std::condition_variable changed;
std::deque<GVariant *> pending;
size_t pendingBytes = 0;
bool stopping = false, overflowReported = false;
void clear() {
for (auto *parameters : pending) g_variant_unref(parameters);
pending.clear();
pendingBytes = 0;
}
bool send(GVariant *parameters, unsigned &failures) {
GError *error = nullptr;
@@ -64,6 +67,7 @@ struct ReaderEventQueue::State {
if (state->stopping) break;
parameters = state->pending.front();
state->pending.pop_front();
state->pendingBytes -= g_variant_get_size(parameters);
}
const char *source = nullptr, *kind = nullptr;
g_variant_get_child(parameters, 0, "&s", &source);
@@ -100,17 +104,20 @@ bool ReaderEventQueue::start(GDBusConnection *connection, const ReaderEndpoint &
bool ReaderEventQueue::submit(GVariant *parameters) {
parameters = g_variant_ref_sink(parameters);
const size_t bytes = g_variant_get_size(parameters);
if (state_) {
std::lock_guard<std::mutex> lock(state_->mutex);
// Keep memory bounded if the reader remains unavailable. Do not reorder
// actions or evict the focus on which queued editor actions depend.
if (!state_->stopping && state_->pending.size() < 128) {
if (!state_->stopping && state_->pending.size() < 128 &&
bytes <= max_reader_event_bytes && bytes <= max_reader_queue_bytes - state_->pendingBytes) {
state_->pending.push_back(parameters);
state_->pendingBytes += bytes;
state_->changed.notify_one();
return true;
}
if (!state_->stopping && !state_->overflowReported) {
std::fprintf(stderr, "wine-access: reader event queue full; feedback dropped\n");
std::fprintf(stderr, "wine-access: reader event size or queue limit reached; feedback dropped\n");
state_->overflowReported = true;
}
}
+60 -11
View File
@@ -4,6 +4,7 @@
#include "native_query.h"
#include "review_x11.h"
#include "review_query.h"
#include "edit_hook.h"
#include <commctrl.h>
#include <cstring>
#include <oleacc.h>
@@ -21,6 +22,7 @@ constexpr DWORD wine_virtual_desktop = 0x80000000;
// Wine's import libraries do not export this IID as a Unix data symbol.
constexpr GUID accessible_iid{0x618736e0,0x3c3d,0x11cf,{0x81,0x0c,0x00,0xaa,0x00,0x38,0x9b,0x71}};
constexpr size_t max_source = 1000000, max_text = 1048576, max_nodes = 4096;
constexpr size_t max_unhooked_text = 16384;
std::string utf8(const WCHAR *text, size_t length) {
if (!length) return {};
int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text,
@@ -56,8 +58,46 @@ bool remote_query(HWND window, UINT message, WPARAM wp, LPARAM lp,
return GetTickCount64() < deadline;
}
bool password(HWND window);
bool read_hooked_edit(HWND window, uint64_t deadline, size_t limit,
std::vector<WCHAR> &text, bool &limited) {
DWORD_PTR answer = 0;
const UINT message = RegisterWindowMessageW(EDIT_REVIEW_MESSAGE);
// The foreground thread's message hook can attach an output editor that
// has never received keyboard focus. First send a scalar attachment probe;
// the following query then enters the newly installed subclass reliably.
if (!GetPropW(window, EDIT_REVIEW_PROPERTY) &&
(!query(window, message, 0, 0, deadline, answer) ||
!GetPropW(window, EDIT_REVIEW_PROPERTY))) return false;
const size_t offset = offsetof(WineAccessEditText, text);
const size_t bytes = offset + (limit + 1) * sizeof(WCHAR);
native_control::RemoteBuffer remote(window, bytes);
if (!remote) return false;
WineAccessEditText request{};
request.capacity = limit + 1;
if (!remote.write(&request, offset) ||
!remote_query(window, message, 0,
remote.address(), deadline, answer) ||
!remote.read(&request, offset) || password(window)) return false;
if (request.status == 2) { limited = true; return false; }
if (request.status != 1 || request.copied > limit) return false;
text.assign(request.copied + 1, 0);
if (!remote.read(text.data(), text.size() * sizeof(WCHAR), offset) || text.back()) return false;
text.resize(request.copied);
return true;
}
bool read_text(HWND window, uint64_t deadline, size_t limit, std::vector<WCHAR> &text,
bool &limited) {
if (password(window)) return false;
if (GetPropW(window, EDIT_REVIEW_PROPERTY))
return read_hooked_edit(window, deadline, limit, text, limited);
WCHAR class_name[128]{};
if (GetClassNameW(window, class_name, 128) && wine_access_class_matches(class_name, L"Edit")) {
if (read_hooked_edit(window, deadline, limit, text, limited)) return true;
if (limited || GetPropW(window, EDIT_REVIEW_PROPERTY)) return false;
}
// Unhooked/older editors cannot service the safe native query. Keep their
// ordinary WM_GETTEXT requests far below Wine's callback-stack limit.
limit = std::min(limit, max_unhooked_text);
DWORD_PTR size = 0, copied = 0;
if (password(window) || !query(window, WM_GETTEXTLENGTH, 0, 0, deadline, size) || password(window)) return false;
if (size > limit) { limited = true; return false; }
@@ -69,6 +109,23 @@ bool read_text(HWND window, uint64_t deadline, size_t limit, std::vector<WCHAR>
text.resize(size);
return true;
}
bool reread_text(HWND window, uint64_t deadline, size_t size, std::vector<WCHAR> &text) {
if (password(window)) return false;
if (GetPropW(window, EDIT_REVIEW_PROPERTY)) {
bool limited = false;
return read_hooked_edit(window, deadline, size, text, limited);
}
if (size > max_unhooked_text) return false;
// The original source already supplies a safe capacity. Avoid another
// length/attachment round trip on frame-based applications.
text.assign(size + 1, 0);
DWORD_PTR copied = 0, after = 0;
if (!query(window, WM_GETTEXT, text.size(), reinterpret_cast<LPARAM>(text.data()), deadline, copied) ||
password(window) || !query(window, WM_GETTEXTLENGTH, 0, 0, deadline, after) ||
password(window) || copied != size || after != size) return false;
text.resize(size);
return true;
}
std::string caption(HWND window) {
// A foreign window's caption is held by USER32. Sending WM_GETTEXT here
// could deadlock against an editor hook currently sending WM_COPYDATA to
@@ -815,17 +872,9 @@ ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocu
RECT bounds{};
if (password(read.window) || !IsWindowVisible(read.window) || !GetWindowRect(read.window, &bounds) ||
!EqualRect(&read.bounds, &bounds)) { capture.fail("stale"); break; }
if (read.text.size() > max_source - capture.source_units) { capture.fail("limited"); break; }
capture.source_units += read.text.size();
// The first read already supplies a bounded capacity. Copy once, then
// verify length, content and protection without another preliminary
// length round trip (growth must still reject a truncated old prefix).
current.assign(read.text.size()+1,0);
DWORD_PTR copied=0,length=0;
if(!query(read.window,WM_GETTEXT,current.size(),reinterpret_cast<LPARAM>(current.data()),deadline,copied) ||
password(read.window) || !query(read.window,WM_GETTEXTLENGTH,0,0,deadline,length) ||
password(read.window) || copied!=read.text.size() || length!=read.text.size())capture.fail("stale");
current.resize(read.text.size());
// This is a second copy of an already budgeted source, not another
// document. Charging it twice made large safe snapshots fail review.
if (!reread_text(read.window, deadline, read.text.size(), current)) capture.fail("stale");
if(current!=read.text)capture.fail("stale");
if (read.layout && !capture.failed) {
EditLayout layout{};
+35
View File
@@ -175,6 +175,41 @@ the affected Wine application as well as the helper.
## Reader reply stalls
Long-running text and native-query regressions are also covered by:
```sh
xvfb-run -a ctest --test-dir build -R 'text-limits|review-capture|hook-safety' --output-on-failure
```
`text-limits` checks MSAA truncation, malformed padding, surrogate boundaries,
controller request limits and a private stalled reader. A burst of 100 large
events must reach the serialized byte budget before the 128-event count limit;
an oversized individual event is rejected without consuming queue capacity.
The review fixtures run with 1 MiB PE stacks in both architectures. They review
a 600000-unit ANSI Edit through the real injected hook and cross-process native
query, preserving visible text without a large `WM_GETTEXT` message. That output
editor starts without a subclass and is attached by review without moving focus.
The ordinary ListView reader is delayed beyond its timeout: its buffer must
remain committed, further remote allocations must pause, and a later successful
read must recover.
The existing delayed capture query remains covered separately.
The hook-safety fixtures also verify complete native review queries at 516000,
600000 and 1040000 UTF-16 units, and 2000 background transcript replacement,
selection and append cycles without full-text queries. Controller integration
checks that oversized speech, braille and SSML requests return an error without
reaching the recording reader.
These are bounded regressions, not a multi-hour soak or proof of audible speech
and physical braille. For live acceptance, restart the helper and MUSHclient,
then check command history, typing, selection/deletion, incoming plugin output,
large-output review, menu return, password exclusion and focus restoration.
During a long session record RSS and CPU for MUSHclient, Wine, the helper, reader
and speech processes; distinguish continuing memory growth from allocator reuse.
If the machine locks up again, retain the previous boot's kernel log as well as
reader and Wine diagnostics.
Run sequentially on a private X display:
```sh
+8
View File
@@ -46,6 +46,14 @@ int main(int argc, char **argv) {
// The reader's SSML status must reach the caller without conversion.
failed |= speakSsml(L"<speak>Controller SSML</speak>", SYMBOL_LEVEL_ALL,
SPEECH_PRIORITY_NORMAL, FALSE) != ERROR_CALL_NOT_IMPLEMENTED;
WCHAR *oversized = malloc(65538 * sizeof(WCHAR));
if (!oversized) return 1;
for (unsigned i = 0; i < 65537; ++i) oversized[i] = L'x';
oversized[65537] = 0;
failed |= speakText(oversized) != ERROR_INVALID_PARAMETER;
failed |= brailleMessage(oversized) != ERROR_INVALID_PARAMETER;
failed |= speakSsml(oversized, SYMBOL_LEVEL_ALL, SPEECH_PRIORITY_NORMAL, FALSE) != ERROR_INVALID_PARAMETER;
free(oversized);
}
RpcBindingFree(&binding);
return failed;
+57 -3
View File
@@ -11,8 +11,9 @@
#define PROBE_DEPTH 16
static WNDPROC original, outer_original;
static uintptr_t deepest, hook_stack;
static BOOL nested, destroy_on_set;
static BOOL nested, destroy_on_set, destroy_on_get, protect_on_length;
static unsigned calls, forwarded_sets;
static unsigned text_queries;
typedef struct EditStateProbe {
WCHAR *text;
@@ -33,6 +34,15 @@ static LRESULT CALLBACK passthrough(HWND window, UINT message, WPARAM wp, LPARAM
static LRESULT CALLBACK application_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
volatile char marker = 0;
++calls;
if (message == WM_GETTEXT) ++text_queries;
if (message == WM_GETTEXTLENGTH && protect_on_length) {
protect_on_length = FALSE;
SendMessageW(window, EM_SETPASSWORDCHAR, '*', 0);
}
if (message == WM_GETTEXT && destroy_on_get) {
DestroyWindow(window);
return 0;
}
if (message == PROBE_MESSAGE) {
if (wp) return SendMessageA(window, PROBE_MESSAGE, wp - 1, 0);
deepest = (uintptr_t)&marker;
@@ -134,6 +144,9 @@ int main(int argc, char **argv) {
}
largeText[largeLength] = 0;
if (!SetWindowTextA(window, largeText)) { free(largeText); return 12; }
// Programmatic transcript replacement is lazy. Navigation/focus still
// obtains a complete snapshot through the stack-safe direct query.
SendMessageW(window, RegisterWindowMessageW(EDIT_FOCUS_MESSAGE), 0, 0);
EditStateProbe *state = (EditStateProbe *)GetPropW(window, EDIT_STATE_PROPERTY);
if (!state || !state->text || state->length != largeLength ||
state->text[largeLength] != 0) {
@@ -148,12 +161,53 @@ int main(int argc, char **argv) {
return 13;
}
}
size_t bytes = offsetof(WineAccessEditText, text) + ((size_t)largeLength + 1) * sizeof(WCHAR);
WineAccessEditText *request = calloc(1, bytes);
if (!request) return 16;
request->capacity = largeLength + 1;
SendMessageW(window, RegisterWindowMessageW(EDIT_REVIEW_MESSAGE), 0, (LPARAM)request);
if (request->status != 1 || request->copied != largeLength || request->text[largeLength]) return 17;
for (DWORD i = 0; i < largeLength; ++i)
if (request->text[i] != (unsigned char)largeText[i]) return 18;
free(request);
free(largeText);
printf("large ANSI snapshot: %lu UTF-16 units\n", (unsigned long)state->length);
}
UnhookWindowsHookEx(text_hook);
// Destroy while the accessibility callback is busy; it must defer freeing
// its state until the application's callback has returned.
unsigned queries_before = text_queries;
for (unsigned i = 0; i < 2000; ++i) {
SetWindowTextA(window, "incoming transcript line");
SendMessageW(window, EM_SETSEL, (WPARAM)-1, (LPARAM)-1);
SendMessageW(window, EM_REPLACESEL, FALSE, (LPARAM)L" another line");
}
if (text_queries != queries_before) return 19;
printf("background transcript burst: no full-text reads for 2000 updates\n");
SendMessageW(window, RegisterWindowMessageW(EDIT_FOCUS_MESSAGE), 0, 0);
WineAccessEditText *private_query = calloc(1, offsetof(WineAccessEditText, text) + 64 * sizeof(WCHAR));
if (!private_query) return 22;
private_query->capacity = 64;
protect_on_length = TRUE;
SendMessageW(window, RegisterWindowMessageW(EDIT_REVIEW_MESSAGE), 0, (LPARAM)private_query);
EditStateProbe *protected_state = (EditStateProbe *)GetPropW(window, EDIT_STATE_PROPERTY);
if (!protected_state || protected_state->text || protected_state->known || private_query->status) return 23;
free(private_query);
SendMessageW(window, EM_SETPASSWORDCHAR, 0, 0);
// Destroy while the native review callback is busy. Its state must survive
// until the application's query returns, then be freed exactly once.
destroy_on_get = TRUE;
WineAccessEditText *destroy_query = calloc(1, offsetof(WineAccessEditText, text) + 64 * sizeof(WCHAR));
if (!destroy_query) return 20;
destroy_query->capacity = 64;
SendMessageW(window, RegisterWindowMessageW(EDIT_REVIEW_MESSAGE), 0, (LPARAM)destroy_query);
if (IsWindow(window) || destroy_query->status) return 21;
free(destroy_query);
destroy_on_get = FALSE;
// Also retain the application's destruction-during-replacement check.
window = CreateWindowA("Edit", "initial", WS_POPUP | ES_MULTILINE,
0, 0, 200, 100, NULL, NULL, GetModuleHandleW(NULL), NULL);
original = (WNDPROC)SetWindowLongPtrA(window, GWLP_WNDPROC, (LONG_PTR)application_proc);
focus.hwnd = window;
callback(HC_ACTION, 0, (LPARAM)&focus);
destroy_on_set = TRUE;
SetWindowTextA(window, "destroy");
if (IsWindow(window)) return 7;
+31
View File
@@ -5,6 +5,9 @@
#include <algorithm>
#include <string>
#include <vector>
#include "../list_view.h"
#include "../native_query.h"
#include "../edit_hook.h"
#if __has_include("../review_capture.h")
#include "../review_capture.h"
#define HAS_CAPTURE 1
@@ -173,6 +176,34 @@ int main(int argc, char **argv) {
}
if (mode != 4) ok &= check(SendMessageW(window, WM_APP + 11, 0, 0), "capture leaves caret and focus unchanged");
}
SendMessageW(window, WM_APP + 10, 34, 0);
auto started = GetTickCount64();
auto delayed_row = read_list_view(GetDlgItem(window, 107));
ok &= check(!delayed_row.available, "timed-out row is not cached as a successful read");
ok &= check(GetTickCount64() - started < 400, "ordinary native read times out promptly");
{
native_control::RemoteBuffer another(GetDlgItem(window,107), 128);
ok &= check(!another, "late native request prevents repeated remote allocations");
}
Sleep(750);
ok &= check(SendMessageW(window,WM_APP + 15,0,0), "ordinary native read retains its remote buffer after timeout");
MSG completion{};
PeekMessageW(&completion,nullptr,0,0,PM_NOREMOVE);
{
native_control::RemoteBuffer recovered(GetDlgItem(window,107),128);
ok &= check(static_cast<bool>(recovered), "completed native request releases allocation backpressure");
}
auto recovered_row = read_list_view(GetDlgItem(window,107));
ok &= check(recovered_row.available && recovered_row.text.find("ready") != std::string::npos,
"ordinary native text recovers after a late reply");
SendMessageW(window, WM_APP + 10, 35, 0);
ok &= check(GetPropW(GetDlgItem(window,115),EDIT_REVIEW_PROPERTY) == nullptr,
"large output starts without a focus-installed subclass");
auto large_capture = capture();
ok &= check(large_capture.status == "ok" && joined(large_capture).find("aaaa") != std::string::npos,
"600000-unit ANSI Edit reviews safely through a real hook on a 1 MiB stack");
ok &= check(GetPropW(GetDlgItem(window,115),EDIT_REVIEW_PROPERTY) != nullptr,
"large review exercised the native Edit query");
// A local provider avoids cross-process WM_GETTEXT marshalling, which
// itself truncates at the first NUL. Its BSTR retains malformed padding.
WNDCLASSW local{};local.hInstance=GetModuleHandleW(nullptr);
+37 -1
View File
@@ -3,6 +3,7 @@
#include <commctrl.h>
#include <stdlib.h>
#include <string.h>
#include "../edit_hook.h"
#define REVIEW_CONFIG (WM_APP + 10)
#define REVIEW_CHECK (WM_APP + 11)
@@ -11,6 +12,8 @@
#define REVIEW_PRIVACY_CHECK (WM_APP + 14)
#define REVIEW_REMOTE_CHECK (WM_APP + 15)
static HWND editor, output, password, hidden;
static HWND ansi_editor;
static HHOOK edit_hook;
static HWND mdi, mdi_active, mdi_background;
static WCHAR *large;
static WNDPROC original_edit;
@@ -113,6 +116,8 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
frame_pump=FALSE;
delay_batch=FALSE;
synthetic_containers=0;
ShowWindow(ansi_editor, SW_HIDE);
ShowWindow(editor, SW_SHOW);
ShowWindow(GetDlgItem(window,112),SW_HIDE);
ShowWindow(GetDlgItem(window,114),SW_HIDE);
ShowWindow(mdi,SW_HIDE);
@@ -134,6 +139,34 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
SendMessageW(editor, EM_SETSEL, 0, 0);
MoveWindow(editor, 20, 35, 320, 90, TRUE);
switch (wp) {
case 35: {
WCHAR path[MAX_PATH];
GetModuleFileNameW(NULL, path, MAX_PATH);
WCHAR *separator = path;
for (WCHAR *p = path; *p; ++p) if (*p == '\\' || *p == '/') separator = p + 1;
#ifdef _WIN64
lstrcpyW(separator, L"wine-access-menu.dll");
#else
lstrcpyW(separator, L"wine-access-menu32.dll");
#endif
HMODULE module = LoadLibraryW(path);
HOOKPROC callback = module ? (HOOKPROC)GetProcAddress(module, "WineAccessMenuHook") : NULL;
if (!callback) ExitProcess(6);
edit_hook = SetWindowsHookExW(WH_CALLWNDPROC, callback, module, GetCurrentThreadId());
if (!edit_hook) ExitProcess(7);
char *text = malloc(600001);
if (!text) ExitProcess(8);
for (unsigned i = 0; i < 600000; ++i) text[i] = i % 80 == 79 ? '\n' : 'a';
text[600000] = 0;
ShowWindow(editor, SW_HIDE);
ShowWindow(ansi_editor, SW_SHOW);
SetFocus(output); // The large output has never received focus.
SendMessageA(ansi_editor, EM_LIMITTEXT, 2000000, 0);
SetWindowTextA(ansi_editor, text);
free(text);
SendMessageW(ansi_editor, EM_SETSEL, 0, 0);
return 1;
}
case 33: ShowWindow(GetDlgItem(window,114),SW_SHOW); break;
case 32: {
WCHAR text[16384]; unsigned used = 0;
@@ -222,7 +255,7 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
case 15: replace_list_during_read = TRUE; break;
case 17: ShowWindow(GetDlgItem(window,107),SW_SHOW); break;
case 18: ShowWindow(GetDlgItem(window,107),SW_SHOW); replace_table_during_read=TRUE; break;
case 21: ShowWindow(GetDlgItem(window,107),SW_SHOW); delay_table_read=TRUE; break;
case 21: case 34: ShowWindow(GetDlgItem(window,107),SW_SHOW); delay_table_read=TRUE; break;
case 22:
ShowWindow(GetDlgItem(window,109),SW_SHOW);
SendMessageW(GetDlgItem(window,109),EM_SETSEL,INT_MAX,INT_MAX);
@@ -289,6 +322,8 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
editor = CreateWindowW(L"Edit", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER |
ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL, 20, 35, 320, 90, window, (HMENU)101, instance, NULL);
original_edit = (WNDPROC)SetWindowLongPtrW(editor, GWLP_WNDPROC, (LONG_PTR)edit_proc);
ansi_editor = CreateWindowA("Edit", "", WS_CHILD | WS_BORDER | ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL,
20, 35, 320, 90, window, (HMENU)115, instance, NULL);
output = CreateWindowW(L"Edit", L"output first\r\noutput second", WS_CHILD | WS_VISIBLE |
ES_MULTILINE | ES_READONLY | WS_BORDER, 20, 205, 320, 80, window, (HMENU)102, instance, NULL);
password = CreateWindowW(L"Edit", L"private-review-secret", WS_CHILD | WS_VISIBLE |
@@ -349,5 +384,6 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
} else if (GetMessageW(&message,NULL,0,0)<=0)break;
TranslateMessage(&message); DispatchMessageW(&message);
}
if (edit_hook) UnhookWindowsHookEx(edit_hook);
return 0;
}
+75
View File
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "../text_limits.h"
#include "../reader_events.h"
#include <cstdio>
#include <vector>
static bool check(bool value, const char *message) {
if (!value) std::fprintf(stderr, "FAIL: %s\n", message);
return value;
}
static GVariant *event(const std::string &text, const char *kind = "value") {
return g_variant_new("(sssssssiis)", "source", kind, text.c_str(), "edit", "", "", "", 0, 0, "test");
}
int main() {
bool ok = true;
std::vector<WCHAR> large(max_controller_units + 2, L'a');
large.back() = 0;
ok &= check(!controller_text_fits(large.data()), "oversized controller input rejected");
large[max_controller_units] = 0;
ok &= check(controller_text_fits(large.data()), "maximum controller input accepted");
ok &= check(bounded_accessible_text(large.data(), large.size()).size() == max_accessible_units,
"large accessible value is bounded before UTF-8 allocation");
large[max_accessible_units - 1] = 0xd800;
large[max_accessible_units] = 0xdc00;
ok &= check(bounded_accessible_text(large.data(), large.size()).size() == max_accessible_units - 1,
"truncation does not split a surrogate pair");
const WCHAR padded[] = {L'o', L'k', 0, 0xd800};
ok &= check(bounded_accessible_text(padded, 4) == "ok", "malformed BSTR padding excluded");
GError *error = nullptr;
auto *connection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
if (!connection) {
std::fprintf(stderr, "FAIL: private session bus: %s\n", error ? error->message : "unavailable");
if (error) g_error_free(error);
return 1;
}
const char *xml = "<node><interface name='org.stormux.LimitTest'>"
"<method name='PresentAccessibleEvent'>"
"<arg type='s' direction='in'/><arg type='s' direction='in'/><arg type='s' direction='in'/>"
"<arg type='s' direction='in'/><arg type='s' direction='in'/><arg type='s' direction='in'/>"
"<arg type='s' direction='in'/><arg type='i' direction='in'/><arg type='i' direction='in'/>"
"<arg type='s' direction='in'/><arg type='b' direction='out'/></method></interface></node>";
auto *info = g_dbus_node_info_new_for_xml(xml, nullptr);
static const GDBusInterfaceVTable table{
[](GDBusConnection *, const gchar *, const gchar *, const gchar *, const gchar *,
GVariant *, GDBusMethodInvocation *invocation, gpointer) {
g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", TRUE));
}, nullptr, nullptr, {nullptr}};
guint object = g_dbus_connection_register_object(connection, "/test", info->interfaces[0],
&table, nullptr, nullptr, nullptr);
ok &= check(object != 0, "recording reader object registered");
ReaderEndpoint endpoint{g_dbus_connection_get_unique_name(connection), "/test", "org.stormux.LimitTest"};
ReaderEventQueue queue;
ok &= check(queue.start(connection, endpoint), "reader queue starts");
ok &= check(queue.submit(event("focus", "focus")), "focus dependency accepted");
// Do not dispatch this connection's default GLib context: its first method
// call withholds the reply while the event thread fills the pending queue.
Sleep(20);
ok &= check(!queue.submit(event(std::string(max_reader_event_bytes, 'x'))),
"oversized single event rejected");
const std::string payload(100000, 'x');
unsigned accepted = 0;
auto started = GetTickCount64();
for (unsigned i = 0; i < 100; ++i) accepted += queue.submit(event(payload));
ok &= check(GetTickCount64() - started < 400, "flood completed before reader reply timeout");
ok &= check(accepted == 10, "byte budget rejects large events before the 128-event count limit");
queue.stop();
if (object) g_dbus_connection_unregister_object(connection, object);
g_dbus_node_info_unref(info);
g_object_unref(connection);
if (ok) std::puts("PASS: bounded MSAA/controller text, Unicode boundaries and reader queue byte budget");
return ok ? 0 : 1;
}
+34
View File
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <windows.h>
#include <algorithm>
#include <string>
constexpr size_t max_accessible_units = 4096;
constexpr size_t max_controller_units = 65536;
constexpr size_t max_reader_event_bytes = 131072;
constexpr size_t max_reader_queue_bytes = 1048576;
inline std::string bounded_accessible_text(const WCHAR *text, size_t length) {
if (!text) return {};
length = std::min(length, max_accessible_units);
// MSAA strings can include padding past their first terminating NUL.
size_t used = 0;
while (used < length && text[used]) ++used;
if (used && text[used - 1] >= 0xd800 && text[used - 1] <= 0xdbff) --used;
if (!used) return {};
int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text,
static_cast<int>(used), nullptr, 0, nullptr, nullptr);
if (size <= 0) return {};
std::string result(size, '\0');
WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text,
static_cast<int>(used), result.data(), size, nullptr, nullptr);
return result;
}
inline bool controller_text_fits(const WCHAR *text) {
if (!text) return false;
for (size_t i = 0; i <= max_controller_units; ++i)
if (!text[i]) return true;
return false;
}