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>
447 lines
12 KiB
C
447 lines
12 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdlib.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include "dat.h"
|
|
#include "fn.h"
|
|
#include "test.h"
|
|
|
|
#undef accept
|
|
#undef listen
|
|
#undef send
|
|
|
|
static int64_t
|
|
nowms(void)
|
|
{
|
|
struct timespec ts;
|
|
|
|
if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0)
|
|
return 0;
|
|
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
|
}
|
|
|
|
static void ipcclientdeadlines(struct ct*);
|
|
static void ipcconnectcloexec(struct ct*);
|
|
|
|
void
|
|
ipc_masks_modifiers(struct ct *t)
|
|
{
|
|
uchar buf[Ipcreqsz];
|
|
u32int key, mod;
|
|
int want;
|
|
|
|
ipcpackreq(buf, 1, Mctrl|Malt|(1<<1)|(1<<4)|(1<<7), 0x1f642);
|
|
ipcunpackreq(buf, &want, &mod, &key);
|
|
CT_EQ_INT(t, 1, want);
|
|
CT_EQ_UINT(t, Mctrl|Malt, mod);
|
|
CT_EQ_UINT(t, 0x1f642, key);
|
|
CT_CHECK(t, !ipcreqreset(buf));
|
|
|
|
ipcpackreset(buf, 1);
|
|
ipcunpackreq(buf, &want, &mod, &key);
|
|
CT_EQ_INT(t, 1, want);
|
|
CT_EQ_UINT(t, 0, mod);
|
|
CT_EQ_UINT(t, 0, key);
|
|
CT_CHECK(t, ipcreqreset(buf));
|
|
|
|
/* Frontends share one keysym and modifier mapping. */
|
|
CT_EQ_UINT(t, 'a', ipckeysym('a', 'a'));
|
|
CT_EQ_UINT(t, '1', ipckeysym(0xffb1, '1')); /* KP_1 */
|
|
CT_EQ_UINT(t, Kret, ipckeysym(0xff0d, '\r'));
|
|
CT_EQ_UINT(t, Kret, ipckeysym(0xff8d, '\r')); /* KP_Enter */
|
|
CT_EQ_UINT(t, Ktab, ipckeysym(0xfe20, 0)); /* ISO_Left_Tab */
|
|
CT_EQ_UINT(t, Kdown, ipckeysym(0xff99, 0)); /* KP_Down */
|
|
CT_EQ_UINT(t, Kback, ipckeysym(0xff08, 8));
|
|
CT_EQ_UINT(t, Kspec + 0xff, ipckeysym(0xffff, 0x7f)); /* Delete */
|
|
CT_EQ_UINT(t, 0x1f642, ipckeysym(0x101f642, 0x1f642));
|
|
CT_EQ_UINT(t, 0, ipckeysym(0xfe03, 0)); /* ISO_Level3_Shift */
|
|
CT_EQ_UINT(t, 0, ipckeysym(0xffe1, 0)); /* Shift_L */
|
|
CT_EQ_UINT(t, Mshift|Mctrl, ipcmod(Mshift|Mctrl|(1<<1)|(1<<4)));
|
|
CT_EQ_UINT(t, Msuper, ipcmod(1<<6));
|
|
CT_EQ_UINT(t, Msuper, ipcmod(1<<26));
|
|
}
|
|
|
|
void
|
|
ipc_control_and_caret_frames(struct ct *t)
|
|
{
|
|
static const uchar capon[Ipcreqsz] = {
|
|
Ipcext|Ipcreqwant, Ipcversion, Ipcopcap, 0, 0, 0,
|
|
};
|
|
static const uchar capoff[Ipcreqsz] = {
|
|
Ipcext, Ipcversion, Ipcopcap, 0, 0, 0,
|
|
};
|
|
static const uchar caret[Ipccaretsz] = {
|
|
Ipcext, Ipcversion, Ipcopcaret, 1,
|
|
0xfe, 0xff, 0xff, 0xff,
|
|
0x04, 0x03, 0x02, 0x01,
|
|
0x06, 0x05, 0x00, 0x00,
|
|
};
|
|
static const uchar nocaret[Ipccaretsz] = {
|
|
Ipcext, Ipcversion, Ipcopcaret, 0,
|
|
};
|
|
uchar buf[Ipccaretsz], bad[Ipccaretsz];
|
|
u32int key, mod;
|
|
int valid, want;
|
|
int32_t x, y, h;
|
|
|
|
ipcpackcap(buf, 1);
|
|
CT_EQ_MEM(t, capon, buf, sizeof capon);
|
|
CT_EQ_INT(t, Ipccap, ipcreqtype(buf));
|
|
/* The probe remains a harmless key-zero request to an old daemon. */
|
|
ipcunpackreq(buf, &want, &mod, &key);
|
|
CT_EQ_INT(t, 1, want);
|
|
CT_EQ_UINT(t, 0, key);
|
|
ipcpackcap(buf, 0);
|
|
CT_EQ_MEM(t, capoff, buf, sizeof capoff);
|
|
ipcunpackreq(buf, &want, &mod, &key);
|
|
CT_EQ_INT(t, 0, want);
|
|
CT_EQ_UINT(t, 0, key);
|
|
|
|
ipcpackcaret(buf, 1, -2, 0x01020304, 0x506);
|
|
CT_EQ_MEM(t, caret, buf, sizeof caret);
|
|
CT_EQ_INT(t, Ipccaret, ipcreqtype(buf));
|
|
if(CT_EQ_INT(t, 0, ipcunpackcaret(buf, &valid, &x, &y, &h))){
|
|
CT_EQ_INT(t, 1, valid);
|
|
CT_EQ_INT(t, -2, x);
|
|
CT_EQ_INT(t, 0x01020304, y);
|
|
CT_EQ_INT(t, 0x506, h);
|
|
}
|
|
ipcpackcaret(buf, 1, INT32_MIN, INT32_MAX, 0);
|
|
if(CT_EQ_INT(t, 0, ipcunpackcaret(buf, &valid, &x, &y, &h))){
|
|
CT_EQ_INT(t, INT32_MIN, x);
|
|
CT_EQ_INT(t, INT32_MAX, y);
|
|
CT_EQ_INT(t, 0, h);
|
|
}
|
|
ipcpackcaret(buf, 0, -2, 3, 4);
|
|
CT_EQ_MEM(t, nocaret, buf, sizeof nocaret);
|
|
if(CT_EQ_INT(t, 0, ipcunpackcaret(buf, &valid, &x, &y, &h)))
|
|
CT_EQ_INT(t, 0, valid);
|
|
|
|
memcpy(bad, caret, sizeof bad);
|
|
bad[1]++;
|
|
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
|
|
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
|
|
memcpy(bad, caret, sizeof bad);
|
|
bad[2]++;
|
|
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
|
|
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
|
|
memcpy(bad, caret, sizeof bad);
|
|
bad[3] = 2;
|
|
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
|
|
memcpy(bad, caret, sizeof bad);
|
|
bad[0] |= Ipcreqwant;
|
|
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
|
|
memcpy(bad, caret, sizeof bad);
|
|
bad[15] = 0x80;
|
|
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
|
|
memcpy(bad, capon, sizeof capon);
|
|
bad[3] = 1;
|
|
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
|
|
|
|
ipcpackreq(buf, 1, 0, 'a');
|
|
CT_EQ_INT(t, Ipckey, ipcreqtype(buf));
|
|
ipcpackreset(buf, 1);
|
|
CT_EQ_INT(t, Ipckey, ipcreqtype(buf));
|
|
}
|
|
|
|
void
|
|
ipc_runtime_path(struct ct *t)
|
|
{
|
|
char buf[128], fallback[128], *old, *saved;
|
|
|
|
old = getenv("XDG_RUNTIME_DIR");
|
|
saved = old == nil ? nil : strdup(old);
|
|
setenv("XDG_RUNTIME_DIR", "/tmp/strans-runtime-test", 1);
|
|
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
|
|
CT_EQ_STR(t, "/tmp/strans-runtime-test/strans.sock", buf);
|
|
setenv("XDG_RUNTIME_DIR", "/tmp/strans-runtime-test/", 1);
|
|
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
|
|
CT_EQ_STR(t, "/tmp/strans-runtime-test/strans.sock", buf);
|
|
setenv("XDG_RUNTIME_DIR", "relative", 1);
|
|
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
|
|
snprint(fallback, sizeof fallback, "/tmp/strans.%d", getuid());
|
|
CT_EQ_STR(t, fallback, buf);
|
|
CT_EQ_INT(t, -1, ipcpath(buf, 4));
|
|
if(saved != nil){
|
|
setenv("XDG_RUNTIME_DIR", saved, 1);
|
|
free(saved);
|
|
}else
|
|
unsetenv("XDG_RUNTIME_DIR");
|
|
}
|
|
|
|
void
|
|
ipc_response_pack_boundaries(struct ct *t)
|
|
{
|
|
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, 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, 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, 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, 0,
|
|
(char*)field, sizeof field, (char*)field, sizeof field, 1);
|
|
CT_EQ_INT(t, Ipcmaxresp, n);
|
|
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, 0,
|
|
(char*)field, sizeof field, (char*)field, sizeof field, 1));
|
|
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
|
|
ipc_response_empty_and_preedit(struct ct *t)
|
|
{
|
|
uchar first[Ipcmaxresp], second[Ipcmaxresp];
|
|
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
|
|
Ipcresp resp;
|
|
int fd[2], nfirst, nsecond;
|
|
|
|
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;
|
|
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
|
|
CT_ERRORF(t, "socketpair failed");
|
|
return;
|
|
}
|
|
if(CT_EQ_INT(t, 0, ipcsend(fd[0], first, nfirst)) &&
|
|
CT_EQ_INT(t, 0, ipcsend(fd[0], second, nsecond)) &&
|
|
CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, preedit, &resp))){
|
|
CT_EQ_INT(t, 0, resp.eaten);
|
|
CT_EQ_SIZE(t, 0, resp.commitlen);
|
|
CT_EQ_SIZE(t, 0, resp.preeditlen);
|
|
CT_EQ_STR(t, "", commit);
|
|
}
|
|
if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit, &resp))){
|
|
CT_EQ_INT(t, 1, resp.eaten);
|
|
CT_EQ_SIZE(t, 2, resp.commitlen);
|
|
CT_EQ_SIZE(t, 4, resp.preeditlen);
|
|
CT_EQ_STR(t, "go", commit);
|
|
CT_EQ_STR(t, "kana", preedit);
|
|
}
|
|
close(fd[0]);
|
|
close(fd[1]);
|
|
}
|
|
|
|
void
|
|
ipc_response_max_and_drain(struct ct *t)
|
|
{
|
|
uchar first[Ipcmaxresp], second[Ipcmaxresp], field[Ipcfieldmax];
|
|
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
|
|
Ipcresp resp;
|
|
int fd[2], nfirst, nsecond;
|
|
|
|
memset(field, 'x', sizeof field);
|
|
nfirst = ipcpackresp(first, sizeof first, 1, 0,
|
|
(char*)field, sizeof field, (char*)field, sizeof field, 1);
|
|
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){
|
|
CT_ERRORF(t, "socketpair failed");
|
|
return;
|
|
}
|
|
if(CT_EQ_INT(t, 0, ipcsend(fd[0], first, nfirst)) &&
|
|
CT_EQ_INT(t, 0, ipcsend(fd[0], second, nsecond)) &&
|
|
CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit, &resp))){
|
|
CT_EQ_SIZE(t, Ipcfieldmax, resp.commitlen);
|
|
CT_EQ_SIZE(t, Ipcfieldmax, resp.preeditlen);
|
|
CT_EQ_MEM(t, field, commit, Ipcfieldmax);
|
|
CT_EQ_MEM(t, field, preedit, Ipcfieldmax);
|
|
CT_EQ_INT(t, 0, commit[Ipcfieldmax]);
|
|
CT_EQ_INT(t, 0, preedit[Ipcfieldmax]);
|
|
}
|
|
if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, preedit, &resp)))
|
|
CT_EQ_STR(t, "ok", commit);
|
|
close(fd[0]);
|
|
close(fd[1]);
|
|
}
|
|
|
|
void
|
|
ipc_response_fragmented_and_truncated(struct ct *t)
|
|
{
|
|
uchar frame[Ipcmaxresp];
|
|
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
|
|
Ipcresp resp;
|
|
int fd[2], i, n;
|
|
|
|
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){
|
|
CT_ERRORF(t, "socketpair failed");
|
|
return;
|
|
}
|
|
for(i = 0; i < n; i++)
|
|
if(ipcsend(fd[0], frame+i, 1) < 0){
|
|
CT_ERRORF(t, "fragment send failed");
|
|
break;
|
|
}
|
|
if(i == n && CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit,
|
|
&resp))){
|
|
CT_EQ_STR(t, "abc", commit);
|
|
CT_EQ_STR(t, "xy", preedit);
|
|
}
|
|
close(fd[0]);
|
|
close(fd[1]);
|
|
|
|
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
|
|
CT_ERRORF(t, "socketpair failed");
|
|
return;
|
|
}
|
|
CT_EQ_INT(t, 0, ipcsend(fd[0], frame, n-1));
|
|
shutdown(fd[0], SHUT_WR);
|
|
CT_EQ_INT(t, -1, ipcreadresp(fd[1], 1, commit, preedit, &resp));
|
|
close(fd[0]);
|
|
close(fd[1]);
|
|
|
|
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
|
|
CT_ERRORF(t, "socketpair failed");
|
|
return;
|
|
}
|
|
frame[0] = 2;
|
|
CT_EQ_INT(t, 0, ipcsend(fd[0], frame, n));
|
|
errno = 0;
|
|
CT_EQ_INT(t, -1, ipcreadresp(fd[1], 1, commit, preedit, &resp));
|
|
CT_EQ_INT(t, EPROTO, errno);
|
|
close(fd[0]);
|
|
close(fd[1]);
|
|
}
|
|
|
|
void
|
|
ipc_broken_peer_send(struct ct *t)
|
|
{
|
|
int fd[2];
|
|
|
|
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
|
|
CT_ERRORF(t, "socketpair failed");
|
|
return;
|
|
}
|
|
close(fd[1]);
|
|
CT_EQ_INT(t, -1, ipcsend(fd[0], "x", 1));
|
|
close(fd[0]);
|
|
ipcclientdeadlines(t);
|
|
ipcconnectcloexec(t);
|
|
}
|
|
|
|
static void
|
|
ipcclientdeadlines(struct ct *t)
|
|
{
|
|
char fill[4096], commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
|
|
Ipcresp resp;
|
|
int fd[2];
|
|
int64_t start, elapsed;
|
|
ssize_t n;
|
|
|
|
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
|
|
CT_ERRORF(t, "socketpair failed");
|
|
return;
|
|
}
|
|
start = nowms();
|
|
errno = 0;
|
|
CT_EQ_INT(t, -1, ipcreadresp(fd[0], 0, commit, preedit, &resp));
|
|
elapsed = nowms() - start;
|
|
CT_EQ_INT(t, ETIMEDOUT, errno);
|
|
CT_CHECK(t, elapsed >= 0 && elapsed < 4*Ipcwaitms);
|
|
|
|
memset(fill, 'x', sizeof fill);
|
|
for(;;){
|
|
n = send(fd[0], fill, sizeof fill, MSG_DONTWAIT|MSG_NOSIGNAL);
|
|
if(n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
|
break;
|
|
if(n <= 0){
|
|
CT_ERRORF(t, "fill send failed: %s", strerror(errno));
|
|
close(fd[0]);
|
|
close(fd[1]);
|
|
return;
|
|
}
|
|
}
|
|
start = nowms();
|
|
errno = 0;
|
|
CT_EQ_INT(t, -1, ipcsend(fd[0], "x", 1));
|
|
elapsed = nowms() - start;
|
|
CT_EQ_INT(t, ETIMEDOUT, errno);
|
|
CT_CHECK(t, elapsed >= 0 && elapsed < 4*Ipcwaitms);
|
|
close(fd[0]);
|
|
close(fd[1]);
|
|
}
|
|
|
|
static void
|
|
ipcconnectcloexec(struct ct *t)
|
|
{
|
|
struct sockaddr_un addr;
|
|
char root[] = "/tmp/strans-ipc.XXXXXX";
|
|
char *old, *saved;
|
|
int accepted, client, flags, full, listener;
|
|
|
|
accepted = client = full = listener = -1;
|
|
old = getenv("XDG_RUNTIME_DIR");
|
|
saved = old == nil ? nil : strdup(old);
|
|
if(mkdtemp(root) == nil){
|
|
CT_ERRORF(t, "mkdtemp failed: %s", strerror(errno));
|
|
free(saved);
|
|
return;
|
|
}
|
|
setenv("XDG_RUNTIME_DIR", root, 1);
|
|
memset(&addr, 0, sizeof addr);
|
|
addr.sun_family = AF_UNIX;
|
|
if(ipcpath(addr.sun_path, sizeof addr.sun_path) < 0 ||
|
|
(listener = socket(AF_UNIX, SOCK_STREAM, 0)) < 0 ||
|
|
bind(listener, (struct sockaddr*)&addr, sizeof addr) < 0 ||
|
|
listen(listener, 0) < 0){
|
|
CT_ERRORF(t, "listen failed: %s", strerror(errno));
|
|
goto Out;
|
|
}
|
|
client = ipcconnect();
|
|
if(!CT_CHECK(t, client >= 0))
|
|
goto Out;
|
|
flags = fcntl(client, F_GETFD);
|
|
CT_CHECK(t, flags >= 0 && (flags & FD_CLOEXEC) != 0);
|
|
errno = 0;
|
|
full = ipcconnect();
|
|
CT_EQ_INT(t, -1, full);
|
|
CT_CHECK(t, errno == EAGAIN || errno == EWOULDBLOCK);
|
|
accepted = accept(listener, nil, nil);
|
|
if(!CT_CHECK(t, accepted >= 0))
|
|
goto Out;
|
|
Out:
|
|
if(full >= 0)
|
|
close(full);
|
|
if(accepted >= 0)
|
|
close(accepted);
|
|
if(client >= 0)
|
|
close(client);
|
|
if(listener >= 0)
|
|
close(listener);
|
|
unlink(addr.sun_path);
|
|
rmdir(root);
|
|
if(saved != nil){
|
|
setenv("XDG_RUNTIME_DIR", saved, 1);
|
|
free(saved);
|
|
}else
|
|
unsetenv("XDG_RUNTIME_DIR");
|
|
}
|