ipc, srv, gtk: the client's text goes over and a take-back comes back

The engine can reach a Hanja reading back into the text the client
already holds, but only if the frontend hands that text over and can
take some of it away again.  GTK 3 has both: retrieve-surrounding
brings the text around the cursor and delete-surrounding removes runes
before it, and a widget that answers neither leaves the text empty, so
nothing is ever reached into or taken from it.

The wire grows a control frame for the text, sent like the caret only
when it changes, and one byte in every response for the runes to take
back.  That byte moves the length fields along, so the version goes to
2: an old daemon and a new module, either way round, fail the handshake
and the module falls through to GtkIMContextSimple rather than misread
a frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 14:31:21 +09:00
parent f7277f54a3
commit 910bf51347
11 changed files with 286 additions and 41 deletions

View File

@@ -297,8 +297,8 @@ rejectold(Test *t, char *where)
static int
openreplacement(Test *t)
{
unsigned char selectjp[] = {1, 0, 0, 0, 0};
unsigned char keyk[] = {1, 0, 0, 1, 0, 'k'};
unsigned char selectjp[] = {1, 0, 0, 0, 0, 0};
unsigned char keyk[] = {1, 0, 0, 0, 1, 0, 'k'};
char path[96];
t->ipcnew = connectsocket(t->l.socket, nowms() + Calltimeout);

View File

@@ -32,6 +32,7 @@ enum
Ecaret,
Ekey,
Ereset,
Esurround,
};
enum
@@ -55,6 +56,7 @@ struct Event
int32_t x;
int32_t y;
int32_t h;
char text[Ipcfieldmax+1];
};
struct Server
@@ -69,6 +71,7 @@ struct Server
int eaten;
int stall;
int response;
int del;
int nclose;
char pre[Ipcfieldmax+1];
Event ev[Maxevent];
@@ -83,6 +86,10 @@ struct Siglog
char shown[Ipcfieldmax+1];
char commit[Ipcfieldmax+1];
int prechanged;
char around[64]; /* what the widget answers retrieve-surrounding */
int aroundcursor;
int deloffset;
int deln;
};
static void
@@ -110,13 +117,14 @@ sendresponse(Server *s, int fd, int eaten, int want, int key)
unsigned char buf[Ipcmaxresp];
const char *commit;
char pre[Ipcfieldmax+1];
int n, response, stall;
int del, n, response, stall;
size_t ncommit, npreedit;
pthread_mutex_lock(&s->lock);
memcpy(pre, s->pre, sizeof pre);
response = s->response;
stall = s->stall;
del = key ? s->del : 0;
pthread_mutex_unlock(&s->lock);
if(stall)
return 1;
@@ -135,7 +143,7 @@ sendresponse(Server *s, int fd, int eaten, int want, int key)
pre[2] = 'y';
npreedit = 3;
}
n = ipcpackresp(buf, sizeof buf, eaten, commit, ncommit,
n = ipcpackresp(buf, sizeof buf, eaten, del, commit, ncommit,
pre, npreedit, want);
return n >= 0 && ipcsend(fd, buf, n) == 0;
}
@@ -146,6 +154,7 @@ request(Server *s, int fd)
unsigned char buf[Ipccaretsz];
uint32_t mod, key;
Event e;
size_t n;
int old, type, want;
if(ipcreadn(fd, buf, Ipcreqsz) < 0)
@@ -168,6 +177,14 @@ request(Server *s, int fd)
e.type = Ecaret;
record(s, &e);
return 1;
case Ipcsurround:
n = ipcsurroundlen(buf);
if(n > 0 && ipcreadn(fd, e.text, n) < 0)
return 0;
e.text[n] = '\0';
e.type = Esurround;
record(s, &e);
return 1;
case Ipckey:
ipcunpackreq(buf, &want, &mod, &key);
e.want = want;
@@ -513,6 +530,25 @@ clearlog(Siglog *l)
memset(l, 0, sizeof *l);
}
/* The widget's half of the surrounding-text contract. */
static gboolean
retrievearound(GtkIMContext *ctx, Siglog *l)
{
if(l->around[0] == '\0')
return FALSE;
gtk_im_context_set_surrounding(ctx, l->around, -1, l->aroundcursor);
return TRUE;
}
static gboolean
deletearound(GtkIMContext *ctx, gint offset, gint n, Siglog *l)
{
(void)ctx;
l->deloffset = offset;
l->deln = n;
return TRUE;
}
static GtkIMContext*
newcontext(Siglog *l)
{
@@ -520,6 +556,8 @@ newcontext(Siglog *l)
ctx = gtk_im_multicontext_new();
gtk_im_multicontext_set_context_id(GTK_IM_MULTICONTEXT(ctx), "strans");
g_signal_connect(ctx, "retrieve-surrounding", G_CALLBACK(retrievearound), l);
g_signal_connect(ctx, "delete-surrounding", G_CALLBACK(deletearound), l);
g_signal_connect(ctx, "preedit-start", G_CALLBACK(prestart), l);
g_signal_connect(ctx, "preedit-changed", G_CALLBACK(prechange), l);
g_signal_connect(ctx, "preedit-end", G_CALLBACK(preend), l);
@@ -667,7 +705,7 @@ main(int argc, char **argv)
Check(eventcount(&srv) == 0, "cursor reporting opened the socket");
setpre(&srv, "");
Check(key(ctx, win, GDK_KEY_a), "initial key was not eaten");
Check(waitcount(&srv, 3), "initial protocol frames timed out");
Check(waitcount(&srv, 4), "initial protocol frames timed out");
e = getevent(&srv, 0);
Check(e.type == Ecap && e.want == 1, "default preedit capability missing");
e = getevent(&srv, 1);
@@ -682,6 +720,9 @@ main(int argc, char **argv)
e.x, e.y, e.h, (ox + rect.x) * scale,
(oy + rect.y) * scale, rect.height * scale);
e = getevent(&srv, 2);
Check(e.type == Esurround && e.text[0] == '\0',
"a widget that keeps no text sent some");
e = getevent(&srv, 3);
Check(e.type == Ekey && e.want == 1, "initial key framing changed");
Check(log.n == 0, "initial empty preedit emitted %s", log.event);
setpre(&srv, "");
@@ -804,6 +845,38 @@ main(int argc, char **argv)
"visible-entry key did not reach the daemon");
gtk_im_context_set_client_window(ctx, win);
/* The widget's own text goes over, and a take-back comes back. */
setpre(&srv, "");
clearlog(&log);
snprintf(log.around, sizeof log.around, "%s", "가나한");
log.aroundcursor = 9;
first = eventcount(&srv);
Check(key(ctx, win, GDK_KEY_j), "surrounding-text key was not eaten");
Check(waitcount(&srv, first + 2), "surrounding text did not reach the daemon");
e = getevent(&srv, first);
Check(e.type == Esurround && strcmp(e.text, "가나한") == 0,
"the daemon was sent %s, not the text before the cursor", e.text);
Check(getevent(&srv, first + 1).type == Ekey,
"the text did not ride ahead of the key");
/* Sent again only when it changes. */
first = eventcount(&srv);
Check(key(ctx, win, GDK_KEY_j), "second surrounding-text key was not eaten");
Check(waitcount(&srv, first + 1) && getevent(&srv, first).type == Ekey,
"unchanged surrounding text was sent again");
setreply(&srv, 1, Rnormal, 0);
pthread_mutex_lock(&srv.lock);
srv.del = 2;
pthread_mutex_unlock(&srv.lock);
log.deln = 0;
Check(key(ctx, win, GDK_KEY_j), "take-back key was not eaten");
Check(log.deloffset == -2 && log.deln == 2,
"the widget was told to take back %d runes at %d", log.deln,
log.deloffset);
pthread_mutex_lock(&srv.lock);
srv.del = 0;
pthread_mutex_unlock(&srv.lock);
log.around[0] = '\0';
setpre(&srv, "reset");
clearlog(&log);
Check(key(ctx, win, GDK_KEY_e), "reset setup key was not eaten");

View File

@@ -23,6 +23,7 @@ typedef struct Response Response;
struct Response
{
int eaten;
int del;
char commit[Ipcfieldmax+1];
char preedit[Ipcfieldmax+1];
};
@@ -50,7 +51,8 @@ readresponseuntil(int fd, int want, Response *res, int64_t deadline,
return rv;
}
res->eaten = hdr[0] != 0;
n = getlen(hdr + 1);
res->del = hdr[1];
n = getlen(hdr + 2);
if(n > Ipcfieldmax){
fail("invalid commit length %zu", n);
return -1;
@@ -119,6 +121,18 @@ sendcap(int fd, int cap)
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendsurround(int fd, char *text)
{
unsigned char req[Ipcreqsz];
size_t n;
n = strlen(text);
ipcpacksurround(req, n);
return ipcsend(fd, req, sizeof req) == 0 &&
(n == 0 || ipcsend(fd, text, n) == 0);
}
static int
expectresponse(int fd, int want, int eaten, char *commit, char *preedit,
char *where)
@@ -152,6 +166,33 @@ requestkey(int fd, int want, uint32_t mod, uint32_t key, int eaten,
return expectresponse(fd, want, eaten, commit, preedit, where);
}
/*
* The Korean habit: 한자 typed, then the Hanja key. The 한 is the
* client's by then, so the daemon must ask for it back and answer with
* the whole word.
*/
static int
requesthanja(int fd)
{
Response res;
if(!sendsurround(fd, "저는 한"))
return fail("send surrounding text: %s", strerror(errno));
if(!requestkey(fd, 1, Mctrl, 's', 1, "", "", "select Korean") ||
!requestkey(fd, 1, 0, 'w', 1, "", "", "jamo") ||
!requestkey(fd, 1, 0, 'k', 1, "", "", "syllable") ||
!requestkey(fd, 1, Mctrl, 'h', 1, "", "", "Hanja search"))
return 0;
if(!sendkey(fd, 1, 0, Kret))
return fail("send Hanja pick: %s", strerror(errno));
if(readresponseuntil(fd, 1, &res, nowms() + Calltimeout, 0) != 1)
return 0;
if(strcmp(res.commit, "漢字") != 0 || res.del != 1)
return fail("Hanja pick committed %s and took back %d",
res.commit, res.del);
return 1;
}
static int
tryclient(char *path, int64_t deadline, int *client)
{
@@ -258,7 +299,8 @@ runsmoke(char *path)
requestkey(client, 1, Mctrl, 'n', 1, "", "",
"select Japanese") &&
requestkey(client, 1, 0, 'k', 1, "", "k", "preedit") &&
requestreset(client, 1, 1, "k", "", "reset");
requestreset(client, 1, 1, "k", "", "reset") &&
requesthanja(client);
close(client);
return ok;
}

View File

@@ -177,34 +177,37 @@ ipc_runtime_path(struct ct *t)
void
ipc_response_pack_boundaries(struct ct *t)
{
static const uchar withpre[] = { 1, 1, 0, 'A', 2, 0, 'x', 'y' };
static const uchar withoutpre[] = { 1, 1, 0, 'A' };
static const uchar withpre[] = { 1, 3, 1, 0, 'A', 2, 0, 'x', 'y' };
static const uchar withoutpre[] = { 1, 0, 1, 0, 'A' };
uchar out[Ipcmaxresp+1], field[Ipcfieldmax];
int n;
n = ipcpackresp(out, sizeof out, 7, "A", 1, "xy", 2, 1);
n = ipcpackresp(out, sizeof out, 7, 3, "A", 1, "xy", 2, 1);
CT_EQ_INT(t, sizeof withpre, n);
CT_EQ_MEM(t, withpre, out, sizeof withpre);
n = ipcpackresp(out, sizeof out, 1, "A", 1, "xy", 2, 0);
n = ipcpackresp(out, sizeof out, 1, 0, "A", 1, "xy", 2, 0);
CT_EQ_INT(t, sizeof withoutpre, n);
CT_EQ_MEM(t, withoutpre, out, sizeof withoutpre);
n = ipcpackresp(out, sizeof out, 0, nil, 0, nil, 0, 1);
n = ipcpackresp(out, sizeof out, 0, 0, nil, 0, nil, 0, 1);
CT_EQ_INT(t, Ipcresphdrsz + Ipclensz, n);
memset(field, 'x', sizeof field);
memset(out, 0xa5, sizeof out);
n = ipcpackresp(out, Ipcmaxresp, 1,
n = ipcpackresp(out, Ipcmaxresp, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1);
CT_EQ_INT(t, Ipcmaxresp, n);
CT_EQ_INT(t, 0, out[1]);
CT_EQ_INT(t, 1, out[2]);
CT_EQ_INT(t, 0, out[3+Ipcfieldmax]);
CT_EQ_INT(t, 1, out[4+Ipcfieldmax]);
CT_EQ_INT(t, 0, out[2]);
CT_EQ_INT(t, 1, out[3]);
CT_EQ_INT(t, 0, out[4+Ipcfieldmax]);
CT_EQ_INT(t, 1, out[5+Ipcfieldmax]);
CT_EQ_INT(t, 0xa5, out[Ipcmaxresp]);
CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1,
CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0,
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 0,
(char*)field, Ipcfieldmax+1, "", 0, 0));
/* A take-back is a rune count and never leaves its byte. */
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, -1, "", 0, "", 0, 0));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 256, "", 0, "", 0, 0));
}
void
@@ -215,8 +218,8 @@ ipc_response_empty_and_preedit(struct ct *t)
Ipcresp resp;
int fd[2], nfirst, nsecond;
nfirst = ipcpackresp(first, sizeof first, 0, "", 0, "", 0, 0);
nsecond = ipcpackresp(second, sizeof second, 1,
nfirst = ipcpackresp(first, sizeof first, 0, 0, "", 0, "", 0, 0);
nsecond = ipcpackresp(second, sizeof second, 1, 0,
"go", 2, "kana", 4, 1);
if(!CT_CHECK(t, nfirst > 0 && nsecond > 0))
return;
@@ -252,9 +255,9 @@ ipc_response_max_and_drain(struct ct *t)
int fd[2], nfirst, nsecond;
memset(field, 'x', sizeof field);
nfirst = ipcpackresp(first, sizeof first, 1,
nfirst = ipcpackresp(first, sizeof first, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1);
nsecond = ipcpackresp(second, sizeof second, 0, "ok", 2, "", 0, 0);
nsecond = ipcpackresp(second, sizeof second, 0, 0, "ok", 2, "", 0, 0);
if(!CT_CHECK(t, nfirst == Ipcmaxresp && nsecond > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
@@ -285,7 +288,7 @@ ipc_response_fragmented_and_truncated(struct ct *t)
Ipcresp resp;
int fd[2], i, n;
n = ipcpackresp(frame, sizeof frame, 1, "abc", 3, "xy", 2, 1);
n = ipcpackresp(frame, sizeof frame, 1, 2, "abc", 3, "xy", 2, 1);
if(!CT_CHECK(t, n > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){

View File

@@ -767,7 +767,8 @@ ipcrequest(int fd, uint32_t mod, uint32_t key, unsigned char *want,
int
ipcprobe(int fd, char *where)
{
unsigned char empty[] = {0, 0, 0, 0, 0};
/* eaten, take-back, no commit, no preedit */
unsigned char empty[] = {0, 0, 0, 0, 0, 0};
return ipcrequest(fd, 0, 0, empty, sizeof empty, where);
}