#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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; }