From a841894015b3ada293fe5ab7c57d383d5c67e615 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 19:41:07 +0900 Subject: [PATCH] ibus: honor preedit and private-input policy --- ibus.c | 192 ++++++++++++++++++++++++++++++++++++++-- tests/ibus_live_test.c | 195 +++++++++++++++++++++++++++++++++++++++-- tests/ibus_test.c | 109 +++++++++++++++++++++-- tests/test.h | 2 + tests/unit_test.c | 2 + 5 files changed, 482 insertions(+), 18 deletions(-) diff --git a/ibus.c b/ibus.c index fff3588..99aac59 100644 --- a/ibus.c +++ b/ibus.c @@ -17,6 +17,11 @@ enum Maxcontexts = 64, Relmask = 1<<30, Ibussupermask = 1<<26, + /* IBus wire constants; the daemon does not link libibus. */ + Ibuscappreedit = 1<<0, + Ibuspurposepassword = 8, + Ibuspurposepin = 9, + Ibushinthidden = 1<<12, }; typedef struct Ictx Ictx; @@ -25,6 +30,9 @@ struct Ictx DBusConnection *conn; char path[64]; int focused; + u32int cap; + u32int purpose; + u32int hints; Caret caret; }; @@ -303,7 +311,7 @@ sendrequest(Ictx *ctx, int op, u32int ks, u32int mod, Keyres *res) memset(&kr, 0, sizeof kr); kr.owner = ctx; - kr.cap = Cclientpreedit; + kr.cap = ctx->cap & Ibuscappreedit ? Cclientpreedit : 0; kr.op = op; kr.ks = ks; kr.mod = mod; @@ -341,10 +349,18 @@ dropconncontexts(DBusConnection *conn) dropcontext(&contexts[i]); } +static int +hidden(Ictx *ctx) +{ + return ctx->purpose == Ibuspurposepassword || + ctx->purpose == Ibuspurposepin || + (ctx->hints & Ibushinthidden) != 0; +} + static int processkey(Ictx *ctx, u32int sym, u32int state, Keyres *res) { - if(state & Relmask || !ctx->focused) + if(state & Relmask || !ctx->focused || hidden(ctx)) return 0; sendrequest(ctx, Keypress, kget(sym), mget(state), res); return 1; @@ -522,7 +538,8 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx) stoutf(&res.preedit, preedit, sizeof preedit); if(commit[0] != '\0') emitcommit(c, dbus_message_get_path(m), commit); - emitpreedit(c, dbus_message_get_path(m), preedit); + if(ctx->cap & Ibuscappreedit) + emitpreedit(c, dbus_message_get_path(m), preedit); return handlebool(c, m, res.eaten); } @@ -562,6 +579,149 @@ handlefocusout(DBusConnection *c, DBusMessage *m, Ictx *ctx) return handlenoop(c, m); } +static DBusHandlerResult +handlecap(DBusConnection *c, DBusMessage *m, Ictx *ctx) +{ + DBusError err; + dbus_uint32_t cap; + Keyres res; + char preedit[Maxutf]; + u32int old; + + dbus_error_init(&err); + if(!dbus_message_get_args(m, &err, DBUS_TYPE_UINT32, &cap, + DBUS_TYPE_INVALID)){ + dbus_error_free(&err); + return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, + "SetCapabilities expects one unsigned integer"); + } + old = ctx->cap; + ctx->cap = cap; + if(ctx->focused){ + sendrequest(ctx, Keycap, 0, 0, &res); + if((old & Ibuscappreedit) != (ctx->cap & Ibuscappreedit)){ + if((ctx->cap & Ibuscappreedit) && res.eaten){ + stoutf(&res.preedit, preedit, sizeof preedit); + emitpreedit(c, ctx->path, preedit); + }else if(!(ctx->cap & Ibuscappreedit)) + emitpreedit(c, ctx->path, ""); + } + } + return handlenoop(c, m); +} + +static int +getcontent(DBusMessage *m, const char **iface, const char **name, + u32int *purpose, u32int *hints) +{ + DBusMessageIter it, v, st; + dbus_uint32_t p, h; + + if(!dbus_message_has_signature(m, "ssv") || + !dbus_message_iter_init(m, &it)) + return 0; + dbus_message_iter_get_basic(&it, iface); + dbus_message_iter_next(&it); + dbus_message_iter_get_basic(&it, name); + dbus_message_iter_next(&it); + dbus_message_iter_recurse(&it, &v); + if(dbus_message_iter_get_arg_type(&v) != DBUS_TYPE_STRUCT) + return 0; + dbus_message_iter_recurse(&v, &st); + if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_UINT32) + return 0; + dbus_message_iter_get_basic(&st, &p); + if(!dbus_message_iter_next(&st) || + dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_UINT32) + return 0; + dbus_message_iter_get_basic(&st, &h); + if(dbus_message_iter_next(&st)) + return 0; + *purpose = p; + *hints = h; + return 1; +} + +static DBusHandlerResult +handlepropertyset(DBusConnection *c, DBusMessage *m, Ictx *ctx) +{ + const char *iface, *name; + u32int purpose, hints; + Keyres res; + int washidden; + + if(!getcontent(m, &iface, &name, &purpose, &hints)) + return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, + "Properties.Set expects ContentType as (uu)"); + if(strcmp(iface, "org.freedesktop.IBus.InputContext") != 0) + return handleerror(c, m, DBUS_ERROR_UNKNOWN_INTERFACE, + "unknown property interface"); + if(strcmp(name, "ContentType") != 0) + return handleerror(c, m, DBUS_ERROR_UNKNOWN_PROPERTY, + "unknown input context property"); + washidden = hidden(ctx); + ctx->purpose = purpose; + ctx->hints = hints; + if(ctx->focused && !washidden && hidden(ctx)){ + sendrequest(ctx, Keyreset, 0, 0, &res); + emitpreedit(c, ctx->path, ""); + } + return handlenoop(c, m); +} + +static DBusHandlerResult +handlepropertyget(DBusConnection *c, DBusMessage *m, Ictx *ctx) +{ + DBusError err; + const char *iface, *name; + + USED(ctx); + dbus_error_init(&err); + if(!dbus_message_get_args(m, &err, + DBUS_TYPE_STRING, &iface, DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)){ + dbus_error_free(&err); + return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, + "Properties.Get expects interface and property names"); + } + if(strcmp(iface, "org.freedesktop.IBus.InputContext") != 0) + return handleerror(c, m, DBUS_ERROR_UNKNOWN_INTERFACE, + "unknown property interface"); + USED(name); + return handleerror(c, m, DBUS_ERROR_UNKNOWN_PROPERTY, + "input context properties are write-only"); +} + +static DBusHandlerResult +handlepropertygetall(DBusConnection *c, DBusMessage *m, Ictx *ctx) +{ + DBusMessage *r; + DBusMessageIter it, a; + DBusError err; + const char *iface; + + USED(ctx); + dbus_error_init(&err); + if(!dbus_message_get_args(m, &err, DBUS_TYPE_STRING, &iface, + DBUS_TYPE_INVALID)){ + dbus_error_free(&err); + return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, + "Properties.GetAll expects an interface name"); + } + if(strcmp(iface, "org.freedesktop.IBus.InputContext") != 0) + return handleerror(c, m, DBUS_ERROR_UNKNOWN_INTERFACE, + "unknown property interface"); + r = dbus_message_new_method_return(m); + if(r == nil) + return DBUS_HANDLER_RESULT_NEED_MEMORY; + dbus_message_iter_init_append(r, &it); + dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &a); + dbus_message_iter_close_container(&it, &a); + dbus_connection_send(c, r, nil); + dbus_message_unref(r); + return DBUS_HANDLER_RESULT_HANDLED; +} + static DBusHandlerResult handledestroy(DBusConnection *c, DBusMessage *m, Ictx *ctx) { @@ -617,10 +777,19 @@ static const char introspectxml[] = "" "\n" " \n" +" \n" " \n" " " "\n" " \n" +" \n" +" " +"\n" +" " +"\n" +" " +"\n" +" \n" "\n"; static DBusHandlerResult @@ -664,6 +833,18 @@ onmsg(DBusConnection *c, DBusMessage *m, void *_) if(strcmp(member, "CreateInputContext") == 0) return handlecreate(c, m); } + if(strcmp(iface, "org.freedesktop.DBus.Properties") == 0){ + ctx = findcontext(c, path); + if(ctx == nil) + return handleerror(c, m, DBUS_ERROR_UNKNOWN_OBJECT, + "unknown input context"); + if(strcmp(member, "Set") == 0) + return handlepropertyset(c, m, ctx); + if(strcmp(member, "Get") == 0) + return handlepropertyget(c, m, ctx); + if(strcmp(member, "GetAll") == 0) + return handlepropertygetall(c, m, ctx); + } if(strcmp(iface, "org.freedesktop.IBus.InputContext") == 0){ ctx = findcontext(c, path); if(ctx == nil) @@ -681,8 +862,9 @@ onmsg(DBusConnection *c, DBusMessage *m, void *_) return handledestroy(c, m, ctx); if(strcmp(member, "SetCursorLocation") == 0) return handlecursor(c, m, ctx); - if(strcmp(member, "SetCapabilities") == 0 - || strcmp(member, "SetEngine") == 0) + if(strcmp(member, "SetCapabilities") == 0) + return handlecap(c, m, ctx); + if(strcmp(member, "SetEngine") == 0) return handlenoop(c, m); } return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; diff --git a/tests/ibus_live_test.c b/tests/ibus_live_test.c index f3c6f29..de8503c 100644 --- a/tests/ibus_live_test.c +++ b/tests/ibus_live_test.c @@ -26,6 +26,12 @@ enum Stoptimeout = 3000, Relmask = 1<<30, Ctrlmask = 1<<2, + Cappreedit = 1<<0, + Purposefree = 0, + Purposepassword = 8, + Purposepin = 9, + Hintprivate = 1<<11, + Hinthidden = 1<<12, }; typedef struct Daemon Daemon; @@ -837,6 +843,66 @@ emptycall(DBusConnection *conn, Siglog *log, char *path, char *member) return 1; } +static int +capcall(DBusConnection *conn, Siglog *log, char *path, dbus_uint32_t cap) +{ + DBusMessage *m, *reply; + + resetsig(log); + m = method(path, "org.freedesktop.IBus.InputContext", "SetCapabilities"); + if(m == NULL || !dbus_message_append_args(m, DBUS_TYPE_UINT32, &cap, + DBUS_TYPE_INVALID)){ + if(m != NULL) dbus_message_unref(m); + return fail("build SetCapabilities call"); + } + reply = sendcall(conn, m); + if(reply == NULL) + return 0; + if(!dbus_message_has_signature(reply, "")){ + dbus_message_unref(reply); + return fail("SetCapabilities returned a nonempty reply"); + } + dbus_message_unref(reply); + return 1; +} + +static int +contentcall(DBusConnection *conn, Siglog *log, char *path, + dbus_uint32_t purpose, dbus_uint32_t hints) +{ + DBusMessage *m, *reply; + DBusMessageIter it, v, st; + const char *iface, *property; + + resetsig(log); + iface = "org.freedesktop.IBus.InputContext"; + property = "ContentType"; + m = method(path, "org.freedesktop.DBus.Properties", "Set"); + if(m == NULL) + return 0; + dbus_message_iter_init_append(m, &it); + if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &iface) || + !dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &property) || + !dbus_message_iter_open_container(&it, DBUS_TYPE_VARIANT, "(uu)", &v) || + !dbus_message_iter_open_container(&v, DBUS_TYPE_STRUCT, NULL, &st) || + !dbus_message_iter_append_basic(&st, DBUS_TYPE_UINT32, &purpose) || + !dbus_message_iter_append_basic(&st, DBUS_TYPE_UINT32, &hints) || + !dbus_message_iter_close_container(&v, &st) || + !dbus_message_iter_close_container(&it, &v)){ + dbus_message_unref(m); + return fail("build Properties.Set ContentType call"); + } + reply = sendcall(conn, m); + if(reply == NULL) + return 0; + if(!dbus_message_has_signature(reply, "")){ + dbus_message_unref(reply); + return fail("Properties.Set ContentType returned a nonempty reply"); + } + dbus_message_unref(reply); + return 1; +} + static int keycall(DBusConnection *conn, Siglog *log, char *path, dbus_uint32_t sym, dbus_uint32_t state, int *eaten) @@ -1037,18 +1103,18 @@ runconnectioncapacity(Daemon *d) !createcontext(conn[i], paths[i], sizeof paths[i]) || !emptycall(conn[i], &logs[i], paths[i], "FocusIn") || !nosignal(&logs[i], "connection capacity FocusIn") || - !expectkey(conn[i], &logs[i], paths[i], 0xffe1, 0, 0, "", + !expectkey(conn[i], &logs[i], paths[i], 0xffe1, 0, 0, NULL, "connection capacity key")) goto out; } if(!overflowrejected(d->address)) goto out; for(i = 0; i < Maxconnections; i++) - if(!expectkey(conn[i], &logs[i], paths[i], 0xffe1, 0, 0, "", + if(!expectkey(conn[i], &logs[i], paths[i], 0xffe1, 0, 0, NULL, "accepted connection after rejection")) goto out; closebus(&conn[Maxconnections-1], &logs[Maxconnections-1]); - if(!expectkey(conn[0], &logs[0], paths[0], 0xffe1, 0, 0, "", + if(!expectkey(conn[0], &logs[0], paths[0], 0xffe1, 0, 0, NULL, "connection close barrier")) goto out; conn[Maxconnections-1] = openbus(d->address, @@ -1061,6 +1127,9 @@ runconnectioncapacity(Daemon *d) !emptycall(conn[Maxconnections-1], &logs[Maxconnections-1], paths[Maxconnections-1], "FocusIn") || !nosignal(&logs[Maxconnections-1], "recovered connection FocusIn") || + !capcall(conn[Maxconnections-1], &logs[Maxconnections-1], + paths[Maxconnections-1], Cappreedit) || + !nosignal(&logs[Maxconnections-1], "recovered connection capability") || !expectkey(conn[Maxconnections-1], &logs[Maxconnections-1], paths[Maxconnections-1], 'n', Ctrlmask, 1, "", "recovered connection select Japanese") || @@ -1069,7 +1138,7 @@ runconnectioncapacity(Daemon *d) "recovered connection key")) goto out; for(i = 0; i < Maxconnections-1; i++) - if(!expectkey(conn[i], &logs[i], paths[i], 0xffe1, 0, 0, "", + if(!expectkey(conn[i], &logs[i], paths[i], 0xffe1, 0, 0, NULL, "accepted connection after recovery")) goto out; ok = 1; @@ -1107,9 +1176,11 @@ runlifecycle(Daemon *d) goto out; if(!expectkey(c1, &s1, a, 'x', 0, 0, NULL, "unfocused key")) goto out; - if(!cursorcall(c1, &s1, a, 10, 20, 0, 14) || + if(!cursorcall(c1, &s1, a, -10, -20, 0, 14) || !nosignal(&s1, "pre-focus cursor") || !emptycall(c1, &s1, a, "FocusIn") || !nosignal(&s1, "FocusIn") || + !capcall(c1, &s1, a, Cappreedit) || + !nosignal(&s1, "A capability") || !expectkey(c1, &s1, a, 'x', Relmask, 0, NULL, "physical release")) goto out; if(!expectkey(c1, &s1, a, 'n', Ctrlmask, 1, "", "select Japanese") || @@ -1117,6 +1188,8 @@ runlifecycle(Daemon *d) !expectkey(c1, &s1, a, 'a', 0, 1, "か", "A key a")) goto out; if(!emptycall(c2, &s2, b, "FocusIn") || !nosignal(&s2, "B FocusIn") || + !capcall(c2, &s2, b, Cappreedit) || + !nosignal(&s2, "inactive B capability") || !expectkey(c2, &s2, b, 'n', 0, 1, "ん", "B takeover") || !expectkey(c1, &s1, a, 0xffe1, 0, 0, "", "stale A state") || !expectkey(c2, &s2, b, 0xffe1, 0, 0, "ん", "active B state")) @@ -1138,6 +1211,8 @@ runlifecycle(Daemon *d) goto out; if(!createcontext(c1, a2, sizeof a2) || !emptycall(c1, &s1, a2, "FocusIn") || !nosignal(&s1, "A2 FocusIn") || + !capcall(c1, &s1, a2, Cappreedit) || + !nosignal(&s1, "A2 capability") || !cursorcall(c1, &s1, a2, 120, 130, 0, 18) || !nosignal(&s1, "A2 stale cursor")) goto out; @@ -1150,7 +1225,7 @@ runlifecycle(Daemon *d) !expectkey(c2, &s2, b, 'k', 0, 1, "k", "reset owner key k") || !expectkey(c2, &s2, b, 'a', 0, 1, "か", "reset owner key a")) goto out; - if(!keycall(c2, &s2, b, 0xff0d, 0, &eaten) || !eaten || + if(!keycall(c2, &s2, b, '0', 0, &eaten) || !eaten || !committed(&s2, b, "か", "active commit")) goto out; if(!expectkey(c2, &s2, b, 'k', 0, 1, "k", "pre-FocusOut key") || @@ -1173,6 +1248,8 @@ runlifecycle(Daemon *d) goto out; if(!emptycall(c2, &s2, active, "FocusIn") || !nosignal(&s2, "capacity owner FocusIn") || + !capcall(c2, &s2, active, Cappreedit) || + !nosignal(&s2, "capacity owner capability") || !expectkey(c2, &s2, active, 'k', 0, 1, "k", "capacity owner key")) goto out; closebus(&c2, &s2); @@ -1188,6 +1265,8 @@ runlifecycle(Daemon *d) if(!limitscall(c3) || !emptycall(c3, &s3, first, "FocusIn") || !nosignal(&s3, "reused context FocusIn") || + !capcall(c3, &s3, first, Cappreedit) || + !nosignal(&s3, "reused context capability") || !expectkey(c3, &s3, first, 'k', 0, 1, "k", "reused context key")) goto out; ok = 1; @@ -1198,6 +1277,106 @@ out: return ok; } +static int +runpolicy(Daemon *d) +{ + DBusConnection *c1, *c2; + Siglog s1, s2; + char n1[32], n2[32], a[96], b[96], typed[16]; + char *query; + int eaten, i, ok; + + c1 = c2 = NULL; + memset(&s1, 0, sizeof s1); + memset(&s2, 0, sizeof s2); + ok = 0; + c1 = openbus(d->address, &s1); + c2 = openbus(d->address, &s2); + if(c1 == NULL || c2 == NULL || + !hello(c1, n1, sizeof n1) || !hello(c2, n2, sizeof n2) || + !createcontext(c1, a, sizeof a) || !createcontext(c2, b, sizeof b) || + !emptycall(c1, &s1, a, "FocusIn") || !nosignal(&s1, "policy FocusIn")) + goto out; + + /* Capability starts at zero; only advertised bit zero affects routing. */ + if(!expectkey(c1, &s1, a, 'n', Ctrlmask, 1, NULL, + "zero-capability language") || + !expectkey(c1, &s1, a, 'k', 0, 1, NULL, "zero-capability key") || + !capcall(c1, &s1, a, 1<<1) || !nosignal(&s1, "non-preedit cap") || + !capcall(c1, &s1, a, Cappreedit|(1<<1)) || + !preedit(&s1, a, "k", 1, 1, "enable inline") || + !capcall(c1, &s1, a, Cappreedit|(1<<1)) || + !nosignal(&s1, "repeat inline capability") || + !capcall(c1, &s1, a, 1<<1) || + !preedit(&s1, a, "", 0, 0, "disable inline") || + !capcall(c1, &s1, a, 1<<1) || + !nosignal(&s1, "repeat popup capability") || + !expectkey(c1, &s1, a, 'a', 0, 1, NULL, "popup-preedit key") || + !capcall(c1, &s1, a, Cappreedit|(1<<1)) || + !preedit(&s1, a, "か", 1, 1, "restore inline")) + goto out; + + /* A focused but inactive context cannot acquire ownership through Keycap. */ + if(!emptycall(c2, &s2, b, "FocusIn") || !nosignal(&s2, "inactive FocusIn") || + !capcall(c2, &s2, b, Cappreedit) || + !nosignal(&s2, "inactive capability") || + !expectkey(c1, &s1, a, 0xffe1, 0, 0, "か", "owner after inactive cap")) + goto out; + + /* Toggling inline display leaves the engine-owned candidate list intact. */ + if(!emptycall(c1, &s1, a, "Reset") || + !preedit(&s1, a, "", 0, 0, "candidate reset") || + !expectkey(c1, &s1, a, 'e', Ctrlmask, 1, "", "begin emoji search")) + goto out; + query = "smile"; + typed[0] = '\0'; + for(i = 0; query[i] != '\0'; i++){ + typed[i] = query[i]; + typed[i+1] = '\0'; + if(!keycall(c1, &s1, a, query[i], 0, &eaten) || !eaten || + !preedit(&s1, a, typed, i+1, 1, "emoji query")) + goto out; + } + if(!capcall(c1, &s1, a, 1<<1) || + !preedit(&s1, a, "", 0, 0, "candidate popup policy") || + !capcall(c1, &s1, a, Cappreedit|(1<<1)) || + !preedit(&s1, a, "smile", 5, 1, "candidate inline policy") || + !keycall(c1, &s1, a, '1', 0, &eaten) || !eaten || + !committed(&s1, a, "😀", "candidate after capability toggle")) + goto out; + + /* PRIVATE preserves composition; hidden purposes and hints reset it. */ + if(!expectkey(c1, &s1, a, 'n', Ctrlmask, 1, "", "private select Japanese") || + !expectkey(c1, &s1, a, 'n', 0, 1, "ん", "private preedit") || + !contentcall(c1, &s1, a, Purposefree, Hintprivate) || + !nosignal(&s1, "PRIVATE property") || + !expectkey(c1, &s1, a, 'a', 0, 1, "な", "PRIVATE key") || + !contentcall(c1, &s1, a, Purposepassword, 0) || + !preedit(&s1, a, "", 0, 0, "PASSWORD transition") || + !expectkey(c1, &s1, a, 'x', 0, 0, NULL, "PASSWORD key") || + !contentcall(c1, &s1, a, Purposefree, 0) || + !nosignal(&s1, "leave PASSWORD") || + !expectkey(c1, &s1, a, 'k', 0, 1, "k", "after PASSWORD") || + !contentcall(c1, &s1, a, Purposepin, 0) || + !preedit(&s1, a, "", 0, 0, "PIN transition") || + !expectkey(c1, &s1, a, 'x', 0, 0, NULL, "PIN key") || + !contentcall(c1, &s1, a, Purposefree, Hintprivate) || + !nosignal(&s1, "leave PIN with PRIVATE") || + !expectkey(c1, &s1, a, 'n', 0, 1, "ん", "PRIVATE resumes") || + !contentcall(c1, &s1, a, Purposefree, Hinthidden) || + !preedit(&s1, a, "", 0, 0, "HIDDEN_TEXT transition") || + !expectkey(c1, &s1, a, 'x', 0, 0, NULL, "HIDDEN_TEXT key") || + !contentcall(c1, &s1, a, Purposefree, 0) || + !nosignal(&s1, "free-form property") || + !expectkey(c1, &s1, a, 'n', 0, 1, "ん", "free-form resumes")) + goto out; + ok = 1; +out: + closebus(&c1, &s1); + closebus(&c2, &s2); + return ok; +} + int main(int argc, char **argv) { @@ -1213,12 +1392,14 @@ main(int argc, char **argv) ok = runconnectioncapacity(&daemon); if(ok) ok = runlifecycle(&daemon); + if(ok) + ok = runpolicy(&daemon); if(!ok) showerrors(&daemon); if(!stopdaemon(&daemon)) ok = 0; if(!ok) return 1; - printf("ibus live lifecycle and connection capacity: ok\n"); + printf("ibus live lifecycle, policy, and connection capacity: ok\n"); return 0; } diff --git a/tests/ibus_test.c b/tests/ibus_test.c index a8a6128..bf49d5e 100644 --- a/tests/ibus_test.c +++ b/tests/ibus_test.c @@ -238,7 +238,8 @@ nexttrace(struct ct *t, Ibusfix *f, int op, Ictx *ctx) return req; CT_EQ_INT(t, op, req.op); CT_EQ_PTR(t, ctx, req.owner); - CT_EQ_INT(t, Cclientpreedit, req.cap); + if(ctx != nil) + CT_EQ_INT(t, ctx->cap & 1 ? Cclientpreedit : 0, req.cap); CT_EQ_PTR(t, replyc, req.reply); return req; } @@ -282,6 +283,102 @@ contextkey(struct ct *t, Ibusfix *f, Ictx *ctx, u32int sym, u32int state) return res; } +void +ibus_capability_policy(struct ct *t) +{ + Ibusfix f; + Ictx *a, *b; + Keyres res; + + if(!ibusbegin(t, &f)) + goto cleanup; + a = newcontext(f.c1, "/context/cap-a"); + b = newcontext(f.c2, "/context/cap-b"); + if(!CT_CHECK(t, a != nil && b != nil)) + goto cleanup; + CT_EQ_INT(t, 0, a->cap); + a->focused = 1; + res = contextkey(t, &f, a, 'n', Testctrlmask); + CT_CHECK(t, res.eaten); + res = contextkey(t, &f, a, 'k', 0); + checkpreedit(t, "k", &res); + CT_EQ_PTR(t, a, testengineowner()); + + a->cap = Cclientpreedit; + memset(&res, 0, sizeof res); + sendrequest(a, Keycap, 0, 0, &res); + nexttrace(t, &f, Keycap, a); + CT_CHECK(t, res.eaten); + checkpreedit(t, "k", &res); + + b->focused = 1; + b->cap = Cclientpreedit; + memset(&res, 0, sizeof res); + sendrequest(b, Keycap, 0, 0, &res); + nexttrace(t, &f, Keycap, b); + CT_CHECK(t, !res.eaten); + CT_EQ_PTR(t, a, testengineowner()); + checkenginepreedit(t, "k"); + + a->cap = 0; + memset(&res, 0, sizeof res); + sendrequest(a, Keycap, 0, 0, &res); + nexttrace(t, &f, Keycap, a); + CT_CHECK(t, res.eaten); + checkpreedit(t, "k", &res); +cleanup: + ibusend(&f); +} + +void +ibus_private_input_policy(struct ct *t) +{ + static const struct { u32int purpose, hints; int want; } cases[] = { + { 0, 0, 0 }, + { Ibuspurposepassword, 0, 1 }, + { Ibuspurposepin, 0, 1 }, + { 0, 1<<11, 0 }, + { 0, Ibushinthidden, 1 }, + { 37, 1<<20, 0 }, + }; + Ibusfix f; + Ictx *ctx; + Keyres res; + int i; + + if(!ibusbegin(t, &f)) + goto cleanup; + ctx = newcontext(f.c1, "/context/private"); + if(!CT_CHECK(t, ctx != nil)) + goto cleanup; + for(i = 0; i < nelem(cases); i++){ + ctx->purpose = cases[i].purpose; + ctx->hints = cases[i].hints; + CT_EQ_INT(t, cases[i].want, hidden(ctx)); + } + + ctx->focused = 1; + ctx->cap = Cclientpreedit; + ctx->purpose = 0; + ctx->hints = 0; + contextkey(t, &f, ctx, 'n', Testctrlmask); + res = contextkey(t, &f, ctx, 'k', 0); + checkpreedit(t, "k", &res); + ctx->purpose = Ibuspurposepassword; + memset(&res, 0, sizeof res); + CT_CHECK(t, !processkey(ctx, 'x', 0, &res)); + CT_CHECK(t, !res.eaten); + notrace(t, &f); + checkenginepreedit(t, "k"); + + ctx->purpose = 0; + ctx->hints = 1<<11; + res = contextkey(t, &f, ctx, 'a', 0); + checkpreedit(t, "か", &res); +cleanup: + ibusend(&f); +} + void ibus_context_lifecycle(struct ct *t) { @@ -307,7 +404,7 @@ ibus_context_lifecycle(struct ct *t) notrace(t, &f); CT_EQ_PTR(t, nil, testengineowner()); - setcursor(a, 10, 20, 14); + setcursor(a, -10, -20, 14); notrace(t, &f); CT_CHECK(t, a->caret.valid); a->focused = 1; @@ -322,14 +419,14 @@ ibus_context_lifecycle(struct ct *t) req = nexttrace(t, &f, Keypress, a); CT_CHECK(t, !res.eaten); CT_CHECK(t, req.caret.valid); - CT_EQ_INT(t, 10, req.caret.x); - CT_EQ_INT(t, 20, req.caret.y); + CT_EQ_INT(t, -10, req.caret.x); + CT_EQ_INT(t, -20, req.caret.y); CT_EQ_INT(t, 14, req.caret.h); CT_EQ_PTR(t, a, testengineowner()); testenginecaret(&at); CT_CHECK(t, at.valid); - CT_EQ_INT(t, 10, at.x); - CT_EQ_INT(t, 20, at.y); + CT_EQ_INT(t, -10, at.x); + CT_EQ_INT(t, -20, at.y); res = contextkey(t, &f, a, 'n', Testctrlmask); CT_CHECK(t, res.eaten); contextkey(t, &f, a, 'k', 0); diff --git a/tests/test.h b/tests/test.h index 6ce3928..59190b0 100644 --- a/tests/test.h +++ b/tests/test.h @@ -82,6 +82,8 @@ void server_extension_stream(struct ct*); void server_rejects_unknown_extension(struct ct*); void ibus_machine_id_fallback(struct ct*); void ibus_startup_requires_ownership(struct ct*); +void ibus_capability_policy(struct ct*); +void ibus_private_input_policy(struct ct*); void ibus_context_lifecycle(struct ct*); void ibus_active_release_lifecycle(struct ct*); void xim_adapter_key_contract(struct ct*); diff --git a/tests/unit_test.c b/tests/unit_test.c index d71d3a9..76f057b 100644 --- a/tests/unit_test.c +++ b/tests/unit_test.c @@ -136,6 +136,8 @@ static const struct ct_test tests[] = { { "server/rejects-unknown-extension", server_rejects_unknown_extension }, { "ibus/machine-id-fallback", ibus_machine_id_fallback }, { "ibus/startup-requires-ownership", ibus_startup_requires_ownership }, + { "ibus/capability-policy", ibus_capability_policy }, + { "ibus/private-input-policy", ibus_private_input_policy }, { "ibus/context-lifecycle", ibus_context_lifecycle }, { "ibus/active-release-lifecycle", ibus_active_release_lifecycle }, { "xim/adapter-key-contract", xim_adapter_key_contract },