#include "ocr.h" #include "richtext.h" #include "storage.h" #include #include #include #include namespace { int passed = 0; void require(bool ok, const char *name) { if (!ok) throw std::runtime_error(name); ++passed; } void rejects(const std::function &f, const char *name) { bool thrown = false; try { f(); } catch (...) { thrown = true; } require(thrown, name); } } // namespace int main() { try { CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); HWND richParent = CreateWindowExW(0, L"STATIC", L"Rich preview test", WS_POPUP, 0, 0, 300, 200, nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); HWND rich = ks::createRichText(richParent, 1, false); require(rich != nullptr, "native RichEdit loads without a browser runtime"); auto styled = ks::textClip(L"plain bold italic under\nnext"); std::string html = "Version:1.0\r\n
plain bold " "italic under
next
"; styled.formats.push_back({0, L"HTML Format", ks::Bytes(html.begin(), html.end())}); require(ks::setRichText(rich, ks::richPreview(styled)), "HTML clipboard preview streams into RichEdit"); auto effects = [&](LONG first, LONG last) { CHARRANGE range{first, last}; SendMessageW(rich, EM_EXSETSEL, 0, (LPARAM)&range); CHARFORMAT2W format{}; format.cbSize = sizeof(format); SendMessageW(rich, EM_GETCHARFORMAT, SCF_SELECTION, (LPARAM)&format); return format.dwEffects; }; require((effects(6, 10) & CFE_BOLD) != 0, "HTML bold is displayed as native bold"); require((effects(11, 17) & CFE_ITALIC) != 0, "HTML italic is displayed as native italic"); require((effects(18, 23) & CFE_UNDERLINE) != 0, "HTML underline is displayed natively"); require((effects(0, 5) & (CFE_BOLD | CFE_ITALIC | CFE_UNDERLINE)) == 0, "inline formatting does not leak into plain text"); wchar_t rendered[256]{}; GetWindowTextW(rich, rendered, 256); require(std::wstring(rendered).find(L"\r\nnext") != std::wstring::npos || std::wstring(rendered).find(L"\rnext") != std::wstring::npos, "HTML line breaks are retained"); html = "Привіт & 🌍" ""; styled.formats.back().data = ks::Bytes(html.begin(), html.end()); ks::setRichText(rich, ks::richPreview(styled)); GetWindowTextW(rich, rendered, 256); require(std::wstring(rendered) == L"Привіт & 🌍", "HTML entities and Unicode survive without script text"); require((effects(0, 6) & CFE_BOLD) != 0, "inline CSS bold is rendered"); auto plain = ks::textClip(L"{\\literal} Привіт"); ks::setRichText(rich, ks::richPreview(plain)); GetWindowTextW(rich, rendered, 256); require(std::wstring(rendered) == plain.text(), "plain Unicode and RTF metacharacters are escaped"); std::string rtf = "{\\rtf1\\ansi \\b native}"; plain.formats.push_back({0, L"Rich Text Format", ks::Bytes(rtf.begin(), rtf.end())}); ks::setRichText(rich, ks::richPreview(plain)); require((effects(0, 6) & CFE_BOLD) != 0, "original RTF formatting is preserved"); DestroyWindow(richParent); ks::Layout en = ks::physicalLayout(); en.id = L"en"; ks::Layout ru{nullptr, L"ru", L"Russian", L"ё1234567890-=йцукенгшщзхъфывапролджэячсмитьбю.\\", L"Ё!\"№;%:?*()_+ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,/"}; ks::Layout uk{nullptr, L"uk", L"Ukrainian", L"'1234567890-=йцукенгшщзхїфівапролджєячсмитьбю.\\", L"₴!\"№;%:?*()_+ЙЦУКЕНГШЩЗХЇФІВАПРОЛДЖЄЯЧСМИТЬБЮ,/"}; require(en.normal.size() == ru.normal.size(), "physical maps align"); require(en.normal.size() == uk.normal.size(), "Ukrainian maps align"); require(ks::convert(L"ghbdtn", en, ru) == L"привет", "English to Russian"); require(ks::convert(L"привіт", uk, en) == L"ghbdsn", "Ukrainian to English"); require(ks::convert(L"hello привет", en, ru) == L"руддщ привет", "mixed text preserves target words"); require(ks::convert(L"htdm.dftim", en, ru) == L"ревьюваешь", "punctuation in words"); std::vector layouts{en, ru, uk}; require(ks::convertTo(L"ghbdtn", layouts, 1, 0) == L"привет", "hint conversion"); require(ks::convertTo(L"привет", layouts, 1, 1) == L"привет", "same source is unchanged"); require(ks::convertTo(L"?", layouts, 1, 0) == L",", "punctuation conversion"); require(ks::cycleCase(L"hello world") == L"Hello World", "title case"); require(ks::cycleCase(L"Hello World") == L"HELLO WORLD", "upper case"); require(ks::cycleCase(L"ПРИВІТ") == L"привіт", "Unicode lower case"); require(ks::transliterate(L"Привет") == L"Privet", "forward transliteration"); require(ks::transliterate(L"Київ") == L"Kyiv", "Ukrainian transliteration"); require(ks::transliterate(L"Shchuka") == L"Щука", "reverse transliteration"); require(ks::parseTemplate(L"a{cursor}b").suffix == L"b", "snippet suffix"); require(ks::parseTemplate(L"{{cursor}}").text == L"{cursor}", "escaped cursor"); require(ks::parseTemplate(L"{cursor}{cursor}").cursors == 2, "duplicate cursor detection"); require(ks::Hotkey::parse(L"Ctrl+Alt+Shift+Space")->key == VK_SPACE, "hotkey parse"); require(!ks::Hotkey::parse(L"Ctrl+F25"), "invalid function key"); require(!ks::Hotkey::parse(L"Ctrl+A+B"), "multiple keys rejected"); require(ks::Hotkey::parse(L"")->key == 0, "disabled shortcut"); ks::HotkeyHook hook; std::vector> bindings{ {5, {MOD_WIN, 'V'}}, {1, {MOD_ALT, VK_F12}}, {100, {MOD_CONTROL | MOD_SHIFT, 'U'}}}; require(hook.update('V', true, MOD_WIN, bindings) == 5, "Win+V dispatches configured action"); require(hook.update('V', true, MOD_WIN, bindings) == -1, "held hotkey does not repeat"); require(hook.update('V', false, 0, {}) == -1, "key-up consumed after settings change"); require(hook.update('V', true, MOD_CONTROL, bindings) == 0, "unassigned Ctrl+V unaffected"); require(hook.update('V', true, MOD_WIN | MOD_SHIFT, bindings) == 0, "modifiers match exactly"); require(hook.update('V', true, MOD_WIN, {}) == 0, "disabled binding leaves OS shortcut intact"); require(hook.update(VK_F12, true, MOD_ALT, bindings) == 1, "conversion uses same hook"); require(hook.update('U', true, MOD_CONTROL | MOD_SHIFT, bindings) == 100, "direct layout uses same hook"); require(hook.update(VK_F12, false, 0, bindings) == -1, "independent key release consumed"); require(hook.update('U', false, 0, bindings) == -1, "second key release consumed"); require(hook.update('V', true, MOD_WIN, bindings) == 5, "hotkey activates again after release"); bindings.push_back({2, {MOD_WIN | MOD_SHIFT, 'S'}}); require(hook.update('S', true, MOD_WIN | MOD_SHIFT, bindings) == 2, "Win+Shift+S can replace the Snipping Tool shortcut"); require(hook.update('S', false, 0, bindings) == -1, "Snipping Tool shortcut release consumed"); for (UINT key : {UINT('K'), UINT(VK_F12), UINT(VK_SPACE), UINT(VK_OEM_PLUS), UINT(VK_LEFT), UINT(VK_NUMPAD7)}) { ks::Hotkey recorded{MOD_CONTROL | MOD_ALT | MOD_SHIFT, key}; require(ks::Hotkey::parse(recorded.format()) == recorded, "recorded shortcut roundtrip"); } ks::Run run; run.record(L'g', (HWND)1, (HKL)1, 1); run.record(L'h', (HWND)1, (HKL)1, 2); require(run.match(L"пр", layouts, en) == 1, "recent input verification"); require(run.match(L"xx", layouts, en) == -1, "unrelated selected text rejected"); require(run.match(L"xпр", layouts, en) == -1, "word selection including older text is rejected before replacement"); require(run.match(L"р", layouts, en) == -1, "coalesced Teams selection of only the final character is rejected"); require(run.match(L"пр ", layouts, en) == -1, "word selection including unrecorded whitespace is rejected"); run.record(L'b', (HWND)1, (HKL)1, 2002); require(run.keys == L"b", "pause resets run"); run.record(L'c', (HWND)2, (HKL)1, 2003); require(run.keys == L"c", "window resets run"); run.record(L'd', (HWND)2, (HKL)2, 2004); require(run.keys == L"d", "layout resets run"); run.record(L' ', (HWND)2, (HKL)2, 2005); run.record(L'e', (HWND)2, (HKL)2, 2006); require(run.keys == L"e", "word boundary resets run"); for (int i = 0; i < 513; ++i) run.record(L'x', (HWND)2, (HKL)2, 2007 + i); require(run.overflow && run.keys.empty(), "overflow does not select partial input"); std::map> processes{ {10, {1, L"ms-teams.exe"}}, {20, {10, L"msedgewebview2.exe"}}, {30, {20, L"msedgewebview2.exe"}}, {40, {1, L"msedgewebview2.exe"}}, {50, {10, L"other.exe"}}, {60, {60, L"msedgewebview2.exe"}}}; auto read = [&](DWORD id) -> std::optional> { auto it = processes.find(id); return it == processes.end() ? std::nullopt : std::optional(it->second); }; require(ks::webViewDescendant(30, 10, read), "Teams renderer belongs to host"); require(!ks::webViewDescendant(40, 10, read), "other WebView rejected"); require(!ks::webViewDescendant(50, 10, read), "arbitrary child rejected"); require(!ks::webViewDescendant(60, 10, read), "process cycles rejected"); require(!ks::webViewDescendant(0, 0, read), "unknown process rejected"); auto clip = ks::textClip(L"Привіт 🌍\r\nline 2"); clip.formats.push_back({0, L"HTML Format", {1, 2, 3, 0}}); auto encoded = ks::encodeClip(clip); require(ks::decodeClip(encoded).text() == clip.text(), "clipboard Unicode roundtrip"); require(ks::decodeClip(encoded).formats[1].name == L"HTML Format", "registered format identity"); auto encrypted = ks::protect(encoded); require(encrypted != encoded, "history ciphertext differs"); require(ks::unprotect(encrypted) == encoded, "Windows DPAPI roundtrip"); encrypted[encrypted.size() / 2] ^= 0x40; rejects([&] { ks::unprotect(encrypted); }, "tampering rejected"); encoded.pop_back(); rejects([&] { ks::decodeClip(encoded); }, "truncated clip rejected"); auto temp = std::filesystem::temp_directory_path() / (L"KSwitcher-tests-" + std::to_wstring(GetCurrentProcessId())); std::filesystem::create_directories(temp); ks::Pixels pixels{2, 1, {10, 20, 30, 255, 40, 50, 60, 255}}; ks::Clip firstImage{{ks::imageDib(pixels), {0, L"Ole Private Data", {1, 2, 3}}}}; auto thumbnail = ks::decodeImage(firstImage, 1, 1); require(thumbnail.width == 1 && thumbnail.height == 1 && thumbnail.bgra.size() == 4, "thumbnail decoding limits the resident pixel buffer"); thumbnail = ks::decodeImage(firstImage, 208, 128); require(thumbnail.width == 2 && thumbnail.height == 1, "thumbnail decoding does not upscale small images"); auto secondImage = firstImage; secondImage.formats[1].data = {4, 5, 6}; secondImage.formats.push_back({0, L"DataObject", {9, 8, 7}}); require(ks::sameClipboardImage(firstImage, secondImage), "OLE metadata does not duplicate an image"); std::reverse(secondImage.formats.begin(), secondImage.formats.end()); require(ks::sameClipboardImage(firstImage, secondImage), "format ordering does not duplicate an image"); auto recoded = firstImage; recoded.formats[0].data[24] = 1; // Different bitmap resolution metadata; identical pixels. require(ks::sameClipboardImage(firstImage, recoded), "decoded image ignores resolution metadata"); pixels.bgra[0]++; ks::Clip changedImage{{ks::imageDib(pixels)}}; require(!ks::sameClipboardImage(firstImage, changedImage), "a changed pixel remains a distinct image"); auto withText = firstImage; withText.formats.push_back(ks::textClip(L"caption").formats.front()); require(!ks::sameClipboardImage(firstImage, withText), "image deduplication preserves accompanying text"); auto withFiles = firstImage; withFiles.formats.push_back({CF_HDROP, L"", {1}}); require(!ks::sameClipboardImage(firstImage, withFiles), "image deduplication preserves file copies"); { ks::History history(temp / L"images"); ks::Settings defaults; history.add(firstImage, L"SnippingTool", defaults); history.pin(history.entries().front().id); history.add(secondImage, L"SnippingTool", defaults); require(history.entries().size() == 1 && history.entries().front().pinned, "Snipping Tool republish retains one pinned history entry"); history.add(changedImage, L"SnippingTool", defaults); require(history.entries().size() == 2, "a new screenshot is retained"); } ks::Settings settings; settings.elevatedAutostart = true; settings.cycle = {L"en", L"uk"}; settings.snippets = {{L";address", L"Line 1\r\n{cursor}Line 2"}}; ks::saveSettings(temp / L"settings.json", settings); auto restored = ks::loadSettings(temp / L"settings.json", {}); require(restored.snippets[0].text == settings.snippets[0].text, "settings snippet roundtrip"); require(restored.cycle == settings.cycle, "layout order roundtrip"); require(restored.elevatedAutostart, "elevated autostart setting roundtrip"); require(!ks::loadSettings(temp / L"defaults.json", {}).elevatedAutostart, "elevated autostart is opt-in for existing and default settings"); auto invalid = settings; invalid.undo = invalid.toggle; rejects([&] { ks::validateSettings(invalid); }, "duplicate shortcut rejected"); invalid = settings; invalid.snippets[0].text = L"{cursor}{cursor}"; rejects([&] { ks::validateSettings(invalid); }, "invalid snippet rejected"); { ks::History history(temp / L"history"); history.add(clip, L"Test", settings); history.add(clip, L"Test", settings); require(history.entries().size() == 1, "history consecutive deduplication"); auto id = history.entries()[0].id; history.pin(id); require(history.entries()[0].pinned, "pin persisted in memory"); require(history.search(L"line 2").size() == 1, "full text search"); require(history.load(id).text() == clip.text(), "history payload roundtrip"); } { ks::History history(temp / L"history"); require(history.entries().size() == 1 && history.entries()[0].pinned, "history index reload"); rejects([&] { history.load(L"../other"); }, "history path traversal rejected"); history.clear(); require(history.entries().empty(), "clear history"); } // Test-owned directory only; never touch the user's clipboard or stored history. std::filesystem::remove_all(temp); CoUninitialize(); std::cout << "Passed " << passed << " native regression checks\n"; return 0; } catch (const std::exception &e) { std::cerr << "FAILED: " << e.what() << '\n'; return 1; } }