Release v0.6.37: isolate image capture and OCR, fingerprint history duplicates
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// Interactive clipboard test. Run with the resident stopped; the original
|
||||
// clipboard is restored. The helper must be beside this test executable.
|
||||
#include "storage.h"
|
||||
#include "ocr.h"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
static void require(bool ok,const char *name) {
|
||||
if(!ok) throw std::runtime_error(name);
|
||||
std::cout << "PASS " << name << '\n';
|
||||
}
|
||||
int main() {
|
||||
CoInitializeEx(nullptr,COINIT_MULTITHREADED);
|
||||
HWND owner=CreateWindowExW(0,L"STATIC",L"Capture test",0,0,0,0,0,HWND_MESSAGE,nullptr,nullptr,nullptr);
|
||||
ks::Clip saved; bool captured=false; int result=0;
|
||||
auto folder=std::filesystem::temp_directory_path()/(L"KSwitcher-image-capture-"+std::to_wstring(GetCurrentProcessId()));
|
||||
try {
|
||||
saved=ks::readClipboard(owner,128*1024*1024); captured=true;
|
||||
ks::Settings settings; settings.history=true;
|
||||
ks::History history(folder);
|
||||
history.add(ks::textClip(L"keep pinned"),L"test",settings);
|
||||
auto pinned=history.entries().front().id; history.pin(pinned);
|
||||
ks::Pixels pixels{32,16,ks::Bytes(32*16*4,255)};
|
||||
ks::Clip image{{ks::imageDib(pixels),{0,L"PNG",ks::encodePng(pixels)}}};
|
||||
auto sequence=ks::writeClipboard(owner,image);
|
||||
require(history.captureImage(sequence,L"capture test",settings),"helper captures image");
|
||||
auto entries=history.entries();
|
||||
require(entries.size()==2 && entries.front().image && entries.front().ocrState==1,"resident reloads metadata with pending OCR");
|
||||
require(entries.front().contentHash.size()==64 && !entries.front().pixelHash.empty(),
|
||||
"helper persists fingerprints and resident reloads them");
|
||||
require(entries.back().id==pinned && entries.back().pinned,"helper preserves existing pinned entry");
|
||||
auto restored=history.load(entries.front().id);
|
||||
auto has=[&](const ks::Format &expected) {
|
||||
return std::any_of(restored.formats.begin(),restored.formats.end(),[&](const ks::Format &actual) {
|
||||
return (expected.name.empty()?actual.id==expected.id:actual.name==expected.name) && actual.data==expected.data;
|
||||
});
|
||||
};
|
||||
require(has(image.formats[0]) && has(image.formats[1]),
|
||||
"encrypted history preserves both image representations");
|
||||
require(ks::readFile(folder/(entries.front().id+L".dat"),1024*1024)!=ks::encodeClip(restored),"saved image is encrypted");
|
||||
require(GetClipboardSequenceNumber()==sequence,"capture leaves clipboard unchanged");
|
||||
require(history.captureImage(sequence,L"capture test",settings) && history.entries().size()==2,"duplicate capture is not added");
|
||||
ks::writeClipboard(owner,ks::textClip(L"new clipboard"));
|
||||
require(!history.captureImage(sequence,L"stale",settings) && history.entries().size()==2,"stale request does not save another clipboard");
|
||||
for(auto name : {L"ExcludeClipboardContentFromMonitorProcessing",L"Clipboard Viewer Ignore",L"CanIncludeInClipboardHistory"}) {
|
||||
auto blocked=image; blocked.formats.push_back({0,name,ks::Bytes(4,0)});
|
||||
sequence=ks::writeClipboard(owner,blocked);
|
||||
require(!history.captureImage(sequence,L"excluded",settings) && history.entries().size()==2,"clipboard privacy flag prevents capture");
|
||||
}
|
||||
pixels={1024,512,ks::Bytes(1024*512*4,255)};
|
||||
sequence=ks::writeClipboard(owner,ks::Clip{{ks::imageDib(pixels)}});
|
||||
settings.maxImageMiB=1;
|
||||
bool rejected=false;
|
||||
try {history.captureImage(sequence,L"oversized",settings);} catch(const std::exception &e) {
|
||||
rejected=std::string(e.what())=="Clipboard exceeds the configured size limit";
|
||||
}
|
||||
require(rejected && history.entries().size()==2,"oversized image is rejected without changing index");
|
||||
require(GetClipboardSequenceNumber()==sequence,"rejection leaves clipboard unchanged");
|
||||
for(const auto &file:std::filesystem::directory_iterator(folder))
|
||||
require(file.path().extension()!=L".request","encrypted capture request is cleaned up");
|
||||
} catch(const std::exception &e) {std::cerr << "FAIL " << e.what() << '\n';result=1;}
|
||||
if(captured) try {ks::writeClipboard(owner,saved);} catch(...) {result=1;}
|
||||
DestroyWindow(owner); CoUninitialize();
|
||||
// Remove only the exact PID-specific fixture under the system temp directory.
|
||||
std::error_code ignored;
|
||||
if(std::filesystem::weakly_canonical(folder).parent_path()==std::filesystem::weakly_canonical(std::filesystem::temp_directory_path()))
|
||||
std::filesystem::remove_all(folder,ignored);
|
||||
return result;
|
||||
}
|
||||
@@ -59,6 +59,24 @@ int main(int argc, char **argv)
|
||||
app.settings.history = true;
|
||||
app.history->add(clip, L"OCR fixture", app.settings);
|
||||
auto id = app.history->entries().front().id;
|
||||
auto isolated = app.history->recognizeImage(id, config);
|
||||
if (isolated != text) throw std::runtime_error("Helper and in-process OCR results differ");
|
||||
bool cancelled = false;
|
||||
try { app.history->recognizeImage(id, config, [] { return true; }); }
|
||||
catch (const std::exception &) { cancelled = true; }
|
||||
if (!cancelled) throw std::runtime_error("Cancelled OCR request was executed");
|
||||
auto invalid = config;
|
||||
invalid.useTesseract = true;
|
||||
invalid.tesseractPath = (temp / L"missing-tesseract.exe").wstring();
|
||||
bool failed = false;
|
||||
try { app.history->recognizeImage(id, invalid); }
|
||||
catch (const std::exception &) { failed = true; }
|
||||
if (!failed || app.history->entries().front().ocrState != 1)
|
||||
throw std::runtime_error("Failed helper modified history metadata");
|
||||
for (const auto &file : std::filesystem::directory_iterator(temp / L"history"))
|
||||
if (file.path().filename().wstring().starts_with(L"ocr-"))
|
||||
throw std::runtime_error("OCR helper left temporary files");
|
||||
std::cout << "PASS isolated OCR equivalence, cancellation, failure and encrypted IPC cleanup\n";
|
||||
if (tesseract)
|
||||
{
|
||||
app.history->saveOcr(id, L"old Windows result", app.settings);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "richtext.h"
|
||||
#include "storage.h"
|
||||
#include "diagnostics.h"
|
||||
#include "json.hpp"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
@@ -635,6 +636,55 @@ int main(int argc, char **argv)
|
||||
"Snipping Tool republish retains one pinned history entry");
|
||||
history.add(changedImage, L"SnippingTool", defaults);
|
||||
require(history.entries().size() == 2, "a new screenshot is retained");
|
||||
auto copiedText = ks::textClip(L"text after screenshot");
|
||||
history.add(copiedText, L"Editor", defaults);
|
||||
history.add(copiedText, L"Editor", defaults);
|
||||
require(history.entries().size() == 3 && !history.entries().front().image,
|
||||
"text after screenshot is retained and consecutive text duplicates are skipped");
|
||||
history.add(changedImage, L"SnippingTool", defaults);
|
||||
history.add(changedImage, L"SnippingTool", defaults);
|
||||
require(history.entries().size() == 4 && history.entries().front().image,
|
||||
"image after text is retained and consecutive image duplicates are skipped");
|
||||
history.add(withText, L"Editor", defaults);
|
||||
require(history.entries().size() == 5,
|
||||
"mixed image and text payload is not discarded by metadata prefilter");
|
||||
}
|
||||
{
|
||||
auto folder = temp / L"fingerprints";
|
||||
ks::Settings defaults;
|
||||
ks::History history(folder);
|
||||
history.add(firstImage, L"test", defaults);
|
||||
auto id = history.entries().front().id;
|
||||
history.pin(id);
|
||||
history.saveOcr(id, L"retained OCR", defaults);
|
||||
ks::History reopened(folder);
|
||||
require(reopened.entries().front().contentHash.size() == 64,
|
||||
"SHA-256 fingerprint survives encrypted index reload");
|
||||
// Deny payload reads: duplicates must still be detected using only the index.
|
||||
auto handle = CreateFileW((folder / (id + L".dat")).c_str(), GENERIC_READ, 0, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
require(handle != INVALID_HANDLE_VALUE, "lock image payload for no-read regression");
|
||||
reopened.add(firstImage, L"test", defaults);
|
||||
reopened.add(secondImage, L"test", defaults);
|
||||
reopened.add(recoded, L"test", defaults);
|
||||
CloseHandle(handle);
|
||||
require(reopened.entries().size() == 1 && reopened.entries().front().pinned &&
|
||||
reopened.ocrText(id) == std::optional<std::wstring>(L"retained OCR"),
|
||||
"exact, OLE and pixel-equivalent duplicates need no payload read and retain pin/OCR");
|
||||
auto index = nlohmann::json::parse(ks::unprotect(ks::readFile(folder / L"index.dat", 1024*1024)));
|
||||
for (auto &entry : index)
|
||||
for (auto key : {"fingerprintVersion", "contentHash", "imageHash", "pixelHash"}) entry.erase(key);
|
||||
auto legacy = index.dump();
|
||||
ks::atomicWrite(folder / L"index.dat", ks::protect(ks::Bytes(legacy.begin(), legacy.end())));
|
||||
ks::History migrated(folder);
|
||||
migrated.add(secondImage, L"test", defaults);
|
||||
ks::History persisted(folder);
|
||||
require(persisted.entries().size() == 1 && persisted.entries().front().contentHash.size() == 64,
|
||||
"legacy duplicate migrates fingerprints without adding or losing an entry");
|
||||
persisted.add(changedImage, L"test", defaults);
|
||||
require(persisted.entries().size() == 2, "pixel fingerprint preserves genuinely changed images");
|
||||
persisted.add(firstImage, L"test", defaults);
|
||||
require(persisted.entries().size() == 3, "hash deduplication remains consecutive rather than global");
|
||||
}
|
||||
ks::Settings settings;
|
||||
settings.writeLog = false;
|
||||
|
||||
Reference in New Issue
Block a user