RequireSurroundingText asks the client to send its text; it arrives through SetSurroundingText as an IBusText and a cursor counted in runes, and DeleteSurroundingText asks for runes back before the commit that replaces them. The official libibus client now proves the whole turn: 한자 typed, the Hanja key, Enter, and 漢字 arrives with the 한 taken away. A client that has set EffectivePostProcessKeyEvent is never asked for its text. It reads the key's commits back after ProcessKeyEvent returns, and a DeleteSurroundingText signal is not one of the things that reply can carry, so the deletion would land after the text it was meant to make room for and eat the wrong runes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
263 lines
6.2 KiB
C
263 lines
6.2 KiB
C
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "live.h"
|
|
#include "livebus.h"
|
|
|
|
enum
|
|
{
|
|
Maxconnections = 64,
|
|
};
|
|
|
|
static int
|
|
invalidcall(DBusConnection *conn, char *path, char *member)
|
|
{
|
|
DBusMessage *reply;
|
|
const char *name;
|
|
int ok;
|
|
|
|
reply = sendcall(conn, contextcall(path, member), member);
|
|
if(reply == NULL)
|
|
return 0;
|
|
name = dbus_message_get_error_name(reply);
|
|
ok = dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR &&
|
|
name != NULL && strcmp(name, DBUS_ERROR_INVALID_ARGS) == 0;
|
|
if(!ok)
|
|
fail("malformed %s returned %s, expected %s", member,
|
|
name != NULL ? name : "a non-error reply",
|
|
DBUS_ERROR_INVALID_ARGS);
|
|
dbus_message_unref(reply);
|
|
return ok;
|
|
}
|
|
|
|
/* libibus destroys through the Service interface; the path must go away. */
|
|
static int
|
|
destroyed(DBusConnection *conn, char *path)
|
|
{
|
|
DBusMessage *m, *reply;
|
|
const char *name;
|
|
int ok;
|
|
|
|
m = dbus_message_new_method_call("org.freedesktop.IBus", path,
|
|
"org.freedesktop.IBus.Service", "Destroy");
|
|
reply = callret(conn, m, "Service.Destroy");
|
|
if(reply == NULL)
|
|
return 0;
|
|
dbus_message_unref(reply);
|
|
reply = sendcall(conn, contextcall(path, "FocusIn"), "FocusIn");
|
|
if(reply == NULL)
|
|
return 0;
|
|
name = dbus_message_get_error_name(reply);
|
|
ok = name != NULL && strcmp(name, DBUS_ERROR_UNKNOWN_OBJECT) == 0;
|
|
if(!ok)
|
|
fail("context survived Service.Destroy");
|
|
dbus_message_unref(reply);
|
|
return ok;
|
|
}
|
|
|
|
static int
|
|
overflowrejected(char *address)
|
|
{
|
|
DBusConnection *conn;
|
|
DBusError err;
|
|
struct pollfd pfd;
|
|
int fd, n, ok, timeout;
|
|
int64_t deadline;
|
|
|
|
dbus_error_init(&err);
|
|
conn = dbus_connection_open_private(address, &err);
|
|
if(conn == NULL){
|
|
ok = dbus_error_is_set(&err);
|
|
dbus_error_free(&err);
|
|
return ok || fail("overflow connection failed without a D-Bus error");
|
|
}
|
|
dbus_error_free(&err);
|
|
dbus_connection_set_exit_on_disconnect(conn, FALSE);
|
|
if(!dbus_connection_get_unix_fd(conn, &fd)){
|
|
closebus(&conn);
|
|
return fail("overflow connection has no Unix descriptor");
|
|
}
|
|
ok = 0;
|
|
deadline = nowms() + Calltimeout;
|
|
for(;;){
|
|
if(!dbus_connection_get_is_connected(conn)){
|
|
ok = 1;
|
|
break;
|
|
}
|
|
timeout = leftms(deadline);
|
|
if(timeout == 0){
|
|
fail("65th IBus connection was not rejected");
|
|
break;
|
|
}
|
|
pfd.fd = fd;
|
|
pfd.events = POLLIN;
|
|
pfd.revents = 0;
|
|
n = poll(&pfd, 1, timeout);
|
|
if(n < 0 && errno == EINTR)
|
|
continue;
|
|
if(n < 0){
|
|
fail("poll overflow IBus connection: %s", strerror(errno));
|
|
break;
|
|
}
|
|
if(n == 0)
|
|
continue;
|
|
if(pfd.revents & POLLNVAL){
|
|
fail("overflow IBus descriptor became invalid");
|
|
break;
|
|
}
|
|
dbus_connection_read_write_dispatch(conn, 0);
|
|
}
|
|
closebus(&conn);
|
|
return ok;
|
|
}
|
|
|
|
static int
|
|
runcontract(char *address)
|
|
{
|
|
static char *bad[] = {
|
|
"ProcessKeyEvent",
|
|
"SetEngine",
|
|
"SetCapabilities",
|
|
"SetCursorLocation",
|
|
"SetCursorLocationRelative",
|
|
"SetSurroundingText",
|
|
};
|
|
DBusConnection *conn;
|
|
char path[96];
|
|
int i, ok;
|
|
|
|
conn = openbus(address, "contract");
|
|
if(conn == NULL)
|
|
return 0;
|
|
ok = hello(conn, NULL, 0, "contract Hello") &&
|
|
createcontext(conn, path, sizeof path, "contract context");
|
|
for(i = 0; ok && i < (int)(sizeof bad / sizeof bad[0]); i++)
|
|
ok = invalidcall(conn, path, bad[i]);
|
|
ok = ok && destroyed(conn, path);
|
|
closebus(&conn);
|
|
return ok;
|
|
}
|
|
|
|
static int
|
|
runcapacity(char *address)
|
|
{
|
|
DBusConnection *conn[Maxconnections];
|
|
int i, ok;
|
|
|
|
for(i = 0; i < Maxconnections; i++)
|
|
conn[i] = NULL;
|
|
ok = 0;
|
|
for(i = 0; i < Maxconnections; i++){
|
|
conn[i] = openbus(address, "capacity");
|
|
if(conn[i] == NULL || !hello(conn[i], NULL, 0, "capacity Hello"))
|
|
goto out;
|
|
}
|
|
if(!overflowrejected(address))
|
|
goto out;
|
|
closebus(&conn[Maxconnections-1]);
|
|
if(!createcontext(conn[0], NULL, 0, "capacity context"))
|
|
goto out;
|
|
conn[Maxconnections-1] = openbus(address, "replacement");
|
|
if(conn[Maxconnections-1] == NULL ||
|
|
!hello(conn[Maxconnections-1], NULL, 0, "replacement Hello") ||
|
|
!createcontext(conn[Maxconnections-1], NULL, 0, "replacement context"))
|
|
goto out;
|
|
ok = 1;
|
|
out:
|
|
for(i = 0; i < Maxconnections; i++)
|
|
closebus(&conn[i]);
|
|
return ok;
|
|
}
|
|
|
|
static int
|
|
runclient(Live *l, char *address, char *program)
|
|
{
|
|
pid_t pid, n;
|
|
int status;
|
|
int64_t deadline;
|
|
|
|
pid = fork();
|
|
if(pid < 0)
|
|
return fail("fork official libibus client: %s", strerror(errno));
|
|
if(pid == 0){
|
|
if(setenv("XDG_RUNTIME_DIR", l->runtime, 1) < 0 ||
|
|
setenv("XDG_CONFIG_HOME", l->config, 1) < 0 ||
|
|
setenv("HOME", l->home, 1) < 0 ||
|
|
unsetenv("IBUS_ADDRESS_FILE") < 0)
|
|
_exit(126);
|
|
execl(program, program, address, (char*)0);
|
|
dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno));
|
|
_exit(127);
|
|
}
|
|
deadline = nowms() + Starttimeout;
|
|
for(;;){
|
|
n = waitpid(pid, &status, WNOHANG);
|
|
if(n == pid)
|
|
break;
|
|
if(n < 0 && errno == EINTR)
|
|
continue;
|
|
if(n < 0)
|
|
return fail("wait official libibus client: %s", strerror(errno));
|
|
if(leftms(deadline) == 0){
|
|
fail("official libibus client timed out");
|
|
kill(pid, SIGKILL);
|
|
do
|
|
n = waitpid(pid, &status, 0);
|
|
while(n < 0 && errno == EINTR);
|
|
return 0;
|
|
}
|
|
pausems(10);
|
|
}
|
|
if(!WIFEXITED(status) || WEXITSTATUS(status) != 0)
|
|
return fail("official libibus client exited with wait status %#x",
|
|
status);
|
|
return 1;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
Daemon daemon;
|
|
Live live;
|
|
char address[512];
|
|
int capacity, ok;
|
|
|
|
testname = "ibus_live_test";
|
|
capacity = argc == 4 && strcmp(argv[1], "--capacity") == 0;
|
|
if(argc != 4){
|
|
fprintf(stderr, "usage: ibus_live_test strans mapdir libibus-client\n");
|
|
fprintf(stderr, " ibus_live_test --capacity strans mapdir\n");
|
|
return 2;
|
|
}
|
|
daemoninit(&daemon, "daemon");
|
|
ok = livesetup(&live, "ibus") &&
|
|
startdaemon(&live, &daemon, argv[capacity ? 2 : 1],
|
|
argv[capacity ? 3 : 2]) &&
|
|
waitready(&live, &daemon, address, sizeof address);
|
|
if(ok)
|
|
ok = capacity ? runcapacity(address) :
|
|
runclient(&live, address, argv[3]) && runcontract(address);
|
|
if(!ok)
|
|
showerrors(&daemon);
|
|
if(!stopdaemon(&daemon))
|
|
ok = 0;
|
|
if(!closeerrors(&daemon))
|
|
ok = 0;
|
|
if(!liveclean(&live))
|
|
ok = 0;
|
|
if(!ok)
|
|
return 1;
|
|
printf(capacity ? "ibus connection capacity: ok\n" :
|
|
"ibus official client and malformed call: ok\n");
|
|
return 0;
|
|
}
|