Files
strans/tests/ipc_test.c
Hojun-Cho 910bf51347 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>
2026-08-18 14:31:31 +09:00

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");
}