Files
2026-10-14 20:55:28 -04:00

336 lines
13 KiB
C

// SPDX-License-Identifier: LGPL-2.1-or-later
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#ifdef WINE_ACCESS_WINFORMS_FIXTURE
static const WCHAR *editClass = L"WindowsForms10.EDIT.app.0.fixture_r7_ad1";
static const WCHAR *staticClass = L"WindowsForms10.STATIC.app.0.fixture_r7_ad1";
static void register_control(HINSTANCE instance, const WCHAR *nativeName, const WCHAR *managedName) {
WNDCLASSEXW cls = {0};
cls.cbSize = sizeof(cls);
if (!GetClassInfoExW(NULL, nativeName, &cls)) ExitProcess(3);
cls.hInstance = instance;
cls.lpszClassName = managedName;
if (!RegisterClassExW(&cls)) ExitProcess(4);
}
#else
static const WCHAR *editClass = L"Edit";
#endif
static HWND editor, password, history;
static int step;
static int history_position;
static DWORD WINAPI update_history_from_worker(void *window) {
SendMessageW((HWND)window, WM_SETTEXT, 0, (LPARAM)L"unrelated worker update");
return 0;
}
static void unrelated_history_update(void) {
HANDLE thread = CreateThread(NULL, 0, update_history_from_worker, history, 0, NULL);
if (!thread) ExitProcess(2);
// Dispatch sent messages without removing a queued message, as GetMessage
// does while waiting for input. Do not rely on worker/timer scheduling.
for (;;) {
DWORD result = MsgWaitForMultipleObjects(1, &thread, FALSE, 2000, QS_SENDMESSAGE);
if (result == WAIT_OBJECT_0) break;
if (result != WAIT_OBJECT_0 + 1) ExitProcess(2);
MSG message;
PeekMessageW(&message, NULL, 0, 0, PM_NOREMOVE);
}
CloseHandle(thread);
WCHAR text[64];
GetWindowTextW(history, text, 64);
if (lstrcmpW(text, L"unrelated worker update")) ExitProcess(2);
}
// Model MUSHclient's PreTranslateMessage: consume queued arrows before the
// Edit procedure, then use SetWindowText and SetSel to recall a command.
static BOOL history_input(const MSG *message) {
if (message->hwnd != history || message->message != WM_KEYDOWN ||
(message->wParam != VK_UP && message->wParam != VK_DOWN)) return FALSE;
if (message->wParam == VK_UP && history_position < 2) ++history_position;
else if (message->wParam == VK_DOWN && history_position > 0) --history_position;
else {
if (step == 49) unrelated_history_update();
return TRUE;
}
const WCHAR *commands[] = {L"", L"history north", L"history south"};
SetWindowTextW(history, commands[history_position]);
int length = GetWindowTextLengthW(history);
SendMessageW(history, EM_SETSEL, length, length);
return TRUE;
}
static void key(UINT key, BOOL control, BOOL shift) {
BYTE saved[256], state[256];
GetKeyboardState(saved);
CopyMemory(state, saved, sizeof(state));
state[VK_CONTROL] = control ? 0x80 : 0;
state[VK_SHIFT] = shift ? 0x80 : 0;
SetKeyboardState(state);
SendMessageW(editor, WM_KEYDOWN, key, 1);
SetKeyboardState(saved);
}
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
if (message == WM_TIMER) {
if (step == 0) SetTimer(window, 1, 350, NULL);
switch (++step) {
case 1: key(VK_RIGHT, FALSE, FALSE); break;
case 2: key(VK_RIGHT, TRUE, FALSE); break;
case 3: key(VK_DOWN, FALSE, FALSE); break;
case 4: key(VK_HOME, FALSE, FALSE); break;
case 5: key(VK_RIGHT, FALSE, TRUE); break;
case 6: key(VK_RIGHT, FALSE, TRUE); break;
case 7: key(VK_LEFT, FALSE, TRUE); break;
case 8: key(VK_LEFT, FALSE, FALSE); break;
case 9: key(VK_DELETE, FALSE, FALSE); break;
case 10:
key(VK_RIGHT, FALSE, FALSE);
SendMessageW(editor, WM_CHAR, VK_BACK, 1);
break;
case 11: SendMessageW(editor, EM_SETSEL, 0, 5); break;
case 12: SendMessageW(editor, WM_CLEAR, 0, 0); break;
case 13: SetFocus(password); break;
case 14: SendMessageW(password, WM_KEYDOWN, VK_LEFT, 1); break;
case 15: SendMessageW(password, WM_CHAR, VK_BACK, 1); break;
case 16:
SetFocus(editor);
SetWindowTextW(editor, L"\U0001f600 cafe");
SendMessageW(editor, EM_SETSEL, 2, 2);
break;
case 17: key(VK_HOME, FALSE, FALSE); break;
case 18:
SendMessageW(editor, EM_SETREADONLY, TRUE, 0);
SetFocus(password); SetFocus(editor);
break;
case 19: key(VK_DELETE, FALSE, FALSE); break;
case 20:
if (GetWindowTextLengthW(editor) != 7) ExitProcess(2);
SendMessageW(editor, EM_SETREADONLY, FALSE, 0);
SetWindowTextW(editor, L"hello\r\nmenu return line");
SendMessageW(editor, EM_SETSEL, 0, 0);
break;
case 21:
key(VK_RIGHT, FALSE, FALSE);
key(VK_RIGHT, FALSE, FALSE);
key(VK_RIGHT, FALSE, FALSE);
break;
case 22:
SendMessageW(editor, EM_SETSEL, 7, 7);
SendMessageW(window, WM_MENUSELECT, MAKEWPARAM(1, 0), (LPARAM)GetMenu(window));
break;
case 23: SendMessageW(window, WM_EXITMENULOOP, 0, 0); break;
case 24:
// Replacement with shorter text is not a Delete/Backspace command.
SendMessageW(editor, EM_SETSEL, 0, 5);
SendMessageW(editor, EM_REPLACESEL, TRUE, (LPARAM)L"hi");
break;
case 25:
SetWindowTextW(editor, L"first\r\nsecond");
SendMessageW(editor, EM_SETSEL, 7, 7);
SendMessageW(editor, WM_CHAR, VK_BACK, 1);
break;
case 26:
SetWindowTextW(editor, L"first\r\nsecond");
SendMessageW(editor, EM_SETSEL, 5, 5);
key(VK_DELETE, FALSE, FALSE);
break;
case 27:
// A surrogate at the 1023-unit line boundary must not discard the line.
{
WCHAR line[1027];
for (int i = 0; i < 1022; ++i) line[i] = L'x';
line[1022] = 0xd83d; line[1023] = 0xde00;
line[1024] = L'y'; line[1025] = 0;
SetWindowTextW(editor, line);
SendMessageW(editor, EM_SETSEL, 0, 0);
SetFocus(password); SetFocus(editor);
}
break;
case 28:
SetWindowTextW(editor, L"undo restored");
SendMessageW(editor, EM_SETSEL, 0, 4);
SendMessageW(editor, EM_REPLACESEL, TRUE, (LPARAM)L"x");
SendMessageW(editor, EM_UNDO, 0, 0);
SendMessageW(editor, EM_SETSEL, 1, 1);
break;
case 29:
key(VK_RIGHT, FALSE, FALSE); // d, not the space in the old snapshot
break;
case 30:
key(VK_END, TRUE, FALSE); // whole restored line
break;
case 31:
SetWindowTextW(editor, L"readonly first\r\nreadonly second");
SendMessageW(editor, EM_SETSEL, 0, 0);
SendMessageW(editor, EM_SETREADONLY, TRUE, 0);
SetFocus(password); SetFocus(editor);
break;
case 32: key(VK_DOWN, FALSE, FALSE); break;
case 33: key(VK_UP, FALSE, FALSE); break;
case 34:
key(VK_DELETE, FALSE, FALSE);
if (GetWindowTextLengthW(editor) != 31) ExitProcess(2);
break;
case 35:
SendMessageW(editor, WM_CHAR, L'!', 1);
if (GetWindowTextLengthW(editor) != 31) ExitProcess(2);
break;
case 36:
SendMessageW(editor, EM_SETREADONLY, FALSE, 0);
SetWindowTextW(editor, L"");
SendMessageW(editor, WM_CHAR, L'z', 1);
SendMessageW(editor, WM_CHAR, L'z', 1);
SendMessageW(editor, WM_CHAR, L' ', 1);
SendMessageW(editor, WM_CHAR, 0x00e9, 1);
SendMessageW(editor, WM_CHAR, 0xd83d, 1);
SendMessageW(editor, WM_CHAR, 0xde00, 1);
SendMessageW(editor, WM_CHAR, VK_RETURN, 1);
SendMessageW(editor, WM_CHAR, VK_TAB, 1);
{
WCHAR text[32];
GetWindowTextW(editor, text, 32);
if (lstrcmpW(text, L"zz \u00e9\U0001f600\r\n\t")) ExitProcess(2);
}
break;
case 37:
// Replacing selected text is still typing, even when length shrinks.
SendMessageW(editor, EM_SETSEL, 0, 4);
SendMessageW(editor, WM_CHAR, L'q', 1);
break;
case 38:
SetFocus(password);
SendMessageW(password, WM_CHAR, L'@', 1);
// Background changes must not produce echo for the focused password.
SendMessageW(editor, WM_CHAR, L'~', 1);
break;
case 39:
SetFocus(editor);
{
DWORD length = 1024 * 1024 + 1;
WCHAR *large = calloc(length + 1, sizeof(WCHAR));
if (!large) ExitProcess(2);
for (DWORD i = 0; i < length; ++i) large[i] = L'x';
SendMessageW(editor, EM_SETLIMITTEXT, length + 3, 0);
SetWindowTextW(editor, large);
free(large);
SendMessageW(editor, EM_SETSEL, length, length);
SendMessageW(editor, WM_CHAR, L'L', 1);
SendMessageW(editor, WM_CHAR, 0xd83d, 1);
SendMessageW(editor, WM_CHAR, 0xde00, 1);
if (GetWindowTextLengthW(editor) != (int)length + 3) {
fprintf(stderr, "large typing: length=%d expected=%lu limit=%ld\n",
GetWindowTextLengthW(editor), (unsigned long)length + 3,
(long)SendMessageW(editor, EM_GETLIMITTEXT, 0, 0));
ExitProcess(2);
}
}
break;
case 40:
SetWindowTextW(editor, L"full");
SendMessageW(editor, EM_SETSEL, 4, 4);
SendMessageW(editor, EM_SETLIMITTEXT, 4, 0);
SendMessageW(editor, WM_CHAR, L'%', 1);
if (GetWindowTextLengthW(editor) != 4) ExitProcess(2);
break;
case 41:
// Keep this field away from the managed-label regression's static labels.
history = CreateWindowW(editClass, L"", WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,
100, 150, 300, 25, window, NULL, GetModuleHandleW(NULL), NULL);
SetFocus(history);
break;
case 42: PostMessageW(history, WM_KEYDOWN, VK_UP, 1); break;
case 43: PostMessageW(history, WM_KEYDOWN, VK_UP, 1); break;
case 44: PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); break;
case 45: PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); break;
case 46:
// Rapid recalls must stay ordered, including equal-length commands.
PostMessageW(history, WM_KEYDOWN, VK_UP, 1);
PostMessageW(history, WM_KEYDOWN, VK_UP, 1);
PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1);
break;
case 47:
// Ordinary navigation after a recall must use the refreshed snapshot.
PostMessageW(history, WM_KEYDOWN, VK_LEFT, 1);
PostMessageW(history, WM_KEYDOWN, VK_RIGHT, 1);
break;
case 48:
SetWindowTextW(history, L"programmatic history sentinel");
PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1);
break;
case 49:
PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); // no history change
break;
case 50:
// The silent worker update must still refresh the navigation snapshot.
PostMessageW(history, WM_KEYDOWN, VK_RIGHT, 1);
break;
case 51:
SendMessageW(history, EM_SETPASSWORDCHAR, L'*', 0);
PostMessageW(history, WM_KEYDOWN, VK_UP, 1);
break;
case 52:
SetFocus(password);
SendMessageW(history, EM_SETPASSWORDCHAR, 0, 0);
PostMessageW(history, WM_KEYDOWN, VK_UP, 1); // background recall
break;
default:
DestroyWindow(window); break;
}
return 0;
}
if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
return DefWindowProcW(window, message, wp, lp);
}
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
(void)previous; (void)args; (void)show;
#ifdef WINE_ACCESS_WINFORMS_FIXTURE
register_control(instance, L"Edit", editClass);
register_control(instance, L"Static", staticClass);
#endif
WNDCLASSW cls = {0};
cls.hInstance = instance; cls.lpszClassName = L"WineAccessEditFixture";
cls.lpfnWndProc = window_proc; RegisterClassW(&cls);
HWND window = CreateWindowW(cls.lpszClassName, L"Wine editor fixture", WS_OVERLAPPEDWINDOW,
120, 120, 500, 300, NULL, NULL, instance, NULL);
editor = CreateWindowW(editClass, L"alpha beta\r\nsecond line",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL,
100, 10, 350, 150, window, NULL, instance, NULL);
password = CreateWindowW(editClass, L"private-password-fixture",
WS_CHILD | WS_VISIBLE | ES_PASSWORD, 100, 180, 300, 30,
window, NULL, instance, NULL);
#ifdef WINE_ACCESS_WINFORMS_FIXTURE
// Labels are created after the fields, like the observed BARD login.
CreateWindowW(staticClass, L"Password:", WS_CHILD | WS_VISIBLE, 10, 183, 85, 20,
window, NULL, instance, NULL);
CreateWindowW(staticClass, L"Email:", WS_CHILD | WS_VISIBLE, 10, 13, 60, 20,
window, NULL, instance, NULL);
CreateWindowW(staticClass, L"Unrelated:", WS_CHILD | WS_VISIBLE, 10, 220, 80, 20,
window, NULL, instance, NULL);
#endif
HMENU menu = CreateMenu();
AppendMenuW(menu, MF_STRING, 1, L"Test menu");
SetMenu(window, menu);
ShowWindow(window, SW_SHOW); SetForegroundWindow(window); SetFocus(editor);
SendMessageW(editor, EM_SETSEL, 0, 0);
WCHAR ready[1024];
if (GetEnvironmentVariableW(L"WINE_ACCESS_FIXTURE_READY", ready, 1024)) {
HANDLE file = CreateFileW(ready, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, 0, NULL);
if (file == INVALID_HANDLE_VALUE) return 2;
CloseHandle(file);
}
// Leave time to attach/restart a helper while this already-focused window
// keeps pumping messages. Subsequent actions use the ordinary short timer.
SetTimer(window, 1, 1500, NULL);
MSG message;
while (GetMessageW(&message, NULL, 0, 0) > 0) {
if (history_input(&message)) continue;
TranslateMessage(&message); DispatchMessageW(&message);
}
return 0;
}