Files
strans/tests/ibus_live_test.c

1406 lines
39 KiB
C

#define _GNU_SOURCE
#include <dbus/dbus.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
enum
{
Maxconnections = 16,
Calltimeout = 4000,
Starttimeout = 8000,
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;
typedef struct Siglog Siglog;
struct Daemon
{
pid_t pid;
int errfd;
char root[256];
char runtime[320];
char config[320];
char ibus[384];
char bus[448];
char home[320];
char socket[384];
char addrfile[512];
char address[512];
char err[4096];
size_t nerr;
};
struct Siglog
{
int invalid;
int npreedit;
int ncommit;
char preeditpath[96];
char commitpath[96];
char preedit[256];
char commit[256];
dbus_uint32_t cursor;
dbus_bool_t visible;
};
static int
fail(char *fmt, ...)
{
va_list ap;
fprintf(stderr, "ibus_live_test: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
return 0;
}
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 int
leftms(int64_t deadline)
{
int64_t n;
n = deadline - nowms();
if(n <= 0)
return 0;
if(n > 0x7fffffff)
return 0x7fffffff;
return n;
}
static int
makedir(char *path)
{
if(mkdir(path, 0700) == 0)
return 1;
return fail("mkdir %s: %s", path, strerror(errno));
}
static void
readerrors(Daemon *d)
{
ssize_t n;
if(d->errfd < 0)
return;
while(d->nerr + 1 < sizeof d->err){
n = read(d->errfd, d->err + d->nerr,
sizeof d->err - d->nerr - 1);
if(n > 0){
d->nerr += n;
continue;
}
if(n < 0 && errno == EINTR)
continue;
break;
}
d->err[d->nerr] = '\0';
}
static void
showerrors(Daemon *d)
{
readerrors(d);
if(d->nerr != 0)
fprintf(stderr, "ibus_live_test: daemon stderr:\n%s", d->err);
}
static int
readaddress(Daemon *d)
{
FILE *fp;
char line[768], *p;
long pid;
int haveaddr, havepid;
fp = fopen(d->addrfile, "r");
if(fp == NULL)
return fail("open %s: %s", d->addrfile, strerror(errno));
haveaddr = havepid = 0;
pid = -1;
while(fgets(line, sizeof line, fp) != NULL){
p = strchr(line, '\n');
if(p != NULL)
*p = '\0';
if(strncmp(line, "IBUS_ADDRESS=", 13) == 0){
if(snprintf(d->address, sizeof d->address, "%s", line + 13)
>= (int)sizeof d->address)
break;
haveaddr = d->address[0] != '\0';
}else if(strncmp(line, "IBUS_DAEMON_PID=", 16) == 0){
errno = 0;
pid = strtol(line + 16, &p, 10);
havepid = errno == 0 && *p == '\0';
}
}
if(fclose(fp) != 0)
return fail("close %s: %s", d->addrfile, strerror(errno));
if(!haveaddr || !havepid)
return fail("invalid IBus address file %s", d->addrfile);
if(pid != d->pid)
return fail("address file PID %ld, expected %ld", pid, (long)d->pid);
return 1;
}
static int
waitaddress(Daemon *d, int notifyfd)
{
struct pollfd pfd[2];
char buf[4096];
struct inotify_event *ev;
ssize_t n;
int i, timeout, status;
int64_t deadline;
deadline = nowms() + Starttimeout;
for(;;){
if(waitpid(d->pid, &status, WNOHANG) == d->pid){
d->pid = -1;
readerrors(d);
return fail("daemon exited before publishing IBus address");
}
timeout = leftms(deadline);
if(timeout == 0)
return fail("timed out waiting for IBus address file");
pfd[0].fd = notifyfd;
pfd[0].events = POLLIN;
pfd[0].revents = 0;
pfd[1].fd = d->errfd;
pfd[1].events = POLLIN;
pfd[1].revents = 0;
i = poll(pfd, 2, timeout);
if(i < 0 && errno == EINTR)
continue;
if(i < 0)
return fail("poll for IBus address: %s", strerror(errno));
if(i == 0)
continue;
if(pfd[1].revents != 0)
readerrors(d);
if(!(pfd[0].revents & POLLIN))
continue;
n = read(notifyfd, buf, sizeof buf);
if(n < 0 && (errno == EINTR || errno == EAGAIN))
continue;
if(n < 0)
return fail("read IBus directory watch: %s", strerror(errno));
for(i = 0; i < n; i += sizeof *ev + ev->len){
ev = (struct inotify_event*)(buf + i);
if(!(ev->mask & IN_MOVED_TO) || ev->len == 0)
continue;
if(snprintf(d->addrfile, sizeof d->addrfile, "%s/%s",
d->bus, ev->name) >= (int)sizeof d->addrfile)
return fail("IBus address path is too long");
return readaddress(d);
}
}
}
static int
startdaemon(Daemon *d, char *program, char *mapdir)
{
int fd, notifyfd, watch, errpipe[2];
long maxfd;
pid_t pid;
memset(d, 0, sizeof *d);
d->pid = -1;
d->errfd = -1;
snprintf(d->root, sizeof d->root, "/tmp/strans-ibus.XXXXXX");
if(mkdtemp(d->root) == NULL)
return fail("mkdtemp: %s", strerror(errno));
if(snprintf(d->runtime, sizeof d->runtime, "%s/runtime", d->root)
>= (int)sizeof d->runtime ||
snprintf(d->config, sizeof d->config, "%s/config", d->root)
>= (int)sizeof d->config ||
snprintf(d->ibus, sizeof d->ibus, "%s/ibus", d->config)
>= (int)sizeof d->ibus ||
snprintf(d->bus, sizeof d->bus, "%s/bus", d->ibus)
>= (int)sizeof d->bus ||
snprintf(d->home, sizeof d->home, "%s/home", d->root)
>= (int)sizeof d->home ||
snprintf(d->socket, sizeof d->socket, "%s/strans.sock", d->runtime)
>= (int)sizeof d->socket)
return fail("temporary path is too long");
if(!makedir(d->runtime) || !makedir(d->config) || !makedir(d->ibus) ||
!makedir(d->bus) || !makedir(d->home))
return 0;
notifyfd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK);
if(notifyfd < 0)
return fail("inotify_init1: %s", strerror(errno));
watch = inotify_add_watch(notifyfd, d->bus, IN_MOVED_TO);
if(watch < 0){
close(notifyfd);
return fail("inotify_add_watch: %s", strerror(errno));
}
if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0){
close(notifyfd);
return fail("pipe2: %s", strerror(errno));
}
pid = fork();
if(pid < 0){
close(errpipe[0]);
close(errpipe[1]);
close(notifyfd);
return fail("fork: %s", strerror(errno));
}
if(pid == 0){
close(errpipe[0]);
close(notifyfd);
if(dup2(errpipe[1], STDOUT_FILENO) < 0 ||
dup2(errpipe[1], STDERR_FILENO) < 0)
_exit(126);
close(errpipe[1]);
if(close_range(3, UINT_MAX, 0) < 0){
maxfd = sysconf(_SC_OPEN_MAX);
if(maxfd < 0)
maxfd = 1024;
for(fd = 3; fd < maxfd; fd++)
close(fd);
}
if(setenv("XDG_RUNTIME_DIR", d->runtime, 1) < 0 ||
setenv("XDG_CONFIG_HOME", d->config, 1) < 0 ||
setenv("HOME", d->home, 1) < 0 || unsetenv("DISPLAY") < 0 ||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS") < 0){
dprintf(STDERR_FILENO, "set daemon environment: %s\n",
strerror(errno));
_exit(126);
}
execl(program, program, mapdir, (char*)0);
dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno));
_exit(127);
}
close(errpipe[1]);
d->pid = pid;
d->errfd = errpipe[0];
if(!waitaddress(d, notifyfd)){
inotify_rm_watch(notifyfd, watch);
close(notifyfd);
return 0;
}
inotify_rm_watch(notifyfd, watch);
close(notifyfd);
return 1;
}
static int
clearbus(Daemon *d)
{
DIR *dir;
struct dirent *de;
char path[576];
int ok;
if(d->bus[0] == '\0')
return 1;
dir = opendir(d->bus);
if(dir == NULL)
return errno == ENOENT || fail("open cleanup directory %s: %s",
d->bus, strerror(errno));
ok = 1;
errno = 0;
while((de = readdir(dir)) != NULL){
if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
continue;
if(snprintf(path, sizeof path, "%s/%s", d->bus, de->d_name)
>= (int)sizeof path){
fail("cleanup path is too long: %s", de->d_name);
ok = 0;
continue;
}
if(unlink(path) < 0 && errno != ENOENT){
fail("remove IBus address file %s: %s", de->d_name,
strerror(errno));
ok = 0;
}
errno = 0;
}
if(errno != 0){
fail("read cleanup directory %s: %s", d->bus, strerror(errno));
ok = 0;
}
closedir(dir);
return ok;
}
static int
rmdirknown(char *path)
{
if(path[0] == '\0' || rmdir(path) == 0 || errno == ENOENT)
return 1;
return fail("rmdir %s: %s", path, strerror(errno));
}
static int
killdaemon(Daemon *d, int *status)
{
int n, ok;
ok = 1;
if(kill(d->pid, SIGKILL) < 0 && errno != ESRCH){
fail("kill -9 %ld: %s", (long)d->pid, strerror(errno));
ok = 0;
}
do
n = waitpid(d->pid, status, 0);
while(n < 0 && errno == EINTR);
if(n != d->pid){
fail("reap %ld after SIGKILL: %s", (long)d->pid,
n < 0 ? strerror(errno) : "wrong child");
ok = 0;
}else
d->pid = -1;
return ok;
}
static int
stopdaemon(Daemon *d)
{
struct pollfd pfd;
int ok, status, n, reaped;
int64_t deadline;
ok = 1;
if(d->pid > 0){
do
n = waitpid(d->pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == d->pid){
fail("daemon exited before test termination with wait status %#x",
status);
d->pid = -1;
ok = 0;
}else if(n < 0){
fail("check daemon %ld before termination: %s",
(long)d->pid, strerror(errno));
if(errno == ECHILD)
d->pid = -1;
ok = 0;
}
}
if(d->pid > 0){
if(kill(d->pid, SIGTERM) < 0){
fail("kill %ld: %s", (long)d->pid, strerror(errno));
ok = 0;
}
reaped = 0;
deadline = nowms() + Stoptimeout;
for(;;){
n = waitpid(d->pid, &status, WNOHANG);
if(n == d->pid){
reaped = 1;
d->pid = -1;
break;
}
if(n < 0 && errno == EINTR)
continue;
if(n < 0){
fail("waitpid %ld: %s", (long)d->pid, strerror(errno));
ok = 0;
if(errno == ECHILD)
d->pid = -1;
break;
}
n = leftms(deadline);
if(n == 0){
fail("daemon %ld did not stop after SIGTERM", (long)d->pid);
ok = 0;
if(!killdaemon(d, &status))
ok = 0;
break;
}
pfd.fd = d->errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
if(poll(&pfd, 1, n) < 0 && errno != EINTR){
fail("poll daemon %ld: %s", (long)d->pid, strerror(errno));
ok = 0;
if(!killdaemon(d, &status))
ok = 0;
break;
}
readerrors(d);
}
/* plan9port turns a caught termination note into exit status 1. */
if(reaped &&
!((WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM) ||
(WIFEXITED(status) && WEXITSTATUS(status) == 1))){
fail("daemon exited with unexpected wait status %#x", status);
ok = 0;
}
}
readerrors(d);
if(d->errfd >= 0){
close(d->errfd);
d->errfd = -1;
}
if(d->socket[0] != '\0' && unlink(d->socket) < 0 && errno != ENOENT){
fail("remove IPC socket %s: %s", d->socket, strerror(errno));
ok = 0;
}
if(!clearbus(d)) ok = 0;
if(!rmdirknown(d->bus)) ok = 0;
if(!rmdirknown(d->ibus)) ok = 0;
if(!rmdirknown(d->config)) ok = 0;
if(!rmdirknown(d->runtime)) ok = 0;
if(!rmdirknown(d->home)) ok = 0;
if(!rmdirknown(d->root)) ok = 0;
return ok;
}
static int
emptyarray(DBusMessageIter *it, int element)
{
DBusMessageIter sub;
if(dbus_message_iter_get_arg_type(it) != DBUS_TYPE_ARRAY ||
dbus_message_iter_get_element_type(it) != element)
return 0;
dbus_message_iter_recurse(it, &sub);
return dbus_message_iter_get_arg_type(&sub) == DBUS_TYPE_INVALID;
}
static int
ibusattrs(DBusMessageIter *it)
{
DBusMessageIter v, st;
const char *name;
if(dbus_message_iter_get_arg_type(it) != DBUS_TYPE_VARIANT)
return 0;
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_STRING)
return 0;
dbus_message_iter_get_basic(&st, &name);
if(strcmp(name, "IBusAttrList") != 0 || !dbus_message_iter_next(&st) ||
!emptyarray(&st, DBUS_TYPE_DICT_ENTRY) ||
!dbus_message_iter_next(&st) || !emptyarray(&st, DBUS_TYPE_VARIANT) ||
dbus_message_iter_next(&st))
return 0;
return !dbus_message_iter_next(&v);
}
static int
ibustext(DBusMessageIter *it, char *text, size_t ntext)
{
DBusMessageIter v, st;
const char *name, *s;
if(dbus_message_iter_get_arg_type(it) != DBUS_TYPE_VARIANT)
return 0;
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_STRING)
return 0;
dbus_message_iter_get_basic(&st, &name);
if(strcmp(name, "IBusText") != 0 || !dbus_message_iter_next(&st) ||
!emptyarray(&st, DBUS_TYPE_DICT_ENTRY) || !dbus_message_iter_next(&st) ||
dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_STRING)
return 0;
dbus_message_iter_get_basic(&st, &s);
if(snprintf(text, ntext, "%s", s) >= (int)ntext ||
!dbus_message_iter_next(&st) || !ibusattrs(&st) ||
dbus_message_iter_next(&st))
return 0;
return !dbus_message_iter_next(&v);
}
static DBusHandlerResult
onsignal(DBusConnection *conn, DBusMessage *m, void *arg)
{
Siglog *log;
DBusMessageIter it;
const char *path;
(void)conn;
log = arg;
if(dbus_message_get_type(m) != DBUS_MESSAGE_TYPE_SIGNAL ||
!dbus_message_has_interface(m, "org.freedesktop.IBus.InputContext"))
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
if(dbus_message_has_member(m, "UpdatePreeditText")){
log->npreedit++;
path = dbus_message_get_path(m);
if(path == NULL || snprintf(log->preeditpath,
sizeof log->preeditpath, "%s", path) >=
(int)sizeof log->preeditpath ||
!dbus_message_has_signature(m, "vub") ||
!dbus_message_iter_init(m, &it) ||
!ibustext(&it, log->preedit, sizeof log->preedit) ||
!dbus_message_iter_next(&it) ||
dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_UINT32){
log->invalid = 1;
return DBUS_HANDLER_RESULT_HANDLED;
}
dbus_message_iter_get_basic(&it, &log->cursor);
if(!dbus_message_iter_next(&it) ||
dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_BOOLEAN){
log->invalid = 1;
return DBUS_HANDLER_RESULT_HANDLED;
}
dbus_message_iter_get_basic(&it, &log->visible);
if(dbus_message_iter_next(&it))
log->invalid = 1;
return DBUS_HANDLER_RESULT_HANDLED;
}
if(dbus_message_has_member(m, "CommitText")){
log->ncommit++;
path = dbus_message_get_path(m);
if(path == NULL || snprintf(log->commitpath,
sizeof log->commitpath, "%s", path) >=
(int)sizeof log->commitpath ||
!dbus_message_has_signature(m, "v") ||
!dbus_message_iter_init(m, &it) ||
!ibustext(&it, log->commit, sizeof log->commit) ||
dbus_message_iter_next(&it))
log->invalid = 1;
return DBUS_HANDLER_RESULT_HANDLED;
}
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
static void
resetsig(Siglog *log)
{
memset(log, 0, sizeof *log);
}
static DBusConnection*
openbus(char *address, Siglog *log)
{
DBusConnection *conn;
DBusError err;
dbus_error_init(&err);
conn = dbus_connection_open_private(address, &err);
if(conn == NULL){
fail("connect to private IBus address: %s", err.message);
dbus_error_free(&err);
return NULL;
}
dbus_connection_set_exit_on_disconnect(conn, FALSE);
if(!dbus_connection_add_filter(conn, onsignal, log, NULL)){
fail("add D-Bus signal filter: out of memory");
dbus_connection_close(conn);
dbus_connection_unref(conn);
return NULL;
}
return conn;
}
static void
closebus(DBusConnection **conn, Siglog *log)
{
if(*conn == NULL)
return;
dbus_connection_remove_filter(*conn, onsignal, log);
dbus_connection_close(*conn);
dbus_connection_unref(*conn);
*conn = NULL;
}
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){
if(!dbus_error_is_set(&err)){
dbus_error_free(&err);
return fail("overflow connection failed without a D-Bus error");
}
dbus_error_free(&err);
return 1;
}
dbus_error_free(&err);
dbus_connection_set_exit_on_disconnect(conn, FALSE);
if(!dbus_connection_get_unix_fd(conn, &fd)){
dbus_connection_close(conn);
dbus_connection_unref(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("overflow 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);
}
dbus_connection_close(conn);
dbus_connection_unref(conn);
return ok;
}
static DBusMessage*
method(char *path, char *iface, char *member)
{
DBusMessage *m;
m = dbus_message_new_method_call("org.freedesktop.IBus", path,
iface, member);
if(m == NULL)
fail("allocate %s call: out of memory", member);
return m;
}
static DBusMessage*
sendcall(DBusConnection *conn, DBusMessage *m)
{
DBusMessage *reply;
DBusPendingCall *pending;
char member[64], path[128];
const char *s;
int timeout;
int64_t deadline;
if(m == NULL)
return NULL;
s = dbus_message_get_member(m);
snprintf(member, sizeof member, "%s", s != NULL ? s : "method");
s = dbus_message_get_path(m);
snprintf(path, sizeof path, "%s", s != NULL ? s : "path");
pending = NULL;
if(!dbus_connection_send_with_reply(conn, m, &pending, Calltimeout) ||
pending == NULL){
dbus_message_unref(m);
fail("queue %s on %s: out of memory", member, path);
return NULL;
}
dbus_message_unref(m);
deadline = nowms() + Calltimeout;
while(!dbus_pending_call_get_completed(pending)){
timeout = leftms(deadline);
if(timeout == 0){
fail("timed out waiting for %s on %s", member, path);
goto fail;
}
if(!dbus_connection_read_write_dispatch(conn, timeout)){
fail("connection closed waiting for %s on %s", member, path);
goto fail;
}
}
reply = dbus_pending_call_steal_reply(pending);
dbus_pending_call_unref(pending);
if(reply == NULL){
fail("%s on %s completed without a reply", member, path);
return NULL;
}
while(dbus_connection_get_dispatch_status(conn) == DBUS_DISPATCH_DATA_REMAINS)
dbus_connection_dispatch(conn);
return reply;
fail:
dbus_pending_call_cancel(pending);
dbus_pending_call_unref(pending);
return NULL;
}
static int
hello(DBusConnection *conn, char *name, size_t nname)
{
DBusMessage *m, *reply;
DBusError err;
const char *s;
m = dbus_message_new_method_call("org.freedesktop.DBus",
"/org/freedesktop/DBus", "org.freedesktop.DBus", "Hello");
reply = sendcall(conn, m);
if(reply == NULL)
return 0;
dbus_error_init(&err);
if(!dbus_message_has_signature(reply, "s") ||
!dbus_message_get_args(reply, &err, DBUS_TYPE_STRING, &s,
DBUS_TYPE_INVALID)){
dbus_message_unref(reply);
dbus_error_free(&err);
return fail("Hello returned an invalid reply");
}
if(snprintf(name, nname, "%s", s) >= (int)nname){
dbus_message_unref(reply);
return fail("Hello name is too long");
}
dbus_message_unref(reply);
return name[0] == ':' || fail("Hello returned invalid name %s", name);
}
static int
createcontext(DBusConnection *conn, char *path, size_t npath)
{
DBusMessage *m, *reply;
DBusError err;
const char *client, *p;
client = "ibus-live-test";
m = method("/org/freedesktop/IBus", "org.freedesktop.IBus",
"CreateInputContext");
if(m == NULL || !dbus_message_append_args(m, DBUS_TYPE_STRING, &client,
DBUS_TYPE_INVALID)){
if(m != NULL) dbus_message_unref(m);
return fail("build CreateInputContext call");
}
reply = sendcall(conn, m);
if(reply == NULL)
return 0;
dbus_error_init(&err);
if(!dbus_message_has_signature(reply, "o") ||
!dbus_message_get_args(reply, &err, DBUS_TYPE_OBJECT_PATH, &p,
DBUS_TYPE_INVALID)){
dbus_message_unref(reply);
dbus_error_free(&err);
return fail("CreateInputContext returned an invalid reply");
}
if(snprintf(path, npath, "%s", p) >= (int)npath){
dbus_message_unref(reply);
return fail("input context path is too long");
}
dbus_message_unref(reply);
return path[0] == '/' || fail("invalid input context path %s", path);
}
static int
emptycall(DBusConnection *conn, Siglog *log, char *path, char *member)
{
DBusMessage *reply;
resetsig(log);
reply = sendcall(conn, method(path,
"org.freedesktop.IBus.InputContext", member));
if(reply == NULL)
return 0;
if(!dbus_message_has_signature(reply, "")){
dbus_message_unref(reply);
return fail("%s returned a nonempty reply", member);
}
dbus_message_unref(reply);
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)
{
DBusMessage *m, *reply;
DBusError err;
dbus_uint32_t code;
dbus_bool_t b;
resetsig(log);
code = 0;
m = method(path, "org.freedesktop.IBus.InputContext", "ProcessKeyEvent");
if(m == NULL || !dbus_message_append_args(m,
DBUS_TYPE_UINT32, &sym, DBUS_TYPE_UINT32, &code,
DBUS_TYPE_UINT32, &state, DBUS_TYPE_INVALID)){
if(m != NULL) dbus_message_unref(m);
return fail("build ProcessKeyEvent call");
}
reply = sendcall(conn, m);
if(reply == NULL)
return 0;
dbus_error_init(&err);
if(!dbus_message_has_signature(reply, "b") ||
!dbus_message_get_args(reply, &err, DBUS_TYPE_BOOLEAN, &b,
DBUS_TYPE_INVALID)){
dbus_message_unref(reply);
dbus_error_free(&err);
return fail("ProcessKeyEvent returned an invalid reply");
}
*eaten = b != FALSE;
dbus_message_unref(reply);
return 1;
}
static int
cursorcall(DBusConnection *conn, Siglog *log, char *path,
int x, int y, int w, int h)
{
DBusMessage *m, *reply;
dbus_int32_t dx, dy, dw, dh;
resetsig(log);
dx = x;
dy = y;
dw = w;
dh = h;
m = method(path, "org.freedesktop.IBus.InputContext",
"SetCursorLocation");
if(m == NULL || !dbus_message_append_args(m,
DBUS_TYPE_INT32, &dx, DBUS_TYPE_INT32, &dy,
DBUS_TYPE_INT32, &dw, DBUS_TYPE_INT32, &dh,
DBUS_TYPE_INVALID)){
if(m != NULL) dbus_message_unref(m);
return fail("build SetCursorLocation call");
}
reply = sendcall(conn, m);
if(reply == NULL)
return 0;
if(!dbus_message_has_signature(reply, "")){
dbus_message_unref(reply);
return fail("SetCursorLocation returned a nonempty reply");
}
dbus_message_unref(reply);
return 1;
}
static int
unknowncall(DBusConnection *conn, char *path, char *member, char *error)
{
DBusMessage *m, *reply;
DBusError err;
m = method(path, "org.freedesktop.IBus.InputContext", member);
if(m == NULL)
return 0;
dbus_error_init(&err);
reply = dbus_connection_send_with_reply_and_block(conn, m,
Calltimeout, &err);
dbus_message_unref(m);
if(reply != NULL){
dbus_message_unref(reply);
dbus_error_free(&err);
return fail("%s on %s unexpectedly succeeded", member, path);
}
if(!dbus_error_has_name(&err, error)){
fail("%s on %s returned %s, expected %s", member, path,
err.name != NULL ? err.name : "no error", error);
dbus_error_free(&err);
return 0;
}
dbus_error_free(&err);
return 1;
}
static int
limitscall(DBusConnection *conn)
{
DBusMessage *m, *reply;
DBusError err;
const char *client;
client = "ibus-live-test-overflow";
m = method("/org/freedesktop/IBus", "org.freedesktop.IBus",
"CreateInputContext");
if(m == NULL || !dbus_message_append_args(m, DBUS_TYPE_STRING, &client,
DBUS_TYPE_INVALID)){
if(m != NULL) dbus_message_unref(m);
return fail("build overflow CreateInputContext call");
}
dbus_error_init(&err);
reply = dbus_connection_send_with_reply_and_block(conn, m,
Calltimeout, &err);
dbus_message_unref(m);
if(reply != NULL){
dbus_message_unref(reply);
dbus_error_free(&err);
return fail("65th input context unexpectedly succeeded");
}
if(!dbus_error_has_name(&err, DBUS_ERROR_LIMITS_EXCEEDED)){
fail("65th input context returned %s, expected %s",
err.name != NULL ? err.name : "no error",
DBUS_ERROR_LIMITS_EXCEEDED);
dbus_error_free(&err);
return 0;
}
dbus_error_free(&err);
return 1;
}
static int
nosignal(Siglog *log, char *where)
{
if(log->invalid || log->npreedit != 0 || log->ncommit != 0)
return fail("%s emitted invalid or unexpected signals", where);
return 1;
}
static int
preedit(Siglog *log, char *path, char *text, unsigned int cursor, int visible,
char *where)
{
if(log->invalid || log->npreedit != 1 || log->ncommit != 0 ||
strcmp(log->preeditpath, path) != 0 ||
strcmp(log->preedit, text) != 0 ||
log->cursor != cursor || (log->visible != FALSE) != (visible != 0))
return fail("%s preedit: count=%d commit=%d path=%s text=%s cursor=%u visible=%d",
where, log->npreedit, log->ncommit, log->preeditpath, log->preedit,
(unsigned)log->cursor, log->visible != FALSE);
return 1;
}
static int
committed(Siglog *log, char *path, char *text, char *where)
{
if(log->invalid || log->npreedit != 1 || log->ncommit != 1 ||
strcmp(log->preeditpath, path) != 0 ||
strcmp(log->commitpath, path) != 0 ||
strcmp(log->commit, text) != 0 ||
strcmp(log->preedit, "") != 0 || log->cursor != 0 ||
log->visible != FALSE)
return fail("%s signals: preedit=%d commit=%d text=%s/%s",
where, log->npreedit, log->ncommit, log->commit, log->preedit);
return 1;
}
static int
expectkey(DBusConnection *conn, Siglog *log, char *path,
dbus_uint32_t sym, dbus_uint32_t state, int eaten, char *text, char *where)
{
int got;
if(!keycall(conn, log, path, sym, state, &got))
return 0;
if(got != eaten)
return fail("%s eaten=%d, expected %d", where, got, eaten);
if(text == NULL)
return nosignal(log, where);
return preedit(log, path, text, (unsigned)strlen(text) == 0 ? 0 : 1,
text[0] != '\0', where);
}
static int
runconnectioncapacity(Daemon *d)
{
DBusConnection *conn[Maxconnections];
Siglog logs[Maxconnections];
char names[Maxconnections][32];
char paths[Maxconnections][96];
int i, ok;
for(i = 0; i < Maxconnections; i++)
conn[i] = NULL;
memset(logs, 0, sizeof logs);
ok = 0;
for(i = 0; i < Maxconnections; i++){
conn[i] = openbus(d->address, &logs[i]);
if(conn[i] == NULL || !hello(conn[i], names[i], sizeof names[i]) ||
!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, 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, 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, NULL,
"connection close barrier"))
goto out;
conn[Maxconnections-1] = openbus(d->address,
&logs[Maxconnections-1]);
if(conn[Maxconnections-1] == NULL ||
!hello(conn[Maxconnections-1], names[Maxconnections-1],
sizeof names[Maxconnections-1]) ||
!createcontext(conn[Maxconnections-1], paths[Maxconnections-1],
sizeof paths[Maxconnections-1]) ||
!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") ||
!expectkey(conn[Maxconnections-1], &logs[Maxconnections-1],
paths[Maxconnections-1], 'k', 0, 1, "k",
"recovered connection key"))
goto out;
for(i = 0; i < Maxconnections-1; i++)
if(!expectkey(conn[i], &logs[i], paths[i], 0xffe1, 0, 0, NULL,
"accepted connection after recovery"))
goto out;
ok = 1;
out:
for(i = 0; i < Maxconnections; i++)
closebus(&conn[i], &logs[i]);
return ok;
}
static int
runlifecycle(Daemon *d)
{
DBusConnection *c1, *c2, *c3;
Siglog s1, s2, s3;
char n1[32], n2[32], n3[32];
char a[96], a2[96], b[96], active[96], path[96], first[96];
int eaten, i, ok;
c1 = c2 = c3 = NULL;
memset(&s1, 0, sizeof s1);
memset(&s2, 0, sizeof s2);
memset(&s3, 0, sizeof s3);
ok = 0;
c1 = openbus(d->address, &s1);
c2 = openbus(d->address, &s2);
if(c1 == NULL || c2 == NULL)
goto out;
if(!hello(c1, n1, sizeof n1) || !hello(c2, n2, sizeof n2) ||
strcmp(n1, n2) == 0)
goto out;
if(!createcontext(c1, a, sizeof a) || !createcontext(c2, b, sizeof b))
goto out;
if(!unknowncall(c1, "/org/freedesktop/IBus/InputContext_missing",
"FocusIn", DBUS_ERROR_UNKNOWN_OBJECT))
goto out;
if(!expectkey(c1, &s1, a, 'x', 0, 0, NULL, "unfocused key"))
goto out;
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") ||
!expectkey(c1, &s1, a, 'k', 0, 1, "k", "A key k") ||
!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"))
goto out;
if(!emptycall(c1, &s1, a, "Reset") ||
!preedit(&s1, a, "", 0, 0, "stale Reset reply") ||
!expectkey(c2, &s2, b, 0xffe1, 0, 0, "", "B after stale Reset") ||
!emptycall(c1, &s1, a, "FocusOut") ||
!preedit(&s1, a, "", 0, 0, "stale FocusOut reply") ||
!expectkey(c2, &s2, b, 0xffe1, 0, 0, "", "B after stale FocusOut"))
goto out;
if(!emptycall(c1, &s1, a, "FocusIn") || !nosignal(&s1, "stale FocusIn") ||
!cursorcall(c1, &s1, a, 90, 100, 0, 20) ||
!nosignal(&s1, "stale cursor") ||
!expectkey(c2, &s2, b, 0xffe1, 0, 0, "", "B after stale cursor") ||
!emptycall(c1, &s1, a, "Destroy") || !nosignal(&s1, "stale Destroy") ||
!expectkey(c2, &s2, b, 0xffe1, 0, 0, "", "B after stale Destroy") ||
!unknowncall(c1, a, "FocusIn", DBUS_ERROR_UNKNOWN_OBJECT))
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;
closebus(&c1, &s1);
if(!expectkey(c2, &s2, b, 0xffe1, 0, 0, "", "B during stale close") ||
!expectkey(c2, &s2, b, 0xffe1, 0, 0, "", "B after stale close"))
goto out;
if(!emptycall(c2, &s2, b, "Reset") ||
!preedit(&s2, b, "", 0, 0, "active Reset") ||
!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, '0', 0, &eaten) || !eaten ||
!committed(&s2, b, "", "active commit"))
goto out;
if(!expectkey(c2, &s2, b, 'k', 0, 1, "k", "pre-FocusOut key") ||
!cursorcall(c2, &s2, b, 50, 60, 0, 12) ||
!nosignal(&s2, "active cursor") ||
!emptycall(c2, &s2, b, "FocusOut") ||
!preedit(&s2, b, "", 0, 0, "active FocusOut") ||
!emptycall(c2, &s2, b, "FocusOut") ||
!preedit(&s2, b, "", 0, 0, "repeated FocusOut") ||
!emptycall(c2, &s2, b, "FocusIn") || !nosignal(&s2, "B refocus") ||
!expectkey(c2, &s2, b, 0xffe1, 0, 0, "", "B state after FocusOut") ||
!expectkey(c2, &s2, b, 'k', 0, 1, "k", "B after refocus") ||
!emptycall(c2, &s2, b, "Destroy") || !nosignal(&s2, "active Destroy") ||
!unknowncall(c2, b, "Reset", DBUS_ERROR_UNKNOWN_OBJECT))
goto out;
if(!createcontext(c2, active, sizeof active))
goto out;
for(i = 1; i < 64; i++)
if(!createcontext(c2, path, sizeof path))
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);
c3 = openbus(d->address, &s3);
if(c3 == NULL || !hello(c3, n3, sizeof n3))
goto out;
for(i = 0; i < 64; i++){
if(!createcontext(c3, path, sizeof path))
goto out;
if(i == 0)
snprintf(first, sizeof first, "%s", path);
}
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;
out:
closebus(&c1, &s1);
closebus(&c2, &s2);
closebus(&c3, &s3);
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)
{
Daemon daemon;
int ok;
if(argc != 3){
fprintf(stderr, "usage: ibus_live_test strans mapdir\n");
return 2;
}
ok = startdaemon(&daemon, argv[1], argv[2]);
if(ok)
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, policy, and connection capacity: ok\n");
return 0;
}