From 9a3a92170ed68569743f0182987dafc93f46a9da Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 18:43:12 +0900 Subject: [PATCH] engine: reject stale dictionary results --- dat.h | 2 ++ dict.c | 1 + strans.c | 7 ++++- tests/dict_test.c | 7 +++++ tests/engine_test.c | 65 +++++++++++++++++++++++++++++++++++++++++++++ tests/test.h | 1 + tests/unit_test.c | 1 + 7 files changed, 83 insertions(+), 1 deletion(-) diff --git a/dat.h b/dat.h index 63262b1..e3d2507 100644 --- a/dat.h +++ b/dat.h @@ -172,6 +172,7 @@ struct Dictreq Str key; Str pre; int lang; + u32int seq; }; typedef struct Dictres Dictres; @@ -181,6 +182,7 @@ struct Dictres Str kouho[Maxkouho]; int nkouho; int lang; + u32int seq; }; extern Lang langs[]; diff --git a/dict.c b/dict.c index cb4739c..b8ab4d0 100644 --- a/dict.c +++ b/dict.c @@ -13,6 +13,7 @@ dictlookup(Dictreq *req, Dictres *res) res->key = req->pre; res->nkouho = 0; res->lang = req->lang; + res->seq = req->seq; if(req->key.n == 0) return; l = getlang(req->lang); diff --git a/strans.c b/strans.c index 41e2039..51da19a 100644 --- a/strans.c +++ b/strans.c @@ -6,6 +6,7 @@ static int visible = 0; static void *activeowner; static int activecap; static Caret caret; +static u32int dictseq; static void dictqmap(Im*); static void dictqjp(Im*); static void backjp(Im*); @@ -34,6 +35,7 @@ int nlang = nelem(langs); static void clearkouho(void) { + dictseq++; im.nkouho = 0; im.sel = -1; } @@ -147,6 +149,7 @@ dictsend(Im *im, Str *key) req.key = *key; req.lang = im->l->lang; + req.seq = dictseq; if(isjp(im)) jpreading(im, &req.pre); else @@ -453,6 +456,7 @@ searchlookup(int lang, Str *key, Dictres *res) req.key = *key; req.pre = req.key; req.lang = lang; + req.seq = 0; dictlookup(&req, res); } @@ -769,6 +773,7 @@ init(void) memset(&caret, 0, sizeof caret); activeowner = nil; activecap = 0; + dictseq = 0; visible = 0; } @@ -833,7 +838,7 @@ dictresult(Dictres *res) Str pre; impre(&pre); - if(search.lang || res->lang != im.l->lang || + if(search.lang || res->seq != dictseq || res->lang != im.l->lang || scmp(&res->key, &pre) != 0) return; setkouho(res); diff --git a/tests/dict_test.c b/tests/dict_test.c index 4bf8639..9868242 100644 --- a/tests/dict_test.c +++ b/tests/dict_test.c @@ -22,10 +22,12 @@ dictionary_candidates(struct ct *t) req.key = key; req.pre = mkstr("preedit-one"); req.lang = LangJP; + req.seq = 17; dictlookup(&req, &res); if(!CT_EQ_INT(t, 2, res.nkouho)) goto cleanup; CT_EQ_INT(t, LangJP, res.lang); + CT_EQ_UINT(t, 17, res.seq); CT_EQ_INT(t, 0, scmp(&req.pre, &res.key)); checkstr(t, "candidate 1", "候補1", &res.kouho[0]); checkstr(t, "candidate 2", "候補2", &res.kouho[1]); @@ -42,10 +44,12 @@ dictionary_candidates(struct ct *t) hmapset(&lang->dict, &key, many, strlen(many)); req.key = key; req.pre = mkstr("preedit-two"); + req.seq = 29; dictlookup(&req, &res); if(!CT_EQ_INT(t, Maxkouho, res.nkouho)) goto cleanup; CT_EQ_INT(t, LangJP, res.lang); + CT_EQ_UINT(t, 29, res.seq); CT_EQ_INT(t, 0, scmp(&req.pre, &res.key)); checkstr(t, "first capped candidate", "c00", &res.kouho[0]); checkstr(t, "last capped candidate", "c31", &res.kouho[31]); @@ -79,11 +83,13 @@ dictionary_misses_clear_result(struct ct *t) req.key = mkstr(cases[i].key); req.pre = mkstr(cases[i].pre); req.lang = LangJP; + req.seq = 0xf00d0000U + i; dictlookup(&req, &res); if(res.nkouho != 0) CT_ERRORF(t, "%s: want 0 candidates, got %d", cases[i].name, res.nkouho); CT_EQ_INT(t, LangJP, res.lang); + CT_EQ_UINT(t, 0xf00d0000U + i, res.seq); checkstr(t, cases[i].name, cases[i].pre, &res.key); } hmapfree(lang->dict); @@ -108,6 +114,7 @@ dictionary_emoji_identity(struct ct *t) req.key = key; req.pre = key; req.lang = LangEMOJI; + req.seq = 0; dictlookup(&req, &res); if(CT_EQ_INT(t, 1, res.nkouho)) checkstr(t, "emoji identity candidate", "é", &res.kouho[0]); diff --git a/tests/engine_test.c b/tests/engine_test.c index 599aecb..d8cc725 100644 --- a/tests/engine_test.c +++ b/tests/engine_test.c @@ -175,6 +175,7 @@ engine_dictionary_queue_latest_wins(struct ct *t) old.key = mkstr("old-key"); old.pre = mkstr("old-preedit"); old.lang = LangJP; + old.seq = 0; CT_CHECK(t, channbsend(dictreqc, &old) > 0); init(); im.l = getlang(LangJP); @@ -185,6 +186,7 @@ engine_dictionary_queue_latest_wins(struct ct *t) checkstr(t, "newest dictionary key", "new-key", &got.key); checkstr(t, "newest dictionary preedit", "new-preedit", &got.pre); CT_EQ_INT(t, LangJP, got.lang); + CT_EQ_UINT(t, dictseq, got.seq); } CT_CHECK(t, channbrecv(dictreqc, &got) <= 0); chanfree(dictreqc); @@ -301,6 +303,66 @@ engine_active_owner_reset(struct ct *t) checkstr(t, "same owner after reset", "ん", &res.preedit); } +void +engine_rejects_stale_dictionary_results(struct ct *t) +{ + Dictreq a, b; + Dictres res; + Hmap *saved; + Lang *jp; + char one, two; + int n; + + jp = getlang(LangJP); + saved = jp->dict; + jp->dict = hmapalloc(1); + init(); + im.l = jp; + while(channbrecv(dictreqc, &a) > 0) + ; + ownerrequest(&one, Keypress, 'k', 0); + ownerrequest(&one, Keypress, 'a', 0); + n = 0; + while(channbrecv(dictreqc, &a) > 0) + n++; + if(!CT_CHECK(t, n > 0)) + goto cleanup; + ownerrequest(&one, Keyreset, 0, 0); + ownerrequest(&two, Keypress, 'k', 0); + ownerrequest(&two, Keypress, 'a', 0); + n = 0; + while(channbrecv(dictreqc, &b) > 0) + n++; + if(!CT_CHECK(t, n > 0)) + goto cleanup; + CT_EQ_INT(t, 0, scmp(&a.key, &b.key)); + CT_EQ_INT(t, 0, scmp(&a.pre, &b.pre)); + CT_EQ_INT(t, a.lang, b.lang); + CT_CHECK(t, a.seq != b.seq); + + memset(&res, 0, sizeof res); + res.key = a.pre; + res.lang = a.lang; + res.seq = a.seq; + res.kouho[0] = mkstr("stale"); + res.nkouho = 1; + dictresult(&res); + CT_EQ_INT(t, 0, im.nkouho); + + res.key = b.pre; + res.lang = b.lang; + res.seq = b.seq; + res.kouho[0] = mkstr("current"); + dictresult(&res); + if(CT_EQ_INT(t, 1, im.nkouho)) + checkstr(t, "current dictionary result", "current", &im.kouho[0]); +cleanup: + while(channbrecv(dictreqc, &a) > 0) + ; + hmapfree(jp->dict); + jp->dict = saved; +} + void engine_active_owner_caret(struct ct *t) { @@ -676,6 +738,7 @@ engine_popup_preedit_capability(struct ct *t) memset(&res, 0, sizeof res); res.lang = LangJP; res.key = mkstr("か"); + res.seq = dictseq; res.kouho[0] = mkstr("家"); res.nkouho = 1; dictresult(&res); @@ -693,6 +756,7 @@ engine_popup_preedit_capability(struct ct *t) memset(&res, 0, sizeof res); res.lang = LangJP; res.key = mkstr("ん"); + res.seq = dictseq; res.kouho[0] = mkstr("ン"); res.nkouho = 1; dictresult(&res); @@ -1267,6 +1331,7 @@ engine_emoji_dictionary_identity(struct ct *t) im.sel = 0; memset(&res, 0, sizeof res); res.key = im.pre; + res.seq = dictseq; res.kouho[0] = mkstr("wrong"); res.nkouho = 1; res.lang = LangJP; diff --git a/tests/test.h b/tests/test.h index e4f3fef..5dea606 100644 --- a/tests/test.h +++ b/tests/test.h @@ -35,6 +35,7 @@ void engine_candidate_shortcut_modifiers(struct ct*); void engine_dictionary_queue_latest_wins(struct ct*); void engine_active_owner_lifecycle(struct ct*); void engine_active_owner_reset(struct ct*); +void engine_rejects_stale_dictionary_results(struct ct*); void engine_active_owner_caret(struct ct*); void engine_popup_preedit_capability(struct ct*); void engine_commit_contract(struct ct*); diff --git a/tests/unit_test.c b/tests/unit_test.c index af37c1d..0e3d1ca 100644 --- a/tests/unit_test.c +++ b/tests/unit_test.c @@ -89,6 +89,7 @@ static const struct ct_test tests[] = { { "engine/dictionary-queue-latest", engine_dictionary_queue_latest_wins }, { "engine/active-owner-lifecycle", engine_active_owner_lifecycle }, { "engine/active-owner-reset", engine_active_owner_reset }, + { "engine/rejects-stale-dictionary", engine_rejects_stale_dictionary_results }, { "engine/active-owner-caret", engine_active_owner_caret }, { "engine/popup-preedit-capability", engine_popup_preedit_capability }, { "engine/commit-contract", engine_commit_contract },