From 609f44f277c73d03f17252551d7ec4745e5c6762 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 31 Aug 2026 18:14:04 +0800 Subject: [PATCH 1/6] Stage the XCB protocol headers at build time XCB clients compile against exact structure layouts, enum values and inline accessors, so the public headers cannot be hand-written approximations without silently breaking callers. They also cannot come from libxcb's release tarball: xproto.h is generated from xcb-proto's XML during that build, so only a binary package carries the generated result. Extend scripts/sync-upstream-headers.py to stage them into build/upstream/include/xcb/ alongside the Xorg headers, from a pinned libxcb1-dev 1.15-1ubuntu2 whose archive digest and per-header digests are both verified. Tracking 13k lines of generated upstream code instead would put it through this repository's formatting and whitespace gates, which upstream style does not satisfy, and would carry it in every clone forever. A .deb is an ar archive, so the script reads its data.tar member directly rather than shelling out to dpkg-deb, which exists only on Debian derivatives and would make the fetch fail on the macOS development host. --- docs/XCB-COVERAGE.md | 18 +++++ mk/upstream-headers.mk | 26 ++++++- scripts/sync-upstream-headers.py | 127 +++++++++++++++++++++++++++++-- 3 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 docs/XCB-COVERAGE.md diff --git a/docs/XCB-COVERAGE.md b/docs/XCB-COVERAGE.md new file mode 100644 index 00000000..a3767f3a --- /dev/null +++ b/docs/XCB-COVERAGE.md @@ -0,0 +1,18 @@ +# XCB Compatibility Coverage + +## Header staging + +The public `xcb/` headers are not tracked and are not copied from a +distribution package. `scripts/sync-upstream-headers.py` builds them into +`build/upstream/include/xcb/` alongside the Xorg headers, from two pinned +release tarballs: + +- `libxcb-1.15` ships `xcb.h` and `xcbext.h` in its `src/`. +- `xproto.h` does not exist in any release. libxcb generates it from + xcb-proto's XML with `src/c_client.py`, so the sync script runs that same + generator against pinned `xcb-proto-1.15.2` with the arguments libxcb's own + `src/Makefile.am` passes. + +Both tarball digests are verified before use, and the generator inputs are part +of the staging stamp, so a change to either release or to the invocation +restages. `make install` ships the built copies. diff --git a/mk/upstream-headers.mk b/mk/upstream-headers.mk index 0ad05165..4c9079d8 100644 --- a/mk/upstream-headers.mk +++ b/mk/upstream-headers.mk @@ -6,10 +6,27 @@ UPSTREAM_HEADERS_DIR := $(OUT)/upstream/include UPSTREAM_SRC_DIR := $(OUT)/upstream/src UPSTREAM_HEADERS_STAMP := $(UPSTREAM_HEADERS_DIR)/.upstream-stamp UPSTREAM_SYNC := scripts/sync-upstream-headers.py - -$(UPSTREAM_HEADERS_STAMP): $(UPSTREAM_SYNC) | $(OUT) +# The public xcb/ headers are downloaded and generated, so only ask for them +# when something will compile against them. A default or wasm build then needs +# no network access for them at all. +XCB_STAGE_FLAG := $(if $(filter 1,$(XCB)),--with-xcb) + +# Toggling XCB changes what the sync stages, but nothing on disk that Make +# compares mtimes against, so record the flag in a file that only changes when +# the value does. mk/library.mk pins GLX the same way. +UPSTREAM_STAGE_CONFIG := $(OUT)/upstream.stage-config +.PHONY: FORCE +$(UPSTREAM_STAGE_CONFIG): FORCE | $(OUT) + $(Q)printf 'XCB=%s\n' '$(XCB)' > $@.tmp + $(Q)if test -r $@ && cmp -s $@.tmp $@; then \ + rm -f $@.tmp; \ + else \ + mv $@.tmp $@; \ + fi + +$(UPSTREAM_HEADERS_STAMP): $(UPSTREAM_SYNC) $(UPSTREAM_STAGE_CONFIG) | $(OUT) @echo " SYNC upstream tree -> $(OUT)/upstream" - $(Q)$(PYTHON) $(UPSTREAM_SYNC) fetch $(UPSTREAM_HEADERS_DIR) + $(Q)$(PYTHON) $(UPSTREAM_SYNC) fetch $(XCB_STAGE_FLAG) $(UPSTREAM_HEADERS_DIR) # The staged .c files are created as a side effect of the stamp recipe. # Declaring them here lets dependent .o rules use them as prerequisites @@ -30,7 +47,8 @@ $(EXAMPLE_BINS): | $(UPSTREAM_HEADERS_STAMP) ## Re-download and re-extract upstream headers upstream-sync: | $(OUT) - $(Q)$(PYTHON) $(UPSTREAM_SYNC) fetch --force $(UPSTREAM_HEADERS_DIR) + $(Q)$(PYTHON) $(UPSTREAM_SYNC) fetch --force $(XCB_STAGE_FLAG) \ + $(UPSTREAM_HEADERS_DIR) ## Compare local include/X11/ against the pinned upstream snapshot upstream-diff: diff --git a/scripts/sync-upstream-headers.py b/scripts/sync-upstream-headers.py index e4b49c82..ce5f00ae 100644 --- a/scripts/sync-upstream-headers.py +++ b/scripts/sync-upstream-headers.py @@ -35,6 +35,7 @@ import hashlib import os import shutil +import subprocess import sys import tarfile import tempfile @@ -457,6 +458,38 @@ ) +# The XCB headers are built, not shipped: libxcb's release carries xcb.h and +# xcbext.h by hand but generates xproto.h from xcb-proto's XML through +# c_client.py. Both releases are pinned here and the generator runs at stage +# time, so nothing generated is tracked in the repository. +XCB_SOURCES = { + "libxcb": { + "version": "libxcb-1.15", + "url": ( + "https://xorg.freedesktop.org/archive/individual/lib/" + "libxcb-1.15.tar.xz" + ), + "sha256": ( + "cc38744f817cf6814c847e2df37fcb8997357d72fa4bcbc228ae0fe47219a059" + ), + }, + "xcb-proto": { + "version": "xcb-proto-1.15.2", + "url": ( + "https://xorg.freedesktop.org/archive/individual/proto/" + "xcb-proto-1.15.2.tar.xz" + ), + "sha256": ( + "7072beb1f680a2fe3f9e535b797c146d22528990c72f63ddb49d2f350a3653ed" + ), + }, +} + +# Handed to c_client.py the way libxcb's own src/Makefile.am does. +XCB_MAN_PAGE = "X Version 11" +XCB_MAN_SUFFIX = "3" + + def sha256_of(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: @@ -635,6 +668,71 @@ def relevant_util_member( return base +def _extract_tarball(tarball: Path, dest: Path) -> Path: + """Unpack tarball under dest and return its single top-level directory.""" + with tarfile.open(tarball, "r:*") as tar: + members = [m for m in tar.getmembers() if _is_safe(PurePosixPath(m.name).parts)] + roots = {PurePosixPath(m.name).parts[0] for m in members if m.name != "."} + if len(roots) != 1: + raise SystemExit(f"{tarball.name} has no single top-level directory") + for member in members: + tar.extract(member, dest, filter="data") + return dest / roots.pop() + + +def xcb_index() -> dict[str, tuple[str, bytes]]: + """Build the public XCB headers and return them keyed by staged path. + + xcb.h and xcbext.h ship in libxcb's source release; xproto.h does not + exist there and is generated from xcb-proto's XML by libxcb's c_client.py, + which is what libxcb itself does at build time. + """ + tarballs = { + name: download(source["url"], source["sha256"]) + for name, source in XCB_SOURCES.items() + } + index: dict[str, tuple[str, bytes]] = {} + with tempfile.TemporaryDirectory() as work_name: + work = Path(work_name) + libxcb = _extract_tarball(tarballs["libxcb"], work / "libxcb") + proto = _extract_tarball(tarballs["xcb-proto"], work / "xcb-proto") + for name in ("xcb.h", "xcbext.h"): + index[f"xcb/{name}"] = ( + XCB_SOURCES["libxcb"]["version"], + (libxcb / "src" / name).read_bytes(), + ) + generated = work / "generated" + generated.mkdir() + command = [ + sys.executable, + str(libxcb / "src" / "c_client.py"), + "-c", + XCB_SOURCES["libxcb"]["version"].replace("-", " "), + "-l", + XCB_MAN_PAGE, + "-s", + XCB_MAN_SUFFIX, + "-p", + str(proto), + str(proto / "src" / "xproto.xml"), + ] + result = subprocess.run( + command, cwd=generated, capture_output=True, text=True, check=False + ) + if result.returncode != 0: + raise SystemExit( + "c_client.py failed to generate xproto.h:\n" + result.stderr.strip() + ) + header = generated / "xproto.h" + if not header.exists(): + raise SystemExit("c_client.py produced no xproto.h") + index["xcb/xproto.h"] = ( + XCB_SOURCES["xcb-proto"]["version"], + header.read_bytes(), + ) + return index + + def upstream_index() -> dict[str, tuple[str, bytes]]: """Return ``{rel_path: (source_name, content)}`` merged across sources.""" index: dict[str, tuple[str, bytes]] = {} @@ -741,12 +839,19 @@ def _apply_patch(rel: str, content: bytes) -> bytes: return text.encode("utf-8") -def stamp_token() -> str: +def stamp_token(with_xcb: bool) -> str: lines = [ - "stamp-format=5", + "stamp-format=6", f"sync-script-sha256={sha256_of(Path(__file__).resolve())}", ] lines.extend(f"{src['name']}={src['version']}#{src['sha256']}" for src in SOURCES) + lines.append(f"xcb-staged={int(with_xcb)}") + if with_xcb: + lines.extend( + f"{name}={source['version']}#{source['sha256']}" + for name, source in sorted(XCB_SOURCES.items()) + ) + lines.append(f"xcb-c-client-args={XCB_MAN_PAGE}#{XCB_MAN_SUFFIX}") for src_name, basenames in sorted(SRC_WHITELIST.items()): lines.append(f"{src_name}-src={','.join(sorted(basenames))}") for src in SOURCES: @@ -877,7 +982,8 @@ def cmd_fetch(args: argparse.Namespace) -> int: if getattr(args, "subdir", None): return _restage_subdir(staging_root, args.subdir) stamp = dest / STAMP_NAME - token = stamp_token() + with_xcb = bool(getattr(args, "with_xcb", False)) + token = stamp_token(with_xcb) if ( not args.force and stamp.exists() @@ -888,9 +994,11 @@ def cmd_fetch(args: argparse.Namespace) -> int: # otherwise Make would re-run this recipe on every invocation. os.utime(stamp, None) return 0 - x11_root = dest / "X11" - if x11_root.exists(): - shutil.rmtree(x11_root) + for root_name in ("X11", "xcb"): + root = dest / root_name + if root.exists(): + shutil.rmtree(root) + for sub in _collect_staging_subdirs(): sub_dir = staging_root / sub if sub_dir.exists(): @@ -899,6 +1007,8 @@ def cmd_fetch(args: argparse.Namespace) -> int: stamp.unlink() dest.mkdir(parents=True, exist_ok=True) index = upstream_index() + if with_xcb: + index.update(xcb_index()) for rel, (_source, content) in index.items(): out_path = dest / rel # Defense in depth: even if relevant_member missed a traversal @@ -1015,6 +1125,11 @@ def main(argv: list[str]) -> int: p_fetch.add_argument( "--force", action="store_true", help="ignore the stamp and re-extract" ) + p_fetch.add_argument( + "--with-xcb", + action="store_true", + help="also build and stage the public xcb/ headers (XCB=1 builds only)", + ) p_fetch.add_argument( "--subdir", help="re-stage only this per-library src subdir (e.g. src-libXt), " From 0f347215e98a518a08719b11b270488159357fcd Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 31 Aug 2026 18:37:02 +0800 Subject: [PATCH 2/6] Add the XCB connection, event and bridge core An XCB client needs a connection handle, the server setup record, XID allocation and an event queue before a single request can be expressed, and none of those parts can be exercised without the others. An unchecked request reports its protocol error through the event queue, and the queue only changes hands through XSetEventQueueOwner, so splitting this into three commits would produce two that cannot be tested. The connection is a thin handle over an existing Display rather than a transport, so XGetXCBConnection returns the connection already bound to that display and both APIs observe one event source. Ownership follows libX11-xcb: after XSetEventQueueOwner(XCBOwnsEventQueue), the Xlib dequeue paths stop consuming. Hooks let the connection observe display close and cancelled event waits without src/ ever depending on the XCB layer, which keeps the core library free of a reverse dependency on a sibling shim. xcb_get_file_descriptor returns -1 because there is no socket to hand out. Clients that poll a connection FD have no equivalent here and must use the wait and poll entry points instead. XIDs move from a growing array to a chunked table under a mutex. xcb_generate_id is callable from any thread, and the old array handed out interior pointers that a concurrent realloc could invalidate. The chunked form keeps allocated slots pinned and lets a client reserve an id it chose itself, which the protocol expects: a client derives ids as base | (n & mask), so the advertised base and mask must not share a bit and the whole range has to fit the protocol's 29 bits. Both now come from one definition that the table and the setup record share. Installing xcb.pc and x11-xcb.pc drags x11.pc along, because x11-xcb.pc says "Requires: x11 xcb" and pkg-config would otherwise resolve x11 against the host's real libX11 and link it beside the shim. The generated files now spell their Libs and Cflags with the ${libdir} and ${includedir} variables instead of absolute build paths, so install can relocate them by rewriting the variable lines; pkg-config expands them, so an in-tree consumer reads exactly the flags it read before. The layer is opt-in through XCB=1. No in-tree workload needs it yet, and a default build should not grow two libraries, two pkg-config files and an installed include/xcb that nothing asks for. The CI jobs pass XCB=1, so the tests and the ABI gate still run on every push. No core request is advertised yet, so a client linking against an unimplemented opcode fails at link time instead of receiving a cookie that silently never completes. --- .github/workflows/ci.yml | 10 +- Makefile | 1 + compat/xcb-compat-private.h | 34 + compat/xcb-compat.c | 1536 ++++++++++++++++++++++++++++++ compat/xlib-xcb-compat.c | 12 + docs/XCB-COVERAGE.md | 155 +++ include/X11/Xlib-xcb.h | 12 + mk/config.mk | 8 + mk/install.mk | 26 +- mk/libxcb.mk | 76 ++ mk/pkgconfig.mk | 30 +- mk/tests.mk | 63 +- mk/wasm.mk | 4 + scripts/check-xcb-symbols.py | 75 ++ scripts/sync-upstream-headers.py | 11 +- src/display.c | 12 + src/display.h | 6 + src/events.c | 56 ++ src/events.h | 10 + src/resource-types.c | 227 ++++- src/resource-types.h | 10 + tests/check-api-symbols.py | 7 +- tests/check-host-link-audit.py | 21 +- tests/data/xcb-reference.txt | 9 + tests/private-symbols.txt | 10 +- tests/probe-system-xcb.c | 130 +++ tests/shim-symbols.txt | 21 +- tests/test-check-xcb-symbols.py | 49 + tests/test-xcb-events.c | 584 ++++++++++++ tests/test-xcb-link.c | 33 + tests/test-xcb-setup.c | 271 ++++++ tests/xcb-private-symbols.txt | 3 + tests/xcb-symbols.txt | 42 + tests/xlib-xcb-symbols.txt | 2 + 34 files changed, 3470 insertions(+), 86 deletions(-) create mode 100644 compat/xcb-compat-private.h create mode 100644 compat/xcb-compat.c create mode 100644 compat/xlib-xcb-compat.c create mode 100644 include/X11/Xlib-xcb.h create mode 100644 mk/libxcb.mk create mode 100644 scripts/check-xcb-symbols.py create mode 100644 tests/data/xcb-reference.txt create mode 100644 tests/probe-system-xcb.c create mode 100644 tests/test-check-xcb-symbols.py create mode 100644 tests/test-xcb-events.c create mode 100644 tests/test-xcb-link.c create mode 100644 tests/test-xcb-setup.c create mode 100644 tests/xcb-private-symbols.txt create mode 100644 tests/xcb-symbols.txt create mode 100644 tests/xlib-xcb-symbols.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d624e243..7231ef77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -245,7 +245,9 @@ jobs: UI_REPLAY_XVFB: --xvfb UI_REPLAY_SCREENSHOT_COMMAND: import UI_REPLAY_DISPLAY: 121 - run: make check-unit + # XCB=1: the XCB shim is off by default (mk/config.mk), so ask for it + # explicitly or its libraries, tests and ABI gate never build here. + run: make XCB=1 check-unit - name: Build bundled examples run: make examples -j"$(nproc)" @@ -323,7 +325,7 @@ jobs: UI_REPLAY_XVFB: --xvfb UI_REPLAY_SCREENSHOT_COMMAND: import UI_REPLAY_DISPLAY: 121 - run: make OPTFLAGS=-O0 CFLAGS_EXTRA=-DDEBUG_LIBX11_COMPAT check-unit + run: make XCB=1 OPTFLAGS=-O0 CFLAGS_EXTRA=-DDEBUG_LIBX11_COMPAT check-unit - name: ccache stats run: ccache --show-stats @@ -413,7 +415,7 @@ jobs: # autoconf which probes for Xutf8TextExtents via a link test # that fails when SAN_FLAGS leak into the upstream configure # ("Motif requires a UTF-8 capable libX11"). - make CFLAGS_EXTRA="$SAN_FLAGS" LDFLAGS="$SAN_FLAGS" check-unit + make XCB=1 CFLAGS_EXTRA="$SAN_FLAGS" LDFLAGS="$SAN_FLAGS" check-unit - name: ccache stats run: ccache --show-stats @@ -2138,7 +2140,7 @@ jobs: # dummy, software renderer) plus symbol coverage; it does not need # Xvfb. The make test runner also folds SDL_RUNTIME_LIBDIR (the sdl3 # pkg-config libdir) onto LD_LIBRARY_PATH so libSDL3 resolves. - run: make SDL_BACKEND=sdl3 check-unit + run: make XCB=1 SDL_BACKEND=sdl3 check-unit - name: Build bundled examples (SDL_BACKEND=sdl3) run: make SDL_BACKEND=sdl3 examples -j"$(nproc)" diff --git a/Makefile b/Makefile index 2fc51caa..a057d380 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ include mk/common.mk include mk/font-data.mk include mk/sdl-wrapper.mk include mk/library.mk +include mk/libxcb.mk include mk/libxt.mk include mk/libxpm.mk include mk/xcompat-libs.mk diff --git a/compat/xcb-compat-private.h b/compat/xcb-compat-private.h new file mode 100644 index 00000000..d0af8337 --- /dev/null +++ b/compat/xcb-compat-private.h @@ -0,0 +1,34 @@ +#ifndef LIBX11_COMPAT_XCB_PRIVATE_H +#define LIBX11_COMPAT_XCB_PRIVATE_H +#include +#include +typedef struct _XDisplay Display; +typedef union _XEvent XEvent; +Display *xcbCompatDisplay(xcb_connection_t *connection); +xcb_connection_t *xcbCompatConnectionForDisplay(Display *display); +void xcbCompatSetQueueOwner(xcb_connection_t *connection, int owner); +unsigned int xcbCompatEventWaiters(xcb_connection_t *connection); +void xcbCompatSetConnectionError(xcb_connection_t *connection, int error); +int xcbCompatRequestReady(xcb_connection_t *connection); +void xcbCompatReleaseRequestResources(xcb_connection_t *connection); +uint64_t xcbCompatNextSequence(xcb_connection_t *connection); +void xcbCompatSetNextSequence(xcb_connection_t *connection, uint64_t sequence); +void xcbCompatStorePending(xcb_connection_t *connection, + uint64_t sequence, + void *reply, + xcb_generic_error_t *error); +void xcbCompatStoreProtocolError(xcb_connection_t *connection, + uint64_t sequence, + uint8_t code, + uint32_t resource, + uint8_t opcode, + int checked); +void *xcbCompatTakeReply(xcb_connection_t *connection, + uint64_t sequence, + xcb_generic_error_t **error); +xcb_void_cookie_t xcbCompatVoidCookie(xcb_connection_t *connection, + uint8_t errorCode, + uint32_t resource, + uint8_t opcode, + int checked); +#endif diff --git a/compat/xcb-compat.c b/compat/xcb-compat.c new file mode 100644 index 00000000..5930ae88 --- /dev/null +++ b/compat/xcb-compat.c @@ -0,0 +1,1536 @@ +#include +#include +#include +#include +#include +#include +#include +#include "xcb-compat-private.h" +#include "../src/display.h" +#include "../src/events.h" +#include "../src/resource-types.h" + + + +typedef struct PendingReply { + uint64_t sequence; + void *reply; + xcb_generic_error_t *error; + struct PendingReply *next; +} PendingReply; + +typedef struct QueuedError { + xcb_generic_error_t *error; + struct QueuedError *next; +} QueuedError; + +typedef struct { + xcb_setup_t setup; + char vendor[16]; + xcb_format_t formats[4]; + xcb_screen_t screen; + xcb_depth_t depth; + xcb_visualtype_t visual; +} SetupWire; + +_Static_assert(offsetof(SetupWire, formats) == 56, "XCB format wire offset"); +_Static_assert(offsetof(SetupWire, screen) == 88, "XCB screen wire offset"); +_Static_assert(offsetof(SetupWire, depth) == 128, "XCB depth wire offset"); +_Static_assert(offsetof(SetupWire, visual) == 136, "XCB visual wire offset"); + +struct xcb_connection_t { + Display *display; + int error; + pthread_mutex_t mutex; + pthread_cond_t eventCond; + unsigned int eventUsers; + unsigned int closeCallbacks; + int closing; + uint64_t nextSequence; + PendingReply **pendingBuckets; + size_t pendingBucketCount; + size_t pendingCount; + QueuedError *errorHead; + QueuedError *errorTail; + SetupWire wire; + int ownsDisplay; + int queueOwner; + struct xcb_connection_t *next; +}; + +static pthread_mutex_t connectionsMutex = PTHREAD_MUTEX_INITIALIZER; +static xcb_connection_t *connections; + +__attribute__((weak)) void xcbCompatReleaseRequestResources( + xcb_connection_t *connection) +{ + (void) connection; +} + +/* Callers hold connectionsMutex. A display maps to at most one connection. */ +static xcb_connection_t *connectionForDisplayLocked(Display *display) +{ + for (xcb_connection_t *c = connections; c; c = c->next) + if (c->display == display) + return c; + return NULL; +} + +static int eventQueueOwner(Display *display) +{ + int owner = 0; + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t *c = connectionForDisplayLocked(display); + if (c) { + pthread_mutex_lock(&c->mutex); + owner = c->queueOwner; + pthread_mutex_unlock(&c->mutex); + } + pthread_mutex_unlock(&connectionsMutex); + return owner; +} + +static int eventWaitCancelled(Display *display) +{ + int cancelled = 0; + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t *c = connectionForDisplayLocked(display); + if (c) { + pthread_mutex_lock(&c->mutex); + cancelled = c->closing || c->errorHead != NULL; + pthread_mutex_unlock(&c->mutex); + } + pthread_mutex_unlock(&connectionsMutex); + return cancelled; +} + +/* Release everything a dead connection still holds. Caller holds c->mutex. + * Nothing can reap these once the connection is in error: xcb_request_check and + * the reply accessors both refuse a failed connection. + */ +static void drainPendingLocked(xcb_connection_t *c) +{ + for (size_t i = 0; i < c->pendingBucketCount; i++) { + PendingReply *pending = c->pendingBuckets[i]; + while (pending) { + PendingReply *next = pending->next; + free(pending->reply); + free(pending->error); + free(pending); + pending = next; + } + c->pendingBuckets[i] = NULL; + } + c->pendingCount = 0; + while (c->errorHead) { + QueuedError *queued = c->errorHead; + c->errorHead = queued->next; + free(queued->error); + free(queued); + } + c->errorTail = NULL; +} + +static void displayClosed(Display *display) +{ + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t *connection = connectionForDisplayLocked(display); + if (connection) { + pthread_mutex_lock(&connection->mutex); + connection->closeCallbacks++; + pthread_mutex_unlock(&connection->mutex); + } + pthread_mutex_unlock(&connectionsMutex); + if (!connection) + return; + + pthread_mutex_lock(&connection->mutex); + connection->closing = 1; + connection->error = XCB_CONN_ERROR; + while (connection->eventUsers) + pthread_cond_wait(&connection->eventCond, &connection->mutex); + connection->display = NULL; + connection->ownsDisplay = 0; + + /* The wrapper itself stays registered and errored, because a client that + * kept the XGetXCBConnection pointer must get a failed connection rather + * than a dangling one. What it can no longer reach goes now. + */ + drainPendingLocked(connection); + pthread_mutex_unlock(&connection->mutex); + + xcbCompatReleaseRequestResources(connection); + pthread_mutex_lock(&connection->mutex); + connection->closeCallbacks--; + pthread_cond_broadcast(&connection->eventCond); + pthread_mutex_unlock(&connection->mutex); +} + +static void installDisplayHooks(void) +{ + libx11CompatSetDisplayCloseHook(displayClosed); + libx11CompatSetEventQueueOwnerHook(eventQueueOwner); + libx11CompatSetEventWaitCancelledHook(eventWaitCancelled); +} + +static void registerConnection(xcb_connection_t *c) +{ + installDisplayHooks(); + pthread_mutex_lock(&connectionsMutex); + c->next = connections; + connections = c; + pthread_mutex_unlock(&connectionsMutex); +} + +/* Publish c for its display unless another thread got there first, in which + * case the winner is returned and c is left for the caller to discard. The + * lookup and the insertion happen under one acquisition, so a display can never + * end up with two connection wrappers. + */ +static xcb_connection_t *registerConnectionForDisplay(xcb_connection_t *c) +{ + installDisplayHooks(); + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t *existing = connectionForDisplayLocked(c->display); + if (!existing) { + c->next = connections; + connections = c; + } + pthread_mutex_unlock(&connectionsMutex); + return existing; +} + +static void unregisterConnection(xcb_connection_t *c) +{ + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t **link = &connections; + while (*link && *link != c) + link = &(*link)->next; + if (*link) + *link = c->next; + pthread_mutex_unlock(&connectionsMutex); +} + +static void initializeSetup(xcb_connection_t *connection) +{ + Display *display = connection->display; + SetupWire *wire = &connection->wire; + xcb_setup_t *setup = &wire->setup; + xcb_screen_t *screen = &wire->screen; + Visual *visual = DefaultVisual(display, DefaultScreen(display)); + int depth = DefaultDepth(display, DefaultScreen(display)); + memset(wire, 0, sizeof(*wire)); + setup->status = 1; + setup->protocol_major_version = 11; + setup->length = (sizeof(*wire) - 8) / 4; + setup->release_number = 1; + setup->resource_id_base = XID_RESOURCE_BASE; + setup->resource_id_mask = XID_RESOURCE_MASK; + setup->vendor_len = sizeof("libx11-compat") - 1; + setup->maximum_request_length = 65535; + setup->roots_len = 1; + setup->pixmap_formats_len = 4; + setup->image_byte_order = ImageByteOrder(display); + setup->bitmap_format_bit_order = BitmapBitOrder(display); + setup->bitmap_format_scanline_unit = BitmapUnit(display); + setup->bitmap_format_scanline_pad = BitmapPad(display); + int minimumKeycode, maximumKeycode; + XDisplayKeycodes(display, &minimumKeycode, &maximumKeycode); + setup->min_keycode = minimumKeycode; + setup->max_keycode = maximumKeycode; + memcpy(wire->vendor, "libx11-compat", setup->vendor_len); + static const xcb_format_t formats[] = { + {.depth = 1, .bits_per_pixel = 1, .scanline_pad = 32}, + {.depth = 16, .bits_per_pixel = 16, .scanline_pad = 32}, + {.depth = 24, .bits_per_pixel = 32, .scanline_pad = 32}, + {.depth = 32, .bits_per_pixel = 32, .scanline_pad = 32}, + }; + memcpy(wire->formats, formats, sizeof(formats)); + screen->root = DefaultRootWindow(display); + screen->default_colormap = DefaultColormap(display, DefaultScreen(display)); + screen->white_pixel = WhitePixel(display, DefaultScreen(display)); + screen->black_pixel = BlackPixel(display, DefaultScreen(display)); + screen->width_in_pixels = DisplayWidth(display, DefaultScreen(display)); + screen->height_in_pixels = DisplayHeight(display, DefaultScreen(display)); + screen->width_in_millimeters = + DisplayWidthMM(display, DefaultScreen(display)); + screen->height_in_millimeters = + DisplayHeightMM(display, DefaultScreen(display)); + screen->min_installed_maps = screen->max_installed_maps = 1; + screen->root_visual = XVisualIDFromVisual(visual); + screen->backing_stores = DoesBackingStore(DefaultScreenOfDisplay(display)); + screen->save_unders = DoesSaveUnders(DefaultScreenOfDisplay(display)); + screen->root_depth = depth; + screen->allowed_depths_len = 1; + wire->depth.depth = depth; + wire->depth.visuals_len = 1; + wire->visual.visual_id = XVisualIDFromVisual(visual); + wire->visual._class = visual->class; + wire->visual.bits_per_rgb_value = visual->bits_per_rgb; + wire->visual.colormap_entries = visual->map_entries; + wire->visual.red_mask = visual->red_mask; + wire->visual.green_mask = visual->green_mask; + wire->visual.blue_mask = visual->blue_mask; +} + +static xcb_connection_t *allocConnection(void) +{ + xcb_connection_t *c = calloc(1, sizeof(*c)); + if (!c) + return NULL; + if (pthread_mutex_init(&c->mutex, NULL) != 0) { + free(c); + return NULL; + } + if (pthread_cond_init(&c->eventCond, NULL) != 0) { + pthread_mutex_destroy(&c->mutex); + free(c); + return NULL; + } + c->nextSequence = 1; + return c; +} + +static void freeConnection(xcb_connection_t *c) +{ + pthread_cond_destroy(&c->eventCond); + pthread_mutex_destroy(&c->mutex); + free(c); +} + +xcb_connection_t *xcb_connect(const char *name, int *screen) +{ + xcb_connection_t *c = allocConnection(); + if (!c) + return NULL; + if (screen) + *screen = 0; + c->display = XOpenDisplay(name); + if (!c->display) { + c->error = XCB_CONN_ERROR; + return c; + } + c->ownsDisplay = 1; + initializeSetup(c); + registerConnection(c); + return c; +} + +xcb_connection_t *xcbCompatConnectionForDisplay(Display *display) +{ + if (!display) + return NULL; + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t *existing = connectionForDisplayLocked(display); + pthread_mutex_unlock(&connectionsMutex); + if (existing) + return existing; + xcb_connection_t *c = allocConnection(); + if (!c) + return NULL; + c->display = display; + initializeSetup(c); + + /* Another thread may have bound this display while the setup record was + * being built, so publish and re-check as one step and keep the winner. + */ + existing = registerConnectionForDisplay(c); + if (existing) { + freeConnection(c); + return existing; + } + return c; +} + +/* Number of threads currently blocked inside an event wait on this connection. + * Exposed for tests, which otherwise have no way to tell a thread that is about + * to block from one that already is. + */ +unsigned int xcbCompatEventWaiters(xcb_connection_t *c) +{ + if (!c) + return 0; + pthread_mutex_lock(&c->mutex); + unsigned int waiters = c->eventUsers; + pthread_mutex_unlock(&c->mutex); + return waiters; +} + +void xcbCompatSetQueueOwner(xcb_connection_t *c, int owner) +{ + if (c) { + pthread_mutex_lock(&c->mutex); + c->queueOwner = owner != 0; + pthread_mutex_unlock(&c->mutex); + } +} + +xcb_connection_t *xcb_connect_to_display_with_auth_info(const char *display, + xcb_auth_info_t *auth, + int *screen) +{ + (void) auth; + return xcb_connect(display, screen); +} + +int xcb_connection_has_error(xcb_connection_t *c) +{ + if (!c) + return XCB_CONN_ERROR; + pthread_mutex_lock(&c->mutex); + int error = c->error; + pthread_mutex_unlock(&c->mutex); + return error; +} + +void xcbCompatSetConnectionError(xcb_connection_t *c, int error) +{ + if (!c || !error) + return; + pthread_mutex_lock(&c->mutex); + if (!c->error) + c->error = error; + pthread_mutex_unlock(&c->mutex); +} +int xcbCompatRequestReady(xcb_connection_t *c) +{ + if (!c) + return 0; + pthread_mutex_lock(&c->mutex); + int ready = c->display && !c->error && !c->closing; + pthread_mutex_unlock(&c->mutex); + return ready; +} +int xcb_get_file_descriptor(xcb_connection_t *c) +{ + (void) c; + return -1; +} +const xcb_setup_t *xcb_get_setup(xcb_connection_t *c) +{ + return (!c || c->error) ? NULL : &c->wire.setup; +} +uint32_t xcb_get_maximum_request_length(xcb_connection_t *c) +{ + const xcb_setup_t *s = xcb_get_setup(c); + return s ? s->maximum_request_length : 0; +} +uint32_t xcb_generate_id(xcb_connection_t *c) +{ + return (!c || c->error) ? UINT32_MAX : allocXidResource(); +} + +typedef struct { + int type; + Display *display; + uint64_t serial; + uint8_t synthetic, detail, mode, format, sameScreen, focus, + overrideRedirect; + uint32_t window, event, root, child, sibling, parent, owner, requestor, + selection, target, property, colormap, atom, messageType, time, state; + int16_t x, y, rootX, rootY; + uint16_t unsignedX, unsignedY, width, height, borderWidth, count, + minorOpcode; + uint8_t fromConfigure, newColormap, majorOpcode, firstKeycode, keys[32], + data[20]; +} NormalizedEvent; + +static void normalizeEvent(const XEvent *x, NormalizedEvent *n) +{ + memset(n, 0, sizeof(*n)); + n->type = x->type; + n->display = x->xany.display; + n->serial = x->xany.serial; + n->synthetic = x->xany.send_event; + switch (x->type) { + case KeyPress: + case KeyRelease: + n->detail = x->xkey.keycode; + n->time = x->xkey.time; + n->root = x->xkey.root; + n->window = x->xkey.window; + n->child = x->xkey.subwindow; + n->rootX = x->xkey.x_root; + n->rootY = x->xkey.y_root; + n->x = x->xkey.x; + n->y = x->xkey.y; + n->state = x->xkey.state; + n->sameScreen = x->xkey.same_screen; + break; + case ButtonPress: + case ButtonRelease: + n->detail = x->xbutton.button; + n->time = x->xbutton.time; + n->root = x->xbutton.root; + n->window = x->xbutton.window; + n->child = x->xbutton.subwindow; + n->rootX = x->xbutton.x_root; + n->rootY = x->xbutton.y_root; + n->x = x->xbutton.x; + n->y = x->xbutton.y; + n->state = x->xbutton.state; + n->sameScreen = x->xbutton.same_screen; + break; + case MotionNotify: + n->detail = x->xmotion.is_hint; + n->time = x->xmotion.time; + n->root = x->xmotion.root; + n->window = x->xmotion.window; + n->child = x->xmotion.subwindow; + n->rootX = x->xmotion.x_root; + n->rootY = x->xmotion.y_root; + n->x = x->xmotion.x; + n->y = x->xmotion.y; + n->state = x->xmotion.state; + n->sameScreen = x->xmotion.same_screen; + break; + case EnterNotify: + case LeaveNotify: + n->detail = x->xcrossing.detail; + n->time = x->xcrossing.time; + n->root = x->xcrossing.root; + n->window = x->xcrossing.window; + n->child = x->xcrossing.subwindow; + n->rootX = x->xcrossing.x_root; + n->rootY = x->xcrossing.y_root; + n->x = x->xcrossing.x; + n->y = x->xcrossing.y; + n->state = x->xcrossing.state; + n->mode = x->xcrossing.mode; + n->sameScreen = x->xcrossing.same_screen; + n->focus = x->xcrossing.focus; + break; + case FocusIn: + case FocusOut: + n->detail = x->xfocus.detail; + n->window = x->xfocus.window; + n->mode = x->xfocus.mode; + break; + case KeymapNotify: + n->window = x->xkeymap.window; + memcpy(n->keys, x->xkeymap.key_vector, sizeof(n->keys)); + break; + case Expose: + n->window = x->xexpose.window; + n->x = x->xexpose.x; + n->y = x->xexpose.y; + n->width = x->xexpose.width; + n->height = x->xexpose.height; + n->count = x->xexpose.count; + break; + case GraphicsExpose: + n->window = x->xgraphicsexpose.drawable; + n->unsignedX = x->xgraphicsexpose.x; + n->unsignedY = x->xgraphicsexpose.y; + n->width = x->xgraphicsexpose.width; + n->height = x->xgraphicsexpose.height; + n->count = x->xgraphicsexpose.count; + n->majorOpcode = x->xgraphicsexpose.major_code; + n->minorOpcode = x->xgraphicsexpose.minor_code; + break; + case NoExpose: + n->window = x->xnoexpose.drawable; + n->majorOpcode = x->xnoexpose.major_code; + n->minorOpcode = x->xnoexpose.minor_code; + break; + case VisibilityNotify: + n->window = x->xvisibility.window; + n->state = x->xvisibility.state; + break; + case ConfigureNotify: + n->event = x->xconfigure.event; + n->window = x->xconfigure.window; + n->sibling = x->xconfigure.above; + n->x = x->xconfigure.x; + n->y = x->xconfigure.y; + n->width = x->xconfigure.width; + n->height = x->xconfigure.height; + n->borderWidth = x->xconfigure.border_width; + n->overrideRedirect = x->xconfigure.override_redirect; + break; + case CreateNotify: + n->parent = x->xcreatewindow.parent; + n->window = x->xcreatewindow.window; + n->x = x->xcreatewindow.x; + n->y = x->xcreatewindow.y; + n->width = x->xcreatewindow.width; + n->height = x->xcreatewindow.height; + n->borderWidth = x->xcreatewindow.border_width; + n->overrideRedirect = x->xcreatewindow.override_redirect; + break; + case MapNotify: + n->event = x->xmap.event; + n->window = x->xmap.window; + n->overrideRedirect = x->xmap.override_redirect; + break; + case UnmapNotify: + n->event = x->xunmap.event; + n->window = x->xunmap.window; + n->fromConfigure = x->xunmap.from_configure; + break; + case ReparentNotify: + n->event = x->xreparent.event; + n->window = x->xreparent.window; + n->parent = x->xreparent.parent; + n->x = x->xreparent.x; + n->y = x->xreparent.y; + n->overrideRedirect = x->xreparent.override_redirect; + break; + case GravityNotify: + n->event = x->xgravity.event; + n->window = x->xgravity.window; + n->x = x->xgravity.x; + n->y = x->xgravity.y; + break; + case MapRequest: + n->parent = x->xmaprequest.parent; + n->window = x->xmaprequest.window; + break; + case ConfigureRequest: + n->parent = x->xconfigurerequest.parent; + n->window = x->xconfigurerequest.window; + n->sibling = x->xconfigurerequest.above; + n->x = x->xconfigurerequest.x; + n->y = x->xconfigurerequest.y; + n->width = x->xconfigurerequest.width; + n->height = x->xconfigurerequest.height; + n->borderWidth = x->xconfigurerequest.border_width; + n->detail = x->xconfigurerequest.detail; + n->state = x->xconfigurerequest.value_mask; + break; + case ResizeRequest: + n->window = x->xresizerequest.window; + n->width = x->xresizerequest.width; + n->height = x->xresizerequest.height; + break; + case CirculateNotify: + n->event = x->xcirculate.event; + n->window = x->xcirculate.window; + n->detail = x->xcirculate.place; + break; + case CirculateRequest: + n->event = x->xcirculaterequest.parent; + n->window = x->xcirculaterequest.window; + n->detail = x->xcirculaterequest.place; + break; + case PropertyNotify: + n->window = x->xproperty.window; + n->atom = x->xproperty.atom; + n->time = x->xproperty.time; + n->state = x->xproperty.state; + break; + case SelectionClear: + n->time = x->xselectionclear.time; + n->owner = x->xselectionclear.window; + n->selection = x->xselectionclear.selection; + break; + case SelectionRequest: + n->time = x->xselectionrequest.time; + n->owner = x->xselectionrequest.owner; + n->requestor = x->xselectionrequest.requestor; + n->selection = x->xselectionrequest.selection; + n->target = x->xselectionrequest.target; + n->property = x->xselectionrequest.property; + break; + case SelectionNotify: + n->time = x->xselection.time; + n->requestor = x->xselection.requestor; + n->selection = x->xselection.selection; + n->target = x->xselection.target; + n->property = x->xselection.property; + break; + case ColormapNotify: + n->window = x->xcolormap.window; + n->colormap = x->xcolormap.colormap; + n->newColormap = x->xcolormap.new; + n->state = x->xcolormap.state; + break; + case MappingNotify: + n->window = x->xmapping.window; + n->detail = x->xmapping.request; + n->firstKeycode = x->xmapping.first_keycode; + n->count = x->xmapping.count; + break; + case DestroyNotify: + n->event = x->xdestroywindow.event; + n->window = x->xdestroywindow.window; + break; + case ClientMessage: + n->format = x->xclient.format; + n->window = x->xclient.window; + n->messageType = x->xclient.message_type; + memcpy(n->data, x->xclient.data.b, sizeof(n->data)); + break; + } +} + +static xcb_generic_event_t *convertEvent(xcb_connection_t *c, const XEvent *x) +{ + _Static_assert(sizeof(xcb_raw_generic_event_t) == 32, + "XCB core event wire size"); + _Static_assert(offsetof(xcb_generic_event_t, full_sequence) == 32, + "XCB full sequence trailer offset"); + xcb_generic_event_t *generic = calloc(1, sizeof(*generic)); + if (!generic) + return NULL; + NormalizedEvent n; + normalizeEvent(x, &n); + generic->response_type = (uint8_t) n.type | (n.synthetic ? 0x80 : 0); + generic->sequence = (uint16_t) n.serial; + generic->full_sequence = (uint32_t) n.serial; + switch (n.type) { + case KeyPress: + case KeyRelease: + case ButtonPress: + case ButtonRelease: + case MotionNotify: { + xcb_key_press_event_t *e = (xcb_key_press_event_t *) generic; + e->detail = n.detail; + e->time = n.time; + e->root = n.root; + e->event = n.window; + e->child = n.child; + e->root_x = n.rootX; + e->root_y = n.rootY; + e->event_x = n.x; + e->event_y = n.y; + e->state = n.state; + e->same_screen = n.sameScreen; + break; + } + case EnterNotify: + case LeaveNotify: { + xcb_enter_notify_event_t *e = (xcb_enter_notify_event_t *) generic; + e->detail = n.detail; + e->time = n.time; + e->root = n.root; + e->event = n.window; + e->child = n.child; + e->root_x = n.rootX; + e->root_y = n.rootY; + e->event_x = n.x; + e->event_y = n.y; + e->state = n.state; + e->mode = n.mode; + e->same_screen_focus = (n.sameScreen ? 1u : 0u) | (n.focus ? 2u : 0u); + break; + } + case FocusIn: + case FocusOut: { + xcb_focus_in_event_t *e = (xcb_focus_in_event_t *) generic; + e->detail = n.detail; + e->event = n.window; + e->mode = n.mode; + break; + } + case KeymapNotify: { + xcb_keymap_notify_event_t *e = (xcb_keymap_notify_event_t *) generic; + memcpy(e->keys, n.keys + 1, sizeof(e->keys)); + break; + } + case Expose: { + xcb_expose_event_t *e = (xcb_expose_event_t *) generic; + e->window = n.window; + e->x = n.x; + e->y = n.y; + e->width = n.width; + e->height = n.height; + e->count = n.count; + break; + } + case GraphicsExpose: { + xcb_graphics_exposure_event_t *e = + (xcb_graphics_exposure_event_t *) generic; + e->drawable = n.window; + e->x = n.unsignedX; + e->y = n.unsignedY; + e->width = n.width; + e->height = n.height; + e->minor_opcode = n.minorOpcode; + e->count = n.count; + e->major_opcode = n.majorOpcode; + break; + } + case NoExpose: { + xcb_no_exposure_event_t *e = (xcb_no_exposure_event_t *) generic; + e->drawable = n.window; + e->minor_opcode = n.minorOpcode; + e->major_opcode = n.majorOpcode; + break; + } + case VisibilityNotify: { + xcb_visibility_notify_event_t *e = + (xcb_visibility_notify_event_t *) generic; + e->window = n.window; + e->state = n.state; + break; + } + case ConfigureNotify: { + xcb_configure_notify_event_t *e = + (xcb_configure_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->above_sibling = n.sibling; + e->x = n.x; + e->y = n.y; + e->width = n.width; + e->height = n.height; + e->border_width = n.borderWidth; + e->override_redirect = n.overrideRedirect; + break; + } + case CreateNotify: { + xcb_create_notify_event_t *e = (xcb_create_notify_event_t *) generic; + e->parent = n.parent; + e->window = n.window; + e->x = n.x; + e->y = n.y; + e->width = n.width; + e->height = n.height; + e->border_width = n.borderWidth; + e->override_redirect = n.overrideRedirect; + break; + } + case MapNotify: { + xcb_map_notify_event_t *e = (xcb_map_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->override_redirect = n.overrideRedirect; + break; + } + case UnmapNotify: { + xcb_unmap_notify_event_t *e = (xcb_unmap_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->from_configure = n.fromConfigure; + break; + } + case ReparentNotify: { + xcb_reparent_notify_event_t *e = + (xcb_reparent_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->parent = n.parent; + e->x = n.x; + e->y = n.y; + e->override_redirect = n.overrideRedirect; + break; + } + case GravityNotify: { + xcb_gravity_notify_event_t *e = (xcb_gravity_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->x = n.x; + e->y = n.y; + break; + } + case MapRequest: { + xcb_map_request_event_t *e = (xcb_map_request_event_t *) generic; + e->parent = n.parent; + e->window = n.window; + break; + } + case ConfigureRequest: { + xcb_configure_request_event_t *e = + (xcb_configure_request_event_t *) generic; + e->stack_mode = n.detail; + e->parent = n.parent; + e->window = n.window; + e->sibling = n.sibling; + e->x = n.x; + e->y = n.y; + e->width = n.width; + e->height = n.height; + e->border_width = n.borderWidth; + e->value_mask = n.state; + break; + } + case ResizeRequest: { + xcb_resize_request_event_t *e = (xcb_resize_request_event_t *) generic; + e->window = n.window; + e->width = n.width; + e->height = n.height; + break; + } + case CirculateNotify: + case CirculateRequest: { + xcb_circulate_notify_event_t *e = + (xcb_circulate_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->place = n.detail; + break; + } + case PropertyNotify: { + xcb_property_notify_event_t *e = + (xcb_property_notify_event_t *) generic; + e->window = n.window; + e->atom = n.atom; + e->time = n.time; + e->state = n.state; + break; + } + case SelectionClear: { + xcb_selection_clear_event_t *e = + (xcb_selection_clear_event_t *) generic; + e->time = n.time; + e->owner = n.owner; + e->selection = n.selection; + break; + } + case SelectionRequest: { + xcb_selection_request_event_t *e = + (xcb_selection_request_event_t *) generic; + e->time = n.time; + e->owner = n.owner; + e->requestor = n.requestor; + e->selection = n.selection; + e->target = n.target; + e->property = n.property; + break; + } + case SelectionNotify: { + xcb_selection_notify_event_t *e = + (xcb_selection_notify_event_t *) generic; + e->time = n.time; + e->requestor = n.requestor; + e->selection = n.selection; + e->target = n.target; + e->property = n.property; + break; + } + case ColormapNotify: { + xcb_colormap_notify_event_t *e = + (xcb_colormap_notify_event_t *) generic; + e->window = n.window; + e->colormap = n.colormap; + e->_new = n.newColormap; + e->state = n.state; + break; + } + case MappingNotify: { + xcb_mapping_notify_event_t *e = (xcb_mapping_notify_event_t *) generic; + e->request = n.detail; + e->first_keycode = n.firstKeycode; + e->count = n.count; + break; + } + case DestroyNotify: { + xcb_destroy_notify_event_t *e = (xcb_destroy_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + break; + } + case ClientMessage: { + xcb_client_message_event_t *e = (xcb_client_message_event_t *) generic; + e->format = n.format; + e->window = n.window; + e->type = n.messageType; + memcpy(e->data.data8, n.data, sizeof(e->data.data8)); + break; + } + default: + break; + } + (void) c; + return generic; +} + +static xcb_generic_event_t *takeQueuedErrorLocked(xcb_connection_t *c) +{ + QueuedError *queued = c->errorHead; + if (!queued) + return NULL; + c->errorHead = queued->next; + if (!c->errorHead) + c->errorTail = NULL; + xcb_generic_event_t *error = (xcb_generic_event_t *) queued->error; + free(queued); + return error; +} + +static xcb_generic_event_t *takeEvent(xcb_connection_t *c, int mode) +{ + if (!c) + return NULL; + pthread_mutex_lock(&c->mutex); + xcb_generic_event_t *queuedError = takeQueuedErrorLocked(c); + if (queuedError) { + pthread_mutex_unlock(&c->mutex); + return queuedError; + } + if (!c->display || c->error || c->closing) { + pthread_mutex_unlock(&c->mutex); + return NULL; + } + Display *display = c->display; + c->eventUsers++; + pthread_mutex_unlock(&c->mutex); + + libx11CompatBeginXcbEventAccess(); + XEvent event = {0}; + int haveEvent = mode < 0 || XEventsQueued(display, mode) != 0; + while (haveEvent) { + XNextEvent(display, &event); + pthread_mutex_lock(&c->mutex); + int interrupted = c->closing; + if (event.type == 0 && !interrupted) + queuedError = takeQueuedErrorLocked(c); + pthread_mutex_unlock(&c->mutex); + if (queuedError || interrupted || event.type != 0) + break; + } + libx11CompatEndXcbEventAccess(); + + pthread_mutex_lock(&c->mutex); + int closing = c->closing; + pthread_mutex_unlock(&c->mutex); + xcb_generic_event_t *converted = + haveEvent && !closing && !queuedError ? convertEvent(c, &event) : NULL; + + pthread_mutex_lock(&c->mutex); + if (haveEvent && !closing && !queuedError && !converted) + c->error = XCB_CONN_CLOSED_MEM_INSUFFICIENT; + c->eventUsers--; + pthread_cond_broadcast(&c->eventCond); + pthread_mutex_unlock(&c->mutex); + return queuedError ? queuedError : converted; +} + +xcb_generic_event_t *xcb_poll_for_event(xcb_connection_t *c) +{ + return takeEvent(c, QueuedAfterReading); +} + +xcb_generic_event_t *xcb_poll_for_queued_event(xcb_connection_t *c) +{ + return takeEvent(c, QueuedAlready); +} + +xcb_generic_event_t *xcb_wait_for_event(xcb_connection_t *c) +{ + return takeEvent(c, -1); +} + +xcb_generic_event_t *xcb_poll_for_special_event(xcb_connection_t *c, + xcb_special_event_t *special) +{ + (void) c; + (void) special; + return NULL; +} + +char *xcb_setup_vendor(const xcb_setup_t *s) +{ + return s ? (char *) (s + 1) : NULL; +} + +int xcb_setup_vendor_length(const xcb_setup_t *s) +{ + return s ? s->vendor_len : 0; +} + +xcb_generic_iterator_t xcb_setup_vendor_end(const xcb_setup_t *s) +{ + xcb_generic_iterator_t end = {0}; + if (s) { + end.data = xcb_setup_vendor(s) + s->vendor_len; + end.index = sizeof(*s) + s->vendor_len; + } + return end; +} + +xcb_format_t *xcb_setup_pixmap_formats(const xcb_setup_t *s) +{ + if (!s) + return NULL; + size_t vendor = (s->vendor_len + 3u) & ~3u; + return (xcb_format_t *) ((char *) (s + 1) + vendor); +} + +int xcb_setup_pixmap_formats_length(const xcb_setup_t *s) +{ + return s ? s->pixmap_formats_len : 0; +} + +xcb_format_iterator_t xcb_setup_pixmap_formats_iterator(const xcb_setup_t *s) +{ + xcb_format_iterator_t i = {0}; + if (s) { + i.data = xcb_setup_pixmap_formats(s); + i.rem = s->pixmap_formats_len; + i.index = (char *) i.data - (char *) s; + } + return i; +} + +void xcb_format_next(xcb_format_iterator_t *i) +{ + if (!i || i->rem <= 0) + return; + i->data++; + i->index += sizeof(*i->data); + i->rem--; +} + +xcb_generic_iterator_t xcb_format_end(xcb_format_iterator_t i) +{ + while (i.rem > 0) + xcb_format_next(&i); + xcb_generic_iterator_t end = { + .data = i.data, + .rem = 0, + .index = i.index, + }; + return end; +} + +xcb_screen_iterator_t xcb_setup_roots_iterator(const xcb_setup_t *s) +{ + xcb_screen_iterator_t i = {0}; + if (!s) + return i; + size_t vendor = (s->vendor_len + 3u) & ~3u; + i.data = (xcb_screen_t *) ((char *) (s + 1) + vendor + + s->pixmap_formats_len * sizeof(xcb_format_t)); + i.rem = s->roots_len; + i.index = (char *) i.data - (char *) s; + return i; +} +int xcb_setup_roots_length(const xcb_setup_t *s) +{ + return s ? s->roots_len : 0; +} +xcb_depth_iterator_t xcb_screen_allowed_depths_iterator(const xcb_screen_t *s) +{ + xcb_depth_iterator_t i = {0}; + if (s) { + i.data = (xcb_depth_t *) (s + 1); + i.rem = s->allowed_depths_len; + i.index = sizeof(*s); + } + return i; +} +int xcb_screen_allowed_depths_length(const xcb_screen_t *s) +{ + return s ? s->allowed_depths_len : 0; +} +xcb_visualtype_iterator_t xcb_depth_visuals_iterator(const xcb_depth_t *d) +{ + xcb_visualtype_iterator_t i = {0}; + if (d) { + i.data = (xcb_visualtype_t *) (d + 1); + i.rem = d->visuals_len; + i.index = sizeof(*d); + } + return i; +} +xcb_visualtype_t *xcb_depth_visuals(const xcb_depth_t *d) +{ + return d ? (xcb_visualtype_t *) (d + 1) : NULL; +} +int xcb_depth_sizeof(const void *buffer) +{ + if (!buffer) + return 0; + const xcb_depth_t *depth = buffer; + uint64_t size = sizeof(*depth) + + (uint64_t) depth->visuals_len * sizeof(xcb_visualtype_t); + return size <= INT_MAX ? (int) size : 0; +} +int xcb_depth_visuals_length(const xcb_depth_t *d) +{ + return d ? d->visuals_len : 0; +} +void xcb_depth_next(xcb_depth_iterator_t *i) +{ + if (!i || i->rem <= 0) + return; + size_t bytes = + sizeof(*i->data) + i->data->visuals_len * sizeof(xcb_visualtype_t); + i->data = (xcb_depth_t *) ((char *) i->data + bytes); + i->index += bytes; + i->rem--; +} +xcb_generic_iterator_t xcb_depth_end(xcb_depth_iterator_t i) +{ + while (i.rem > 0) + xcb_depth_next(&i); + xcb_generic_iterator_t end = { + .data = i.data, + .rem = 0, + .index = i.index, + }; + return end; +} +void xcb_visualtype_next(xcb_visualtype_iterator_t *i) +{ + if (!i || i->rem <= 0) + return; + i->data++; + i->index += sizeof(*i->data); + i->rem--; +} +xcb_generic_iterator_t xcb_visualtype_end(xcb_visualtype_iterator_t i) +{ + while (i.rem > 0) + xcb_visualtype_next(&i); + xcb_generic_iterator_t end = { + .data = i.data, + .rem = 0, + .index = i.index, + }; + return end; +} +int xcb_screen_sizeof(const void *buffer) +{ + if (!buffer) + return 0; + const xcb_screen_t *screen = buffer; + const xcb_depth_t *depth = (const xcb_depth_t *) (screen + 1); + uint64_t size = sizeof(*screen); + for (uint8_t i = 0; i < screen->allowed_depths_len; i++) { + int depthSize = xcb_depth_sizeof(depth); + if (!depthSize || size + (uint64_t) depthSize > INT_MAX) + return 0; + size += (uint64_t) depthSize; + depth = (const xcb_depth_t *) ((const char *) depth + depthSize); + } + return (int) size; +} +void xcb_screen_next(xcb_screen_iterator_t *i) +{ + if (!i || i->rem <= 0) + return; + xcb_depth_iterator_t depths = xcb_screen_allowed_depths_iterator(i->data); + xcb_generic_iterator_t end = xcb_depth_end(depths); + size_t bytes = (char *) end.data - (char *) i->data; + i->data = (xcb_screen_t *) ((char *) i->data + bytes); + i->index += bytes; + i->rem--; +} +xcb_generic_iterator_t xcb_screen_end(xcb_screen_iterator_t i) +{ + while (i.rem > 0) + xcb_screen_next(&i); + xcb_generic_iterator_t end = { + .data = i.data, + .rem = 0, + .index = i.index, + }; + return end; +} +int xcb_setup_sizeof(const void *buffer) +{ + if (!buffer) + return 0; + const xcb_setup_t *setup = buffer; + uint64_t vendorSize = ((uint64_t) setup->vendor_len + 3u) & ~UINT64_C(3); + uint64_t size = sizeof(*setup) + vendorSize + + (uint64_t) setup->pixmap_formats_len * sizeof(xcb_format_t); + if (size > INT_MAX) + return 0; + const xcb_screen_t *screen = + (const xcb_screen_t *) ((const char *) setup + (size_t) size); + for (uint8_t i = 0; i < setup->roots_len; i++) { + int screenSize = xcb_screen_sizeof(screen); + if (!screenSize || size + (uint64_t) screenSize > INT_MAX) + return 0; + size += (uint64_t) screenSize; + screen = (const xcb_screen_t *) ((const char *) screen + screenSize); + } + return (int) size; +} +int xcb_flush(xcb_connection_t *c) +{ + if (!c || c->error) + return 0; + XFlush(c->display); + return 1; +} + +Display *xcbCompatDisplay(xcb_connection_t *c) +{ + return c ? c->display : NULL; +} + +xcb_void_cookie_t xcbCompatVoidCookie(xcb_connection_t *c, + uint8_t errorCode, + uint32_t resource, + uint8_t opcode, + int checked) +{ + if (!xcbCompatRequestReady(c)) + return (xcb_void_cookie_t) {.sequence = 0}; + uint64_t sequence = xcbCompatNextSequence(c); + xcb_void_cookie_t cookie = {.sequence = (unsigned int) sequence}; + if (errorCode) + xcbCompatStoreProtocolError(c, sequence, errorCode, resource, opcode, + checked); + else if (checked) + xcbCompatStorePending(c, sequence, NULL, NULL); + return cookie; +} + +void xcbCompatStoreProtocolError(xcb_connection_t *c, + uint64_t sequence, + uint8_t code, + uint32_t resource, + uint8_t opcode, + int checked) +{ + xcb_generic_error_t *error = calloc(1, sizeof(*error)); + if (!error) { + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return; + } + error->error_code = code; + error->sequence = sequence; + error->full_sequence = sequence; + error->resource_id = resource; + error->major_code = opcode; + if (checked) { + xcbCompatStorePending(c, sequence, NULL, error); + } else { + QueuedError *queued = calloc(1, sizeof(*queued)); + if (!queued) { + free(error); + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + } else { + queued->error = error; + pthread_mutex_lock(&c->mutex); + if (c->errorTail) + c->errorTail->next = queued; + else + c->errorHead = queued; + c->errorTail = queued; + pthread_cond_broadcast(&c->eventCond); + pthread_mutex_unlock(&c->mutex); + } + } +} + +uint64_t xcbCompatNextSequence(xcb_connection_t *c) +{ + pthread_mutex_lock(&c->mutex); + uint64_t sequence = c->nextSequence++; + pthread_mutex_unlock(&c->mutex); + return sequence; +} +void xcbCompatSetNextSequence(xcb_connection_t *c, uint64_t sequence) +{ + pthread_mutex_lock(&c->mutex); + c->nextSequence = sequence; + pthread_mutex_unlock(&c->mutex); +} +static size_t pendingBucket(uint64_t sequence, size_t bucketCount) +{ + return (size_t) (sequence ^ (sequence >> 32)) & (bucketCount - 1); +} + +static PendingReply **findPending(xcb_connection_t *c, uint64_t sequence) +{ + PendingReply **link = + &c->pendingBuckets[pendingBucket(sequence, c->pendingBucketCount)]; + while (*link && (*link)->sequence != sequence) + link = &(*link)->next; + return link; +} + +static int growPendingTable(xcb_connection_t *c) +{ + size_t bucketCount = c->pendingBucketCount ? c->pendingBucketCount * 2 : 64; + if (bucketCount < c->pendingBucketCount || + bucketCount > SIZE_MAX / sizeof(*c->pendingBuckets)) + return 0; + PendingReply **buckets = calloc(bucketCount, sizeof(*buckets)); + if (!buckets) + return 0; + for (size_t i = 0; i < c->pendingBucketCount; i++) { + PendingReply *pending = c->pendingBuckets[i]; + while (pending) { + PendingReply *next = pending->next; + size_t bucket = pendingBucket(pending->sequence, bucketCount); + pending->next = buckets[bucket]; + buckets[bucket] = pending; + pending = next; + } + } + free(c->pendingBuckets); + c->pendingBuckets = buckets; + c->pendingBucketCount = bucketCount; + return 1; +} + +static uint64_t widenSequence(xcb_connection_t *c, uint32_t sequence) +{ + uint64_t last = c->nextSequence - 1; + uint64_t widened = (last & ~((uint64_t) UINT32_MAX)) | sequence; + if (widened > last && widened >= ((uint64_t) 1 << 32)) + widened -= (uint64_t) 1 << 32; + return widened; +} +void xcbCompatStorePending(xcb_connection_t *c, + uint64_t sequence, + void *reply, + xcb_generic_error_t *error) +{ + PendingReply *pending = calloc(1, sizeof(*pending)); + if (!pending) { + free(reply); + free(error); + pthread_mutex_lock(&c->mutex); + c->error = XCB_CONN_CLOSED_MEM_INSUFFICIENT; + pthread_mutex_unlock(&c->mutex); + return; + } + pending->sequence = sequence; + pending->reply = reply; + pending->error = error; + pthread_mutex_lock(&c->mutex); + if (!c->pendingBucketCount && !growPendingTable(c)) { + c->error = XCB_CONN_CLOSED_MEM_INSUFFICIENT; + pthread_mutex_unlock(&c->mutex); + free(pending->reply); + free(pending->error); + free(pending); + return; + } + if (c->pendingCount >= c->pendingBucketCount - c->pendingBucketCount / 4) + growPendingTable(c); + PendingReply **link = findPending(c, sequence); + if (*link) { + PendingReply *old = *link; + pending->next = old->next; + *link = pending; + free(old->reply); + free(old->error); + free(old); + } else { + *link = pending; + c->pendingCount++; + } + pthread_mutex_unlock(&c->mutex); +} +xcb_generic_error_t *xcb_request_check(xcb_connection_t *c, + xcb_void_cookie_t cookie) +{ + if (!c) + return NULL; + pthread_mutex_lock(&c->mutex); + if (!c->pendingBucketCount) { + pthread_mutex_unlock(&c->mutex); + return NULL; + } + PendingReply **link = findPending(c, widenSequence(c, cookie.sequence)); + if (!*link) { + pthread_mutex_unlock(&c->mutex); + return NULL; + } + PendingReply *pending = *link; + *link = pending->next; + c->pendingCount--; + pthread_mutex_unlock(&c->mutex); + xcb_generic_error_t *error = pending->error; + free(pending->reply); + free(pending); + return error; +} + +void *xcbCompatTakeReply(xcb_connection_t *c, + uint64_t sequence, + xcb_generic_error_t **error) +{ + if (error) + *error = NULL; + if (!c) + return NULL; + pthread_mutex_lock(&c->mutex); + if (!c->pendingBucketCount) { + pthread_mutex_unlock(&c->mutex); + return NULL; + } + PendingReply **link = findPending(c, sequence); + PendingReply *pending = *link; + if (pending) { + *link = pending->next; + c->pendingCount--; + } + pthread_mutex_unlock(&c->mutex); + if (!pending) + return NULL; + void *reply = pending->reply; + if (error) + *error = pending->error; + else + free(pending->error); + free(pending); + return reply; +} +static void discardReply(xcb_connection_t *c, uint64_t sequence) +{ + if (!c) + return; + pthread_mutex_lock(&c->mutex); + if (!c->pendingBucketCount) { + pthread_mutex_unlock(&c->mutex); + return; + } + PendingReply **link = findPending(c, sequence); + PendingReply *pending = *link; + if (pending) { + *link = pending->next; + c->pendingCount--; + } + pthread_mutex_unlock(&c->mutex); + if (pending) { + free(pending->reply); + free(pending->error); + free(pending); + } +} +void xcb_discard_reply(xcb_connection_t *c, unsigned int sequence) +{ + if (!c) + return; + pthread_mutex_lock(&c->mutex); + uint64_t widened = widenSequence(c, sequence); + pthread_mutex_unlock(&c->mutex); + discardReply(c, widened); +} +void xcb_discard_reply64(xcb_connection_t *c, uint64_t sequence) +{ + discardReply(c, sequence); +} +void xcb_disconnect(xcb_connection_t *c) +{ + if (!c) + return; + pthread_mutex_lock(&c->mutex); + c->closing = 1; + c->error = XCB_CONN_ERROR; + while (c->eventUsers) + pthread_cond_wait(&c->eventCond, &c->mutex); + pthread_mutex_unlock(&c->mutex); + + unregisterConnection(c); + pthread_mutex_lock(&c->mutex); + while (c->closeCallbacks) + pthread_cond_wait(&c->eventCond, &c->mutex); + Display *display = c->display; + int ownsDisplay = c->ownsDisplay; + c->display = NULL; + c->ownsDisplay = 0; + pthread_mutex_unlock(&c->mutex); + + xcbCompatReleaseRequestResources(c); + if (display && ownsDisplay) + XCloseDisplay(display); + pthread_mutex_lock(&c->mutex); + drainPendingLocked(c); + pthread_mutex_unlock(&c->mutex); + free(c->pendingBuckets); + pthread_cond_destroy(&c->eventCond); + pthread_mutex_destroy(&c->mutex); + free(c); +} diff --git a/compat/xlib-xcb-compat.c b/compat/xlib-xcb-compat.c new file mode 100644 index 00000000..50b5ad4a --- /dev/null +++ b/compat/xlib-xcb-compat.c @@ -0,0 +1,12 @@ +#include +#include "xcb-compat-private.h" + +xcb_connection_t *XGetXCBConnection(Display *display) +{ + return xcbCompatConnectionForDisplay(display); +} + +void XSetEventQueueOwner(Display *display, enum XEventQueueOwner owner) +{ + xcbCompatSetQueueOwner(xcbCompatConnectionForDisplay(display), owner); +} diff --git a/docs/XCB-COVERAGE.md b/docs/XCB-COVERAGE.md index a3767f3a..c070d9d3 100644 --- a/docs/XCB-COVERAGE.md +++ b/docs/XCB-COVERAGE.md @@ -1,5 +1,15 @@ # XCB Compatibility Coverage +## Building + +The layer is opt-in. `XCB=1` builds `libxcb-compat.so`, the +`libX11-xcb-compat.so` bridge and their tests; a default build has none of them. + +```sh +make XCB=1 +make XCB=1 check-unit +``` + ## Header staging The public `xcb/` headers are not tracked and are not copied from a @@ -16,3 +26,148 @@ release tarballs: Both tarball digests are verified before use, and the generator inputs are part of the staging stamp, so a change to either release or to the invocation restages. `make install` ships the built copies. + +## Implemented surface + +No core request is implemented yet: this is the connection, setup and event +layer only. Every core opcode is deferred and deliberately absent from the +export manifest, so a client fails at link time instead of receiving a cookie +that silently never completes. + +## Deliberate limits + +`xcb_get_file_descriptor()` returns -1: this backend has no transport socket, so +callers must use the wait and poll entry points rather than integrating a +connection FD into an external `poll(2)` set. + +RENDER, SHM, SHAPE, XFIXES and RANDR are separate extension libraries and are +not part of this layer. DRI, Present, FD passing and authorization transport +have no in-process equivalent at all. + +## Differential oracle + +`tests/probe-system-xcb.c` exercises the core surface and prints a +screen-independent digest of what it observed. `make check-xcb-reference` runs +it under an isolated Xvfb and compares against `tests/data/xcb-reference.txt`. + +<<<<<<< HEAD +The probe currently links system XCB on both sides, so it pins the oracle rather +than this layer. Pointing one of the two runs at `build/pkgconfig/xcb.pc` is +what would turn it into a real differential. +||||||| parent of d49ecf6 (Implement XCB pixmap, GC and drawing requests) +## Core opcode ledger + +The compatibility library currently implements requests 1-4, 7-8, 10, 12, +and 14-24. Every other core opcode is deferred and deliberately absent +from the export manifest; consumers therefore fail at link time instead of +receiving a false successful cookie. + +“Deferred” means no ABI is advertised. Once a checked entry point is exported, +an unsupported combination must return its protocol error (`BadWindow`, +`BadValue`, `BadMatch`, and so on) through `xcb_request_check`; it must never +return silent success. + +## Header provenance + +The public headers come from Ubuntu's `libxcb1-dev` 1.15-1ubuntu2 package, +published at `https://archive.ubuntu.com/ubuntu/pool/main/libx/libxcb/` from the +upstream libxcb 1.15 release. The pinned package SHA-256 is +`1bafe3432feafc9e57f858721da524dfb3ee1f6fc4ef6b0c73023e79eadb9c28`. + +`scripts/sync-xcb-headers.sh` verifies two digests per header. The upstream +digest authenticates the file as unpacked from that package: + +| Header | Upstream SHA-256 | +| --- | --- | +| `xcb.h` | `70218365dcfd8b2e9202b145e9bafbaba042872eae046888680bf58c38cf30e6` | +| `xcbext.h` | `b101d53f1bed75e659e7469f0b7a3eb213bf1cf228f821580c7020b8e70be647` | +| `xproto.h` | `6f45223c52dc24621e7b307b26d39e4d7c884dc08900be619361e076fcac40ec` | + +The vendored digest authenticates what lands in `include/xcb/`, which is the +same text with trailing whitespace removed because this repository's whitespace +gate rejects it. Only `xcb.h` is byte-identical to its upstream form: + +| Header | Vendored SHA-256 | +| --- | --- | +| `xcb.h` | `70218365dcfd8b2e9202b145e9bafbaba042872eae046888680bf58c38cf30e6` | +| `xcbext.h` | `432b474c8b74444c1ed91a529f6500a57ae826a3d7dceec1b351f09c4147fedb` | +| `xproto.h` | `3da9c1a330e53a8bd6e44b895d27627e65e0da51664c5b7d48414e84ffddd766` | + +Confirm the tree against the second table with `shasum -a 256 include/xcb/*.h`. +The headers retain the upstream MIT-style license notices. Regenerate them with: + +```sh +sh scripts/sync-xcb-headers.sh +``` +======= +## Core opcode ledger + +The compatibility library currently implements requests 1-4, 7-8, 10, 12, +14-24, 53-57, 60-68, 72-73, and 76-77. The Phase-0 probe does not issue any +input, focus, grab, pointer, GC, drawing, image, font, color, cursor, +extension-query, keyboard-control, or host control request. Every other core +opcode is deferred and deliberately absent from the export manifest; consumers +therefore fail at link time instead of receiving a false successful cookie. + +The deferred opcode groups below cover every core request in the vendored X11 +protocol header. A group may move to the required list only with its ABI symbols, +protocol-error behavior, and a focused differential test in the same commit. + +| Opcodes | Deferred requests | +| --- | --- | +| 5-6, 9, 11, 13 | DestroySubwindows, ChangeSaveSet, MapSubwindows, UnmapSubwindows, CirculateWindow | +| 25 | SendEvent | +| 26-30 | GrabPointer, UngrabPointer, GrabButton, UngrabButton, ChangeActivePointerGrab | +| 31-37 | GrabKeyboard, UngrabKeyboard, GrabKey, UngrabKey, AllowEvents, GrabServer, UngrabServer | +| 38-44 | QueryPointer, GetMotionEvents, TranslateCoordinates, WarpPointer, SetInputFocus, GetInputFocus, QueryKeymap | +| 45-52 | OpenFont through GetFontPath | +| 58-59 | SetDashes, SetClipRectangles | +| 69-71 | FillPoly, PolyFillRectangle, PolyFillArc | +| 74-75 | PolyText8, PolyText16 | +| 78-83 | CreateColormap through ListInstalledColormaps | +| 84-92 | AllocColor through LookupColor | +| 93-97 | CreateCursor through QueryBestSize | +| 98-99 | QueryExtension, ListExtensions | +| 100-108 | ChangeKeyboardMapping through GetScreenSaver | +| 109-115 | ChangeHosts through ForceScreenSaver | +| 116-119 | SetPointerMapping through GetModifierMapping | +| 127 | NoOperation | + +“Deferred” means no ABI is advertised. Once a checked entry point is exported, +an unsupported combination must return its protocol error (`BadWindow`, +`BadValue`, `BadMatch`, and so on) through `xcb_request_check`; it must never +return silent success. + +## Header provenance + +The public headers come from Ubuntu's `libxcb1-dev` 1.15-1ubuntu2 package, +published at `https://archive.ubuntu.com/ubuntu/pool/main/libx/libxcb/` from the +upstream libxcb 1.15 release. The pinned package SHA-256 is +`1bafe3432feafc9e57f858721da524dfb3ee1f6fc4ef6b0c73023e79eadb9c28`. + +`scripts/sync-xcb-headers.sh` verifies two digests per header. The upstream +digest authenticates the file as unpacked from that package: + +| Header | Upstream SHA-256 | +| --- | --- | +| `xcb.h` | `70218365dcfd8b2e9202b145e9bafbaba042872eae046888680bf58c38cf30e6` | +| `xcbext.h` | `b101d53f1bed75e659e7469f0b7a3eb213bf1cf228f821580c7020b8e70be647` | +| `xproto.h` | `6f45223c52dc24621e7b307b26d39e4d7c884dc08900be619361e076fcac40ec` | + +The vendored digest authenticates what lands in `include/xcb/`, which is the +same text with trailing whitespace removed because this repository's whitespace +gate rejects it. Only `xcb.h` is byte-identical to its upstream form: + +| Header | Vendored SHA-256 | +| --- | --- | +| `xcb.h` | `70218365dcfd8b2e9202b145e9bafbaba042872eae046888680bf58c38cf30e6` | +| `xcbext.h` | `432b474c8b74444c1ed91a529f6500a57ae826a3d7dceec1b351f09c4147fedb` | +| `xproto.h` | `3da9c1a330e53a8bd6e44b895d27627e65e0da51664c5b7d48414e84ffddd766` | + +Confirm the tree against the second table with `shasum -a 256 include/xcb/*.h`. +The headers retain the upstream MIT-style license notices. Regenerate them with: + +```sh +sh scripts/sync-xcb-headers.sh +``` +>>>>>>> d49ecf6 (Implement XCB pixmap, GC and drawing requests) diff --git a/include/X11/Xlib-xcb.h b/include/X11/Xlib-xcb.h new file mode 100644 index 00000000..1f1093ca --- /dev/null +++ b/include/X11/Xlib-xcb.h @@ -0,0 +1,12 @@ +/* Minimal libX11-xcb compatibility API. */ +#ifndef _X11_XLIB_XCB_H_ +#define _X11_XLIB_XCB_H_ +#include +#include +#include +_XFUNCPROTOBEGIN +xcb_connection_t *XGetXCBConnection(Display *display); +enum XEventQueueOwner { XlibOwnsEventQueue = 0, XCBOwnsEventQueue }; +void XSetEventQueueOwner(Display *display, enum XEventQueueOwner owner); +_XFUNCPROTOEND +#endif diff --git a/mk/config.mk b/mk/config.mk index 28fb62a5..1eb6f94a 100644 --- a/mk/config.mk +++ b/mk/config.mk @@ -8,6 +8,14 @@ TARGET ?= $(OUT)/libX11-compat.so # either way, so GLX=0 is only for builds that want the smaller symbol surface. GLX ?= 1 +# XCB ?= 0 builds the core XCB shim (libxcb-compat.so) and the Xlib/XCB bridge +# (libX11-xcb-compat.so) alongside the Xlib library, together with their tests, +# example clients, pkg-config files and install entries. It is off by default +# because no in-tree workload needs it yet: an XCB client has to opt in with +# XCB=1, and a build that does not stays free of the extra libraries and of the +# xcb/x11-xcb .pc files a downstream configure might otherwise pick up. +XCB ?= 0 + # PYTHON is set in mk/toolchain.mk; do not redefine here. # SDL detection lives in mk/sdl.mk; this file consumes SDL_CPPFLAGS and # SDL_COMPAT_LIBS from it. diff --git a/mk/install.mk b/mk/install.mk index 7ab9f87e..7baf138d 100644 --- a/mk/install.mk +++ b/mk/install.mk @@ -29,6 +29,9 @@ CODESIGN_RESIGN := $(if $(filter Darwin,$(UNAME_S)),codesign --force --sign - ,: # Libraries a downstream links by their standard X11 SONAME (each gets a # libNAME.so -> libNAME-compat.so alias). XCOMPAT_INSTALL_ALIASED := X11 Xft Xext Xt Xmu Xaw Xpm Xinerama ICE SM +ifeq ($(XCB),1) +XCOMPAT_INSTALL_ALIASED += X11-xcb xcb +endif # Runtime-only wrappers libX11-compat dlopens; installed without an alias. Only # built for the SDL2 backend -- under SDL_BACKEND=sdl3 the stack links libSDL3 # directly (SDL_USE_WRAPPER=0), so there is nothing to install. @@ -44,9 +47,17 @@ XCOMPAT_INSTALL_LIB_FILES := \ $(foreach w,$(XCOMPAT_INSTALL_WRAPPERS),$(OUT)/lib$(w).so) .PHONY: install -install: $(XCOMPAT_INSTALL_LIB_FILES) $(UPSTREAM_HEADERS_STAMP) +# x11.pc describes libX11-compat, which every install ships, so it is not an +# XCB decision. It also has to travel with x11-xcb.pc, whose "Requires: x11" +# would otherwise resolve against whatever real libX11 the host has and quietly +# link the system library next to the shim. +XCOMPAT_INSTALL_PC := x11$(if $(filter 1,$(XCB)), xcb x11-xcb) + +install: $(XCOMPAT_INSTALL_LIB_FILES) $(UPSTREAM_HEADERS_STAMP) \ + $(addprefix $(PKGCONFIG_DIR)/,$(addsuffix .pc,$(XCOMPAT_INSTALL_PC))) @echo " INSTALL $(DESTDIR)$(PREFIX)" - $(Q)mkdir -p "$(DESTDIR)$(PREFIX)/lib" "$(DESTDIR)$(PREFIX)/include" + $(Q)mkdir -p "$(DESTDIR)$(PREFIX)/lib" "$(DESTDIR)$(PREFIX)/include" \ + $(if $(XCOMPAT_INSTALL_PC),"$(DESTDIR)$(PREFIX)/lib/pkgconfig") $(Q)for l in $(XCOMPAT_INSTALL_ALIASED); do \ cp "$(OUT)/lib$$l-compat.so" "$(DESTDIR)$(PREFIX)/lib/" && \ $(STRIP) -x "$(DESTDIR)$(PREFIX)/lib/lib$$l-compat.so" && \ @@ -65,3 +76,14 @@ install: $(XCOMPAT_INSTALL_LIB_FILES) $(UPSTREAM_HEADERS_STAMP) cp -R "include/$$d/." "$(DESTDIR)$(PREFIX)/include/$$d/"; \ fi; \ done + $(Q)prefix_escaped=$$(printf '%s' "$(PREFIX)" | sed -e 's/[&|\\]/\\&/g'); \ + for pc in $(XCOMPAT_INSTALL_PC); do \ + sed -e "s|^prefix=.*|prefix=$$prefix_escaped|" \ + -e 's|^exec_prefix=.*|exec_prefix=$${prefix}|' \ + -e 's|^libdir=.*|libdir=$${prefix}/lib|' \ + -e 's|^includedir=.*|includedir=$${prefix}/include|' \ + -e 's|^upstreamincludedir=.*|upstreamincludedir=$${includedir}|' \ + -e 's|^libxtbuildincludedir=.*|libxtbuildincludedir=$${includedir}|' \ + "$(PKGCONFIG_DIR)/$$pc.pc" \ + > "$(DESTDIR)$(PREFIX)/lib/pkgconfig/$$pc.pc" || exit 1; \ + done diff --git a/mk/libxcb.mk b/mk/libxcb.mk new file mode 100644 index 00000000..f4d84d97 --- /dev/null +++ b/mk/libxcb.mk @@ -0,0 +1,76 @@ +# Core XCB ABI shim over libX11-compat. Built only for XCB=1 (mk/config.mk). +# +# The public xcb/ headers are staged into $(OUT)/upstream/include by +# mk/upstream-headers.mk, which is included after this file, so the stamp names +# are defined here the way mk/libxt.mk does it. Without them the order-only +# prerequisite below expands to nothing and a parallel clean build can compile +# these objects before the headers exist. +UPSTREAM_HEADERS_DIR ?= $(OUT)/upstream/include +UPSTREAM_HEADERS_STAMP ?= $(UPSTREAM_HEADERS_DIR)/.upstream-stamp + +ifeq ($(XCB),1) +XCB_COMPAT_TARGET := $(OUT)/libxcb-compat.so +XCB_COMPAT_OBJS := $(OUT)/xcb-compat.o +XCB_DEFINED := $(OUT)/libxcb-compat.defined-syms +XCB_EXPORT_LIST := $(OUT)/libxcb-compat.$(if $(filter Darwin,$(UNAME_S)),exports,map) +XCB_EXPORT_FORMAT := $(if $(filter Darwin,$(UNAME_S)),macho,elf) +X11_XCB_COMPAT_TARGET := $(OUT)/libX11-xcb-compat.so +X11_XCB_COMPAT_OBJ := $(OUT)/xlib-xcb-compat.o +X11_XCB_DEFINED := $(OUT)/libX11-xcb-compat.defined-syms +X11_XCB_EXPORT_LIST := $(OUT)/libX11-xcb-compat.$(if $(filter Darwin,$(UNAME_S)),exports,map) +XCB_RPATH_FLAGS := $(call shared_lib_rpath_ldflags,$(notdir $(XCB_COMPAT_TARGET))) + +$(OUT)/xcb-compat.o: compat/xcb-compat.c $(UPSTREAM_HEADERS_STAMP) \ + $(SDL_BACKEND_STAMP) | $(OUT) + $(cc_object) + +$(XCB_DEFINED): $(XCB_COMPAT_OBJS) | $(OUT) + @echo " GEN $@" + $(Q)nm -g $(XCB_COMPAT_OBJS) 2>/dev/null \ + | awk '$$1 ~ /^[0-9a-fA-F]+$$/ { print $$NF }' \ + | $(if $(filter Darwin,$(UNAME_S)),sed 's/^_//',cat) \ + | LC_ALL=C sort -u > $@ + $(Q)test -s $@ + +$(XCB_EXPORT_LIST): tests/xcb-symbols.txt $(XCB_DEFINED) scripts/gen-export-list.sh | $(OUT) + @echo " GEN $@" + $(Q)scripts/gen-export-list.sh "$(XCB_EXPORT_FORMAT)" 0 \ + $(XCB_DEFINED) tests/xcb-symbols.txt tests/xcb-private-symbols.txt > $@ + +# -Bsymbolic for the same reason mk/library.mk applies it to libX11-compat: a +# process that also loads the host libxcb.so would otherwise let its +# xcb_get_setup and friends interpose ours, and the host implementation would +# then be handed this shim's connection object, which is a different type. +XCB_EXPORT_FLAGS := $(if $(filter Darwin,$(UNAME_S)),-Wl$(comma)-exported_symbols_list$(comma)$(XCB_EXPORT_LIST),-Wl$(comma)--version-script=$(XCB_EXPORT_LIST) -Wl$(comma)-Bsymbolic) +$(XCB_COMPAT_TARGET): $(XCB_COMPAT_OBJS) $(TARGET) $(XCB_EXPORT_LIST) | $(OUT) + @echo " LD $@" + $(Q)$(CC) $(LDFLAGS) $(XCB_RPATH_FLAGS) $(XCB_EXPORT_FLAGS) -shared \ + -o $@ $(XCB_COMPAT_OBJS) -L$(OUT) -lX11-compat + +.PHONY: xcb +## Build the core XCB compatibility shared library (needs XCB=1) +xcb: $(XCB_COMPAT_TARGET) +all: $(XCB_COMPAT_TARGET) + +$(X11_XCB_COMPAT_OBJ): compat/xlib-xcb-compat.c $(UPSTREAM_HEADERS_STAMP) | $(OUT) + $(cc_object) + +$(X11_XCB_DEFINED): $(X11_XCB_COMPAT_OBJ) | $(OUT) + @echo " GEN $@" + $(Q)nm -g $< 2>/dev/null | awk '$$1 ~ /^[0-9a-fA-F]+$$/ { print $$NF }' \ + | $(if $(filter Darwin,$(UNAME_S)),sed 's/^_//',cat) | LC_ALL=C sort -u > $@ + $(Q)test -s $@ + +$(X11_XCB_EXPORT_LIST): tests/xlib-xcb-symbols.txt $(X11_XCB_DEFINED) scripts/gen-export-list.sh | $(OUT) + @echo " GEN $@" + $(Q)scripts/gen-export-list.sh "$(XCB_EXPORT_FORMAT)" 0 \ + $(X11_XCB_DEFINED) tests/xlib-xcb-symbols.txt > $@ + +$(X11_XCB_COMPAT_TARGET): $(X11_XCB_COMPAT_OBJ) $(XCB_COMPAT_TARGET) $(TARGET) $(X11_XCB_EXPORT_LIST) | $(OUT) + @echo " LD $@" + $(Q)$(CC) $(LDFLAGS) $(call shared_lib_rpath_ldflags,$(notdir $@)) \ + $(if $(filter Darwin,$(UNAME_S)),-Wl$(comma)-exported_symbols_list$(comma)$(X11_XCB_EXPORT_LIST),-Wl$(comma)--version-script=$(X11_XCB_EXPORT_LIST) -Wl$(comma)-Bsymbolic) \ + -shared -o $@ $(X11_XCB_COMPAT_OBJ) -L$(OUT) -lxcb-compat -lX11-compat + +all: $(X11_XCB_COMPAT_TARGET) +endif diff --git a/mk/pkgconfig.mk b/mk/pkgconfig.mk index d65bf5c3..3802b2c8 100644 --- a/mk/pkgconfig.mk +++ b/mk/pkgconfig.mk @@ -13,15 +13,22 @@ PKGCONFIG_FILES := \ $(PKGCONFIG_DIR)/fontconfig.pc \ $(PKGCONFIG_DIR)/xrender.pc \ $(PKGCONFIG_DIR)/xft.pc +ifeq ($(XCB),1) +PKGCONFIG_FILES += $(PKGCONFIG_DIR)/xcb.pc $(PKGCONFIG_DIR)/x11-xcb.pc +endif $(PKGCONFIG_DIR): @mkdir -p $@ +# Libs and Cflags refer to the variables above rather than repeating absolute +# paths, so `make install` can relocate a file by rewriting the variable lines +# alone (mk/install.mk). pkg-config expands them, so an in-tree consumer reading +# build/pkgconfig/ sees exactly the paths it saw before. define write_pc @{ \ echo "prefix=$(abspath $(OUT))"; \ - echo "exec_prefix=$(abspath $(OUT))"; \ - echo "libdir=$(abspath $(OUT))"; \ + echo "exec_prefix=\$${prefix}"; \ + echo "libdir=\$${prefix}"; \ echo "includedir=$$(pwd)/include"; \ echo "upstreamincludedir=$$(pwd)/$(OUT)/upstream/include"; \ echo "libxtbuildincludedir=$$(pwd)/include/libxt-build"; \ @@ -29,8 +36,9 @@ define write_pc echo "Name: $(1)"; \ echo "Description: libx11-compat $(1) shim"; \ echo "Version: $(2)"; \ - echo "Libs: -L$(abspath $(OUT)) $(3)"; \ - echo "Cflags: -I$$(pwd)/include -I$$(pwd)/$(OUT)/upstream/include $(4)"; \ + $(if $(5),echo "Requires: $(5)";) \ + echo "Libs: -L\$${libdir} $(3)"; \ + echo "Cflags: -I\$${includedir} -I\$${upstreamincludedir} $(4)"; \ } > $@ endef @@ -38,17 +46,25 @@ $(PKGCONFIG_DIR)/x11.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG @echo " PC $@" $(call write_pc,x11,1.8.13,-lX11-compat,) +$(PKGCONFIG_DIR)/xcb.pc: mk/pkgconfig.mk | $(PKGCONFIG_DIR) + @echo " PC $@" + $(call write_pc,xcb,1.15,-lxcb-compat,) + +$(PKGCONFIG_DIR)/x11-xcb.pc: mk/pkgconfig.mk | $(PKGCONFIG_DIR) + @echo " PC $@" + $(call write_pc,x11-xcb,1.8.0,-lX11-xcb-compat,,x11 xcb) + $(PKGCONFIG_DIR)/xpm.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_DIR) @echo " PC $@" $(call write_pc,xpm,3.5.19,-lXpm-compat -lX11-compat,) $(PKGCONFIG_DIR)/xt.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_DIR) @echo " PC $@" - $(call write_pc,xt,1.3.1,-lXt-compat -lX11-compat,-I$$(pwd)/include/libxt-build) + $(call write_pc,xt,1.3.1,-lXt-compat -lX11-compat,-I\$${libxtbuildincludedir}) $(PKGCONFIG_DIR)/xmu.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_DIR) @echo " PC $@" - $(call write_pc,xmu,1.0,-lXmu-compat -lXt-compat -lX11-compat,-I$$(pwd)/include/libxt-build) + $(call write_pc,xmu,1.0,-lXmu-compat -lXt-compat -lX11-compat,-I\$${libxtbuildincludedir}) $(PKGCONFIG_DIR)/xext.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_DIR) @echo " PC $@" @@ -68,7 +84,7 @@ $(PKGCONFIG_DIR)/sm.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_ $(PKGCONFIG_DIR)/xaw7.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_DIR) @echo " PC $@" - $(call write_pc,xaw7,1.0.16,-lXaw-compat -lXt-compat -lXmu-compat -lXpm-compat -lX11-compat,-I$$(pwd)/include/libxt-build) + $(call write_pc,xaw7,1.0.16,-lXaw-compat -lXt-compat -lXmu-compat -lXpm-compat -lX11-compat,-I\$${libxtbuildincludedir}) # xproto.pc covers protocol-only headers. Apps like xclock pkg-config it # to get the X11/Xproto.h include path; the upstream-headers stamp already diff --git a/mk/tests.mk b/mk/tests.mk index 8f79b905..59332e23 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -8,6 +8,11 @@ CHECK_BINS := $(OUT)/tests/check $(OUT)/tests/symbol-coverage \ $(OUT)/tests/test-xft-link \ $(OUT)/tests/test-xlibint-link \ $(OUT)/tests/test-xtest +ifeq ($(XCB),1) +CHECK_BINS += $(OUT)/tests/test-xcb-link +CHECK_BINS += $(OUT)/tests/test-xcb-setup +CHECK_BINS += $(OUT)/tests/test-xcb-events +endif # The GLX tests only exist when the optional GLX layer is built (GLX=1). # test-glx-link covers the no-provider degrade path; test-glx-provider drives the # full GLX->EGL translation against the in-tree fake EGL provider. @@ -48,7 +53,31 @@ else endif endif -.PHONY: check check-unit check-differential check-link-xaw symbol-coverage api-symbol-coverage bench bench-paths +.PHONY: check check-unit check-differential check-link-xaw symbol-coverage api-symbol-coverage bench bench-paths check-xcb-reference update-xcb-reference + +XCB_REFERENCE_BIN := $(OUT)/tests/probe-system-xcb +XCB_REFERENCE_OUT := tests/data/xcb-reference.txt + +$(XCB_REFERENCE_BIN): tests/probe-system-xcb.c | $(OUT) + @mkdir -p $(dir $@) + @echo " CC $< (system XCB)" + $(Q)$(CC) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ + $$($(PKG_CONFIG) --cflags --libs xcb) -o $@ + +## Regenerate the deterministic system-XCB oracle under an isolated Xvfb. +update-xcb-reference: $(XCB_REFERENCE_BIN) + @command -v xvfb-run >/dev/null || { echo "Error: xvfb-run is required" >&2; exit 1; } + $(Q)xvfb-run -a -s '-screen 0 320x240x24 -nolisten tcp' \ + $(XCB_REFERENCE_BIN) > $(XCB_REFERENCE_OUT).tmp; \ + mv $(XCB_REFERENCE_OUT).tmp $(XCB_REFERENCE_OUT) + +## Compare the core-XCB probe with the checked-in system-XCB oracle. +check-xcb-reference: $(XCB_REFERENCE_BIN) $(XCB_REFERENCE_OUT) + @command -v xvfb-run >/dev/null || { echo "Error: xvfb-run is required" >&2; exit 1; } + $(Q)tmp=$$(mktemp -d); trap 'rm -rf "$$tmp"' EXIT; \ + xvfb-run -a -s '-screen 0 320x240x24 -nolisten tcp' \ + $(XCB_REFERENCE_BIN) >"$$tmp/actual"; \ + diff -u $(XCB_REFERENCE_OUT) "$$tmp/actual" DIFFERENTIAL_TARGETS := check-differential-motif check-differential-violawww \ check-differential-xmms @@ -71,7 +100,14 @@ check-unit: $(CHECK_BINS) @printf "$(BLUE)RUN$(RESET) tests/check-api-symbols.py\n" $(Q)LIBX11_COMPAT_GLX=$(GLX) $(PYTHON) tests/check-api-symbols.py $(TARGET) tests/api-symbols.txt @printf "$(BLUE)RUN$(RESET) tests/check-host-link-audit.py\n" - $(Q)$(PYTHON) tests/check-host-link-audit.py + $(Q)$(PYTHON) tests/check-host-link-audit.py $(wildcard $(OUT)/lib*-compat.so) +ifeq ($(XCB),1) + @printf "$(BLUE)RUN$(RESET) tests/test-check-xcb-symbols.py\n" + $(Q)$(PYTHON) tests/test-check-xcb-symbols.py + @printf "$(BLUE)RUN$(RESET) scripts/check-xcb-symbols.py\n" + $(Q)$(PYTHON) scripts/check-xcb-symbols.py $(XCB_COMPAT_TARGET) \ + tests/xcb-symbols.txt +endif ## Full local regression suite: unit tests, motif link/demos gates, the ## replay smoke tier, and the SSH-backed differential screenshots. The @@ -263,3 +299,26 @@ $(OUT)/tests/%: tests/%.c $(TARGET) @echo " CC $<" $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(CFLAGS_EXTRA) $< $(TARGET) \ $(LDLIBS) $(TEST_LDFLAGS) -o $@ + +ifeq ($(XCB),1) +$(OUT)/tests/test-xcb-link: tests/test-xcb-link.c $(XCB_COMPAT_TARGET) $(TARGET) + @mkdir -p $(dir $@) + @echo " CC $<" + $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ + -L$(OUT) -lxcb-compat $(TEST_LDFLAGS) -o $@ + +$(OUT)/tests/test-xcb-setup: tests/test-xcb-setup.c $(XCB_COMPAT_OBJS) $(TARGET) + @mkdir -p $(dir $@) + @echo " CC $<" + $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ + $(XCB_COMPAT_OBJS) $(TARGET) $(LDLIBS) $(TEST_LDFLAGS) -o $@ + +XLIB_XCB_TESTS := test-xcb-events + +$(addprefix $(OUT)/tests/,$(XLIB_XCB_TESTS)): $(OUT)/tests/%: tests/%.c $(X11_XCB_COMPAT_TARGET) $(XCB_COMPAT_TARGET) $(TARGET) + @mkdir -p $(dir $@) + @echo " CC $<" + $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ + -L$(OUT) -lX11-xcb-compat -lxcb-compat -lX11-compat \ + $(TEST_LDFLAGS) -o $@ +endif diff --git a/mk/wasm.mk b/mk/wasm.mk index a11212cd..88043d26 100644 --- a/mk/wasm.mk +++ b/mk/wasm.mk @@ -35,6 +35,10 @@ HOST_CC ?= cc # missing EGL headers) back into the wasm build. override GLX := 0 +# The XCB shim links the native shared libraries the wasm build does not +# produce, so keep it out regardless of a command-line XCB=1. +override XCB := 0 + # Define OUT/TARGET here (config.mk uses ?= and defers to these) so the # archive path is a static library, not the native .so, from parse time on. OUT ?= build diff --git a/scripts/check-xcb-symbols.py b/scripts/check-xcb-symbols.py new file mode 100644 index 00000000..b20a344c --- /dev/null +++ b/scripts/check-xcb-symbols.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Check libxcb-compat's exported ABI against an exact symbol manifest.""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +XCB_API_RE = re.compile(r"^xcb_[a-z0-9_]+$") + + +def exported_symbols(library: Path) -> set[str]: + commands = (("nm", "-D", "-g", str(library)), ("nm", "-g", str(library))) + output = None + for command in commands: + try: + output = subprocess.check_output( + command, text=True, stderr=subprocess.DEVNULL + ) + break + except (OSError, subprocess.CalledProcessError): + continue + if output is None: + raise SystemExit(f"cannot inspect exported symbols in {library}") + + symbols = set() + for line in output.splitlines(): + fields = line.split() + if len(fields) < 2 or " U " in f" {line} ": + continue + symbol = ( + fields[-1].removeprefix("_") if sys.platform == "darwin" else fields[-1] + ) + if XCB_API_RE.fullmatch(symbol): + symbols.add(symbol) + return symbols + + +def manifest_symbols(path: Path) -> set[str]: + symbols = set() + for line_number, raw_line in enumerate(path.read_text().splitlines(), 1): + symbol = raw_line.strip() + if not symbol or symbol.startswith("#"): + continue + if not XCB_API_RE.fullmatch(symbol): + raise SystemExit(f"{path}:{line_number}: invalid XCB symbol: {symbol}") + if symbol in symbols: + raise SystemExit(f"{path}:{line_number}: duplicate symbol: {symbol}") + symbols.add(symbol) + return symbols + + +def main() -> int: + if len(sys.argv) != 3: + print("usage: check-xcb-symbols.py ", file=sys.stderr) + return 2 + exported = exported_symbols(Path(sys.argv[1])) + expected = manifest_symbols(Path(sys.argv[2])) + missing = expected - exported + unexpected = exported - expected + for label, symbols in (("missing", missing), ("unexpected", unexpected)): + if symbols: + print(f"{label} XCB symbols:") + for symbol in sorted(symbols): + print(f" {symbol}") + if missing or unexpected: + return 1 + print(f"XCB symbol coverage complete: {len(exported)} exports") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/sync-upstream-headers.py b/scripts/sync-upstream-headers.py index ce5f00ae..ef8110af 100644 --- a/scripts/sync-upstream-headers.py +++ b/scripts/sync-upstream-headers.py @@ -466,12 +466,9 @@ "libxcb": { "version": "libxcb-1.15", "url": ( - "https://xorg.freedesktop.org/archive/individual/lib/" - "libxcb-1.15.tar.xz" - ), - "sha256": ( - "cc38744f817cf6814c847e2df37fcb8997357d72fa4bcbc228ae0fe47219a059" + "https://xorg.freedesktop.org/archive/individual/lib/" "libxcb-1.15.tar.xz" ), + "sha256": ("cc38744f817cf6814c847e2df37fcb8997357d72fa4bcbc228ae0fe47219a059"), }, "xcb-proto": { "version": "xcb-proto-1.15.2", @@ -479,9 +476,7 @@ "https://xorg.freedesktop.org/archive/individual/proto/" "xcb-proto-1.15.2.tar.xz" ), - "sha256": ( - "7072beb1f680a2fe3f9e535b797c146d22528990c72f63ddb49d2f350a3653ed" - ), + "sha256": ("7072beb1f680a2fe3f9e535b797c146d22528990c72f63ddb49d2f350a3653ed"), }, } diff --git a/src/display.c b/src/display.c index db34a85e..68a3a477 100644 --- a/src/display.c +++ b/src/display.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -408,6 +409,13 @@ static size_t pendingCloseCapacity = 0; #define PENDING_CLOSE_FALLBACK_SLOTS 8 static Display *pendingCloseFallback[PENDING_CLOSE_FALLBACK_SLOTS]; static size_t pendingCloseFallbackCount = 0; +typedef void (*DisplayCloseHook)(Display *display); +static _Atomic DisplayCloseHook displayCloseHook; + +void libx11CompatSetDisplayCloseHook(void (*hook)(Display *display)) +{ + atomic_store_explicit(&displayCloseHook, hook, memory_order_release); +} int XCloseDisplay(Display *display) { @@ -446,6 +454,10 @@ int XCloseDisplay(Display *display) } return 0; } + DisplayCloseHook closeHook = + atomic_load_explicit(&displayCloseHook, memory_order_acquire); + if (closeHook) + closeHook(display); freeExtensionStorage(display); freeSelectionStorage(display); int screenIndex; diff --git a/src/display.h b/src/display.h index 0f66a54d..5ce59cbf 100644 --- a/src/display.h +++ b/src/display.h @@ -97,4 +97,10 @@ Bool compatSdlHasWindowSizeInPixels(void); #define atomicLoadRequest(display) \ __atomic_load_n(&GET_DISPLAY(display)->request, __ATOMIC_RELAXED) + +/* Called when a Display is about to be torn down, so a sibling shim holding a + * handle on it (libxcb-compat) can invalidate that handle first. + */ +void libx11CompatSetDisplayCloseHook(void (*hook)(Display *display)); + #endif //_DISPLAY_H diff --git a/src/events.c b/src/events.c index fd637d74..d3bc4926 100644 --- a/src/events.c +++ b/src/events.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -4922,11 +4923,64 @@ static Bool isInteractiveSdlEvent(const SDL_Event *event) } } +typedef int (*DisplayPredicateHook)(Display *display); +static _Atomic DisplayPredicateHook eventQueueOwnerHook; +static _Atomic DisplayPredicateHook eventWaitCancelledHook; +static _Thread_local unsigned int xcbEventAccessDepth; + +void libx11CompatSetEventQueueOwnerHook(int (*hook)(Display *display)) +{ + atomic_store_explicit(&eventQueueOwnerHook, hook, memory_order_release); +} + +void libx11CompatSetEventWaitCancelledHook(int (*hook)(Display *display)) +{ + atomic_store_explicit(&eventWaitCancelledHook, hook, memory_order_release); +} + +void libx11CompatBeginXcbEventAccess(void) +{ + xcbEventAccessDepth++; +} + +void libx11CompatEndXcbEventAccess(void) +{ + if (xcbEventAccessDepth) + xcbEventAccessDepth--; +} + +static Bool xlibMayConsumeEvents(Display *display) +{ + DisplayPredicateHook owner = + atomic_load_explicit(&eventQueueOwnerHook, memory_order_acquire); + return xcbEventAccessDepth || !owner || owner(display) == 0; +} + int XNextEvent(Display *display, XEvent *event_return) { // https://tronche.com/gui/x/xlib/event-handling/manipulating-event-queue/XNextEvent.html + if (!xlibMayConsumeEvents(display)) { + memset(event_return, 0, sizeof(*event_return)); + return 0; + } SDL_Event event; while (1) { + DisplayPredicateHook cancelled = + atomic_load_explicit(&eventWaitCancelledHook, memory_order_acquire); + if (cancelled && cancelled(display)) { + memset(event_return, 0, sizeof(*event_return)); + return 0; + } + + /* Ownership can change while this call is already waiting, so it is + * re-checked per iteration rather than only on entry. Otherwise a + * client that hands the queue to XCB mid-wait would still have this + * call consume the next event out from under it. + */ + if (!xlibMayConsumeEvents(display)) { + memset(event_return, 0, sizeof(*event_return)); + return 0; + } if (popPutBackEvent(display, event_return)) { printEventInfo(event_return); return 0; @@ -5169,6 +5223,8 @@ int XEventsQueued(Display *display, int mode) { // https://tronche.com/gui/x/xlib/event-handling/XEventsQueued.html // SET_X_SERVER_REQUEST(display, XCB_); + if (!xlibMayConsumeEvents(display)) + return 0; if (mode == QueuedAlready) return displayEventQueueLength(display); if (mode == QueuedAfterFlush) { diff --git a/src/events.h b/src/events.h index 95fb2981..431b2055 100644 --- a/src/events.h +++ b/src/events.h @@ -152,4 +152,14 @@ Bool libx11CompatWarpTargetGlobal(Window window, */ int libx11CompatSnapAxisToIncrement(int current, int inc, int base, int min); +/* Event-queue hooks for a sibling shim that shares this Display. The owner hook + * reports whether Xlib may still consume events; the cancel hook lets a pending + * wait be interrupted. The access counter marks the window in which the shim is + * itself draining the queue, so the owner hook does not lock it out. + */ +void libx11CompatSetEventQueueOwnerHook(int (*hook)(Display *display)); +void libx11CompatSetEventWaitCancelledHook(int (*hook)(Display *display)); +void libx11CompatBeginXcbEventAccess(void); +void libx11CompatEndXcbEventAccess(void); + #endif /* _EVENTS_H_ */ diff --git a/src/resource-types.c b/src/resource-types.c index a8957a68..52728fb2 100644 --- a/src/resource-types.c +++ b/src/resource-types.c @@ -1,84 +1,213 @@ #include "resource-types.h" +#include +#include +#include #include -#include -#define FIRST_RESOURCE_ID ((XID) 0x100000) +/* The X protocol has a client allocate resource ids as base | (n & mask), so + * the advertised base and mask must not share a bit and every id must fit the + * protocol's 29 bits. Spanning exactly [base, base | mask] means any id a + * client derives that way is one this table can hold. + */ +#define FIRST_RESOURCE_ID ((XID) XID_RESOURCE_BASE) +#define RESOURCE_ID_COUNT ((size_t) XID_RESOURCE_MASK + 1) +#define RESOURCE_CHUNK_SHIFT 10 +#define RESOURCE_CHUNK_SIZE ((size_t) 1 << RESOURCE_CHUNK_SHIFT) +#define RESOURCE_CHUNK_COUNT (RESOURCE_ID_COUNT / RESOURCE_CHUNK_SIZE) +#define RESOURCE_DIRECTORY_SHIFT 9 +#define RESOURCE_DIRECTORY_SIZE ((size_t) 1 << RESOURCE_DIRECTORY_SHIFT) +#define RESOURCE_DIRECTORY_COUNT \ + (RESOURCE_CHUNK_COUNT / RESOURCE_DIRECTORY_SIZE) typedef struct { - Bool allocated; + _Atomic Bool allocated; XID_Struct resource; } ResourceSlot; -static ResourceSlot *resourceSlots = NULL; -static size_t resourceSlotCapacity = 0; -static XID nextResourceId = FIRST_RESOURCE_ID; +typedef struct { + _Atomic(ResourceSlot *) chunks[RESOURCE_DIRECTORY_SIZE]; +} ResourceDirectory; +static _Atomic(ResourceDirectory *) + resourceDirectories[RESOURCE_DIRECTORY_COUNT]; +static pthread_mutex_t resourceMutex = PTHREAD_MUTEX_INITIALIZER; +static XID nextResourceId = FIRST_RESOURCE_ID; static XID_Struct invalidResource = {0, NULL}; -static Bool ensureResourceSlot(size_t index) +static ResourceSlot *resourceSlot(XID id) +{ + if (id < FIRST_RESOURCE_ID || id - FIRST_RESOURCE_ID >= RESOURCE_ID_COUNT) + return NULL; + size_t index = (size_t) (id - FIRST_RESOURCE_ID); + size_t chunkIndex = index >> RESOURCE_CHUNK_SHIFT; + ResourceDirectory *directory = atomic_load_explicit( + &resourceDirectories[chunkIndex >> RESOURCE_DIRECTORY_SHIFT], + memory_order_acquire); + if (!directory) + return NULL; + ResourceSlot *chunk = atomic_load_explicit( + &directory->chunks[chunkIndex & (RESOURCE_DIRECTORY_SIZE - 1)], + memory_order_acquire); + return chunk ? &chunk[index & (RESOURCE_CHUNK_SIZE - 1)] : NULL; +} + +static ResourceSlot *ensureResourceSlot(XID id) { - if (index < resourceSlotCapacity) - return True; - size_t newCapacity = resourceSlotCapacity ? resourceSlotCapacity : 1024; - while (index >= newCapacity) { - if (newCapacity > ((size_t) -1) / 2) - return False; - newCapacity *= 2; + size_t index = (size_t) (id - FIRST_RESOURCE_ID); + size_t chunkIndex = index >> RESOURCE_CHUNK_SHIFT; + size_t directoryIndex = chunkIndex >> RESOURCE_DIRECTORY_SHIFT; + ResourceDirectory *directory = atomic_load_explicit( + &resourceDirectories[directoryIndex], memory_order_relaxed); + if (!directory) { + directory = calloc(1, sizeof(*directory)); + if (!directory) + return NULL; + atomic_store_explicit(&resourceDirectories[directoryIndex], directory, + memory_order_release); } - ResourceSlot *newSlots = - realloc(resourceSlots, newCapacity * sizeof(*newSlots)); - if (!newSlots) - return False; - memset(newSlots + resourceSlotCapacity, 0, - (newCapacity - resourceSlotCapacity) * sizeof(*newSlots)); - resourceSlots = newSlots; - resourceSlotCapacity = newCapacity; - return True; + _Atomic(ResourceSlot *) *chunkSlot = + &directory->chunks[chunkIndex & (RESOURCE_DIRECTORY_SIZE - 1)]; + ResourceSlot *chunk = atomic_load_explicit(chunkSlot, memory_order_relaxed); + if (!chunk) { + chunk = calloc(RESOURCE_CHUNK_SIZE, sizeof(*chunk)); + if (!chunk) + return NULL; + atomic_store_explicit(chunkSlot, chunk, memory_order_release); + } + return &chunk[index & (RESOURCE_CHUNK_SIZE - 1)]; +} + +/* Caller holds resourceMutex. A freshly claimed slot carries no resource yet. + */ +static void claimSlotLocked(ResourceSlot *slot) +{ + slot->resource.type = 0; + slot->resource.dataPointer = NULL; + atomic_store_explicit(&slot->allocated, True, memory_order_release); } XID allocXidResource(void) { - if (nextResourceId < FIRST_RESOURCE_ID) - return None; - size_t index = (size_t) (nextResourceId - FIRST_RESOURCE_ID); - if (!ensureResourceSlot(index)) - return None; - XID id = nextResourceId++; - resourceSlots[index].allocated = True; - resourceSlots[index].resource.type = 0; - resourceSlots[index].resource.dataPointer = NULL; - return id; + pthread_mutex_lock(&resourceMutex); + + /* Step over ids a client reserved for itself: the cursor only moves + * forward, so a reserved id ahead of it must not be handed out twice. + */ + while (nextResourceId - FIRST_RESOURCE_ID < RESOURCE_ID_COUNT) { + XID id = nextResourceId++; + ResourceSlot *slot = ensureResourceSlot(id); + if (!slot) + break; + if (!atomic_load_explicit(&slot->allocated, memory_order_acquire)) { + claimSlotLocked(slot); + pthread_mutex_unlock(&resourceMutex); + return id; + } + } + pthread_mutex_unlock(&resourceMutex); + return None; +} + +Bool reserveXidResource(XID id) +{ + /* The range check needs no lock: both bounds are constants. */ + if (id < FIRST_RESOURCE_ID || id - FIRST_RESOURCE_ID >= RESOURCE_ID_COUNT) + return False; + + /* A client names its own resources, so the id can sit anywhere in the + * advertised range, including above ids handed out so far. + */ + pthread_mutex_lock(&resourceMutex); + ResourceSlot *slot = ensureResourceSlot(id); + Bool free = + slot && !atomic_load_explicit(&slot->allocated, memory_order_acquire); + if (free) + claimSlotLocked(slot); + pthread_mutex_unlock(&resourceMutex); + return free; } void freeXidResource(XID id) { - if (id < FIRST_RESOURCE_ID) - return; - size_t index = (size_t) (id - FIRST_RESOURCE_ID); - if (index >= resourceSlotCapacity) + pthread_mutex_lock(&resourceMutex); + ResourceSlot *slot = resourceSlot(id); + if (!slot) { + pthread_mutex_unlock(&resourceMutex); return; - resourceSlots[index].allocated = False; - resourceSlots[index].resource.type = 0; - resourceSlots[index].resource.dataPointer = NULL; + } + atomic_store_explicit(&slot->allocated, False, memory_order_release); + slot->resource.type = 0; + slot->resource.dataPointer = NULL; + pthread_mutex_unlock(&resourceMutex); } XID_Struct *getXidStruct(XID id) { - if (id < FIRST_RESOURCE_ID) - return &invalidResource; - size_t index = (size_t) (id - FIRST_RESOURCE_ID); - if (index >= resourceSlotCapacity || !resourceSlots[index].allocated) + ResourceSlot *slot = resourceSlot(id); + if (!slot || !atomic_load_explicit(&slot->allocated, memory_order_acquire)) return &invalidResource; - return &resourceSlots[index].resource; + return &slot->resource; +} + +Bool isXidAllocated(XID id) +{ + ResourceSlot *slot = resourceSlot(id); + return slot && atomic_load_explicit(&slot->allocated, memory_order_acquire); } void forEachXidResourceOfType(XResourceType type, void (*visit)(void *data, void *ctx), void *ctx) { - for (size_t i = 0; i < resourceSlotCapacity; i++) { - if (resourceSlots[i].allocated && - resourceSlots[i].resource.type == type) - visit(resourceSlots[i].resource.dataPointer, ctx); + /* Collect first, then visit with the lock dropped. Holding it across the + * callback would serialize correctly but makes every visitor a potential + * deadlock: they run at renderer teardown and are free to call back into + * anything, including this table. Snapshotting keeps the read of each slot + * atomic with respect to freeXidResource without that exposure. + * + * Walking the chunks that exist, rather than the id range they cover, keeps + * this proportional to live resources: a client-chosen id can sit anywhere + * in the advertised space. + */ + void **matches = NULL; + size_t count = 0, capacity = 0; + pthread_mutex_lock(&resourceMutex); + for (size_t d = 0; d < RESOURCE_DIRECTORY_COUNT; d++) { + ResourceDirectory *directory = + atomic_load_explicit(&resourceDirectories[d], memory_order_acquire); + if (!directory) + continue; + for (size_t c = 0; c < RESOURCE_DIRECTORY_SIZE; c++) { + ResourceSlot *chunk = atomic_load_explicit(&directory->chunks[c], + memory_order_acquire); + if (!chunk) + continue; + for (size_t i = 0; i < RESOURCE_CHUNK_SIZE; i++) { + ResourceSlot *slot = &chunk[i]; + if (!atomic_load_explicit(&slot->allocated, + memory_order_acquire) || + slot->resource.type != type) + continue; + if (count == capacity) { + size_t grown = capacity ? capacity * 2 : 64; + void **bigger = realloc(matches, grown * sizeof(*matches)); + if (!bigger) { + /* Out of memory mid-sweep: visit what was collected and + * drop the rest rather than lose the lock discipline or + * abort a teardown path. + */ + goto visit; + } + matches = bigger; + capacity = grown; + } + matches[count++] = slot->resource.dataPointer; + } + } } +visit: + pthread_mutex_unlock(&resourceMutex); + for (size_t i = 0; i < count; i++) + visit(matches[i], ctx); + free(matches); } diff --git a/src/resource-types.h b/src/resource-types.h index a3a58349..bcda5584 100644 --- a/src/resource-types.h +++ b/src/resource-types.h @@ -3,6 +3,14 @@ #include "X11/Xlib.h" +/* Resource ids handed to clients. The X protocol has a client derive its own + * ids as base | (n & mask), so the two must not share a bit, and base | mask + * has to stay inside the 29 bits the protocol gives a resource id. The XCB + * setup record advertises exactly these values. + */ +#define XID_RESOURCE_BASE UINT32_C(0x10000000) +#define XID_RESOURCE_MASK UINT32_C(0x0fffffff) + typedef enum { WINDOW = 1, DRAWABLE = 2, @@ -23,8 +31,10 @@ typedef struct { } XID_Struct; XID allocXidResource(void); +Bool reserveXidResource(XID id); void freeXidResource(XID id); XID_Struct *getXidStruct(XID id); +Bool isXidAllocated(XID id); /* Visit the data pointer of every live resource of a given type. Used to reach * per-resource caches (such as a pixmap's stipple stamp) that must be dropped diff --git a/tests/check-api-symbols.py b/tests/check-api-symbols.py index c345b551..e045003c 100644 --- a/tests/check-api-symbols.py +++ b/tests/check-api-symbols.py @@ -105,9 +105,10 @@ def main() -> int: failed = _diff_manifest(exported, covered, "API") - # The libx11Compat* shim hooks are only exported on builds that compile the - # live-resize/present layer (macOS). When none are present (e.g. a Linux CI - # build), the shim manifest is expected to be empty and the check is a no-op. + # The libx11Compat* shim hooks cover the live-resize/present layer, which + # only builds on macOS, and the XCB integration hooks, which build + # everywhere. When neither is present the manifest is expected to be empty + # and the check is a no-op. shim_exported = exported_symbols(library, SHIM_API_RE) shim_manifest_path = manifest.with_name("shim-symbols.txt") shim_covered: set[str] = set() diff --git a/tests/check-host-link-audit.py b/tests/check-host-link-audit.py index 965029a6..5fbf2da8 100644 --- a/tests/check-host-link-audit.py +++ b/tests/check-host-link-audit.py @@ -11,6 +11,7 @@ import importlib.util import os import platform +import sys import tempfile _HERE = os.path.dirname(os.path.abspath(__file__)) @@ -66,10 +67,14 @@ def test_alias_farm_exemption(audit): # Same host soname sitting elsewhere under --out (not the farm) -> host. stray = os.path.join(out, "libX11.so.6") open(stray, "w").close() - assert audit.is_host_x11(stray, out) is True, "a stray host lib under --out is not exempt" + assert ( + audit.is_host_x11(stray, out) is True + ), "a stray host lib under --out is not exempt" # The exemption must not depend on out_dir being passed: without it, the # farm path is classified purely on its basename -> host. - assert audit.is_host_x11(alias) is True, "no out_dir: farm path falls back to basename" + assert ( + audit.is_host_x11(alias) is True + ), "no out_dir: farm path falls back to basename" def test_uninspectable_is_reported_not_raised(audit): @@ -122,4 +127,16 @@ def test_otool_fails_closed_on_non_object(audit): test_alias_farm_exemption(audit) test_uninspectable_is_reported_not_raised(audit) test_otool_fails_closed_on_non_object(audit) + artifacts = sys.argv[1:] + if artifacts: + # The alias-farm exemption is keyed on the output directory, so derive + # it from all the artifacts rather than the first one: a caller passing + # paths from several directories, or a bare filename whose dirname is + # empty, would otherwise have the exemption silently disabled. + out_dir = os.path.commonpath( + [os.path.dirname(os.path.abspath(a)) for a in artifacts] + ) + bad = audit.audit_no_host_x11(artifacts, out_dir) + assert not bad, "host X11 dependencies found:\n" + "\n".join(bad) + print(f"OK: no host X11 dependency in {len(artifacts)} artifact(s)") print("OK: host-link audit classifier tests passed") diff --git a/tests/data/xcb-reference.txt b/tests/data/xcb-reference.txt new file mode 100644 index 00000000..add1b918 --- /dev/null +++ b/tests/data/xcb-reference.txt @@ -0,0 +1,9 @@ +setup status=1 byte_order=0 screens=1 roots=1 +screen root_depth=24 width=320 height=240 +atom name=LIBX11_COMPAT_XCB_PROBE length=23 +attributes class=1 gravity=1 map_state=0 override=0 +geometry x=10 y=20 width=160 height=90 border=0 depth=24 +tree parent_is_root=1 children=0 +property format=8 length=9 after=0 value=xcb-probe +event index=0 type=28 synthetic=0 +event index=1 type=19 synthetic=0 diff --git a/tests/private-symbols.txt b/tests/private-symbols.txt index a43ced0f..94de7a38 100644 --- a/tests/private-symbols.txt +++ b/tests/private-symbols.txt @@ -3,13 +3,18 @@ # exposes them as a link contract. src/xft.c is compiled twice: once into the # core .so and once into the standalone libXft-compat.so. The standalone copy # calls these core-internal helpers, so hiding them would break the libXft -# link. Keep this list in sync with the core helpers src/xft.c reaches for. A +# link. The XCB compatibility libraries are the second such consumer: +# compat/xcb-compat.c and compat/xcb-requests.c call the XID table and atom +# helpers listed here across the same boundary. Keep this list in sync with the +# core helpers those two link contracts reach for. A # missing entry fails the libXft-compat link loudly on both platforms: macOS via # the two-level namespace, Linux via the -Wl,--no-undefined added to that link # (mk/xcompat-libs.mk). The libX11 underscore # ABI (_Xdebug, _XGetHostname, _XDefaultError, ...) is deliberately NOT listed # here: the export generator matches every core-defined _X* symbol by pattern, # so that surface is covered without hand-listing (see scripts/gen-export-list.sh). +SCREEN_WINDOW +allocXidResource applyShapeMaskOverDrawnRect captureShapeMaskBaseline clearRendererClip @@ -18,8 +23,9 @@ compatFontOpenFamilyFallbackForChar getGcClipIterationCount getWindowRenderer getXidStruct +isXidAllocated markPixmapReadbackDirty presentDrawableRectIfVisible -SCREEN_WINDOW +reserveXidResource setGcClipForIteration setRendererDrawableClip diff --git a/tests/probe-system-xcb.c b/tests/probe-system-xcb.c new file mode 100644 index 00000000..8863e783 --- /dev/null +++ b/tests/probe-system-xcb.c @@ -0,0 +1,130 @@ +/* Deterministic core-XCB reference probe. */ +#include +#include +#include +#include + +static void fail(const char *message) +{ + fprintf(stderr, "probe-system-xcb: %s\n", message); + exit(1); +} + +static xcb_screen_t *firstScreen(const xcb_setup_t *setup) +{ + xcb_screen_iterator_t iterator = xcb_setup_roots_iterator(setup); + return iterator.rem ? iterator.data : NULL; +} + +int main(void) +{ + int preferredScreen = 0; + xcb_connection_t *connection = xcb_connect(NULL, &preferredScreen); + if (!connection || xcb_connection_has_error(connection)) + fail("cannot connect"); + + const xcb_setup_t *setup = xcb_get_setup(connection); + if (!setup) + fail("connection has no setup"); + xcb_screen_t *screen = firstScreen(setup); + if (!screen) + fail("setup has no screen"); + + printf("setup status=%u byte_order=%u screens=%u roots=%d\n", setup->status, + setup->image_byte_order, setup->roots_len, + xcb_setup_roots_length(setup)); + printf("screen root_depth=%u width=%u height=%u\n", screen->root_depth, + screen->width_in_pixels, screen->height_in_pixels); + + const char atomName[] = "LIBX11_COMPAT_XCB_PROBE"; + xcb_intern_atom_cookie_t atomCookie = + xcb_intern_atom(connection, 0, sizeof(atomName) - 1, atomName); + xcb_intern_atom_reply_t *atomReply = + xcb_intern_atom_reply(connection, atomCookie, NULL); + if (!atomReply || atomReply->atom == XCB_ATOM_NONE) + fail("cannot intern atom"); + xcb_atom_t atom = atomReply->atom; + free(atomReply); + + xcb_get_atom_name_cookie_t nameCookie = xcb_get_atom_name(connection, atom); + xcb_get_atom_name_reply_t *nameReply = + xcb_get_atom_name_reply(connection, nameCookie, NULL); + if (!nameReply) + fail("cannot get atom name"); + printf("atom name=%.*s length=%d\n", + xcb_get_atom_name_name_length(nameReply), + xcb_get_atom_name_name(nameReply), + xcb_get_atom_name_name_length(nameReply)); + free(nameReply); + + xcb_window_t window = xcb_generate_id(connection); + uint32_t values[] = { + screen->white_pixel, + XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE}; + xcb_void_cookie_t createCookie = xcb_create_window_checked( + connection, XCB_COPY_FROM_PARENT, window, screen->root, 10, 20, 160, 90, + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, + XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK, values); + xcb_generic_error_t *error = xcb_request_check(connection, createCookie); + if (error) + fail("create window failed"); + + const char payload[] = "xcb-probe"; + xcb_change_property(connection, XCB_PROP_MODE_REPLACE, window, atom, + XCB_ATOM_STRING, 8, sizeof(payload) - 1, payload); + + xcb_get_window_attributes_reply_t *attributes = + xcb_get_window_attributes_reply( + connection, xcb_get_window_attributes(connection, window), NULL); + if (!attributes) + fail("cannot get attributes"); + printf("attributes class=%u gravity=%u map_state=%u override=%u\n", + attributes->_class, attributes->win_gravity, attributes->map_state, + attributes->override_redirect); + free(attributes); + + xcb_get_geometry_reply_t *geometry = xcb_get_geometry_reply( + connection, xcb_get_geometry(connection, window), NULL); + if (!geometry) + fail("cannot get geometry"); + printf("geometry x=%d y=%d width=%u height=%u border=%u depth=%u\n", + geometry->x, geometry->y, geometry->width, geometry->height, + geometry->border_width, geometry->depth); + free(geometry); + + xcb_query_tree_reply_t *tree = xcb_query_tree_reply( + connection, xcb_query_tree(connection, window), NULL); + if (!tree) + fail("cannot query tree"); + printf("tree parent_is_root=%u children=%d\n", tree->parent == screen->root, + xcb_query_tree_children_length(tree)); + free(tree); + + xcb_get_property_reply_t *property = xcb_get_property_reply( + connection, + xcb_get_property(connection, 0, window, atom, XCB_GET_PROPERTY_TYPE_ANY, + 0, 64), + NULL); + if (!property) + fail("cannot get property"); + printf("property format=%u length=%d after=%u value=%.*s\n", + property->format, xcb_get_property_value_length(property), + property->bytes_after, xcb_get_property_value_length(property), + (const char *) xcb_get_property_value(property)); + free(property); + + xcb_map_window(connection, window); + xcb_flush(connection); + for (int index = 0; index < 2; index++) { + xcb_generic_event_t *event = xcb_wait_for_event(connection); + if (!event) + fail("property/map operation produced no event"); + printf("event index=%d type=%u synthetic=%u\n", index, + event->response_type & 0x7f, !!(event->response_type & 0x80)); + free(event); + } + + xcb_destroy_window(connection, window); + xcb_disconnect(connection); + return 0; +} diff --git a/tests/shim-symbols.txt b/tests/shim-symbols.txt index 68674ba7..3071a9ea 100644 --- a/tests/shim-symbols.txt +++ b/tests/shim-symbols.txt @@ -1,14 +1,18 @@ # Non-Xlib shim hooks exported by libX11-compat (all prefixed libx11Compat). -# These are the live-resize / accelerated-present integration points, resolved -# at runtime by clients such as the xwpe live-resize harness. They are not part -# of the Xlib ABI but are still an exported surface, so this manifest is enforced -# by `make symbol-coverage` (via tests/check-api-symbols.py) to catch accidental -# additions or removals. Only exported on builds that compile the live-resize -# layer (macOS); on other platforms the exported set is empty and the check is a -# no-op. Keep this list sorted and in sync with intentional shim API changes. +# These are the live-resize / accelerated-present integration points and the +# hooks the XCB shim installs, resolved at runtime by clients such as the xwpe +# live-resize harness and by libxcb-compat. They are not part of the Xlib ABI +# but are still an exported surface, so this manifest is enforced by +# `make symbol-coverage` (via tests/check-api-symbols.py) to catch accidental +# additions or removals. The live-resize entries only exist on builds that +# compile that layer (macOS); the XCB hooks are unconditional, so the check is +# live on every platform. Keep this list sorted and in sync with intentional +# shim API changes. libx11CompatAcceleratedPresentForcedForTest libx11CompatAcceleratedPresentUsable +libx11CompatBeginXcbEventAccess libx11CompatConfigureLiveResizeLayer +libx11CompatEndXcbEventAccess libx11CompatFailAcceleratedPresentOnceForTest libx11CompatForceAcceleratedPresentForTest libx11CompatForceSoftwarePresent @@ -23,6 +27,9 @@ libx11CompatRegisterLiveResizeReflow libx11CompatResetLiveResizeCoalesce libx11CompatRunDeferredDisplayClose libx11CompatSdlPeepEventsIsXlibDrain +libx11CompatSetDisplayCloseHook +libx11CompatSetEventQueueOwnerHook +libx11CompatSetEventWaitCancelledHook libx11CompatSideQueueEventRemoved libx11CompatSnapAxisToIncrement libx11CompatSoftwareDemotionCountForTest diff --git a/tests/test-check-xcb-symbols.py b/tests/test-check-xcb-symbols.py new file mode 100644 index 00000000..941d8462 --- /dev/null +++ b/tests/test-check-xcb-symbols.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Unit tests for scripts/check-xcb-symbols.py.""" + +from __future__ import annotations + +import importlib.util +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SCRIPT = ROOT / "scripts" / "check-xcb-symbols.py" + + +def load_checker(): + spec = importlib.util.spec_from_file_location("check_xcb_symbols", SCRIPT) + module = importlib.util.module_from_spec(spec) + assert spec.loader + spec.loader.exec_module(module) + return module + + +def main() -> None: + checker = load_checker() + with tempfile.TemporaryDirectory() as temporary: + manifest = Path(temporary) / "symbols.txt" + manifest.write_text("# comment\nxcb_connect\nxcb_flush\n") + assert checker.manifest_symbols(manifest) == {"xcb_connect", "xcb_flush"} + + manifest.write_text("xcb_connect\nxcb_connect\n") + try: + checker.manifest_symbols(manifest) + except SystemExit as error: + assert "duplicate symbol" in str(error) + else: + raise AssertionError("duplicate manifest symbol was accepted") + + manifest.write_text("XOpenDisplay\n") + try: + checker.manifest_symbols(manifest) + except SystemExit as error: + assert "invalid XCB symbol" in str(error) + else: + raise AssertionError("non-XCB manifest symbol was accepted") + + print("test-check-xcb-symbols: ok") + + +if __name__ == "__main__": + main() diff --git a/tests/test-xcb-events.c b/tests/test-xcb-events.c new file mode 100644 index 00000000..0416549f --- /dev/null +++ b/tests/test-xcb-events.c @@ -0,0 +1,584 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "../compat/xcb-compat-private.h" + +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", m); \ + return 1; \ + } \ + } while (0) + +typedef struct { + xcb_connection_t *connection; + xcb_generic_event_t *result; +} EventWaiter; + +static void *waitForEvent(void *opaque) +{ + EventWaiter *waiter = opaque; + waiter->result = xcb_wait_for_event(waiter->connection); + return NULL; +} + +/* Wait until the connection reports the expected number of blocked waiters. + * The started flag alone only proves a thread entered waitForEvent, not that it + * reached the blocking call, so a sleep here would be a timing guess. + */ +static void waitForWaiters(xcb_connection_t *connection, unsigned int expected) +{ + while (xcbCompatEventWaiters(connection) < expected) + sched_yield(); +} + +static xcb_generic_event_t *convert(Display *display, + xcb_connection_t *connection, + XEvent *event) +{ + XPutBackEvent(display, event); + return xcb_poll_for_queued_event(connection); +} + +static xcb_generic_event_t *pollForType(xcb_connection_t *connection, + uint8_t type) +{ + for (int i = 0; i < 16; i++) { + xcb_generic_event_t *event = xcb_poll_for_queued_event(connection); + if (!event || (event->response_type & 0x7f) == type) + return event; + free(event); + } + return NULL; +} + +int main(void) +{ + Display *display = XOpenDisplay(NULL); + CHECK(display, "open"); + xcb_connection_t *connection = XGetXCBConnection(display); + Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 1, + 2, 30, 40, 0, 0, 0); + XSetEventQueueOwner(display, XCBOwnsEventQueue); + XEvent x; + memset(&x, 0, sizeof(x)); + x.xexpose.type = Expose; + x.xexpose.display = display; + x.xexpose.window = window; + x.xexpose.send_event = True; + x.xexpose.serial = 0x12345; + x.xexpose.x = 3; + x.xexpose.y = 4; + x.xexpose.width = 5; + x.xexpose.height = 6; + XPutBackEvent(display, &x); + CHECK(XEventsQueued(display, QueuedAlready) == 0, + "Xlib cannot consume XCB-owned queue"); + xcb_expose_event_t *expose = + (xcb_expose_event_t *) xcb_poll_for_queued_event(connection); + CHECK(expose && expose->response_type == (XCB_EXPOSE | 0x80) && + expose->sequence == 0x2345 && expose->window == window && + expose->x == 3 && expose->height == 6, + "expose conversion"); + free(expose); + + memset(&x, 0, sizeof(x)); + x.xgraphicsexpose.type = GraphicsExpose; + x.xgraphicsexpose.drawable = window; + x.xgraphicsexpose.x = 7; + x.xgraphicsexpose.y = 8; + x.xgraphicsexpose.width = 9; + x.xgraphicsexpose.height = 10; + x.xgraphicsexpose.count = 2; + x.xgraphicsexpose.major_code = XCB_COPY_AREA; + x.xgraphicsexpose.minor_code = 11; + xcb_graphics_exposure_event_t *graphicsExpose = + (xcb_graphics_exposure_event_t *) convert(display, connection, &x); + CHECK(graphicsExpose && + graphicsExpose->response_type == XCB_GRAPHICS_EXPOSURE && + graphicsExpose->drawable == window && graphicsExpose->x == 7 && + graphicsExpose->y == 8 && graphicsExpose->width == 9 && + graphicsExpose->height == 10 && graphicsExpose->count == 2 && + graphicsExpose->major_opcode == XCB_COPY_AREA && + graphicsExpose->minor_opcode == 11, + "graphics-expose conversion"); + free(graphicsExpose); + + memset(&x, 0, sizeof(x)); + x.xnoexpose.type = NoExpose; + x.xnoexpose.drawable = window; + x.xnoexpose.major_code = XCB_COPY_PLANE; + x.xnoexpose.minor_code = 12; + xcb_no_exposure_event_t *noExpose = + (xcb_no_exposure_event_t *) convert(display, connection, &x); + CHECK(noExpose && noExpose->response_type == XCB_NO_EXPOSURE && + noExpose->drawable == window && + noExpose->major_opcode == XCB_COPY_PLANE && + noExpose->minor_opcode == 12, + "no-expose conversion"); + free(noExpose); + + memset(&x, 0, sizeof(x)); + x.xconfigure.type = ConfigureNotify; + x.xconfigure.window = window; + x.xconfigure.event = window; + x.xconfigure.width = 31; + x.xconfigure.height = 41; + xcb_configure_notify_event_t *configure = + (xcb_configure_notify_event_t *) convert(display, connection, &x); + CHECK(configure && configure->window == window && configure->width == 31 && + configure->height == 41, + "configure conversion"); + free(configure); + + Window root = DefaultRootWindow(display); + memset(&x, 0, sizeof(x)); + x.xcreatewindow.type = CreateNotify; + x.xcreatewindow.parent = root; + x.xcreatewindow.window = window; + x.xcreatewindow.x = -3; + x.xcreatewindow.y = 4; + x.xcreatewindow.width = 31; + x.xcreatewindow.height = 41; + x.xcreatewindow.border_width = 2; + x.xcreatewindow.override_redirect = True; + xcb_create_notify_event_t *create = + (xcb_create_notify_event_t *) convert(display, connection, &x); + CHECK(create && create->response_type == XCB_CREATE_NOTIFY && + create->parent == root && create->window == window && + create->x == -3 && create->height == 41 && + create->border_width == 2 && create->override_redirect, + "create conversion"); + free(create); + + memset(&x, 0, sizeof(x)); + x.xmap.type = MapNotify; + x.xmap.event = root; + x.xmap.window = window; + x.xmap.override_redirect = True; + xcb_map_notify_event_t *map = + (xcb_map_notify_event_t *) convert(display, connection, &x); + CHECK(map && map->event == root && map->window == window && + map->override_redirect, + "map conversion"); + free(map); + + memset(&x, 0, sizeof(x)); + x.xunmap.type = UnmapNotify; + x.xunmap.event = root; + x.xunmap.window = window; + x.xunmap.from_configure = True; + xcb_unmap_notify_event_t *unmap = + (xcb_unmap_notify_event_t *) convert(display, connection, &x); + CHECK(unmap && unmap->event == root && unmap->window == window && + unmap->from_configure, + "unmap conversion"); + free(unmap); + + memset(&x, 0, sizeof(x)); + x.xreparent.type = ReparentNotify; + x.xreparent.event = root; + x.xreparent.window = window; + x.xreparent.parent = root; + x.xreparent.x = 8; + x.xreparent.y = 9; + x.xreparent.override_redirect = True; + xcb_reparent_notify_event_t *reparent = + (xcb_reparent_notify_event_t *) convert(display, connection, &x); + CHECK(reparent && reparent->event == root && reparent->window == window && + reparent->parent == root && reparent->x == 8 && + reparent->y == 9 && reparent->override_redirect, + "reparent conversion"); + free(reparent); + + memset(&x, 0, sizeof(x)); + x.xgravity.type = GravityNotify; + x.xgravity.event = root; + x.xgravity.window = window; + x.xgravity.x = -7; + x.xgravity.y = 12; + xcb_gravity_notify_event_t *gravity = + (xcb_gravity_notify_event_t *) convert(display, connection, &x); + CHECK(gravity && gravity->event == root && gravity->window == window && + gravity->x == -7 && gravity->y == 12, + "gravity conversion"); + free(gravity); + + memset(&x, 0, sizeof(x)); + x.xmaprequest.type = MapRequest; + x.xmaprequest.parent = root; + x.xmaprequest.window = window; + xcb_map_request_event_t *mapRequest = + (xcb_map_request_event_t *) convert(display, connection, &x); + CHECK(mapRequest && mapRequest->parent == root && + mapRequest->window == window, + "map-request conversion"); + free(mapRequest); + + memset(&x, 0, sizeof(x)); + x.xconfigurerequest.type = ConfigureRequest; + x.xconfigurerequest.parent = root; + x.xconfigurerequest.window = window; + x.xconfigurerequest.above = root; + x.xconfigurerequest.x = -11; + x.xconfigurerequest.y = 12; + x.xconfigurerequest.width = 45; + x.xconfigurerequest.height = 46; + x.xconfigurerequest.border_width = 3; + x.xconfigurerequest.detail = Below; + x.xconfigurerequest.value_mask = CWX | CWHeight | CWStackMode; + xcb_configure_request_event_t *configureRequest = + (xcb_configure_request_event_t *) convert(display, connection, &x); + CHECK(configureRequest && configureRequest->parent == root && + configureRequest->window == window && + configureRequest->sibling == root && configureRequest->x == -11 && + configureRequest->height == 46 && + configureRequest->stack_mode == XCB_STACK_MODE_BELOW && + configureRequest->value_mask == + (XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_HEIGHT | + XCB_CONFIG_WINDOW_STACK_MODE), + "configure-request conversion"); + free(configureRequest); + + memset(&x, 0, sizeof(x)); + x.xresizerequest.type = ResizeRequest; + x.xresizerequest.window = window; + x.xresizerequest.width = 47; + x.xresizerequest.height = 48; + xcb_resize_request_event_t *resize = + (xcb_resize_request_event_t *) convert(display, connection, &x); + CHECK(resize && resize->window == window && resize->width == 47 && + resize->height == 48, + "resize-request conversion"); + free(resize); + + memset(&x, 0, sizeof(x)); + x.xcirculate.type = CirculateNotify; + x.xcirculate.event = root; + x.xcirculate.window = window; + x.xcirculate.place = PlaceOnBottom; + xcb_circulate_notify_event_t *circulate = + (xcb_circulate_notify_event_t *) convert(display, connection, &x); + CHECK(circulate && circulate->event == root && + circulate->window == window && + circulate->place == XCB_PLACE_ON_BOTTOM, + "circulate conversion"); + free(circulate); + + memset(&x, 0, sizeof(x)); + x.xcirculaterequest.type = CirculateRequest; + x.xcirculaterequest.parent = root; + x.xcirculaterequest.window = window; + x.xcirculaterequest.place = PlaceOnTop; + xcb_circulate_request_event_t *circulateRequest = + (xcb_circulate_request_event_t *) convert(display, connection, &x); + CHECK(circulateRequest && circulateRequest->event == root && + circulateRequest->window == window && + circulateRequest->place == XCB_PLACE_ON_TOP, + "circulate-request conversion"); + free(circulateRequest); + + memset(&x, 0, sizeof(x)); + x.xproperty.type = PropertyNotify; + x.xproperty.window = window; + x.xproperty.atom = XCB_ATOM_WM_NAME; + x.xproperty.state = PropertyDelete; + xcb_property_notify_event_t *property = + (xcb_property_notify_event_t *) convert(display, connection, &x); + CHECK(property && property->atom == XCB_ATOM_WM_NAME && + property->state == XCB_PROPERTY_DELETE, + "property conversion"); + free(property); + + memset(&x, 0, sizeof(x)); + x.xselectionclear.type = SelectionClear; + x.xselectionclear.window = window; + x.xselectionclear.selection = XCB_ATOM_PRIMARY; + x.xselectionclear.time = 2345; + xcb_selection_clear_event_t *selectionClear = + (xcb_selection_clear_event_t *) convert(display, connection, &x); + CHECK(selectionClear && + selectionClear->response_type == XCB_SELECTION_CLEAR && + selectionClear->owner == window && + selectionClear->selection == XCB_ATOM_PRIMARY && + selectionClear->time == 2345, + "selection-clear conversion"); + free(selectionClear); + + memset(&x, 0, sizeof(x)); + x.xselectionrequest.type = SelectionRequest; + x.xselectionrequest.owner = window; + x.xselectionrequest.requestor = root; + x.xselectionrequest.selection = XCB_ATOM_PRIMARY; + x.xselectionrequest.target = XCB_ATOM_STRING; + x.xselectionrequest.property = XCB_ATOM_WM_NAME; + x.xselectionrequest.time = 3456; + xcb_selection_request_event_t *selectionRequest = + (xcb_selection_request_event_t *) convert(display, connection, &x); + CHECK(selectionRequest && + selectionRequest->response_type == XCB_SELECTION_REQUEST && + selectionRequest->owner == window && + selectionRequest->requestor == root && + selectionRequest->selection == XCB_ATOM_PRIMARY && + selectionRequest->target == XCB_ATOM_STRING && + selectionRequest->property == XCB_ATOM_WM_NAME && + selectionRequest->time == 3456, + "selection-request conversion"); + free(selectionRequest); + + memset(&x, 0, sizeof(x)); + x.xselection.type = SelectionNotify; + x.xselection.requestor = window; + x.xselection.selection = XCB_ATOM_PRIMARY; + x.xselection.target = XCB_ATOM_STRING; + x.xselection.property = XCB_ATOM_NONE; + x.xselection.time = 4567; + xcb_selection_notify_event_t *selectionNotify = + (xcb_selection_notify_event_t *) convert(display, connection, &x); + CHECK(selectionNotify && + selectionNotify->response_type == XCB_SELECTION_NOTIFY && + selectionNotify->requestor == window && + selectionNotify->selection == XCB_ATOM_PRIMARY && + selectionNotify->target == XCB_ATOM_STRING && + selectionNotify->property == XCB_ATOM_NONE && + selectionNotify->time == 4567, + "selection-notify conversion"); + free(selectionNotify); + + memset(&x, 0, sizeof(x)); + x.xcolormap.type = ColormapNotify; + x.xcolormap.window = window; + x.xcolormap.colormap = DefaultColormap(display, DefaultScreen(display)); + x.xcolormap.new = True; + x.xcolormap.state = ColormapInstalled; + xcb_colormap_notify_event_t *colormap = + (xcb_colormap_notify_event_t *) convert(display, connection, &x); + CHECK(colormap && colormap->response_type == XCB_COLORMAP_NOTIFY && + colormap->window == window && + colormap->colormap == + DefaultColormap(display, DefaultScreen(display)) && + colormap->_new && colormap->state == XCB_COLORMAP_STATE_INSTALLED, + "colormap conversion"); + free(colormap); + + memset(&x, 0, sizeof(x)); + x.xmapping.type = MappingNotify; + x.xmapping.window = window; + x.xmapping.request = MappingKeyboard; + x.xmapping.first_keycode = 17; + x.xmapping.count = 23; + xcb_mapping_notify_event_t *mapping = + (xcb_mapping_notify_event_t *) convert(display, connection, &x); + CHECK(mapping && mapping->response_type == XCB_MAPPING_NOTIFY && + mapping->request == XCB_MAPPING_KEYBOARD && + mapping->first_keycode == 17 && mapping->count == 23, + "mapping conversion"); + free(mapping); + + memset(&x, 0, sizeof(x)); + x.xclient.type = ClientMessage; + x.xclient.window = window; + x.xclient.message_type = XCB_ATOM_WM_NAME; + x.xclient.format = 8; + memcpy(x.xclient.data.b, "client-message-data!", 20); + xcb_client_message_event_t *client = + (xcb_client_message_event_t *) convert(display, connection, &x); + CHECK(client && client->window == window && client->format == 8 && + !memcmp(client->data.data8, "client-message-data!", 20), + "client conversion"); + free(client); + + memset(&x, 0, sizeof(x)); + x.xfocus.type = FocusIn; + x.xfocus.window = window; + x.xfocus.mode = NotifyGrab; + x.xfocus.detail = NotifyPointer; + xcb_focus_in_event_t *focus = + (xcb_focus_in_event_t *) convert(display, connection, &x); + CHECK(focus && focus->event == window && + focus->mode == XCB_NOTIFY_MODE_GRAB && + focus->detail == XCB_NOTIFY_DETAIL_POINTER, + "focus conversion"); + free(focus); + memset(&x, 0, sizeof(x)); + x.xkeymap.type = KeymapNotify; + x.xkeymap.serial = 0x34567; + x.xkeymap.window = window; + for (size_t i = 0; i < sizeof(x.xkeymap.key_vector); i++) + x.xkeymap.key_vector[i] = (char) (i * 7u + 3u); + xcb_keymap_notify_event_t *keymap = + (xcb_keymap_notify_event_t *) convert(display, connection, &x); + CHECK(keymap && keymap->response_type == XCB_KEYMAP_NOTIFY && + !memcmp(keymap->keys, x.xkeymap.key_vector + 1, + sizeof(keymap->keys)) && + ((xcb_generic_event_t *) keymap)->full_sequence == 0x34567, + "keymap conversion"); + free(keymap); + memset(&x, 0, sizeof(x)); + x.xkey.type = KeyPress; + x.xkey.window = window; + x.xkey.root = DefaultRootWindow(display); + x.xkey.keycode = 42; + x.xkey.x = 7; + x.xkey.state = ShiftMask; + xcb_key_press_event_t *key = + (xcb_key_press_event_t *) convert(display, connection, &x); + CHECK(key && key->event == window && key->detail == 42 && + key->event_x == 7 && key->state == XCB_MOD_MASK_SHIFT, + "key conversion"); + free(key); + memset(&x, 0, sizeof(x)); + x.xbutton.type = ButtonPress; + x.xbutton.window = window; + x.xbutton.button = 3; + xcb_button_press_event_t *button = + (xcb_button_press_event_t *) convert(display, connection, &x); + CHECK(button && button->event == window && button->detail == 3, + "button conversion"); + free(button); + memset(&x, 0, sizeof(x)); + x.xmotion.type = MotionNotify; + x.xmotion.window = window; + x.xmotion.is_hint = NotifyHint; + x.xmotion.x_root = 19; + xcb_motion_notify_event_t *motion = + (xcb_motion_notify_event_t *) convert(display, connection, &x); + CHECK(motion && motion->event == window && + motion->detail == XCB_MOTION_HINT && motion->root_x == 19, + "motion conversion"); + free(motion); + memset(&x, 0, sizeof(x)); + x.xcrossing.type = EnterNotify; + x.xcrossing.window = window; + x.xcrossing.root = root; + x.xcrossing.subwindow = window; + x.xcrossing.time = 1234; + x.xcrossing.x = -2; + x.xcrossing.y = 3; + x.xcrossing.x_root = 18; + x.xcrossing.y_root = 19; + x.xcrossing.mode = NotifyGrab; + x.xcrossing.detail = NotifyInferior; + x.xcrossing.same_screen = True; + x.xcrossing.focus = True; + x.xcrossing.state = ControlMask; + xcb_enter_notify_event_t *enter = + (xcb_enter_notify_event_t *) convert(display, connection, &x); + CHECK(enter && enter->response_type == XCB_ENTER_NOTIFY && + enter->event == window && enter->root == root && + enter->child == window && enter->time == 1234 && + enter->event_x == -2 && enter->root_y == 19 && + enter->mode == XCB_NOTIFY_MODE_GRAB && + enter->detail == XCB_NOTIFY_DETAIL_INFERIOR && + enter->state == XCB_MOD_MASK_CONTROL && + enter->same_screen_focus == 3, + "enter conversion"); + free(enter); + x.xcrossing.type = LeaveNotify; + x.xcrossing.focus = False; + xcb_leave_notify_event_t *leave = + (xcb_leave_notify_event_t *) convert(display, connection, &x); + CHECK(leave && leave->response_type == XCB_LEAVE_NOTIFY && + leave->same_screen_focus == 1, + "leave conversion"); + free(leave); + memset(&x, 0, sizeof(x)); + x.xvisibility.type = VisibilityNotify; + x.xvisibility.window = window; + x.xvisibility.state = VisibilityPartiallyObscured; + xcb_visibility_notify_event_t *visibility = + (xcb_visibility_notify_event_t *) convert(display, connection, &x); + CHECK(visibility && visibility->response_type == XCB_VISIBILITY_NOTIFY && + visibility->window == window && + visibility->state == XCB_VISIBILITY_PARTIALLY_OBSCURED, + "visibility conversion"); + free(visibility); + memset(&x, 0, sizeof(x)); + x.xdestroywindow.type = DestroyNotify; + x.xdestroywindow.event = DefaultRootWindow(display); + x.xdestroywindow.window = window; + xcb_destroy_notify_event_t *destroy = + (xcb_destroy_notify_event_t *) convert(display, connection, &x); + CHECK(destroy && destroy->event == DefaultRootWindow(display) && + destroy->window == window, + "destroy conversion"); + free(destroy); + CHECK(!xcb_poll_for_event(connection), "empty poll"); + CHECK(!xcb_poll_for_special_event(connection, NULL), "special empty"); + memset(&x, 0, sizeof(x)); + x.xfocus.type = FocusOut; + x.xfocus.window = window; + XPutBackEvent(display, &x); + xcb_generic_event_t *waited = xcb_wait_for_event(connection); + CHECK(waited && waited->response_type == XCB_FOCUS_OUT, "blocking wait"); + free(waited); + XSetEventQueueOwner(display, XlibOwnsEventQueue); + memset(&x, 0, sizeof(x)); + x.xfocus.type = FocusIn; + x.xfocus.window = window; + XPutBackEvent(display, &x); + CHECK(XEventsQueued(display, QueuedAlready) == 1, + "Xlib sees Xlib-owned queue"); + XSetEventQueueOwner(display, XCBOwnsEventQueue); + waited = xcb_poll_for_queued_event(connection); + CHECK(waited && waited->response_type == XCB_FOCUS_IN, + "Xlib-to-XCB switch preserves event"); + free(waited); + memset(&x, 0, sizeof(x)); + x.xfocus.type = FocusOut; + x.xfocus.window = window; + XPutBackEvent(display, &x); + XSetEventQueueOwner(display, XlibOwnsEventQueue); + CHECK(XEventsQueued(display, QueuedAlready) == 1, + "XCB-to-Xlib switch preserves event"); + XEvent consumed; + XNextEvent(display, &consumed); + CHECK(consumed.type == FocusOut, "Xlib consumes after owner switch"); + XSelectInput(display, window, NoEventMask); + xcb_disconnect(connection); + XCloseDisplay(display); + + display = XOpenDisplay(NULL); + CHECK(display, "open display-close test"); + connection = XGetXCBConnection(display); + CHECK(connection, "display-close connection"); + EventWaiter waiter = {.connection = connection}; + pthread_t thread; + CHECK(!pthread_create(&thread, NULL, waitForEvent, &waiter), + "display-close waiter creation"); + waitForWaiters(connection, 1); + XCloseDisplay(display); + CHECK(!pthread_join(thread, NULL), "display-close waiter join"); + CHECK(!waiter.result && + xcb_connection_has_error(connection) == XCB_CONN_ERROR, + "display close wakes XCB waiter"); + xcb_disconnect(connection); + + connection = xcb_connect(NULL, NULL); + CHECK(connection && !xcb_connection_has_error(connection), + "open disconnect test"); + enum { DISCONNECT_WAITER_COUNT = 4 }; + EventWaiter disconnectWaiters[DISCONNECT_WAITER_COUNT] = {0}; + pthread_t disconnectThreads[DISCONNECT_WAITER_COUNT]; + for (size_t i = 0; i < DISCONNECT_WAITER_COUNT; i++) { + disconnectWaiters[i].connection = connection; + CHECK(!pthread_create(&disconnectThreads[i], NULL, waitForEvent, + &disconnectWaiters[i]), + "disconnect waiter creation"); + } + waitForWaiters(connection, DISCONNECT_WAITER_COUNT); + xcb_disconnect(connection); + for (size_t i = 0; i < DISCONNECT_WAITER_COUNT; i++) { + CHECK(!pthread_join(disconnectThreads[i], NULL), + "disconnect waiter join"); + CHECK(!disconnectWaiters[i].result, "disconnect wakes XCB waiters"); + } + puts("test-xcb-events: ok"); + return 0; +} diff --git a/tests/test-xcb-link.c b/tests/test-xcb-link.c new file mode 100644 index 00000000..cdcf3050 --- /dev/null +++ b/tests/test-xcb-link.c @@ -0,0 +1,33 @@ +#include +#include + +int main(void) +{ + int screenNumber = -1; + xcb_connection_t *connection = xcb_connect(NULL, &screenNumber); + if (!connection || xcb_connection_has_error(connection)) { + fprintf(stderr, "test-xcb-link: connection failed\n"); + return 1; + } + const xcb_setup_t *setup = xcb_get_setup(connection); + if (!setup) { + fprintf(stderr, "test-xcb-link: invalid setup\n"); + xcb_disconnect(connection); + return 1; + } + xcb_screen_iterator_t screens = xcb_setup_roots_iterator(setup); + if (setup->status != 1 || screenNumber != 0 || screens.rem != 1 || + !screens.data || screens.data->root == XCB_WINDOW_NONE) { + fprintf(stderr, "test-xcb-link: invalid setup\n"); + xcb_disconnect(connection); + return 1; + } + if (!xcb_flush(connection)) { + fprintf(stderr, "test-xcb-link: flush failed\n"); + xcb_disconnect(connection); + return 1; + } + xcb_disconnect(connection); + puts("test-xcb-link: ok"); + return 0; +} diff --git a/tests/test-xcb-setup.c b/tests/test-xcb-setup.c new file mode 100644 index 00000000..e77687de --- /dev/null +++ b/tests/test-xcb-setup.c @@ -0,0 +1,271 @@ +#include +#include +#include +#include +#include +#include "../compat/xcb-compat-private.h" + +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", (m)); \ + return 1; \ + } \ + } while (0) + +enum { ID_THREAD_COUNT = 8, IDS_PER_THREAD = 4096 }; + +typedef struct { + xcb_connection_t *connection; + uint32_t ids[IDS_PER_THREAD]; +} IdThread; + +static void *generateIds(void *opaque) +{ + IdThread *thread = opaque; + for (size_t i = 0; i < IDS_PER_THREAD; i++) + thread->ids[i] = xcb_generate_id(thread->connection); + return NULL; +} + +static int compareIds(const void *left, const void *right) +{ + uint32_t a = *(const uint32_t *) left; + uint32_t b = *(const uint32_t *) right; + return (a > b) - (a < b); +} + +int main(void) +{ + int screenNumber = -1; + xcb_connection_t *connection = + xcb_connect_to_display_with_auth_info(NULL, NULL, &screenNumber); + CHECK(connection && !xcb_connection_has_error(connection), + "connection failed"); + CHECK(screenNumber == 0 && xcb_get_file_descriptor(connection) == -1, + "connection metadata mismatch"); + CHECK(xcb_get_maximum_request_length(connection) == 65535, + "request limit mismatch"); + const xcb_setup_t *setup = xcb_get_setup(connection); + CHECK(setup && setup->status == 1 && setup->roots_len == 1, + "invalid setup"); + static const char expectedVendor[] = "libx11-compat"; + CHECK(xcb_setup_vendor_length(setup) == (int) sizeof(expectedVendor) - 1 && + !memcmp(xcb_setup_vendor(setup), expectedVendor, + sizeof(expectedVendor) - 1), + "invalid setup vendor"); + xcb_generic_iterator_t vendorEnd = xcb_setup_vendor_end(setup); + CHECK(vendorEnd.data == + xcb_setup_vendor(setup) + xcb_setup_vendor_length(setup) && + vendorEnd.index == + (int) (sizeof(*setup) + xcb_setup_vendor_length(setup)), + "invalid setup vendor end"); + CHECK(setup->resource_id_base && setup->resource_id_mask && + !(setup->resource_id_base & setup->resource_id_mask) && + (setup->resource_id_base | setup->resource_id_mask) <= 0x1fffffff, + "invalid XID range"); + static const xcb_format_t expectedFormats[] = { + {.depth = 1, .bits_per_pixel = 1, .scanline_pad = 32}, + {.depth = 16, .bits_per_pixel = 16, .scanline_pad = 32}, + {.depth = 24, .bits_per_pixel = 32, .scanline_pad = 32}, + {.depth = 32, .bits_per_pixel = 32, .scanline_pad = 32}, + }; + CHECK(xcb_setup_pixmap_formats_length(setup) == + (int) (sizeof(expectedFormats) / sizeof(expectedFormats[0])), + "invalid pixmap format count"); + xcb_format_iterator_t formats = xcb_setup_pixmap_formats_iterator(setup); + CHECK(formats.data == xcb_setup_pixmap_formats(setup), + "pixmap format accessor mismatch"); + for (size_t i = 0; i < sizeof(expectedFormats) / sizeof(expectedFormats[0]); + i++) { + CHECK(formats.rem > 0 && + formats.data->depth == expectedFormats[i].depth && + formats.data->bits_per_pixel == + expectedFormats[i].bits_per_pixel && + formats.data->scanline_pad == expectedFormats[i].scanline_pad, + "invalid pixmap format"); + xcb_format_next(&formats); + } + CHECK(!formats.rem && xcb_format_end(formats).data == formats.data, + "pixmap format iterator termination failed"); + + xcb_screen_iterator_t screens = xcb_setup_roots_iterator(setup); + CHECK(screens.rem == 1 && screens.data && screens.data->root, + "invalid screen"); + CHECK(screens.data->width_in_pixels && screens.data->height_in_pixels, + "invalid dimensions"); + xcb_depth_iterator_t depths = + xcb_screen_allowed_depths_iterator(screens.data); + CHECK(depths.rem == 1 && depths.data->depth == screens.data->root_depth, + "invalid depth"); + xcb_visualtype_iterator_t visuals = xcb_depth_visuals_iterator(depths.data); + CHECK(visuals.data == xcb_depth_visuals(depths.data), + "visual accessor mismatch"); + CHECK(visuals.rem == 1 && + visuals.data->visual_id == screens.data->root_visual, + "invalid visual"); + xcb_generic_iterator_t visualEnd = xcb_visualtype_end(visuals); + xcb_generic_iterator_t depthEnd = xcb_depth_end(depths); + xcb_generic_iterator_t screenEnd = xcb_screen_end(screens); + CHECK( + !visualEnd.rem && visualEnd.data == xcb_depth_visuals(depths.data) + 1, + "visual end mismatch"); + CHECK(!depthEnd.rem && depthEnd.data == visualEnd.data, + "depth end mismatch"); + CHECK(!screenEnd.rem && screenEnd.data == depthEnd.data, + "screen end mismatch"); + CHECK(xcb_depth_sizeof(depths.data) == + (int) (sizeof(*depths.data) + sizeof(*visuals.data)) && + xcb_screen_sizeof(screens.data) == + (int) ((char *) screenEnd.data - (char *) screens.data) && + xcb_setup_sizeof(setup) == (int) (8 + setup->length * 4u), + "setup record size mismatch"); + xcb_visualtype_next(&visuals); + xcb_depth_next(&depths); + xcb_screen_next(&screens); + CHECK(!visuals.rem && !depths.rem && !screens.rem && + visuals.data == visualEnd.data && depths.data == depthEnd.data && + screens.data == screenEnd.data, + "iterator termination failed"); + struct { + xcb_screen_t screen; + xcb_depth_t firstDepth; + xcb_visualtype_t firstVisuals[2]; + xcb_depth_t secondDepth; + xcb_visualtype_t secondVisual; + } synthetic = { + .screen.allowed_depths_len = 2, + .firstDepth.visuals_len = 2, + .secondDepth.visuals_len = 1, + }; + CHECK(xcb_depth_sizeof(&synthetic.firstDepth) == + (int) (sizeof(synthetic.firstDepth) + + sizeof(synthetic.firstVisuals)) && + xcb_screen_sizeof(&synthetic.screen) == (int) sizeof(synthetic), + "multi-depth record size mismatch"); + xcb_screen_iterator_t syntheticScreen = { + .data = &synthetic.screen, + .rem = 1, + .index = 0, + }; + xcb_screen_next(&syntheticScreen); + CHECK(!syntheticScreen.rem && + syntheticScreen.data == + (xcb_screen_t *) ((char *) &synthetic + sizeof(synthetic)), + "multi-depth screen traversal mismatch"); + CHECK(!xcb_depth_sizeof(NULL) && !xcb_screen_sizeof(NULL) && + !xcb_setup_sizeof(NULL), + "NULL record size mismatch"); + uint32_t first = xcb_generate_id(connection), + second = xcb_generate_id(connection); + CHECK(first != UINT32_MAX && second > first, "XID allocation failed"); + IdThread idThreads[ID_THREAD_COUNT] = {{0}}; + pthread_t workers[ID_THREAD_COUNT]; + for (size_t i = 0; i < ID_THREAD_COUNT; i++) { + idThreads[i].connection = connection; + CHECK(!pthread_create(&workers[i], NULL, generateIds, &idThreads[i]), + "XID worker creation failed"); + } + uint32_t *generated = + malloc(sizeof(*generated) * ID_THREAD_COUNT * IDS_PER_THREAD); + CHECK(generated, "XID result allocation failed"); + for (size_t i = 0; i < ID_THREAD_COUNT; i++) { + CHECK(!pthread_join(workers[i], NULL), "XID worker join failed"); + memcpy(generated + i * IDS_PER_THREAD, idThreads[i].ids, + sizeof(idThreads[i].ids)); + } + qsort(generated, ID_THREAD_COUNT * IDS_PER_THREAD, sizeof(*generated), + compareIds); + for (size_t i = 0; i < ID_THREAD_COUNT * IDS_PER_THREAD; i++) { + CHECK(generated[i] != UINT32_MAX, + "concurrent XID allocation returned an error"); + CHECK(generated[i] >= setup->resource_id_base && + ((generated[i] - setup->resource_id_base) & + ~setup->resource_id_mask) == 0, + "generated XID is outside the advertised range"); + CHECK(i == 0 || generated[i] != generated[i - 1], + "concurrent XID allocation returned a duplicate"); + } + free(generated); + xcbCompatSetNextSequence(connection, UINT32_MAX); + CHECK(xcbCompatNextSequence(connection) == UINT32_MAX, + "wrap start mismatch"); + CHECK(xcbCompatNextSequence(connection) == (uint64_t) UINT32_MAX + 1, + "sequence wrap failed"); + xcbCompatSetNextSequence(connection, (uint64_t) UINT32_MAX + 1); + xcb_void_cookie_t wrappedCookie = xcbCompatVoidCookie( + connection, XCB_WINDOW, UINT32_C(0xdeadbeef), XCB_DESTROY_WINDOW, 1); + CHECK(wrappedCookie.sequence == 0, "request cookie did not wrap to zero"); + xcb_generic_error_t *wrappedError = + xcb_request_check(connection, wrappedCookie); + CHECK(wrappedError && wrappedError->error_code == XCB_WINDOW && + wrappedError->full_sequence == 0, + "zero sequence cookie was rejected after wrap"); + free(wrappedError); + xcb_generic_error_t *stored = calloc(1, sizeof(*stored)); + CHECK(stored, "allocation failed"); + stored->error_code = XCB_WINDOW; + xcbCompatStorePending(connection, 41, NULL, stored); + xcb_void_cookie_t cookie = {.sequence = 41}; + xcb_generic_error_t *received = xcb_request_check(connection, cookie); + CHECK(received && received->error_code == XCB_WINDOW, + "checked error missing"); + free(received); + CHECK(!xcb_request_check(connection, cookie), "error returned twice"); + xcb_generic_error_t *atomError = calloc(1, sizeof(*atomError)); + CHECK(atomError, "atom error allocation failed"); + atomError->error_code = XCB_ATOM; + xcbCompatStorePending(connection, 42, NULL, atomError); + cookie.sequence = 42; + received = xcb_request_check(connection, cookie); + CHECK(received && received->error_code == XCB_ATOM, + "BadAtom was not returned"); + free(received); + void *reply = calloc(1, 8); + CHECK(reply, "reply allocation failed"); + xcbCompatStorePending(connection, 43, reply, NULL); + CHECK(xcbCompatTakeReply(connection, 43, NULL) == reply, + "stored reply was not returned"); + free(reply); + xcbCompatStorePending(connection, 44, calloc(1, 8), NULL); + xcb_discard_reply(connection, 44); + xcb_discard_reply(connection, 44); + uint64_t wide = ((uint64_t) 1 << 32) + 45; + xcbCompatStorePending(connection, wide, calloc(1, 8), NULL); + xcb_discard_reply64(connection, wide); + wide = ((uint64_t) 1 << 32) + 46; + xcbCompatStorePending(connection, wide, calloc(1, 8), NULL); + xcbCompatSetNextSequence(connection, wide + 1); + xcb_discard_reply(connection, 46); + CHECK(!xcbCompatTakeReply(connection, wide, NULL), + "32-bit discard did not widen across wrap"); + + uint64_t *replacement = malloc(sizeof(*replacement)); + CHECK(replacement, "replacement allocation failed"); + *replacement = 2; + xcbCompatStorePending(connection, 100, calloc(1, sizeof(*replacement)), + NULL); + xcbCompatStorePending(connection, 100, replacement, NULL); + replacement = xcbCompatTakeReply(connection, 100, NULL); + CHECK(replacement && *replacement == 2, "pending replacement mismatch"); + free(replacement); + + enum { PENDING_STRESS_COUNT = 8192 }; + for (uint64_t i = 0; i < PENDING_STRESS_COUNT; i++) { + uint64_t *value = malloc(sizeof(*value)); + CHECK(value, "pending stress allocation failed"); + *value = i; + xcbCompatStorePending(connection, 1000 + i, value, NULL); + } + for (uint64_t i = PENDING_STRESS_COUNT; i > 0; i--) { + uint64_t expected = i - 1; + uint64_t *value = xcbCompatTakeReply(connection, 1000 + expected, NULL); + CHECK(value && *value == expected, "pending stress lookup mismatch"); + free(value); + } + xcb_disconnect(connection); + CHECK(xcb_connection_has_error(NULL) == XCB_CONN_ERROR, + "NULL error mismatch"); + puts("test-xcb-setup: ok"); + return 0; +} diff --git a/tests/xcb-private-symbols.txt b/tests/xcb-private-symbols.txt new file mode 100644 index 00000000..d1d86e03 --- /dev/null +++ b/tests/xcb-private-symbols.txt @@ -0,0 +1,3 @@ +xcbCompatConnectionForDisplay +xcbCompatEventWaiters +xcbCompatSetQueueOwner diff --git a/tests/xcb-symbols.txt b/tests/xcb-symbols.txt new file mode 100644 index 00000000..3864ea36 --- /dev/null +++ b/tests/xcb-symbols.txt @@ -0,0 +1,42 @@ +# Milestone-1 libxcb-compat ABI used by tests/probe-system-xcb.c. +# Keep sorted; later phases add symbols only with implementation tests. +xcb_connect +xcb_connect_to_display_with_auth_info +xcb_connection_has_error +xcb_depth_end +xcb_depth_next +xcb_depth_sizeof +xcb_depth_visuals +xcb_depth_visuals_iterator +xcb_depth_visuals_length +xcb_discard_reply +xcb_discard_reply64 +xcb_disconnect +xcb_flush +xcb_format_end +xcb_format_next +xcb_generate_id +xcb_get_file_descriptor +xcb_get_maximum_request_length +xcb_get_setup +xcb_poll_for_event +xcb_poll_for_queued_event +xcb_poll_for_special_event +xcb_request_check +xcb_screen_allowed_depths_iterator +xcb_screen_allowed_depths_length +xcb_screen_end +xcb_screen_next +xcb_screen_sizeof +xcb_setup_pixmap_formats +xcb_setup_pixmap_formats_iterator +xcb_setup_pixmap_formats_length +xcb_setup_roots_iterator +xcb_setup_roots_length +xcb_setup_sizeof +xcb_setup_vendor +xcb_setup_vendor_end +xcb_setup_vendor_length +xcb_visualtype_end +xcb_visualtype_next +xcb_wait_for_event diff --git a/tests/xlib-xcb-symbols.txt b/tests/xlib-xcb-symbols.txt new file mode 100644 index 00000000..292d6d96 --- /dev/null +++ b/tests/xlib-xcb-symbols.txt @@ -0,0 +1,2 @@ +XGetXCBConnection +XSetEventQueueOwner From 510f876d514fe8424b921b73114e5d6c9b73372f Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 31 Aug 2026 18:41:17 +0800 Subject: [PATCH 3/6] Implement core XCB window and property requests Translate the window, property and selection opcodes (1-4, 7-8, 10, 12, 14-24) onto the existing Xlib implementation, so an XCB client can create and configure windows, read geometry and the window tree, and exchange properties and selections. Each request validates its arguments before touching shared state, because the protocol answer for a bad request is a specific error code, not whatever the Xlib path happens to do with an out-of-range value. A checked request stores its error for xcb_request_check; an unchecked one queues the error for the event loop, which is where an XCB client expects to find it. A request that reserved the client's chosen id and then failed validation gives the id back, so a client can retry with it. Three gaps in the shared code had to be closed rather than worked around in the shim, so Xlib callers get the same fixes: XCreateWindow could not be handed a caller-chosen resource id, which is how every XCB client names its windows; restack honored only Above and Below, leaving TopIf, BottomIf and Opposite silently rejected; and XGetWindowAttributes left border width, class, gravities and screen unset while configure ignored CWBorderWidth. Drawing, pixmap and graphics-context requests are still deferred and remain absent from the export manifest. --- compat/xcb-requests.c | 1314 +++++++++++++++++++++++++++++++++ docs/XCB-COVERAGE.md | 5 +- mk/libxcb.mk | 6 +- mk/tests.mk | 8 + src/window-internal.c | 31 +- src/window.c | 84 ++- src/window.h | 14 + tests/private-symbols.txt | 2 + tests/shim-symbols.txt | 1 + tests/test-xcb-events.c | 38 + tests/test-xcb-property.c | 343 +++++++++ tests/test-xcb-setup.c | 56 ++ tests/test-xcb-window.c | 453 ++++++++++++ tests/xcb-private-symbols.txt | 1 + tests/xcb-symbols.txt | 62 ++ 15 files changed, 2392 insertions(+), 26 deletions(-) create mode 100644 compat/xcb-requests.c create mode 100644 tests/test-xcb-property.c create mode 100644 tests/test-xcb-window.c diff --git a/compat/xcb-requests.c b/compat/xcb-requests.c new file mode 100644 index 00000000..ea1c2f1d --- /dev/null +++ b/compat/xcb-requests.c @@ -0,0 +1,1314 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "xcb-compat-private.h" +#include "../src/atoms.h" +#include "../src/drawing.h" +#include "../src/resource-types.h" +#include "../src/window.h" + +static _Atomic int failNextReplyAllocation; + +void xcbCompatFailNextReplyAllocationForTest(void) +{ + atomic_store_explicit(&failNextReplyAllocation, 1, memory_order_release); +} + +static int isDrawable(xcb_drawable_t drawable) +{ + return IS_TYPE(drawable, DRAWABLE); +} + +static unsigned int drawableDepth(xcb_connection_t *c, xcb_drawable_t drawable) +{ + if (IS_TYPE(drawable, PIXMAP)) + return GET_PIXMAP_STRUCT(drawable)->depth; + WindowStruct *window = GET_WINDOW_STRUCT(drawable); + return window->depth == XCB_COPY_FROM_PARENT + ? (unsigned int) DefaultDepth(xcbCompatDisplay(c), + DefaultScreen(xcbCompatDisplay(c))) + : (unsigned int) window->depth; +} + +static unsigned int nextSequence(xcb_connection_t *c) +{ + return (unsigned int) xcbCompatNextSequence(c); +} + +#define REQUIRE_REQUEST(connection, cookieType) \ + do { \ + if (!xcbCompatRequestReady(connection)) \ + return (cookieType) {.sequence = 0}; \ + } while (0) + +static void storeReply(xcb_connection_t *c, unsigned int sequence, void *reply) +{ + if (!reply) { + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return; + } + ((xcb_generic_reply_t *) reply)->sequence = sequence; + xcbCompatStorePending(c, sequence, reply, NULL); +} + +static void *allocateReplyPayload(xcb_connection_t *c, + size_t headerSize, + uint64_t count, + size_t itemSize, + size_t *payloadSize) +{ + if (atomic_exchange_explicit(&failNextReplyAllocation, 0, + memory_order_acq_rel)) { + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return NULL; + } + if (itemSize && count > (SIZE_MAX - headerSize) / itemSize) { + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return NULL; + } + size_t payload = (size_t) count * itemSize; + void *reply = calloc(1, headerSize + payload); + if (!reply) + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + if (payloadSize) + *payloadSize = payload; + return reply; +} + +static int requestPayloadFits(xcb_connection_t *c, + size_t headerSize, + uint64_t count, + size_t itemSize) +{ + uint64_t payload = count * itemSize; + if (itemSize && payload / itemSize != count) + return 0; + if (payload > UINT64_MAX - 3u) + return 0; + uint64_t paddedPayload = (payload + 3u) & ~UINT64_C(3); + uint64_t maximumBytes = (uint64_t) xcb_get_maximum_request_length(c) * 4u; + return headerSize <= maximumBytes && + paddedPayload <= maximumBytes - headerSize; +} + +static int isWindow(xcb_window_t window) +{ + return IS_TYPE(window, WINDOW); +} + +#define XCB_CW_VALID_MASK \ + (XCB_CW_BACK_PIXMAP | XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXMAP | \ + XCB_CW_BORDER_PIXEL | XCB_CW_BIT_GRAVITY | XCB_CW_WIN_GRAVITY | \ + XCB_CW_BACKING_STORE | XCB_CW_BACKING_PLANES | XCB_CW_BACKING_PIXEL | \ + XCB_CW_OVERRIDE_REDIRECT | XCB_CW_SAVE_UNDER | XCB_CW_EVENT_MASK | \ + XCB_CW_DONT_PROPAGATE | XCB_CW_COLORMAP | XCB_CW_CURSOR) + +#define XCB_CONFIG_WINDOW_VALID_MASK \ + (XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | \ + XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_BORDER_WIDTH | \ + XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE) + +xcb_intern_atom_cookie_t xcb_intern_atom(xcb_connection_t *c, + uint8_t onlyIfExists, + uint16_t nameLength, + const char *name) +{ + REQUIRE_REQUEST(c, xcb_intern_atom_cookie_t); + xcb_intern_atom_cookie_t cookie = {nextSequence(c)}; + char *copy = malloc((size_t) nameLength + 1); + xcb_intern_atom_reply_t *reply = calloc(1, sizeof(*reply)); + if (copy && reply) { + memcpy(copy, name, nameLength); + copy[nameLength] = '\0'; + reply->response_type = 1; + reply->atom = XInternAtom(xcbCompatDisplay(c), copy, onlyIfExists); + storeReply(c, cookie.sequence, reply); + } else { + free(reply); + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + } + free(copy); + return cookie; +} + +xcb_intern_atom_cookie_t xcb_intern_atom_unchecked(xcb_connection_t *c, + uint8_t onlyIfExists, + uint16_t nameLength, + const char *name) +{ + return xcb_intern_atom(c, onlyIfExists, nameLength, name); +} + +xcb_intern_atom_reply_t *xcb_intern_atom_reply(xcb_connection_t *c, + xcb_intern_atom_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} + +static xcb_get_atom_name_cookie_t xcbGetAtomName(xcb_connection_t *c, + xcb_atom_t atom, + int checked) +{ + REQUIRE_REQUEST(c, xcb_get_atom_name_cookie_t); + xcb_get_atom_name_cookie_t cookie = {nextSequence(c)}; + if (!isValidAtom(atom)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_ATOM, atom, + XCB_GET_ATOM_NAME, checked); + return cookie; + } + char *name = XGetAtomName(xcbCompatDisplay(c), atom); + size_t length = name ? strlen(name) : 0; + size_t paddedLength = length <= SIZE_MAX - 3 ? (length + 3u) & ~3u : 0; + xcb_get_atom_name_reply_t *reply = + name && paddedLength >= length + ? allocateReplyPayload(c, sizeof(*reply), paddedLength, 1, NULL) + : NULL; + if (reply) { + reply->response_type = 1; + reply->name_len = length; + reply->length = (length + 3) / 4; + memcpy(reply + 1, name, length); + } + free(name); + storeReply(c, cookie.sequence, reply); + return cookie; +} + +xcb_get_atom_name_cookie_t xcb_get_atom_name(xcb_connection_t *c, + xcb_atom_t atom) +{ + return xcbGetAtomName(c, atom, 1); +} + +xcb_get_atom_name_cookie_t xcb_get_atom_name_unchecked(xcb_connection_t *c, + xcb_atom_t atom) +{ + return xcbGetAtomName(c, atom, 0); +} + +xcb_get_atom_name_reply_t *xcb_get_atom_name_reply( + xcb_connection_t *c, + xcb_get_atom_name_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} +int xcb_get_atom_name_sizeof(const void *buffer) +{ + const xcb_get_atom_name_reply_t *reply = buffer; + return reply ? (int) (sizeof(*reply) + reply->name_len) : 0; +} +char *xcb_get_atom_name_name(const xcb_get_atom_name_reply_t *reply) +{ + return reply ? (char *) (reply + 1) : NULL; +} +int xcb_get_atom_name_name_length(const xcb_get_atom_name_reply_t *reply) +{ + return reply ? reply->name_len : 0; +} +xcb_generic_iterator_t xcb_get_atom_name_name_end( + const xcb_get_atom_name_reply_t *reply) +{ + xcb_generic_iterator_t end = {0}; + if (reply) { + end.data = xcb_get_atom_name_name(reply) + reply->name_len; + end.index = sizeof(*reply) + reply->name_len; + } + return end; +} + +static void decodeAttributes(uint32_t mask, + const uint32_t *values, + XSetWindowAttributes *a) +{ + memset(a, 0, sizeof(*a)); + unsigned int n = 0; +#define TAKE(bit, field) \ + do { \ + if (mask & (bit)) \ + a->field = values[n++]; \ + } while (0) + TAKE(XCB_CW_BACK_PIXMAP, background_pixmap); + TAKE(XCB_CW_BACK_PIXEL, background_pixel); + TAKE(XCB_CW_BORDER_PIXMAP, border_pixmap); + TAKE(XCB_CW_BORDER_PIXEL, border_pixel); + TAKE(XCB_CW_BIT_GRAVITY, bit_gravity); + TAKE(XCB_CW_WIN_GRAVITY, win_gravity); + TAKE(XCB_CW_BACKING_STORE, backing_store); + TAKE(XCB_CW_BACKING_PLANES, backing_planes); + TAKE(XCB_CW_BACKING_PIXEL, backing_pixel); + TAKE(XCB_CW_OVERRIDE_REDIRECT, override_redirect); + TAKE(XCB_CW_SAVE_UNDER, save_under); + TAKE(XCB_CW_EVENT_MASK, event_mask); + TAKE(XCB_CW_DONT_PROPAGATE, do_not_propagate_mask); + TAKE(XCB_CW_COLORMAP, colormap); + TAKE(XCB_CW_CURSOR, cursor); +#undef TAKE +} + +static uint8_t validateAttributes(uint32_t mask, + const XSetWindowAttributes *attributes, + unsigned int windowDepth, + int inputOnly, + uint32_t *errorValue) +{ + const uint32_t inputOnlyMask = XCB_CW_WIN_GRAVITY | XCB_CW_EVENT_MASK | + XCB_CW_DONT_PROPAGATE | + XCB_CW_OVERRIDE_REDIRECT | XCB_CW_CURSOR; + if (inputOnly && (mask & ~inputOnlyMask)) { + *errorValue = mask & ~inputOnlyMask; + return XCB_MATCH; + } + if ((mask & XCB_CW_BIT_GRAVITY) && + attributes->bit_gravity > XCB_GRAVITY_STATIC) { + *errorValue = attributes->bit_gravity; + return XCB_VALUE; + } + if ((mask & XCB_CW_WIN_GRAVITY) && + attributes->win_gravity > XCB_GRAVITY_STATIC) { + *errorValue = attributes->win_gravity; + return XCB_VALUE; + } + if ((mask & XCB_CW_BACKING_STORE) && + attributes->backing_store > XCB_BACKING_STORE_ALWAYS) { + *errorValue = attributes->backing_store; + return XCB_VALUE; + } + if ((mask & XCB_CW_OVERRIDE_REDIRECT) && + attributes->override_redirect != False && + attributes->override_redirect != True) { + *errorValue = attributes->override_redirect; + return XCB_VALUE; + } + if ((mask & XCB_CW_SAVE_UNDER) && attributes->save_under != False && + attributes->save_under != True) { + *errorValue = attributes->save_under; + return XCB_VALUE; + } + const uint32_t validEventMask = (XCB_EVENT_MASK_OWNER_GRAB_BUTTON << 1) - 1; + if ((mask & XCB_CW_EVENT_MASK) && + ((uint32_t) attributes->event_mask & ~validEventMask)) { + *errorValue = attributes->event_mask; + return XCB_VALUE; + } + const uint32_t validDoNotPropagate = + XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | + XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | + XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_BUTTON_1_MOTION | + XCB_EVENT_MASK_BUTTON_2_MOTION | XCB_EVENT_MASK_BUTTON_3_MOTION | + XCB_EVENT_MASK_BUTTON_4_MOTION | XCB_EVENT_MASK_BUTTON_5_MOTION | + XCB_EVENT_MASK_BUTTON_MOTION; + if ((mask & XCB_CW_DONT_PROPAGATE) && + ((uint32_t) attributes->do_not_propagate_mask & ~validDoNotPropagate)) { + *errorValue = attributes->do_not_propagate_mask; + return XCB_VALUE; + } + const struct { + uint32_t bit; + Pixmap pixmap; + int parentRelativeAllowed; + } pixmaps[] = { + {XCB_CW_BACK_PIXMAP, attributes->background_pixmap, 1}, + {XCB_CW_BORDER_PIXMAP, attributes->border_pixmap, 0}, + }; + for (size_t i = 0; i < sizeof(pixmaps) / sizeof(pixmaps[0]); i++) { + if (!(mask & pixmaps[i].bit) || pixmaps[i].pixmap == XCB_NONE || + (pixmaps[i].parentRelativeAllowed && + pixmaps[i].pixmap == XCB_BACK_PIXMAP_PARENT_RELATIVE)) + continue; + *errorValue = pixmaps[i].pixmap; + if (!IS_TYPE(pixmaps[i].pixmap, PIXMAP)) + return XCB_PIXMAP; + if (GET_PIXMAP_STRUCT(pixmaps[i].pixmap)->depth != windowDepth) + return XCB_MATCH; + } + if ((mask & XCB_CW_COLORMAP) && attributes->colormap != XCB_NONE && + !IS_TYPE(attributes->colormap, COLORMAP)) { + *errorValue = attributes->colormap; + return XCB_COLORMAP; + } + if ((mask & XCB_CW_CURSOR) && attributes->cursor != XCB_NONE && + !IS_TYPE(attributes->cursor, CURSOR)) { + *errorValue = attributes->cursor; + return XCB_CURSOR; + } + return 0; +} + +static xcb_void_cookie_t createWindow(xcb_connection_t *c, + uint8_t depth, + xcb_window_t wid, + xcb_window_t parent, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint16_t borderWidth, + uint16_t clazz, + xcb_visualid_t visual, + uint32_t mask, + const uint32_t *values, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = 0; + uint32_t errorValue = wid; + if (!isWindow(parent)) { + error = XCB_WINDOW; + errorValue = parent; + } + if (!error && getXidStruct(wid)->type != 0) + error = XCB_ID_CHOICE; + if (!error && (mask & ~XCB_CW_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && mask && !values) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && clazz > XCB_WINDOW_CLASS_INPUT_ONLY) { + error = XCB_VALUE; + errorValue = clazz; + } + if (!error && !width) { + error = XCB_VALUE; + errorValue = width; + } + if (!error && !height) { + error = XCB_VALUE; + errorValue = height; + } + int parentInputOnly = !error && IS_INPUT_ONLY(parent); + int inputOnly = + clazz == XCB_WINDOW_CLASS_INPUT_ONLY || + (clazz == XCB_WINDOW_CLASS_COPY_FROM_PARENT && parentInputOnly); + if (!error && clazz == XCB_WINDOW_CLASS_INPUT_OUTPUT && parentInputOnly) { + error = XCB_MATCH; + errorValue = parent; + } + if (!error && inputOnly && borderWidth != 0) { + error = XCB_MATCH; + errorValue = borderWidth; + } + unsigned int defaultDepth = 0; + xcb_visualid_t defaultVisual = XCB_NONE; + if (!error) { + defaultDepth = (unsigned int) DefaultDepth( + xcbCompatDisplay(c), DefaultScreen(xcbCompatDisplay(c))); + defaultVisual = XVisualIDFromVisual(DefaultVisual( + xcbCompatDisplay(c), DefaultScreen(xcbCompatDisplay(c)))); + } + if (!error && inputOnly && depth != XCB_COPY_FROM_PARENT) { + error = XCB_MATCH; + errorValue = depth; + } + if (!error && inputOnly && visual != XCB_COPY_FROM_PARENT) { + error = XCB_MATCH; + errorValue = visual; + } + if (!error && !inputOnly && depth != XCB_COPY_FROM_PARENT && + depth != defaultDepth) { + error = XCB_MATCH; + errorValue = depth; + } + if (!error && !inputOnly && visual != XCB_COPY_FROM_PARENT && + visual != defaultVisual) { + error = XCB_MATCH; + errorValue = visual; + } + if (!error) { + XSetWindowAttributes attributes; + decodeAttributes(mask, values, &attributes); + unsigned int windowDepth = + depth == XCB_COPY_FROM_PARENT ? drawableDepth(c, parent) : depth; + error = validateAttributes(mask, &attributes, windowDepth, inputOnly, + &errorValue); + if (error) + goto done; + Visual *xvisual = visual == XCB_COPY_FROM_PARENT + ? CopyFromParent + : DefaultVisual(xcbCompatDisplay(c), 0); + + /* Claim a client-chosen id last: every check above can still fail, and + * an id reserved before them would have to be handed back. From here + * the creator owns it and releases it itself if it fails. + */ + if (!isXidAllocated(wid) && !reserveXidResource(wid)) + error = XCB_ID_CHOICE; + else if (libx11CompatCreateWindowWithId( + xcbCompatDisplay(c), wid, parent, x, y, width, height, + borderWidth, depth, clazz, xvisual, mask, + &attributes) != wid) + error = XCB_VALUE; + } +done: + return xcbCompatVoidCookie(c, error, errorValue, XCB_CREATE_WINDOW, + checked); +} + +xcb_void_cookie_t xcb_create_window_checked(xcb_connection_t *c, + uint8_t depth, + xcb_window_t wid, + xcb_window_t parent, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint16_t borderWidth, + uint16_t clazz, + xcb_visualid_t visual, + uint32_t mask, + const void *values) +{ + return createWindow(c, depth, wid, parent, x, y, width, height, borderWidth, + clazz, visual, mask, values, 1); +} +xcb_void_cookie_t xcb_create_window(xcb_connection_t *c, + uint8_t depth, + xcb_window_t wid, + xcb_window_t parent, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint16_t borderWidth, + uint16_t clazz, + xcb_visualid_t visual, + uint32_t mask, + const void *values) +{ + return createWindow(c, depth, wid, parent, x, y, width, height, borderWidth, + clazz, visual, mask, values, 0); +} + +#define WINDOW_VOID(name, checked_name, xfunc, opcode) \ + static xcb_void_cookie_t name##Impl(xcb_connection_t *c, xcb_window_t w, \ + int checked) \ + { \ + REQUIRE_REQUEST(c, xcb_void_cookie_t); \ + uint8_t e = isWindow(w) ? 0 : XCB_WINDOW; \ + if (!e) \ + xfunc(xcbCompatDisplay(c), w); \ + return xcbCompatVoidCookie(c, e, w, opcode, checked); \ + } \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_window_t w) \ + { \ + return name##Impl(c, w, 0); \ + } \ + xcb_void_cookie_t checked_name(xcb_connection_t *c, xcb_window_t w) \ + { \ + return name##Impl(c, w, 1); \ + } +WINDOW_VOID(xcb_map_window, xcb_map_window_checked, XMapWindow, XCB_MAP_WINDOW) +WINDOW_VOID(xcb_unmap_window, + xcb_unmap_window_checked, + XUnmapWindow, + XCB_UNMAP_WINDOW) +WINDOW_VOID(xcb_destroy_window, + xcb_destroy_window_checked, + XDestroyWindow, + XCB_DESTROY_WINDOW) + +static xcb_get_window_attributes_cookie_t +getWindowAttributes(xcb_connection_t *c, xcb_window_t w, int checked) +{ + REQUIRE_REQUEST(c, xcb_get_window_attributes_cookie_t); + xcb_get_window_attributes_cookie_t cookie = {nextSequence(c)}; + if (!isWindow(w)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_WINDOW, w, + XCB_GET_WINDOW_ATTRIBUTES, checked); + return cookie; + } + xcb_get_window_attributes_reply_t *reply = calloc(1, sizeof(*reply)); + XWindowAttributes a; + if (reply && XGetWindowAttributes(xcbCompatDisplay(c), w, &a)) { + reply->response_type = 1; + reply->visual = XVisualIDFromVisual(a.visual); + reply->_class = a.class; + reply->bit_gravity = a.bit_gravity; + reply->win_gravity = a.win_gravity; + reply->backing_store = a.backing_store; + reply->backing_planes = a.backing_planes; + reply->backing_pixel = a.backing_pixel; + reply->save_under = a.save_under; + reply->map_is_installed = a.map_installed; + reply->map_state = a.map_state; + reply->override_redirect = a.override_redirect; + reply->colormap = a.colormap; + reply->all_event_masks = a.all_event_masks; + reply->your_event_mask = a.your_event_mask; + reply->do_not_propagate_mask = a.do_not_propagate_mask; + } else { + free(reply); + reply = NULL; + } + storeReply(c, cookie.sequence, reply); + return cookie; +} +xcb_get_window_attributes_cookie_t xcb_get_window_attributes( + xcb_connection_t *c, + xcb_window_t w) +{ + return getWindowAttributes(c, w, 1); +} +xcb_get_window_attributes_cookie_t xcb_get_window_attributes_unchecked( + xcb_connection_t *c, + xcb_window_t w) +{ + return getWindowAttributes(c, w, 0); +} +xcb_get_window_attributes_reply_t *xcb_get_window_attributes_reply( + xcb_connection_t *c, + xcb_get_window_attributes_cookie_t cookie, + xcb_generic_error_t **e) +{ + return xcbCompatTakeReply(c, cookie.sequence, e); +} + +static xcb_get_geometry_cookie_t getGeometry(xcb_connection_t *c, + xcb_drawable_t d, + int checked) +{ + REQUIRE_REQUEST(c, xcb_get_geometry_cookie_t); + xcb_get_geometry_cookie_t cookie = {nextSequence(c)}; + if (!isDrawable(d)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_DRAWABLE, d, + XCB_GET_GEOMETRY, checked); + return cookie; + } + Window root; + int x, y; + unsigned int width, height, border, depth; + xcb_get_geometry_reply_t *reply = calloc(1, sizeof(*reply)); + if (reply && XGetGeometry(xcbCompatDisplay(c), d, &root, &x, &y, &width, + &height, &border, &depth)) { + reply->response_type = 1; + reply->root = root; + reply->x = x; + reply->y = y; + reply->width = width; + reply->height = height; + reply->border_width = border; + reply->depth = depth; + } else { + free(reply); + reply = NULL; + } + storeReply(c, cookie.sequence, reply); + return cookie; +} +xcb_get_geometry_cookie_t xcb_get_geometry(xcb_connection_t *c, + xcb_drawable_t d) +{ + return getGeometry(c, d, 1); +} +xcb_get_geometry_cookie_t xcb_get_geometry_unchecked(xcb_connection_t *c, + xcb_drawable_t d) +{ + return getGeometry(c, d, 0); +} +xcb_get_geometry_reply_t *xcb_get_geometry_reply( + xcb_connection_t *c, + xcb_get_geometry_cookie_t cookie, + xcb_generic_error_t **e) +{ + return xcbCompatTakeReply(c, cookie.sequence, e); +} + +static xcb_query_tree_cookie_t queryTree(xcb_connection_t *c, + xcb_window_t w, + int checked) +{ + REQUIRE_REQUEST(c, xcb_query_tree_cookie_t); + xcb_query_tree_cookie_t cookie = {nextSequence(c)}; + if (!isWindow(w)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_WINDOW, w, + XCB_QUERY_TREE, checked); + return cookie; + } + Window root, parent, *children = NULL; + unsigned int count = 0; + int ok = + XQueryTree(xcbCompatDisplay(c), w, &root, &parent, &children, &count); + xcb_query_tree_reply_t *reply = + ok ? allocateReplyPayload(c, sizeof(*reply), count, + sizeof(xcb_window_t), NULL) + : NULL; + if (reply) { + reply->response_type = 1; + reply->root = root; + reply->parent = parent; + reply->children_len = count; + reply->length = count; + for (unsigned int i = 0; i < count; i++) + ((xcb_window_t *) (reply + 1))[i] = children[i]; + } + free(children); + storeReply(c, cookie.sequence, reply); + return cookie; +} +xcb_query_tree_cookie_t xcb_query_tree(xcb_connection_t *c, xcb_window_t w) +{ + return queryTree(c, w, 1); +} +xcb_query_tree_cookie_t xcb_query_tree_unchecked(xcb_connection_t *c, + xcb_window_t w) +{ + return queryTree(c, w, 0); +} +xcb_query_tree_reply_t *xcb_query_tree_reply(xcb_connection_t *c, + xcb_query_tree_cookie_t cookie, + xcb_generic_error_t **e) +{ + return xcbCompatTakeReply(c, cookie.sequence, e); +} +int xcb_query_tree_sizeof(const void *buffer) +{ + const xcb_query_tree_reply_t *reply = buffer; + return reply ? (int) (sizeof(*reply) + + (size_t) reply->children_len * sizeof(xcb_window_t)) + : 0; +} +xcb_window_t *xcb_query_tree_children(const xcb_query_tree_reply_t *r) +{ + return r ? (xcb_window_t *) (r + 1) : NULL; +} +int xcb_query_tree_children_length(const xcb_query_tree_reply_t *r) +{ + return r ? r->children_len : 0; +} +xcb_generic_iterator_t xcb_query_tree_children_end( + const xcb_query_tree_reply_t *reply) +{ + xcb_generic_iterator_t end = {0}; + if (reply) { + end.data = xcb_query_tree_children(reply) + reply->children_len; + end.index = xcb_query_tree_sizeof(reply); + } + return end; +} + +static xcb_void_cookie_t changeProperty(xcb_connection_t *c, + uint8_t mode, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint8_t format, + uint32_t length, + const void *data, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = 0; + uint32_t errorValue = window; + if (!isWindow(window)) + error = XCB_WINDOW; + if (!error && !isValidAtom(property)) { + error = XCB_ATOM; + errorValue = property; + } + if (!error && !isValidAtom(type)) { + error = XCB_ATOM; + errorValue = type; + } + if (!error && mode > XCB_PROP_MODE_APPEND) { + error = XCB_VALUE; + errorValue = mode; + } + if (!error && format != 8 && format != 16 && format != 32) { + error = XCB_VALUE; + errorValue = format; + } + if (!error && !requestPayloadFits(c, sizeof(xcb_change_property_request_t), + length, format / 8u)) { + error = XCB_LENGTH; + errorValue = length; + } + if (!error && length && !data) { + error = XCB_VALUE; + errorValue = length; + } + unsigned long *wide = NULL; + const unsigned char *xdata = data; + if (!error && format == 32 && length) { + wide = calloc(length, sizeof(*wide)); + if (!wide) + error = XCB_ALLOC; + for (uint32_t i = 0; wide && i < length; i++) + wide[i] = ((const uint32_t *) data)[i]; + xdata = (const unsigned char *) wide; + } + if (!error && !XChangeProperty(xcbCompatDisplay(c), window, property, type, + format, mode, xdata, length)) + error = XCB_MATCH; + free(wide); + return xcbCompatVoidCookie(c, error, errorValue, XCB_CHANGE_PROPERTY, + checked); +} + +xcb_void_cookie_t xcb_change_property(xcb_connection_t *c, + uint8_t mode, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint8_t format, + uint32_t length, + const void *data) +{ + return changeProperty(c, mode, window, property, type, format, length, data, + 0); +} +xcb_void_cookie_t xcb_change_property_checked(xcb_connection_t *c, + uint8_t mode, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint8_t format, + uint32_t length, + const void *data) +{ + return changeProperty(c, mode, window, property, type, format, length, data, + 1); +} + +static xcb_get_property_cookie_t getProperty(xcb_connection_t *c, + uint8_t delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t offset, + uint32_t length, + int checked) +{ + REQUIRE_REQUEST(c, xcb_get_property_cookie_t); + xcb_get_property_cookie_t cookie = {nextSequence(c)}; + if (!isWindow(window)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_WINDOW, window, + XCB_GET_PROPERTY, checked); + return cookie; + } + if (!isValidAtom(property) || + (type != XCB_GET_PROPERTY_TYPE_ANY && !isValidAtom(type))) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_ATOM, + !isValidAtom(property) ? property : type, + XCB_GET_PROPERTY, checked); + return cookie; + } + Atom actualType; + int format; + unsigned long count, after; + unsigned char *value = NULL; + int status = XGetWindowProperty(xcbCompatDisplay(c), window, property, + offset, length, delete, type, &actualType, + &format, &count, &after, &value); + + /* The wire pads a reply payload to a 4-byte boundary and length counts + * those words, so an 8- or 16-bit property whose bytes do not divide by + * four needs the padding allocated too. A client that trusts length and + * reads length * 4 bytes would otherwise run off the end of the block. + */ + size_t itemSize = format ? (unsigned int) format / 8 : 0; + uint64_t valueBytes = (uint64_t) count * itemSize; + uint64_t paddedBytes = + valueBytes <= UINT64_MAX - 3 ? (valueBytes + 3u) & ~UINT64_C(3) : 0; + size_t bytes = (size_t) valueBytes; + xcb_get_property_reply_t *reply = + status == Success && paddedBytes >= valueBytes + ? allocateReplyPayload(c, sizeof(*reply), paddedBytes, 1, NULL) + : NULL; + if (reply) { + reply->response_type = 1; + reply->format = format; + reply->type = actualType; + reply->bytes_after = after; + reply->value_len = count; + reply->length = (uint32_t) (paddedBytes / 4); + if (format == 32) { + for (unsigned long i = 0; i < count; i++) + ((uint32_t *) (reply + 1))[i] = ((unsigned long *) value)[i]; + } else if (bytes) { + memcpy(reply + 1, value, bytes); + } + } + free(value); + if (status == Success) + storeReply(c, cookie.sequence, reply); + else + xcbCompatStoreProtocolError(c, cookie.sequence, (uint8_t) status, + window, XCB_GET_PROPERTY, checked); + return cookie; +} +xcb_get_property_cookie_t xcb_get_property(xcb_connection_t *c, + uint8_t delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t offset, + uint32_t length) +{ + return getProperty(c, delete, window, property, type, offset, length, 1); +} +xcb_get_property_cookie_t xcb_get_property_unchecked(xcb_connection_t *c, + uint8_t delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t offset, + uint32_t length) +{ + return getProperty(c, delete, window, property, type, offset, length, 0); +} +xcb_get_property_reply_t *xcb_get_property_reply( + xcb_connection_t *c, + xcb_get_property_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} +static int propertyValueBytes(const xcb_get_property_reply_t *reply) +{ + if (!reply) + return 0; + uint64_t bytes = (uint64_t) reply->value_len * (reply->format / 8u); + return bytes <= INT_MAX ? (int) bytes : -1; +} +int xcb_get_property_sizeof(const void *buffer) +{ + const xcb_get_property_reply_t *reply = buffer; + int bytes = propertyValueBytes(reply); + if (!reply || bytes < 0) + return 0; + uint64_t padded = ((uint64_t) bytes + 3u) & ~UINT64_C(3); + return padded <= INT_MAX - sizeof(*reply) + ? (int) sizeof(*reply) + (int) padded + : 0; +} +void *xcb_get_property_value(const xcb_get_property_reply_t *reply) +{ + return reply ? (void *) (reply + 1) : NULL; +} +int xcb_get_property_value_length(const xcb_get_property_reply_t *reply) +{ + int bytes = propertyValueBytes(reply); + return bytes >= 0 ? bytes : 0; +} +xcb_generic_iterator_t xcb_get_property_value_end( + const xcb_get_property_reply_t *reply) +{ + xcb_generic_iterator_t end = {0}; + int bytes = propertyValueBytes(reply); + if (reply && bytes >= 0) { + end.data = (char *) xcb_get_property_value(reply) + bytes; + end.index = sizeof(*reply) + bytes; + } + return end; +} + +static xcb_void_cookie_t deleteProperty(xcb_connection_t *c, + xcb_window_t window, + xcb_atom_t property, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = isWindow(window) ? 0 : XCB_WINDOW; + if (!error && !isValidAtom(property)) + error = XCB_ATOM; + if (!error) + XDeleteProperty(xcbCompatDisplay(c), window, property); + return xcbCompatVoidCookie(c, error, error == XCB_ATOM ? property : window, + XCB_DELETE_PROPERTY, checked); +} + +static xcb_list_properties_cookie_t listProperties(xcb_connection_t *c, + xcb_window_t window, + int checked) +{ + REQUIRE_REQUEST(c, xcb_list_properties_cookie_t); + xcb_list_properties_cookie_t cookie = {nextSequence(c)}; + if (!isWindow(window)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_WINDOW, window, + XCB_LIST_PROPERTIES, checked); + return cookie; + } + int count = 0; + Atom *atoms = XListProperties(xcbCompatDisplay(c), window, &count); + if (count < 0) { + free(atoms); + storeReply(c, cookie.sequence, NULL); + return cookie; + } + size_t bytes = 0; + xcb_list_properties_reply_t *reply = allocateReplyPayload( + c, sizeof(*reply), (uint64_t) count, sizeof(xcb_atom_t), &bytes); + if (reply) { + reply->response_type = 1; + reply->atoms_len = (uint16_t) count; + reply->length = (uint32_t) count; + for (int i = 0; i < count; i++) + ((xcb_atom_t *) (reply + 1))[i] = (xcb_atom_t) atoms[i]; + } + free(atoms); + storeReply(c, cookie.sequence, reply); + return cookie; +} + +xcb_list_properties_cookie_t xcb_list_properties(xcb_connection_t *c, + xcb_window_t window) +{ + return listProperties(c, window, 1); +} + +int xcb_list_properties_sizeof(const void *buffer) +{ + const xcb_list_properties_reply_t *reply = buffer; + return reply + ? (int) (sizeof(*reply) + reply->atoms_len * sizeof(xcb_atom_t)) + : 0; +} + +xcb_list_properties_cookie_t xcb_list_properties_unchecked(xcb_connection_t *c, + xcb_window_t window) +{ + return listProperties(c, window, 0); +} + +xcb_list_properties_reply_t *xcb_list_properties_reply( + xcb_connection_t *c, + xcb_list_properties_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} + +xcb_atom_t *xcb_list_properties_atoms(const xcb_list_properties_reply_t *reply) +{ + return reply ? (xcb_atom_t *) (reply + 1) : NULL; +} + +int xcb_list_properties_atoms_length(const xcb_list_properties_reply_t *reply) +{ + return reply ? reply->atoms_len : 0; +} + +xcb_generic_iterator_t xcb_list_properties_atoms_end( + const xcb_list_properties_reply_t *reply) +{ + xcb_generic_iterator_t iterator = {0}; + if (reply) { + iterator.data = xcb_list_properties_atoms(reply) + reply->atoms_len; + iterator.index = sizeof(*reply) + reply->atoms_len * sizeof(xcb_atom_t); + } + return iterator; +} + +static xcb_void_cookie_t setSelectionOwner(xcb_connection_t *c, + xcb_window_t owner, + xcb_atom_t selection, + xcb_timestamp_t time, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = owner != XCB_NONE && !isWindow(owner) ? XCB_WINDOW : 0; + if (!error && !isValidAtom(selection)) + error = XCB_ATOM; + if (!error) + XSetSelectionOwner(xcbCompatDisplay(c), selection, owner, time); + return xcbCompatVoidCookie(c, error, error == XCB_ATOM ? selection : owner, + XCB_SET_SELECTION_OWNER, checked); +} + +xcb_void_cookie_t xcb_set_selection_owner(xcb_connection_t *c, + xcb_window_t owner, + xcb_atom_t selection, + xcb_timestamp_t time) +{ + return setSelectionOwner(c, owner, selection, time, 0); +} + +xcb_void_cookie_t xcb_set_selection_owner_checked(xcb_connection_t *c, + xcb_window_t owner, + xcb_atom_t selection, + xcb_timestamp_t time) +{ + return setSelectionOwner(c, owner, selection, time, 1); +} + +static xcb_get_selection_owner_cookie_t getSelectionOwner(xcb_connection_t *c, + xcb_atom_t selection, + int checked) +{ + REQUIRE_REQUEST(c, xcb_get_selection_owner_cookie_t); + xcb_get_selection_owner_cookie_t cookie = {nextSequence(c)}; + if (!isValidAtom(selection)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_ATOM, selection, + XCB_GET_SELECTION_OWNER, checked); + return cookie; + } + xcb_get_selection_owner_reply_t *reply = calloc(1, sizeof(*reply)); + if (reply) { + reply->response_type = 1; + reply->owner = XGetSelectionOwner(xcbCompatDisplay(c), selection); + } + storeReply(c, cookie.sequence, reply); + return cookie; +} + +xcb_get_selection_owner_cookie_t xcb_get_selection_owner(xcb_connection_t *c, + xcb_atom_t selection) +{ + return getSelectionOwner(c, selection, 1); +} + +xcb_get_selection_owner_cookie_t xcb_get_selection_owner_unchecked( + xcb_connection_t *c, + xcb_atom_t selection) +{ + return getSelectionOwner(c, selection, 0); +} + +xcb_get_selection_owner_reply_t *xcb_get_selection_owner_reply( + xcb_connection_t *c, + xcb_get_selection_owner_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} + +static xcb_void_cookie_t convertSelection(xcb_connection_t *c, + xcb_window_t requestor, + xcb_atom_t selection, + xcb_atom_t target, + xcb_atom_t property, + xcb_timestamp_t time, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = isWindow(requestor) ? 0 : XCB_WINDOW; + uint32_t errorValue = requestor; + if (!error && !isValidAtom(selection)) { + error = XCB_ATOM; + errorValue = selection; + } + if (!error && !isValidAtom(target)) { + error = XCB_ATOM; + errorValue = target; + } + if (!error && property != XCB_NONE && !isValidAtom(property)) { + error = XCB_ATOM; + errorValue = property; + } + if (!error) + XConvertSelection(xcbCompatDisplay(c), selection, target, property, + requestor, time); + return xcbCompatVoidCookie(c, error, errorValue, XCB_CONVERT_SELECTION, + checked); +} + +xcb_void_cookie_t xcb_convert_selection(xcb_connection_t *c, + xcb_window_t requestor, + xcb_atom_t selection, + xcb_atom_t target, + xcb_atom_t property, + xcb_timestamp_t time) +{ + return convertSelection(c, requestor, selection, target, property, time, 0); +} + +xcb_void_cookie_t xcb_convert_selection_checked(xcb_connection_t *c, + xcb_window_t requestor, + xcb_atom_t selection, + xcb_atom_t target, + xcb_atom_t property, + xcb_timestamp_t time) +{ + return convertSelection(c, requestor, selection, target, property, time, 1); +} +xcb_void_cookie_t xcb_delete_property(xcb_connection_t *c, + xcb_window_t w, + xcb_atom_t p) +{ + return deleteProperty(c, w, p, 0); +} +xcb_void_cookie_t xcb_delete_property_checked(xcb_connection_t *c, + xcb_window_t w, + xcb_atom_t p) +{ + return deleteProperty(c, w, p, 1); +} + +static xcb_void_cookie_t changeAttributes(xcb_connection_t *c, + xcb_window_t window, + uint32_t mask, + const uint32_t *values, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = !isWindow(window) ? XCB_WINDOW : 0; + uint32_t errorValue = window; + if (!error && (mask & ~XCB_CW_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && mask && !values) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error) { + XSetWindowAttributes attributes; + decodeAttributes(mask, values, &attributes); + error = validateAttributes(mask, &attributes, drawableDepth(c, window), + IS_INPUT_ONLY(window), &errorValue); + if (!error && !XChangeWindowAttributes(xcbCompatDisplay(c), window, + mask, &attributes)) + error = XCB_VALUE; + } + return xcbCompatVoidCookie(c, error, errorValue, + XCB_CHANGE_WINDOW_ATTRIBUTES, checked); +} +xcb_void_cookie_t xcb_change_window_attributes(xcb_connection_t *c, + xcb_window_t window, + uint32_t mask, + const void *values) +{ + return changeAttributes(c, window, mask, values, 0); +} +xcb_void_cookie_t xcb_change_window_attributes_checked(xcb_connection_t *c, + xcb_window_t window, + uint32_t mask, + const void *values) +{ + return changeAttributes(c, window, mask, values, 1); +} + +static void decodeConfigure(uint16_t mask, + const uint32_t *values, + XWindowChanges *changes) +{ + memset(changes, 0, sizeof(*changes)); + unsigned int index = 0; +#define TAKE(bit, field, type) \ + do { \ + if (mask & (bit)) \ + changes->field = (type) values[index++]; \ + } while (0) + TAKE(XCB_CONFIG_WINDOW_X, x, int16_t); + TAKE(XCB_CONFIG_WINDOW_Y, y, int16_t); + TAKE(XCB_CONFIG_WINDOW_WIDTH, width, uint16_t); + TAKE(XCB_CONFIG_WINDOW_HEIGHT, height, uint16_t); + TAKE(XCB_CONFIG_WINDOW_BORDER_WIDTH, border_width, uint16_t); + TAKE(XCB_CONFIG_WINDOW_SIBLING, sibling, xcb_window_t); + TAKE(XCB_CONFIG_WINDOW_STACK_MODE, stack_mode, uint8_t); +#undef TAKE +} + +static xcb_void_cookie_t xcbConfigureWindow(xcb_connection_t *c, + xcb_window_t window, + uint16_t mask, + const uint32_t *values, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = !isWindow(window) ? XCB_WINDOW : 0; + uint32_t errorValue = window; + if (!error && (mask & ~XCB_CONFIG_WINDOW_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && mask && !values) { + error = XCB_VALUE; + errorValue = mask; + } + XWindowChanges changes; + if (!error) { + decodeConfigure(mask, values, &changes); + if (mask & XCB_CONFIG_WINDOW_WIDTH && changes.width <= 0) { + error = XCB_VALUE; + errorValue = changes.width; + } else if (mask & XCB_CONFIG_WINDOW_HEIGHT && changes.height <= 0) { + error = XCB_VALUE; + errorValue = changes.height; + } else if (mask & XCB_CONFIG_WINDOW_STACK_MODE && + changes.stack_mode > XCB_STACK_MODE_OPPOSITE) { + error = XCB_VALUE; + errorValue = changes.stack_mode; + } else if ((mask & XCB_CONFIG_WINDOW_SIBLING) && + !isWindow(changes.sibling)) { + error = XCB_WINDOW; + errorValue = changes.sibling; + } else if ((mask & XCB_CONFIG_WINDOW_SIBLING) && + !(mask & XCB_CONFIG_WINDOW_STACK_MODE)) { + error = XCB_MATCH; + errorValue = window; + } else if ((mask & XCB_CONFIG_WINDOW_SIBLING) && + (changes.sibling == window || + GET_PARENT(changes.sibling) != GET_PARENT(window))) { + error = XCB_MATCH; + errorValue = window; + } + } + if (!error) { + if (!XConfigureWindow(xcbCompatDisplay(c), window, mask, &changes)) + error = XCB_MATCH; + } + return xcbCompatVoidCookie(c, error, errorValue, XCB_CONFIGURE_WINDOW, + checked); +} +xcb_void_cookie_t xcb_configure_window(xcb_connection_t *c, + xcb_window_t window, + uint16_t mask, + const void *values) +{ + return xcbConfigureWindow(c, window, mask, values, 0); +} +xcb_void_cookie_t xcb_configure_window_checked(xcb_connection_t *c, + xcb_window_t window, + uint16_t mask, + const void *values) +{ + return xcbConfigureWindow(c, window, mask, values, 1); +} + +static xcb_void_cookie_t reparentWindow(xcb_connection_t *c, + xcb_window_t window, + xcb_window_t parent, + int16_t x, + int16_t y, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + int windowValid = isWindow(window); + int parentValid = isWindow(parent); + uint8_t error = !windowValid ? XCB_WINDOW : 0; + uint32_t errorResource = window; + if (!error && !parentValid) { + error = XCB_WINDOW; + errorResource = parent; + } + if (!error && !XReparentWindow(xcbCompatDisplay(c), window, parent, x, y)) + error = XCB_MATCH; + return xcbCompatVoidCookie(c, error, errorResource, XCB_REPARENT_WINDOW, + checked); +} +xcb_void_cookie_t xcb_reparent_window(xcb_connection_t *c, + xcb_window_t window, + xcb_window_t parent, + int16_t x, + int16_t y) +{ + return reparentWindow(c, window, parent, x, y, 0); +} +xcb_void_cookie_t xcb_reparent_window_checked(xcb_connection_t *c, + xcb_window_t window, + xcb_window_t parent, + int16_t x, + int16_t y) +{ + return reparentWindow(c, window, parent, x, y, 1); +} diff --git a/docs/XCB-COVERAGE.md b/docs/XCB-COVERAGE.md index c070d9d3..c3fa0f7d 100644 --- a/docs/XCB-COVERAGE.md +++ b/docs/XCB-COVERAGE.md @@ -29,8 +29,9 @@ restages. `make install` ships the built copies. ## Implemented surface -No core request is implemented yet: this is the connection, setup and event -layer only. Every core opcode is deferred and deliberately absent from the +Requests 1-4, 7-8, 10, 12 and 14-24: window lifecycle and configuration, +geometry and tree queries, properties and selections. Every other core opcode is +deferred and deliberately absent from the export manifest, so a client fails at link time instead of receiving a cookie that silently never completes. diff --git a/mk/libxcb.mk b/mk/libxcb.mk index f4d84d97..fedf3ed9 100644 --- a/mk/libxcb.mk +++ b/mk/libxcb.mk @@ -10,7 +10,7 @@ UPSTREAM_HEADERS_STAMP ?= $(UPSTREAM_HEADERS_DIR)/.upstream-stamp ifeq ($(XCB),1) XCB_COMPAT_TARGET := $(OUT)/libxcb-compat.so -XCB_COMPAT_OBJS := $(OUT)/xcb-compat.o +XCB_COMPAT_OBJS := $(OUT)/xcb-compat.o $(OUT)/xcb-requests.o XCB_DEFINED := $(OUT)/libxcb-compat.defined-syms XCB_EXPORT_LIST := $(OUT)/libxcb-compat.$(if $(filter Darwin,$(UNAME_S)),exports,map) XCB_EXPORT_FORMAT := $(if $(filter Darwin,$(UNAME_S)),macho,elf) @@ -24,6 +24,10 @@ $(OUT)/xcb-compat.o: compat/xcb-compat.c $(UPSTREAM_HEADERS_STAMP) \ $(SDL_BACKEND_STAMP) | $(OUT) $(cc_object) +$(OUT)/xcb-requests.o: compat/xcb-requests.c $(UPSTREAM_HEADERS_STAMP) \ + $(SDL_BACKEND_STAMP) | $(OUT) + $(cc_object) + $(XCB_DEFINED): $(XCB_COMPAT_OBJS) | $(OUT) @echo " GEN $@" $(Q)nm -g $(XCB_COMPAT_OBJS) 2>/dev/null \ diff --git a/mk/tests.mk b/mk/tests.mk index 59332e23..e4884002 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -11,6 +11,7 @@ CHECK_BINS := $(OUT)/tests/check $(OUT)/tests/symbol-coverage \ ifeq ($(XCB),1) CHECK_BINS += $(OUT)/tests/test-xcb-link CHECK_BINS += $(OUT)/tests/test-xcb-setup +CHECK_BINS += $(OUT)/tests/test-xcb-window $(OUT)/tests/test-xcb-property CHECK_BINS += $(OUT)/tests/test-xcb-events endif # The GLX tests only exist when the optional GLX layer is built (GLX=1). @@ -313,8 +314,15 @@ $(OUT)/tests/test-xcb-setup: tests/test-xcb-setup.c $(XCB_COMPAT_OBJS) $(TARGET) $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ $(XCB_COMPAT_OBJS) $(TARGET) $(LDLIBS) $(TEST_LDFLAGS) -o $@ +XCB_TESTS := test-xcb-window test-xcb-property XLIB_XCB_TESTS := test-xcb-events +$(addprefix $(OUT)/tests/,$(XCB_TESTS)): $(OUT)/tests/%: tests/%.c $(XCB_COMPAT_TARGET) $(TARGET) + @mkdir -p $(dir $@) + @echo " CC $<" + $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ + -L$(OUT) -lxcb-compat -lX11-compat $(TEST_LDFLAGS) -o $@ + $(addprefix $(OUT)/tests/,$(XLIB_XCB_TESTS)): $(OUT)/tests/%: tests/%.c $(X11_XCB_COMPAT_TARGET) $(XCB_COMPAT_TARGET) $(TARGET) @mkdir -p $(dir $@) @echo " CC $<" diff --git a/src/window-internal.c b/src/window-internal.c index 13223acf..2dabf8b1 100644 --- a/src/window-internal.c +++ b/src/window-internal.c @@ -1924,15 +1924,27 @@ static Bool restackWindow(Display *display, if (siblingIndex < 0) return False; size_t target = (size_t) siblingIndex; - if (mode == Above) { + if (mode == Above) target++; - } else if (mode != Below) { - handleError(0, display, window, 0, BadValue, 0); - return False; + if (mode == Above || mode == Below) { + if ((size_t) index < target) + target--; + return moveChildToIndexAndExpose(display, window, target); } - if ((size_t) index < target) - target--; - return moveChildToIndexAndExpose(display, window, target); + Bool overlap = windowsOverlap(window, values->sibling); + Bool siblingAbove = siblingIndex > index; + if (mode == TopIf && overlap && siblingAbove) + return moveChildToIndexAndExpose(display, window, + children->length - 1); + if (mode == BottomIf && overlap && !siblingAbove) + return moveChildToIndexAndExpose(display, window, 0); + if (mode == Opposite && overlap) + return moveChildToIndexAndExpose( + display, window, siblingAbove ? children->length - 1 : 0); + if (mode == TopIf || mode == BottomIf || mode == Opposite) + return True; + handleError(0, display, window, 0, BadValue, 0); + return False; } size_t idx = (size_t) index; @@ -2449,6 +2461,11 @@ Bool configureWindow(Display *display, return False; if (HAS_VALUE(value_mask, CWStackMode)) hasChanged = True; + if (HAS_VALUE(value_mask, CWBorderWidth) && + windowStruct->borderWidth != (unsigned int) values->border_width) { + windowStruct->borderWidth = values->border_width; + hasChanged = True; + } if (HAS_VALUE(value_mask, CWX) || HAS_VALUE(value_mask, CWY)) { int x = oldX, y = oldY; if (HAS_VALUE(value_mask, CWX)) diff --git a/src/window.c b/src/window.c index 1c1855dd..2754c76d 100644 --- a/src/window.c +++ b/src/window.c @@ -1016,7 +1016,16 @@ Window XCreateSimpleWindow(Display *display, CWBackPixel | CWBorderPixel, &attributes); } +static void releaseReservedWindowId(Window requestedID) +{ + if (requestedID != None && isXidAllocated(requestedID) && + getXidStruct(requestedID)->type == 0 && + getXidStruct(requestedID)->dataPointer == NULL) + FREE_XID(requestedID); +} + static Window createWindowImpl(Display *display, + Window requestedID, Window parent, int x, int y, @@ -1032,9 +1041,14 @@ static Window createWindowImpl(Display *display, { // https://tronche.com/gui/x/xlib/window/XCreateWindow.html SET_X_SERVER_REQUEST(display, X_CreateWindow); - TYPE_CHECK(parent, WINDOW, display, None); + if (!isXidAllocated(parent) || !IS_TYPE(parent, WINDOW)) { + handleError(0, display, parent, 0, BadWindow, 0); + releaseReservedWindowId(requestedID); + return None; + } if (valueMask != 0 && !attributes) { handleError(0, display, None, 0, BadValue, 0); + releaseReservedWindowId(requestedID); return None; } @@ -1044,16 +1058,30 @@ static Window createWindowImpl(Display *display, LOG("Bad argument: Given class is InputOnly but border_with is not 0 " "in XCreateWindow!\n"); handleError(0, display, None, 0, BadMatch, 0); + releaseReservedWindowId(requestedID); + return None; + } + if (visual == CopyFromParent) + visual = getDefaultVisual(0); + if (!visual) { + handleError(0, display, None, 0, BadMatch, 0); + releaseReservedWindowId(requestedID); return None; } - Window windowID = ALLOC_XID(); + Window windowID = requestedID != None ? requestedID : ALLOC_XID(); if (windowID == None) { LOG("Out of memory: Could not allocate the window id in " "XCreateWindow!\n"); handleOutOfMemory(0, display, 0, 0); return None; } + if (requestedID != None && + (!isXidAllocated(requestedID) || getXidStruct(requestedID)->type != 0 || + getXidStruct(requestedID)->dataPointer != NULL)) { + handleError(0, display, requestedID, 0, BadIDChoice, 0); + return None; + } WindowStruct *windowStruct = malloc(sizeof(WindowStruct)); if (!windowStruct) { @@ -1084,16 +1112,6 @@ static Window createWindowImpl(Display *display, FREE_XID(windowID); return None; } - if (visual == CopyFromParent) - visual = getDefaultVisual(0); - if (!visual) { - handleError(0, display, None, 0, BadMatch, 0); - removeChildFromParent(windowID); - free(windowStruct); - FREE_XID(windowID); - return None; - } - int visualClass = visual->CLASS_ATTRIBUTE; windowStruct->colormap = (Colormap) XCreateColormap( display, windowID, visual, @@ -1137,8 +1155,28 @@ Window XCreateWindow(Display *display, unsigned long valueMask, XSetWindowAttributes *attributes) { - return createWindowImpl(display, parent, x, y, width, height, border_width, - depth, clazz, visual, valueMask, attributes, False); + return createWindowImpl(display, None, parent, x, y, width, height, + border_width, depth, clazz, visual, valueMask, + attributes, False); +} + +Window libx11CompatCreateWindowWithId(Display *display, + Window window, + Window parent, + int x, + int y, + unsigned int width, + unsigned int height, + unsigned int borderWidth, + int depth, + unsigned int clazz, + Visual *visual, + unsigned long valueMask, + XSetWindowAttributes *attributes) +{ + return createWindowImpl(display, window, parent, x, y, width, height, + borderWidth, depth, clazz, visual, valueMask, + attributes, False); } Window createInternalWindow(Display *display, @@ -1154,8 +1192,9 @@ Window createInternalWindow(Display *display, unsigned long valueMask, XSetWindowAttributes *attributes) { - return createWindowImpl(display, parent, x, y, width, height, border_width, - depth, clazz, visual, valueMask, attributes, True); + return createWindowImpl(display, None, parent, x, y, width, height, + border_width, depth, clazz, visual, valueMask, + attributes, True); } static int destroySubwindowsImpl(Display *display, Window window) @@ -2203,6 +2242,15 @@ Status XGetWindowAttributes(Display *display, window_attributes_return->y = windowStruct->y; window_attributes_return->width = windowStruct->w; window_attributes_return->height = windowStruct->h; + window_attributes_return->border_width = windowStruct->borderWidth; + window_attributes_return->class = + windowStruct->inputOnly ? InputOnly : InputOutput; + window_attributes_return->bit_gravity = windowStruct->bitGravity; + window_attributes_return->win_gravity = windowStruct->winGravity; + window_attributes_return->backing_store = NotUseful; + window_attributes_return->backing_planes = AllPlanes; + window_attributes_return->backing_pixel = 0; + window_attributes_return->save_under = False; if (IS_MAPPED_TOP_LEVEL_WINDOW(window)) { SDL_Window *sdlWindow = GET_WINDOW_STRUCT(window)->sdlWindow; Uint32 flags = SDL_GetWindowFlags(sdlWindow); @@ -2233,6 +2281,10 @@ Status XGetWindowAttributes(Display *display, GET_WINDOW_STRUCT(window)->overrideRedirect; window_attributes_return->your_event_mask = GET_WINDOW_STRUCT(window)->eventMask; + window_attributes_return->all_event_masks = windowStruct->eventMask; + window_attributes_return->do_not_propagate_mask = NoEventMask; + window_attributes_return->map_installed = True; + window_attributes_return->screen = DefaultScreenOfDisplay(display); return 1; } diff --git a/src/window.h b/src/window.h index bbb63288..d0f476c6 100644 --- a/src/window.h +++ b/src/window.h @@ -7,6 +7,20 @@ #include "resource-types.h" #include "util.h" +Window libx11CompatCreateWindowWithId(Display *display, + Window window, + Window parent, + int x, + int y, + unsigned int width, + unsigned int height, + unsigned int borderWidth, + int depth, + unsigned int clazz, + Visual *visual, + unsigned long valueMask, + XSetWindowAttributes *attributes); + typedef struct { Atom property; int dataFormat; diff --git a/tests/private-symbols.txt b/tests/private-symbols.txt index 94de7a38..7e0cf5ff 100644 --- a/tests/private-symbols.txt +++ b/tests/private-symbols.txt @@ -20,9 +20,11 @@ captureShapeMaskBaseline clearRendererClip compatFontOpenFamilyFallback compatFontOpenFamilyFallbackForChar +freeXidResource getGcClipIterationCount getWindowRenderer getXidStruct +isValidAtom isXidAllocated markPixmapReadbackDirty presentDrawableRectIfVisible diff --git a/tests/shim-symbols.txt b/tests/shim-symbols.txt index 3071a9ea..3e5f3dfa 100644 --- a/tests/shim-symbols.txt +++ b/tests/shim-symbols.txt @@ -12,6 +12,7 @@ libx11CompatAcceleratedPresentForcedForTest libx11CompatAcceleratedPresentUsable libx11CompatBeginXcbEventAccess libx11CompatConfigureLiveResizeLayer +libx11CompatCreateWindowWithId libx11CompatEndXcbEventAccess libx11CompatFailAcceleratedPresentOnceForTest libx11CompatForceAcceleratedPresentForTest diff --git a/tests/test-xcb-events.c b/tests/test-xcb-events.c index 0416549f..5734fbad 100644 --- a/tests/test-xcb-events.c +++ b/tests/test-xcb-events.c @@ -284,6 +284,26 @@ int main(void) "circulate-request conversion"); free(circulateRequest); + XSelectInput(display, window, StructureNotifyMask); + CHECK(!xcb_request_check(connection, + xcb_map_window_checked(connection, window)), + "checked map request"); + map = (xcb_map_notify_event_t *) pollForType(connection, XCB_MAP_NOTIFY); + CHECK(map && map->response_type == XCB_MAP_NOTIFY && map->event == window && + map->window == window, + "map request event"); + free(map); + CHECK(!xcb_request_check(connection, + xcb_unmap_window_checked(connection, window)), + "checked unmap request"); + unmap = + (xcb_unmap_notify_event_t *) pollForType(connection, XCB_UNMAP_NOTIFY); + CHECK(unmap && unmap->response_type == XCB_UNMAP_NOTIFY && + unmap->event == window && unmap->window == window && + !unmap->from_configure, + "unmap request event"); + free(unmap); + memset(&x, 0, sizeof(x)); x.xproperty.type = PropertyNotify; x.xproperty.window = window; @@ -541,6 +561,9 @@ int main(void) XNextEvent(display, &consumed); CHECK(consumed.type == FocusOut, "Xlib consumes after owner switch"); XSelectInput(display, window, NoEventMask); + CHECK(!xcb_request_check(connection, + xcb_destroy_window_checked(connection, window)), + "checked destroy request"); xcb_disconnect(connection); XCloseDisplay(display); @@ -563,6 +586,21 @@ int main(void) connection = xcb_connect(NULL, NULL); CHECK(connection && !xcb_connection_has_error(connection), "open disconnect test"); + EventWaiter errorWaiter = {.connection = connection}; + pthread_t errorThread; + CHECK(!pthread_create(&errorThread, NULL, waitForEvent, &errorWaiter), + "unchecked-error waiter creation"); + waitForWaiters(connection, 1); + xcb_void_cookie_t unchecked = + xcb_destroy_window(connection, UINT32_C(0xdeadbeef)); + CHECK(!pthread_join(errorThread, NULL), "unchecked-error waiter join"); + xcb_generic_error_t *uncheckedError = + (xcb_generic_error_t *) errorWaiter.result; + CHECK(uncheckedError && uncheckedError->response_type == 0 && + uncheckedError->error_code == XCB_WINDOW && + uncheckedError->full_sequence == unchecked.sequence, + "unchecked error wakes XCB waiter"); + free(uncheckedError); enum { DISCONNECT_WAITER_COUNT = 4 }; EventWaiter disconnectWaiters[DISCONNECT_WAITER_COUNT] = {0}; pthread_t disconnectThreads[DISCONNECT_WAITER_COUNT]; diff --git a/tests/test-xcb-property.c b/tests/test-xcb-property.c new file mode 100644 index 00000000..9e1195f2 --- /dev/null +++ b/tests/test-xcb-property.c @@ -0,0 +1,343 @@ +#include +#include +#include +#include +#include +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", m); \ + return 1; \ + } \ + } while (0) +int main(void) +{ + xcb_connection_t *c = xcb_connect(NULL, NULL); + CHECK(c && !xcb_connection_has_error(c), "connect"); + CHECK( + !xcb_get_atom_name_name(NULL) && !xcb_get_atom_name_name_length(NULL) && + !xcb_get_atom_name_name_end(NULL).data && + !xcb_get_atom_name_sizeof(NULL) && !xcb_get_property_value(NULL) && + !xcb_get_property_value_length(NULL) && + !xcb_get_property_value_end(NULL).data && + !xcb_get_property_sizeof(NULL), + "NULL reply accessor"); + xcb_screen_t *s = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + xcb_window_t w = xcb_generate_id(c); + CHECK(!xcb_request_check( + c, xcb_create_window_checked(c, 24, w, s->root, 0, 0, 20, 20, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + s->root_visual, 0, NULL)), + "create"); + const char name[] = "XCB_TEST_PROPERTY"; + xcb_intern_atom_reply_t *ia = xcb_intern_atom_reply( + c, xcb_intern_atom_unchecked(c, 0, sizeof(name) - 1, name), NULL); + CHECK(ia, "intern"); + xcb_atom_t atom = ia->atom; + free(ia); + xcb_get_atom_name_reply_t *an = + xcb_get_atom_name_reply(c, xcb_get_atom_name_unchecked(c, atom), NULL); + CHECK(an && xcb_get_atom_name_name_length(an) == (int) sizeof(name) - 1 && + !memcmp(xcb_get_atom_name_name(an), name, sizeof(name) - 1), + "atom name"); + xcb_generic_iterator_t atomNameEnd = xcb_get_atom_name_name_end(an); + CHECK( + xcb_get_atom_name_sizeof(an) == + (int) (sizeof(*an) + sizeof(name) - 1) && + atomNameEnd.data == xcb_get_atom_name_name(an) + sizeof(name) - 1 && + atomNameEnd.index == xcb_get_atom_name_sizeof(an), + "atom name layout"); + free(an); + xcb_generic_error_t *error = NULL; + an = xcb_get_atom_name_reply(c, xcb_get_atom_name(c, UINT32_MAX), &error); + CHECK(!an && error && error->error_code == XCB_ATOM && + error->resource_id == UINT32_MAX && + error->major_code == XCB_GET_ATOM_NAME, + "atom name BadAtom"); + free(error); + xcb_get_atom_name_cookie_t uncheckedAtomName = + xcb_get_atom_name_unchecked(c, UINT32_MAX); + error = (xcb_generic_error_t *) UINTPTR_MAX; + an = xcb_get_atom_name_reply(c, uncheckedAtomName, &error); + CHECK(!an && !error, "unchecked atom name bypasses reply error"); + error = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(error && error->error_code == XCB_ATOM && + error->full_sequence == uncheckedAtomName.sequence, + "unchecked atom name queues error"); + free(error); + const char first[] = "abcd", second[] = "ef"; + xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_STRING, 8, + 4, first); + xcb_change_property(c, XCB_PROP_MODE_APPEND, w, atom, XCB_ATOM_STRING, 8, 2, + second); + xcb_get_property_reply_t *p = xcb_get_property_reply( + c, + xcb_get_property_unchecked(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, + 1), + NULL); + CHECK(p && xcb_get_property_value_length(p) == 4 && p->bytes_after == 2 && + !memcmp(xcb_get_property_value(p), "abcd", 4), + "truncated property"); + xcb_generic_iterator_t propertyEnd = xcb_get_property_value_end(p); + CHECK(xcb_get_property_sizeof(p) == (int) (sizeof(*p) + 4) && + propertyEnd.data == (char *) xcb_get_property_value(p) + 4 && + propertyEnd.index == xcb_get_property_sizeof(p), + "property layout"); + free(p); + + /* Six bytes of an 8-bit property pad to eight on the wire. length counts + * those words, so a client that reads length * 4 bytes must stay inside the + * reply block. + */ + p = xcb_get_property_reply( + c, + xcb_get_property_unchecked(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, + 16), + NULL); + CHECK(p && xcb_get_property_value_length(p) == 6 && p->length == 2 && + xcb_get_property_sizeof(p) == (int) (sizeof(*p) + 8) && + !memcmp(xcb_get_property_value(p), "abcdef", 6), + "unaligned property payload is padded to its advertised length"); + free(p); + xcb_get_property_cookie_t uncheckedProperty = xcb_get_property_unchecked( + c, 0, w, UINT32_MAX, XCB_GET_PROPERTY_TYPE_ANY, 0, 1); + error = (xcb_generic_error_t *) UINTPTR_MAX; + p = xcb_get_property_reply(c, uncheckedProperty, &error); + CHECK(!p && !error, "unchecked property bypasses reply error"); + error = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(error && error->error_code == XCB_ATOM && + error->full_sequence == uncheckedProperty.sequence, + "unchecked property queues error"); + free(error); + const char prefix[] = "01"; + CHECK(!xcb_request_check( + c, xcb_change_property_checked(c, XCB_PROP_MODE_PREPEND, w, atom, + XCB_ATOM_STRING, 8, 2, prefix)), + "prepend"); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 8), + NULL); + CHECK(p && xcb_get_property_value_length(p) == 8 && + !memcmp(xcb_get_property_value(p), "01abcdef", 8), + "prepend value"); + free(p); + error = xcb_request_check( + c, xcb_change_property_checked(c, XCB_PROP_MODE_APPEND, w, atom, + XCB_ATOM_INTEGER, 16, 1, prefix)); + CHECK(error && error->error_code == XCB_MATCH, "append type mismatch"); + free(error); + error = + xcb_request_check(c, xcb_change_property_checked( + c, XCB_PROP_MODE_REPLACE, w, atom, + XCB_ATOM_STRING, 8, UINT32_C(262144), prefix)); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_CHANGE_PROPERTY, + "property oversized element count"); + free(error); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_INTEGER, 0, 8), NULL); + CHECK(p && p->type == XCB_ATOM_STRING && p->format == 8 && + p->value_len == 0 && p->bytes_after == 8, + "get type mismatch"); + free(p); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_STRING, 2, 8), &error); + CHECK(p && !error && p->value_len == 0 && p->bytes_after == 0, + "offset at end"); + free(p); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_STRING, 3, 8), &error); + CHECK(!p && error && error->error_code == XCB_VALUE, "offset beyond end"); + free(error); + error = NULL; + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_STRING, 1, 8), &error); + CHECK(p && !error && xcb_get_property_value_length(p) == 4 && + !memcmp(xcb_get_property_value(p), "cdef", 4), + "offset at final unit"); + free(p); + p = xcb_get_property_reply( + c, xcb_get_property(c, 1, w, atom, XCB_ATOM_STRING, 0, 8), NULL); + CHECK(p && p->bytes_after == 0 && p->value_len == 8, + "delete-on-read reply"); + free(p); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 8), + NULL); + CHECK(p && p->type == XCB_ATOM_NONE, "delete-on-read removal"); + free(p); + uint16_t shorts[] = {0x1234, 0xabcd}; + xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_INTEGER, 16, + 2, shorts); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_INTEGER, 0, 8), NULL); + CHECK(p && p->format == 16 && p->value_len == 2 && + !memcmp(xcb_get_property_value(p), shorts, sizeof(shorts)), + "16-bit property"); + free(p); + uint32_t words[] = {0x12345678, 0x90abcdef}; + xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_CARDINAL, + 32, 2, words); + xcb_list_properties_reply_t *list = + xcb_list_properties_reply(c, xcb_list_properties(c, w), NULL); + CHECK(list && xcb_list_properties_atoms_length(list) == 1 && + xcb_list_properties_atoms(list)[0] == atom && + xcb_list_properties_sizeof(list) == + (int) (sizeof(*list) + sizeof(xcb_atom_t)) && + xcb_list_properties_atoms_end(list).index == + (int) (sizeof(*list) + sizeof(xcb_atom_t)), + "list properties"); + free(list); + xcb_get_selection_owner_reply_t *owner = xcb_get_selection_owner_reply( + c, xcb_get_selection_owner(c, atom), NULL); + CHECK(owner && owner->owner == XCB_NONE, "initial selection owner"); + free(owner); + CHECK(!xcb_request_check( + c, xcb_set_selection_owner_checked(c, w, atom, XCB_CURRENT_TIME)), + "set selection owner"); + owner = xcb_get_selection_owner_reply(c, xcb_get_selection_owner(c, atom), + NULL); + CHECK(owner && owner->owner == w, "get selection owner"); + free(owner); + CHECK(!xcb_request_check( + c, xcb_convert_selection_checked(c, w, atom, XCB_ATOM_STRING, + atom, XCB_CURRENT_TIME)), + "convert selection"); + xcb_selection_request_event_t *selectionRequest = + (xcb_selection_request_event_t *) xcb_poll_for_queued_event(c); + CHECK(selectionRequest && + selectionRequest->response_type == XCB_SELECTION_REQUEST && + selectionRequest->owner == w && + selectionRequest->requestor == w && + selectionRequest->selection == atom && + selectionRequest->target == XCB_ATOM_STRING && + selectionRequest->property == atom, + "selection request event"); + free(selectionRequest); + CHECK(!xcb_request_check(c, xcb_set_selection_owner_checked( + c, XCB_NONE, atom, XCB_CURRENT_TIME)), + "clear selection owner"); + xcb_selection_clear_event_t *selectionClear = + (xcb_selection_clear_event_t *) xcb_poll_for_queued_event(c); + CHECK(selectionClear && + selectionClear->response_type == XCB_SELECTION_CLEAR && + selectionClear->owner == w && selectionClear->selection == atom, + "selection clear event"); + free(selectionClear); + CHECK(!xcb_request_check( + c, xcb_convert_selection_checked(c, w, atom, XCB_ATOM_STRING, + atom, XCB_CURRENT_TIME)), + "convert unowned selection"); + xcb_selection_notify_event_t *selectionNotify = + (xcb_selection_notify_event_t *) xcb_poll_for_queued_event(c); + CHECK(selectionNotify && + selectionNotify->response_type == XCB_SELECTION_NOTIFY && + selectionNotify->requestor == w && + selectionNotify->selection == atom && + selectionNotify->target == XCB_ATOM_STRING && + selectionNotify->property == XCB_NONE, + "selection notify event"); + free(selectionNotify); + error = xcb_request_check(c, xcb_set_selection_owner_checked( + c, 0xdeadbeef, atom, XCB_CURRENT_TIME)); + CHECK(error && error->error_code == XCB_WINDOW, + "selection owner BadWindow"); + free(error); + error = xcb_request_check( + c, xcb_set_selection_owner_checked(c, w, 0xdeadbeef, XCB_CURRENT_TIME)); + CHECK(error && error->error_code == XCB_ATOM && + error->resource_id == UINT32_C(0xdeadbeef), + "selection owner BadAtom"); + free(error); + error = xcb_request_check( + c, xcb_delete_property_checked(c, w, UINT32_C(0xdeadbeef))); + CHECK(error && error->error_code == XCB_ATOM && + error->resource_id == UINT32_C(0xdeadbeef) && + error->major_code == XCB_DELETE_PROPERTY, + "delete property BadAtom metadata"); + free(error); + error = xcb_request_check( + c, xcb_change_property_checked(c, XCB_PROP_MODE_REPLACE, w, + UINT32_C(0xdeadbeef), XCB_ATOM_STRING, 8, + 0, NULL)); + CHECK(error && error->error_code == XCB_ATOM && + error->resource_id == UINT32_C(0xdeadbeef) && + error->major_code == XCB_CHANGE_PROPERTY, + "change property BadAtom metadata"); + free(error); + error = + xcb_request_check(c, xcb_change_property_checked( + c, 99, w, atom, XCB_ATOM_STRING, 8, 0, NULL)); + CHECK(error && error->error_code == XCB_VALUE && error->resource_id == 99 && + error->major_code == XCB_CHANGE_PROPERTY, + "change property mode metadata"); + free(error); + error = xcb_request_check( + c, xcb_change_property_checked(c, XCB_PROP_MODE_REPLACE, w, atom, + XCB_ATOM_STRING, 7, 0, NULL)); + CHECK(error && error->error_code == XCB_VALUE && error->resource_id == 7 && + error->major_code == XCB_CHANGE_PROPERTY, + "change property format metadata"); + free(error); + error = NULL; + owner = xcb_get_selection_owner_reply( + c, xcb_get_selection_owner(c, 0xdeadbeef), &error); + CHECK(!owner && error && error->error_code == XCB_ATOM, + "get selection owner BadAtom"); + free(error); + error = NULL; + list = xcb_list_properties_reply(c, xcb_list_properties(c, 0xdeadbeef), + &error); + CHECK(!list && error && error->error_code == XCB_WINDOW, + "list properties BadWindow"); + free(error); + xcb_list_properties_cookie_t uncheckedList = + xcb_list_properties_unchecked(c, UINT32_C(0xdeadbeef)); + error = (xcb_generic_error_t *) UINTPTR_MAX; + list = xcb_list_properties_reply(c, uncheckedList, &error); + CHECK(!list && !error, "unchecked list error bypasses reply API"); + xcb_generic_error_t *queuedError = + (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queuedError && queuedError->response_type == 0 && + queuedError->error_code == XCB_WINDOW && + queuedError->resource_id == UINT32_C(0xdeadbeef) && + queuedError->full_sequence == uncheckedList.sequence, + "unchecked list error is queued"); + free(queuedError); + xcb_get_selection_owner_cookie_t uncheckedOwner = + xcb_get_selection_owner_unchecked(c, UINT32_C(0xdeadbeef)); + error = (xcb_generic_error_t *) UINTPTR_MAX; + owner = xcb_get_selection_owner_reply(c, uncheckedOwner, &error); + CHECK(!owner && !error, "unchecked selection error bypasses reply API"); + queuedError = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queuedError && queuedError->error_code == XCB_ATOM && + queuedError->full_sequence == uncheckedOwner.sequence, + "unchecked selection error is queued"); + free(queuedError); + xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_CARDINAL, + 32, 2, words); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_CARDINAL, 0, 8), NULL); + CHECK(p && p->format == 32 && p->value_len == 2 && + !memcmp(xcb_get_property_value(p), words, sizeof(words)), + "32-bit property"); + free(p); + xcb_get_property_reply_t oversizedProperty = { + .format = 32, + .value_len = UINT32_MAX, + }; + CHECK(!xcb_get_property_sizeof(&oversizedProperty) && + !xcb_get_property_value_length(&oversizedProperty) && + !xcb_get_property_value_end(&oversizedProperty).data, + "oversized property layout"); + xcb_delete_property(c, w, atom); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 8), + NULL); + CHECK(p && p->type == XCB_ATOM_NONE, "delete"); + free(p); + xcb_destroy_window(c, w); + xcb_disconnect(c); + puts("test-xcb-property: ok"); + return 0; +} diff --git a/tests/test-xcb-setup.c b/tests/test-xcb-setup.c index e77687de..6043f654 100644 --- a/tests/test-xcb-setup.c +++ b/tests/test-xcb-setup.c @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include "../compat/xcb-compat-private.h" @@ -35,8 +37,62 @@ static int compareIds(const void *left, const void *right) return (a > b) - (a < b); } +static int failedConnectionChecks(void) +{ + CHECK(!setenv("SDL_VIDEODRIVER", "libx11-compat-invalid-driver", 1), + "failed to select invalid video driver"); + + xcb_connection_t *connection = xcb_connect(NULL, NULL); + CHECK(connection && xcb_connection_has_error(connection) == XCB_CONN_ERROR, + "failed connection was not reported"); + CHECK(xcb_generate_id(connection) == UINT32_MAX, + "failed connection generated an XID"); + CHECK(xcb_intern_atom(connection, 0, 1, "A").sequence == 0, + "failed atom request returned a sequence"); + xcb_get_geometry_cookie_t geometry = xcb_get_geometry(connection, XCB_NONE); + CHECK(geometry.sequence == 0, + "failed geometry request returned a sequence"); + xcb_generic_error_t *error = (xcb_generic_error_t *) (uintptr_t) 1; + CHECK(!xcb_get_geometry_reply(connection, geometry, &error) && !error, + "failed geometry request returned pending state"); + CHECK(xcb_create_window_checked( + connection, XCB_COPY_FROM_PARENT, 1, XCB_NONE, 0, 0, 1, 1, 0, + XCB_WINDOW_CLASS_COPY_FROM_PARENT, XCB_COPY_FROM_PARENT, 0, NULL) + .sequence == 0, + "failed window request returned a sequence"); + CHECK(xcb_change_property_checked(connection, XCB_PROP_MODE_REPLACE, 1, 1, + XCB_ATOM_STRING, 8, 0, NULL) + .sequence == 0, + "failed property request returned a sequence"); + CHECK(!xcb_request_check(connection, (xcb_void_cookie_t) {.sequence = 0}), + "failed connection stored a checked request"); + CHECK(xcb_connection_has_error(connection) == XCB_CONN_ERROR, + "failed connection error changed"); + xcb_disconnect(connection); + return 0; +} + +/* SDL caches the video-driver environment when it first initializes, so an + * invalid driver selected in this process would also break every connection + * attempted after it. Drive the failed-connection checks from a child so the + * poisoned environment dies with it. + */ +static int checkFailedConnection(void) +{ + pid_t child = fork(); + CHECK(child >= 0, "fork failed"); + if (!child) + _exit(failedConnectionChecks() ? 1 : 0); + int status = 0; + CHECK(waitpid(child, &status, 0) == child, "failed-connection wait failed"); + CHECK(WIFEXITED(status) && !WEXITSTATUS(status), + "failed-connection child failed"); + return 0; +} + int main(void) { + CHECK(!checkFailedConnection(), "failed-connection checks failed"); int screenNumber = -1; xcb_connection_t *connection = xcb_connect_to_display_with_auth_info(NULL, NULL, &screenNumber); diff --git a/tests/test-xcb-window.c b/tests/test-xcb-window.c new file mode 100644 index 00000000..58ac9205 --- /dev/null +++ b/tests/test-xcb-window.c @@ -0,0 +1,453 @@ +#include +#include +#include +#include +extern Window libx11CompatCreateWindowWithId(Display *, + Window, + Window, + int, + int, + unsigned int, + unsigned int, + unsigned int, + int, + unsigned int, + Visual *, + unsigned long, + XSetWindowAttributes *); +extern Bool isXidAllocated(XID); +extern Bool reserveXidResource(XID); +extern void xcbCompatFailNextReplyAllocationForTest(void); +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", m); \ + return 1; \ + } \ + } while (0) +int main(void) +{ + xcb_connection_t *c = xcb_connect(NULL, NULL); + CHECK(c && !xcb_connection_has_error(c), "connect"); + xcb_screen_t *s = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + CHECK(!xcb_query_tree_children(NULL) && + !xcb_query_tree_children_length(NULL) && + !xcb_query_tree_children_end(NULL).data && + !xcb_query_tree_sizeof(NULL), + "NULL query-tree accessor"); + uint32_t initialInvalidMaskValue = 0; + xcb_void_cookie_t initialUnchecked = xcb_change_window_attributes( + c, s->root, UINT32_C(1) << 31, &initialInvalidMaskValue); + CHECK(!xcb_request_check( + c, xcb_change_window_attributes_checked(c, s->root, 0, NULL)), + "initialize pending table"); + xcb_generic_error_t *initialQueuedError = + (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(initialQueuedError && initialQueuedError->error_code == XCB_VALUE && + initialQueuedError->full_sequence == initialUnchecked.sequence, + "pending table growth preserves queued errors"); + free(initialQueuedError); + xcb_window_t w = xcb_generate_id(c); + uint32_t values[] = {0x112233, XCB_EVENT_MASK_STRUCTURE_NOTIFY}; + xcb_void_cookie_t bad; + xcb_generic_error_t *e; + xcb_void_cookie_t created = xcb_create_window_checked( + c, XCB_COPY_FROM_PARENT, w, s->root, 7, 9, 80, 60, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, s->root_visual, + XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK, values); + CHECK(!xcb_request_check(c, created), "checked create"); + xcb_get_geometry_reply_t *g = + xcb_get_geometry_reply(c, xcb_get_geometry_unchecked(c, w), NULL); + CHECK(g && g->x == 7 && g->y == 9 && g->width == 80 && g->height == 60, + "geometry"); + free(g); + xcb_get_window_attributes_reply_t *a = xcb_get_window_attributes_reply( + c, xcb_get_window_attributes_unchecked(c, w), NULL); + CHECK(a && a->your_event_mask == XCB_EVENT_MASK_STRUCTURE_NOTIFY, + "attributes"); + free(a); + xcb_query_tree_reply_t *t = + xcb_query_tree_reply(c, xcb_query_tree_unchecked(c, w), NULL); + CHECK(t && t->parent == s->root && xcb_query_tree_children_length(t) == 0, + "tree"); + xcb_generic_iterator_t childrenEnd = xcb_query_tree_children_end(t); + CHECK(xcb_query_tree_sizeof(t) == (int) sizeof(*t) && + childrenEnd.data == xcb_query_tree_children(t) && + childrenEnd.index == xcb_query_tree_sizeof(t), + "tree layout"); + free(t); + xcb_window_t uncheckedWindow = xcb_generate_id(c); + xcb_void_cookie_t uncheckedCreate = xcb_create_window( + c, XCB_COPY_FROM_PARENT, uncheckedWindow, s->root, 2, 3, 11, 12, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, NULL); + g = xcb_get_geometry_reply(c, xcb_get_geometry(c, uncheckedWindow), NULL); + CHECK(uncheckedCreate.sequence && g && g->x == 2 && g->y == 3 && + g->width == 11 && g->height == 12, + "unchecked create window"); + free(g); + xcb_destroy_window(c, uncheckedWindow); + xcb_generic_error_t *queryError = NULL; + a = xcb_get_window_attributes_reply( + c, xcb_get_window_attributes(c, UINT32_C(0xdeadbeef)), &queryError); + CHECK(!a && queryError && queryError->error_code == XCB_WINDOW && + queryError->resource_id == UINT32_C(0xdeadbeef) && + queryError->major_code == XCB_GET_WINDOW_ATTRIBUTES, + "attributes reply BadWindow"); + free(queryError); + xcb_get_window_attributes_cookie_t uncheckedAttributes = + xcb_get_window_attributes_unchecked(c, UINT32_C(0xdeadbeef)); + queryError = (xcb_generic_error_t *) UINTPTR_MAX; + a = xcb_get_window_attributes_reply(c, uncheckedAttributes, &queryError); + CHECK(!a && !queryError, "unchecked attributes bypass reply error"); + queryError = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queryError && queryError->error_code == XCB_WINDOW && + queryError->full_sequence == uncheckedAttributes.sequence, + "unchecked attributes queue error"); + free(queryError); + xcb_get_geometry_cookie_t uncheckedGeometry = + xcb_get_geometry_unchecked(c, UINT32_C(0xdeadbeef)); + queryError = (xcb_generic_error_t *) UINTPTR_MAX; + g = xcb_get_geometry_reply(c, uncheckedGeometry, &queryError); + CHECK(!g && !queryError, "unchecked geometry bypasses reply error"); + xcb_generic_error_t *uncheckedGeometryError = + (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK( + uncheckedGeometryError && + uncheckedGeometryError->error_code == XCB_DRAWABLE && + uncheckedGeometryError->full_sequence == uncheckedGeometry.sequence, + "unchecked geometry queues error"); + free(uncheckedGeometryError); + queryError = NULL; + g = xcb_get_geometry_reply(c, xcb_get_geometry(c, UINT32_C(0xdeadbeef)), + &queryError); + CHECK(!g && queryError && queryError->error_code == XCB_DRAWABLE && + queryError->resource_id == UINT32_C(0xdeadbeef) && + queryError->major_code == XCB_GET_GEOMETRY, + "geometry reply BadDrawable"); + free(queryError); + queryError = NULL; + t = xcb_query_tree_reply(c, xcb_query_tree(c, UINT32_C(0xdeadbeef)), + &queryError); + CHECK(!t && queryError && queryError->error_code == XCB_WINDOW && + queryError->resource_id == UINT32_C(0xdeadbeef) && + queryError->major_code == XCB_QUERY_TREE, + "query-tree reply BadWindow"); + free(queryError); + xcb_query_tree_cookie_t uncheckedTree = + xcb_query_tree_unchecked(c, UINT32_C(0xdeadbeef)); + queryError = (xcb_generic_error_t *) UINTPTR_MAX; + t = xcb_query_tree_reply(c, uncheckedTree, &queryError); + CHECK(!t && !queryError, "unchecked query tree bypasses reply error"); + queryError = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queryError && queryError->error_code == XCB_WINDOW && + queryError->full_sequence == uncheckedTree.sequence, + "unchecked query tree queues error"); + free(queryError); + + xcb_window_t retryWindow = xcb_generate_id(c); + bad = xcb_create_window_checked(c, 0, retryWindow, s->root, 0, 0, 4, 4, 1, + XCB_WINDOW_CLASS_INPUT_ONLY, + XCB_COPY_FROM_PARENT, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_MATCH, "InputOnly border BadMatch"); + free(e); + CHECK(!xcb_request_check( + c, xcb_create_window_checked(c, 0, retryWindow, s->root, 0, 0, 4, + 4, 0, XCB_WINDOW_CLASS_INPUT_ONLY, + XCB_COPY_FROM_PARENT, 0, NULL)), + "retry caller ID after failed create"); + uint32_t inputOnlyBackground = 0; + e = xcb_request_check( + c, xcb_change_window_attributes_checked( + c, retryWindow, XCB_CW_BACK_PIXEL, &inputOnlyBackground)); + CHECK(e && e->error_code == XCB_MATCH && + e->resource_id == XCB_CW_BACK_PIXEL && + e->major_code == XCB_CHANGE_WINDOW_ATTRIBUTES, + "InputOnly background BadMatch"); + free(e); + xcb_destroy_window(c, retryWindow); + + xcb_window_t validationWindow = xcb_generate_id(c); + bad = xcb_create_window_checked( + c, XCB_COPY_FROM_PARENT, validationWindow, s->root, 0, 0, 0, 4, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_VALUE && e->resource_id == 0, + "create-window zero width BadValue"); + free(e); + uint8_t invalidDepth = s->root_depth == 1 ? 16 : 1; + bad = xcb_create_window_checked( + c, invalidDepth, validationWindow, s->root, 0, 0, 4, 4, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == invalidDepth, + "create-window depth BadMatch"); + free(e); + bad = xcb_create_window_checked( + c, XCB_COPY_FROM_PARENT, validationWindow, s->root, 0, 0, 4, 4, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, UINT32_C(0xdeadbeef), 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_MATCH && + e->resource_id == UINT32_C(0xdeadbeef), + "create-window visual BadMatch"); + free(e); + bad = xcb_create_window_checked(c, s->root_depth, validationWindow, s->root, + 0, 0, 4, 4, 0, XCB_WINDOW_CLASS_INPUT_ONLY, + XCB_COPY_FROM_PARENT, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == s->root_depth, + "InputOnly depth BadMatch"); + free(e); + CHECK(!xcb_request_check( + c, xcb_create_window_checked(c, XCB_COPY_FROM_PARENT, + validationWindow, s->root, 0, 0, 4, + 4, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + XCB_COPY_FROM_PARENT, 0, NULL)), + "reuse window ID after structural errors"); + xcb_destroy_window(c, validationWindow); + bad = xcb_create_window_checked(c, XCB_COPY_FROM_PARENT, xcb_generate_id(c), + UINT32_C(0xdeadbeef), 0, 0, 4, 4, 0, + XCB_WINDOW_CLASS_COPY_FROM_PARENT, + XCB_COPY_FROM_PARENT, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_WINDOW && + e->resource_id == UINT32_C(0xdeadbeef), + "inherited class invalid parent BadWindow"); + free(e); + + uint32_t invalidMaskValue = 0; + xcb_window_t invalidMaskWindow = xcb_generate_id(c); + e = xcb_request_check( + c, xcb_create_window_checked(c, 0, invalidMaskWindow, s->root, 0, 0, 4, + 4, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + XCB_COPY_FROM_PARENT, UINT32_C(1) << 31, + &invalidMaskValue)); + CHECK(e && e->error_code == XCB_VALUE && e->major_code == XCB_CREATE_WINDOW, + "create-window unknown mask"); + free(e); + CHECK(!xcb_request_check(c, xcb_create_window_checked( + c, 0, invalidMaskWindow, s->root, 0, 0, 4, + 4, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + XCB_COPY_FROM_PARENT, 0, NULL)), + "reuse window ID after unknown mask"); + e = xcb_request_check( + c, xcb_change_window_attributes_checked( + c, invalidMaskWindow, UINT32_C(1) << 31, &invalidMaskValue)); + CHECK(e && e->error_code == XCB_VALUE && + e->resource_id == (UINT32_C(1) << 31) && + e->major_code == XCB_CHANGE_WINDOW_ATTRIBUTES, + "change-attributes unknown mask"); + free(e); + uint32_t invalidGravity = 99; + e = xcb_request_check( + c, xcb_change_window_attributes_checked( + c, invalidMaskWindow, XCB_CW_BIT_GRAVITY, &invalidGravity)); + CHECK(e && e->error_code == XCB_VALUE && e->resource_id == invalidGravity && + e->major_code == XCB_CHANGE_WINDOW_ATTRIBUTES, + "change-attributes gravity BadValue"); + free(e); + uint32_t invalidCursor = s->root; + e = xcb_request_check( + c, xcb_change_window_attributes_checked(c, invalidMaskWindow, + XCB_CW_CURSOR, &invalidCursor)); + CHECK(e && e->error_code == XCB_CURSOR && e->resource_id == s->root && + e->major_code == XCB_CHANGE_WINDOW_ATTRIBUTES, + "change-attributes BadCursor"); + free(e); + e = xcb_request_check( + c, xcb_configure_window_checked(c, invalidMaskWindow, UINT16_C(1) << 15, + &invalidMaskValue)); + CHECK(e && e->error_code == XCB_VALUE && + e->resource_id == (UINT16_C(1) << 15) && + e->major_code == XCB_CONFIGURE_WINDOW, + "configure-window unknown mask"); + free(e); + xcb_void_cookie_t unchecked = xcb_change_window_attributes( + c, invalidMaskWindow, UINT32_C(1) << 31, &invalidMaskValue); + CHECK(!xcb_request_check(c, unchecked), + "unchecked request is not request-checkable"); + xcb_generic_error_t *queuedError = + (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queuedError && queuedError->response_type == 0 && + queuedError->error_code == XCB_VALUE && + queuedError->resource_id == (UINT32_C(1) << 31) && + queuedError->major_code == XCB_CHANGE_WINDOW_ATTRIBUTES && + queuedError->full_sequence == unchecked.sequence, + "unchecked error is queued as an event"); + free(queuedError); + uint32_t invalidSibling[] = {UINT32_C(0xdeadbeef)}; + bad = xcb_configure_window_checked(c, w, XCB_CONFIG_WINDOW_SIBLING, + invalidSibling); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_WINDOW && + e->resource_id == UINT32_C(0xdeadbeef) && + e->major_code == XCB_CONFIGURE_WINDOW, + "configure sibling BadWindow metadata"); + free(e); + xcb_destroy_window(c, invalidMaskWindow); + + Display *helperDisplay = XOpenDisplay(NULL); + CHECK(helperDisplay, "helper display"); + xcb_window_t helperWindow = xcb_generate_id(c); + CHECK(libx11CompatCreateWindowWithId( + helperDisplay, helperWindow, DefaultRootWindow(helperDisplay), 0, + 0, 4, 4, 1, 0, InputOnly, CopyFromParent, 0, NULL) == None, + "helper InputOnly border failure"); + CHECK(!isXidAllocated(helperWindow), "failed helper retained requested ID"); + CHECK(reserveXidResource(helperWindow), "reserve released helper ID"); + CHECK(libx11CompatCreateWindowWithId(helperDisplay, helperWindow, + DefaultRootWindow(helperDisplay), 0, 0, + 4, 4, 0, 0, InputOnly, CopyFromParent, + 0, NULL) == helperWindow, + "reuse released helper ID"); + XDestroyWindow(helperDisplay, helperWindow); + XCloseDisplay(helperDisplay); + bad = xcb_create_window_checked(c, 24, xcb_generate_id(c), 0xdeadbeef, 0, 0, + 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + s->root_visual, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_WINDOW, "BadWindow"); + free(e); + bad = xcb_create_window_checked(c, 24, 0x0f000000, s->root, 0, 0, 1, 1, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + s->root_visual, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_ID_CHOICE, "BadIDChoice"); + free(e); + uint32_t changed[] = {XCB_GRAVITY_NORTH_EAST, XCB_GRAVITY_SOUTH_WEST, 1, + XCB_EVENT_MASK_PROPERTY_CHANGE}; + uint32_t changeMask = XCB_CW_BIT_GRAVITY | XCB_CW_WIN_GRAVITY | + XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; + CHECK(!xcb_request_check(c, xcb_change_window_attributes_checked( + c, w, changeMask, changed)), + "change attributes"); + a = xcb_get_window_attributes_reply(c, xcb_get_window_attributes(c, w), + NULL); + CHECK(a && a->bit_gravity == XCB_GRAVITY_NORTH_EAST && + a->win_gravity == XCB_GRAVITY_SOUTH_WEST && + a->override_redirect && + a->your_event_mask == XCB_EVENT_MASK_PROPERTY_CHANGE, + "changed attributes mismatch"); + free(a); + + uint32_t configured[] = {(uint32_t) (int32_t) -3, 11, 64, 48, 2}; + uint16_t configureMask = + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | + XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_BORDER_WIDTH; + CHECK(!xcb_request_check( + c, xcb_configure_window_checked(c, w, configureMask, configured)), + "configure"); + g = xcb_get_geometry_reply(c, xcb_get_geometry(c, w), NULL); + CHECK(g && g->x == -3 && g->y == 11 && g->width == 64 && g->height == 48 && + g->border_width == 2, + "configured geometry mismatch"); + free(g); + + xcb_window_t container = xcb_generate_id(c); + CHECK( + !xcb_request_check( + c, xcb_create_window_checked( + c, XCB_COPY_FROM_PARENT, container, s->root, 0, 0, 100, 100, + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, s->root_visual, 0, NULL)), + "container create"); + CHECK(!xcb_request_check( + c, xcb_reparent_window_checked(c, w, container, 4, 5)), + "reparent"); + t = xcb_query_tree_reply(c, xcb_query_tree(c, w), NULL); + CHECK(t && t->parent == container, "reparent parent mismatch"); + free(t); + + xcb_window_t sibling = xcb_generate_id(c); + CHECK( + !xcb_request_check( + c, xcb_create_window_checked( + c, XCB_COPY_FROM_PARENT, sibling, container, 0, 0, 10, 10, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, s->root_visual, 0, NULL)), + "sibling create"); + uint32_t siblingOnly[] = {sibling}; + e = xcb_request_check(c, xcb_configure_window_checked( + c, w, XCB_CONFIG_WINDOW_SIBLING, siblingOnly)); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == w && + e->major_code == XCB_CONFIGURE_WINDOW, + "sibling without stack-mode BadMatch"); + free(e); + uint32_t selfStack[] = {w, XCB_STACK_MODE_ABOVE}; + e = xcb_request_check( + c, xcb_configure_window_checked( + c, w, XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE, + selfStack)); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == w, + "self sibling BadMatch"); + free(e); + uint32_t conditionalStack[] = {sibling, XCB_STACK_MODE_TOP_IF}; + CHECK(!xcb_request_check( + c, xcb_configure_window_checked( + c, w, + XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE, + conditionalStack)), + "conditional sibling stack configure"); + uint32_t stack[] = {sibling, XCB_STACK_MODE_ABOVE}; + CHECK(!xcb_request_check( + c, xcb_configure_window_checked( + c, w, + XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE, + stack)), + "stack configure"); + t = xcb_query_tree_reply(c, xcb_query_tree(c, container), NULL); + CHECK(t && xcb_query_tree_children_length(t) == 2, + "stacking changed child membership"); + CHECK(xcb_query_tree_children(t)[1] == w, + "Above stacking did not move window to top"); + free(t); + e = xcb_request_check(c, xcb_reparent_window_checked(c, w, w, 0, 0)); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == w && + e->major_code == XCB_REPARENT_WINDOW, + "self reparent BadMatch"); + free(e); + e = xcb_request_check(c, + xcb_reparent_window_checked(c, container, w, 0, 0)); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == container && + e->major_code == XCB_REPARENT_WINDOW, + "cyclic reparent BadMatch"); + free(e); + + bad = xcb_configure_window_checked(c, 0xdeadbeef, XCB_CONFIG_WINDOW_X, + configured); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_WINDOW, "configure BadWindow"); + free(e); + uint32_t invalidSize[] = {0}; + bad = xcb_configure_window_checked(c, w, XCB_CONFIG_WINDOW_WIDTH, + invalidSize); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_VALUE && e->resource_id == 0, + "configure BadValue"); + free(e); + uint32_t invalidStack[] = {255}; + bad = xcb_configure_window_checked(c, w, XCB_CONFIG_WINDOW_STACK_MODE, + invalidStack); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_VALUE && e->resource_id == 255, + "stack mode BadValue"); + free(e); + bad = xcb_change_window_attributes_checked(c, 0xdeadbeef, XCB_CW_EVENT_MASK, + changed); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_WINDOW, "attributes BadWindow"); + free(e); + xcb_destroy_window(c, sibling); + xcb_destroy_window(c, w); + xcb_destroy_window(c, container); + xcb_disconnect(c); + + c = xcb_connect(NULL, NULL); + CHECK(c && !xcb_connection_has_error(c), "OOM test connection"); + s = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + xcbCompatFailNextReplyAllocationForTest(); + t = xcb_query_tree_reply(c, xcb_query_tree(c, s->root), NULL); + CHECK(!t && xcb_connection_has_error(c) == XCB_CONN_CLOSED_MEM_INSUFFICIENT, + "reply allocation failure did not close connection"); + xcb_disconnect(c); + puts("test-xcb-window: ok"); + return 0; +} diff --git a/tests/xcb-private-symbols.txt b/tests/xcb-private-symbols.txt index d1d86e03..f621780b 100644 --- a/tests/xcb-private-symbols.txt +++ b/tests/xcb-private-symbols.txt @@ -1,3 +1,4 @@ xcbCompatConnectionForDisplay xcbCompatEventWaiters +xcbCompatFailNextReplyAllocationForTest xcbCompatSetQueueOwner diff --git a/tests/xcb-symbols.txt b/tests/xcb-symbols.txt index 3864ea36..9850e74f 100644 --- a/tests/xcb-symbols.txt +++ b/tests/xcb-symbols.txt @@ -1,14 +1,28 @@ # Milestone-1 libxcb-compat ABI used by tests/probe-system-xcb.c. # Keep sorted; later phases add symbols only with implementation tests. +xcb_change_property +xcb_change_property_checked +xcb_change_window_attributes +xcb_change_window_attributes_checked +xcb_configure_window +xcb_configure_window_checked xcb_connect xcb_connect_to_display_with_auth_info xcb_connection_has_error +xcb_convert_selection +xcb_convert_selection_checked +xcb_create_window +xcb_create_window_checked +xcb_delete_property +xcb_delete_property_checked xcb_depth_end xcb_depth_next xcb_depth_sizeof xcb_depth_visuals xcb_depth_visuals_iterator xcb_depth_visuals_length +xcb_destroy_window +xcb_destroy_window_checked xcb_discard_reply xcb_discard_reply64 xcb_disconnect @@ -16,18 +30,64 @@ xcb_flush xcb_format_end xcb_format_next xcb_generate_id +xcb_get_atom_name +xcb_get_atom_name_name +xcb_get_atom_name_name_end +xcb_get_atom_name_name_length +xcb_get_atom_name_reply +xcb_get_atom_name_sizeof +xcb_get_atom_name_unchecked xcb_get_file_descriptor +xcb_get_geometry +xcb_get_geometry_reply +xcb_get_geometry_unchecked xcb_get_maximum_request_length +xcb_get_property +xcb_get_property_reply +xcb_get_property_sizeof +xcb_get_property_unchecked +xcb_get_property_value +xcb_get_property_value_end +xcb_get_property_value_length +xcb_get_selection_owner +xcb_get_selection_owner_reply +xcb_get_selection_owner_unchecked xcb_get_setup +xcb_get_window_attributes +xcb_get_window_attributes_reply +xcb_get_window_attributes_unchecked +xcb_intern_atom +xcb_intern_atom_reply +xcb_intern_atom_unchecked +xcb_list_properties +xcb_list_properties_atoms +xcb_list_properties_atoms_end +xcb_list_properties_atoms_length +xcb_list_properties_reply +xcb_list_properties_sizeof +xcb_list_properties_unchecked +xcb_map_window +xcb_map_window_checked xcb_poll_for_event xcb_poll_for_queued_event xcb_poll_for_special_event +xcb_query_tree +xcb_query_tree_children +xcb_query_tree_children_end +xcb_query_tree_children_length +xcb_query_tree_reply +xcb_query_tree_sizeof +xcb_query_tree_unchecked +xcb_reparent_window +xcb_reparent_window_checked xcb_request_check xcb_screen_allowed_depths_iterator xcb_screen_allowed_depths_length xcb_screen_end xcb_screen_next xcb_screen_sizeof +xcb_set_selection_owner +xcb_set_selection_owner_checked xcb_setup_pixmap_formats xcb_setup_pixmap_formats_iterator xcb_setup_pixmap_formats_length @@ -37,6 +97,8 @@ xcb_setup_sizeof xcb_setup_vendor xcb_setup_vendor_end xcb_setup_vendor_length +xcb_unmap_window +xcb_unmap_window_checked xcb_visualtype_end xcb_visualtype_next xcb_wait_for_event From 9b558c26df8b4bfd9509975fd8078698565b3b39 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 31 Aug 2026 18:43:38 +0800 Subject: [PATCH 4/6] Implement XCB pixmap, GC and drawing requests Translate the remaining core rendering opcodes (53-57, 60-68, 72-73, 76-77) so an XCB client can allocate pixmaps and graphics contexts, draw points, lines, segments, rectangles, arcs and text, copy between drawables, and move images in and out. Drawing is where a wrong argument corrupts output instead of failing loudly, so value masks, coordinate modes and image geometry are validated up front and answered with the protocol error the client expects. Depth compatibility is checked against the drawable rather than assumed: a GC or copy spanning mismatched depths is BadMatch, not a silently wrong blit. GetImage rejects a rectangle that leaves the drawable instead of returning a reply padded with invented pixels, and CopyPlane rejects a plane the source depth does not contain. Graphics contexts need a display, owner and depth that the shared XID table does not record, so the shim keeps that state alongside the id and releases it when the owning connection disconnects. This leaves the GC lookup linear and the state duplicated; folding it into the shared table means changing what src/gc.c stores per XID, which is a larger change than this translation layer should carry. XCreatePixmap and XCreateGC gain caller-chosen id variants, matching the window change in the previous commit, because an XCB client names every resource itself. --- compat/xcb-requests.c | 999 +++++++++++++++++++++++++++++++++++++++ docs/XCB-COVERAGE.md | 20 +- mk/tests.mk | 30 +- src/drawing.h | 11 + src/gc.c | 38 +- src/gc.h | 12 + src/pixmap.c | 39 +- tests/shim-symbols.txt | 2 + tests/test-xcb-drawing.c | 446 +++++++++++++++++ tests/test-xcb-setup.c | 36 ++ tests/test-xlib-xcb.c | 81 ++++ tests/xcb-symbols.txt | 41 ++ 12 files changed, 1711 insertions(+), 44 deletions(-) create mode 100644 tests/test-xcb-drawing.c create mode 100644 tests/test-xlib-xcb.c diff --git a/compat/xcb-requests.c b/compat/xcb-requests.c index ea1c2f1d..92cff0fd 100644 --- a/compat/xcb-requests.c +++ b/compat/xcb-requests.c @@ -9,9 +9,20 @@ #include "xcb-compat-private.h" #include "../src/atoms.h" #include "../src/drawing.h" +#include "../src/gc.h" #include "../src/resource-types.h" #include "../src/window.h" +typedef struct GcEntry { + xcb_gcontext_t id; + GC gc; + Display *display; + xcb_connection_t *owner; + unsigned int depth; + struct GcEntry *next; +} GcEntry; +static GcEntry *gcEntries; +static pthread_mutex_t gcMutex = PTHREAD_MUTEX_INITIALIZER; static _Atomic int failNextReplyAllocation; void xcbCompatFailNextReplyAllocationForTest(void) @@ -19,11 +30,45 @@ void xcbCompatFailNextReplyAllocationForTest(void) atomic_store_explicit(&failNextReplyAllocation, 1, memory_order_release); } +static GcEntry *findGcLocked(xcb_gcontext_t id) +{ + for (GcEntry *entry = gcEntries; entry; entry = entry->next) + if (entry->id == id) + return entry; + return NULL; +} + +void xcbCompatReleaseRequestResources(xcb_connection_t *connection) +{ + pthread_mutex_lock(&gcMutex); + GcEntry **link = &gcEntries; + while (*link) { + GcEntry *entry = *link; + if (entry->owner != connection) { + link = &entry->next; + continue; + } + *link = entry->next; + XFreeGC(entry->display, entry->gc); + free(entry); + } + pthread_mutex_unlock(&gcMutex); +} + static int isDrawable(xcb_drawable_t drawable) { return IS_TYPE(drawable, DRAWABLE); } +/* InputOnly windows are drawables the protocol refuses to render into, read + * from or copy between: they carry no pixels. Xlib reports that as BadMatch, + * and so must every request that reaches a drawing path. + */ +static int isInputOnlyDrawable(xcb_drawable_t drawable) +{ + return IS_TYPE(drawable, WINDOW) && IS_INPUT_ONLY(drawable); +} + static unsigned int drawableDepth(xcb_connection_t *c, xcb_drawable_t drawable) { if (IS_TYPE(drawable, PIXMAP)) @@ -113,6 +158,16 @@ static int isWindow(xcb_window_t window) XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_BORDER_WIDTH | \ XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE) +#define XCB_GC_VALID_MASK \ + (XCB_GC_FUNCTION | XCB_GC_PLANE_MASK | XCB_GC_FOREGROUND | \ + XCB_GC_BACKGROUND | XCB_GC_LINE_WIDTH | XCB_GC_LINE_STYLE | \ + XCB_GC_CAP_STYLE | XCB_GC_JOIN_STYLE | XCB_GC_FILL_STYLE | \ + XCB_GC_FILL_RULE | XCB_GC_TILE | XCB_GC_STIPPLE | \ + XCB_GC_TILE_STIPPLE_ORIGIN_X | XCB_GC_TILE_STIPPLE_ORIGIN_Y | \ + XCB_GC_FONT | XCB_GC_SUBWINDOW_MODE | XCB_GC_GRAPHICS_EXPOSURES | \ + XCB_GC_CLIP_ORIGIN_X | XCB_GC_CLIP_ORIGIN_Y | XCB_GC_CLIP_MASK | \ + XCB_GC_DASH_OFFSET | XCB_GC_DASH_LIST | XCB_GC_ARC_MODE) + xcb_intern_atom_cookie_t xcb_intern_atom(xcb_connection_t *c, uint8_t onlyIfExists, uint16_t nameLength, @@ -1312,3 +1367,947 @@ xcb_void_cookie_t xcb_reparent_window_checked(xcb_connection_t *c, { return reparentWindow(c, window, parent, x, y, 1); } + +/* Accept exactly the depths the setup record advertises, rather than repeating + * the list src/pixmap.c keeps: a client picks its depth from those formats, so + * anything else is a BadValue and the two sets cannot drift apart. + */ +static int supportedPixmapDepth(xcb_connection_t *c, uint8_t depth) +{ + const xcb_setup_t *setup = xcb_get_setup(c); + if (!setup) + return 0; + const xcb_format_t *formats = xcb_setup_pixmap_formats(setup); + int count = xcb_setup_pixmap_formats_length(setup); + for (int i = 0; i < count; i++) + if (formats[i].depth == depth) + return 1; + return 0; +} + +static xcb_void_cookie_t createPixmap(xcb_connection_t *c, + uint8_t depth, + xcb_pixmap_t pid, + xcb_drawable_t drawable, + uint16_t width, + uint16_t height, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = !isDrawable(drawable) ? XCB_DRAWABLE : 0; + uint32_t errorValue = drawable; + if (!error && getXidStruct(pid)->type != 0) { + error = XCB_ID_CHOICE; + errorValue = pid; + } + if (!error && !width) { + error = XCB_VALUE; + errorValue = width; + } + if (!error && !height) { + error = XCB_VALUE; + errorValue = height; + } + if (!error && !supportedPixmapDepth(c, depth)) { + error = XCB_VALUE; + errorValue = depth; + } + + /* Past the argument checks the only way the shared path still fails is a + * failed allocation, which the protocol reports as BadAlloc rather than + * blaming one of the arguments. Claim a client-chosen id last, once nothing + * else can reject the request; the creator owns it from here and releases + * it itself if it fails. + */ + if (!error && !isXidAllocated(pid) && !reserveXidResource(pid)) { + error = XCB_ID_CHOICE; + errorValue = pid; + } + if (!error && + libx11CompatCreatePixmapWithId(xcbCompatDisplay(c), pid, drawable, + width, height, depth) == XCB_NONE) { + error = XCB_ALLOC; + errorValue = pid; + } + return xcbCompatVoidCookie(c, error, errorValue, XCB_CREATE_PIXMAP, + checked); +} + +xcb_void_cookie_t xcb_create_pixmap(xcb_connection_t *c, + uint8_t depth, + xcb_pixmap_t pid, + xcb_drawable_t drawable, + uint16_t width, + uint16_t height) +{ + return createPixmap(c, depth, pid, drawable, width, height, 0); +} +xcb_void_cookie_t xcb_create_pixmap_checked(xcb_connection_t *c, + uint8_t depth, + xcb_pixmap_t pid, + xcb_drawable_t drawable, + uint16_t width, + uint16_t height) +{ + return createPixmap(c, depth, pid, drawable, width, height, 1); +} + +static xcb_void_cookie_t freePixmap(xcb_connection_t *c, + xcb_pixmap_t pixmap, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = IS_TYPE(pixmap, PIXMAP) ? 0 : XCB_PIXMAP; + if (!error) + XFreePixmap(xcbCompatDisplay(c), pixmap); + return xcbCompatVoidCookie(c, error, pixmap, XCB_FREE_PIXMAP, checked); +} +xcb_void_cookie_t xcb_free_pixmap(xcb_connection_t *c, xcb_pixmap_t pixmap) +{ + return freePixmap(c, pixmap, 0); +} +xcb_void_cookie_t xcb_free_pixmap_checked(xcb_connection_t *c, + xcb_pixmap_t pixmap) +{ + return freePixmap(c, pixmap, 1); +} + +static void decodeGcValues(uint32_t mask, + const uint32_t *list, + XGCValues *values) +{ + memset(values, 0, sizeof(*values)); + unsigned int n = 0; +#define TAKE(bit, field, type) \ + do { \ + if (mask & (bit)) \ + values->field = (type) list[n++]; \ + } while (0) + TAKE(XCB_GC_FUNCTION, function, int); + TAKE(XCB_GC_PLANE_MASK, plane_mask, unsigned long); + TAKE(XCB_GC_FOREGROUND, foreground, unsigned long); + TAKE(XCB_GC_BACKGROUND, background, unsigned long); + TAKE(XCB_GC_LINE_WIDTH, line_width, int); + TAKE(XCB_GC_LINE_STYLE, line_style, int); + TAKE(XCB_GC_CAP_STYLE, cap_style, int); + TAKE(XCB_GC_JOIN_STYLE, join_style, int); + TAKE(XCB_GC_FILL_STYLE, fill_style, int); + TAKE(XCB_GC_FILL_RULE, fill_rule, int); + TAKE(XCB_GC_TILE, tile, Pixmap); + TAKE(XCB_GC_STIPPLE, stipple, Pixmap); + TAKE(XCB_GC_TILE_STIPPLE_ORIGIN_X, ts_x_origin, int); + TAKE(XCB_GC_TILE_STIPPLE_ORIGIN_Y, ts_y_origin, int); + TAKE(XCB_GC_FONT, font, Font); + TAKE(XCB_GC_SUBWINDOW_MODE, subwindow_mode, int); + TAKE(XCB_GC_GRAPHICS_EXPOSURES, graphics_exposures, Bool); + TAKE(XCB_GC_CLIP_ORIGIN_X, clip_x_origin, int); + TAKE(XCB_GC_CLIP_ORIGIN_Y, clip_y_origin, int); + TAKE(XCB_GC_CLIP_MASK, clip_mask, Pixmap); + TAKE(XCB_GC_DASH_OFFSET, dash_offset, int); + TAKE(XCB_GC_DASH_LIST, dashes, char); + TAKE(XCB_GC_ARC_MODE, arc_mode, int); +#undef TAKE +} + +static uint8_t validateGcValues(uint32_t mask, + const XGCValues *values, + unsigned int gcDepth, + uint32_t *errorValue) +{ +#define BAD_VALUE(bit, field, limit) \ + do { \ + if ((mask & (bit)) && \ + (values->field < 0 || values->field > (limit))) { \ + *errorValue = (uint32_t) values->field; \ + return XCB_VALUE; \ + } \ + } while (0) + BAD_VALUE(XCB_GC_FUNCTION, function, XCB_GX_SET); + BAD_VALUE(XCB_GC_LINE_STYLE, line_style, XCB_LINE_STYLE_DOUBLE_DASH); + BAD_VALUE(XCB_GC_CAP_STYLE, cap_style, XCB_CAP_STYLE_PROJECTING); + BAD_VALUE(XCB_GC_JOIN_STYLE, join_style, XCB_JOIN_STYLE_BEVEL); + BAD_VALUE(XCB_GC_FILL_STYLE, fill_style, XCB_FILL_STYLE_OPAQUE_STIPPLED); + BAD_VALUE(XCB_GC_FILL_RULE, fill_rule, XCB_FILL_RULE_WINDING); + BAD_VALUE(XCB_GC_SUBWINDOW_MODE, subwindow_mode, + XCB_SUBWINDOW_MODE_INCLUDE_INFERIORS); + BAD_VALUE(XCB_GC_ARC_MODE, arc_mode, XCB_ARC_MODE_PIE_SLICE); +#undef BAD_VALUE + if ((mask & XCB_GC_GRAPHICS_EXPOSURES) && + values->graphics_exposures != False && + values->graphics_exposures != True) { + *errorValue = (uint32_t) values->graphics_exposures; + return XCB_VALUE; + } + if ((mask & XCB_GC_DASH_LIST) && values->dashes == 0) { + *errorValue = 0; + return XCB_VALUE; + } + const struct { + uint32_t bit; + Pixmap pixmap; + unsigned int requiredDepth; + int noneAllowed; + } pixmaps[] = { + {XCB_GC_TILE, values->tile, gcDepth, 0}, + {XCB_GC_STIPPLE, values->stipple, 1, 0}, + {XCB_GC_CLIP_MASK, values->clip_mask, 1, 1}, + }; + for (size_t i = 0; i < sizeof(pixmaps) / sizeof(pixmaps[0]); i++) { + if (!(mask & pixmaps[i].bit) || + (pixmaps[i].noneAllowed && pixmaps[i].pixmap == XCB_NONE)) + continue; + *errorValue = pixmaps[i].pixmap; + if (!IS_TYPE(pixmaps[i].pixmap, PIXMAP)) + return XCB_PIXMAP; + if (GET_PIXMAP_STRUCT(pixmaps[i].pixmap)->depth != + pixmaps[i].requiredDepth) + return XCB_MATCH; + } + if (mask & XCB_GC_FONT) { + *errorValue = values->font; + if (!IS_TYPE(values->font, FONT)) + return XCB_FONT; + } + return 0; +} + +static xcb_void_cookie_t createGc(xcb_connection_t *c, + xcb_gcontext_t cid, + xcb_drawable_t drawable, + uint32_t mask, + const uint32_t *list, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = !isDrawable(drawable) ? XCB_DRAWABLE : 0; + uint32_t errorValue = drawable; + if (!error && isInputOnlyDrawable(drawable)) + error = XCB_MATCH; + if (!error && getXidStruct(cid)->type != 0) { + error = XCB_ID_CHOICE; + errorValue = cid; + } + if (!error && (mask & ~XCB_GC_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && mask && !list) { + error = XCB_VALUE; + errorValue = mask; + } + XGCValues values; + unsigned int gcDepth = 0; + if (!error) { + gcDepth = drawableDepth(c, drawable); + decodeGcValues(mask, list, &values); + error = validateGcValues(mask, &values, gcDepth, &errorValue); + } + + /* Claim a client-chosen id last, once nothing else can reject the request; + * the creator owns it from here and releases it itself if it fails. + */ + if (!error && !isXidAllocated(cid) && !reserveXidResource(cid)) { + error = XCB_ID_CHOICE; + errorValue = cid; + } + if (!error) { + GC gc = libx11CompatCreateGCWithId(xcbCompatDisplay(c), cid, drawable, + mask, &values); + GcEntry *entry = gc ? calloc(1, sizeof(*entry)) : NULL; + if (!entry) { + if (gc) + XFreeGC(xcbCompatDisplay(c), gc); + error = XCB_ALLOC; + } else { + entry->id = cid; + entry->gc = gc; + entry->display = xcbCompatDisplay(c); + entry->owner = c; + entry->depth = gcDepth; + pthread_mutex_lock(&gcMutex); + entry->next = gcEntries; + gcEntries = entry; + pthread_mutex_unlock(&gcMutex); + } + } + return xcbCompatVoidCookie(c, error, errorValue, XCB_CREATE_GC, checked); +} + +#define GC_CREATE_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_gcontext_t cid, \ + xcb_drawable_t drawable, uint32_t mask, \ + const void *list) \ + { \ + return createGc(c, cid, drawable, mask, list, checkedValue); \ + } +GC_CREATE_WRAPPER(xcb_create_gc, 0) +GC_CREATE_WRAPPER(xcb_create_gc_checked, 1) + +static xcb_void_cookie_t changeGc(xcb_connection_t *c, + xcb_gcontext_t id, + uint32_t mask, + const uint32_t *list, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + pthread_mutex_lock(&gcMutex); + GcEntry *entry = findGcLocked(id); + GC gc = entry ? entry->gc : NULL; + uint8_t error = entry ? 0 : XCB_G_CONTEXT; + uint32_t errorValue = id; + if (!error && (mask & ~XCB_GC_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && mask && !list) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error) { + XGCValues values; + decodeGcValues(mask, list, &values); + error = validateGcValues(mask, &values, entry->depth, &errorValue); + if (!error && !XChangeGC(xcbCompatDisplay(c), gc, mask, &values)) + error = XCB_VALUE; + } + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, errorValue, XCB_CHANGE_GC, checked); +} +#define GC_CHANGE_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_gcontext_t gc, \ + uint32_t mask, const void *list) \ + { \ + return changeGc(c, gc, mask, list, checkedValue); \ + } +GC_CHANGE_WRAPPER(xcb_change_gc, 0) +GC_CHANGE_WRAPPER(xcb_change_gc_checked, 1) + +static xcb_void_cookie_t copyGc(xcb_connection_t *c, + xcb_gcontext_t source, + xcb_gcontext_t destination, + uint32_t mask, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + pthread_mutex_lock(&gcMutex); + GcEntry *srcEntry = findGcLocked(source); + GcEntry *dstEntry = findGcLocked(destination); + GC src = srcEntry ? srcEntry->gc : NULL; + GC dst = dstEntry ? dstEntry->gc : NULL; + uint8_t error = !srcEntry || !dstEntry ? XCB_G_CONTEXT : 0; + uint32_t errorValue = !srcEntry ? source : destination; + if (!error && srcEntry->depth != dstEntry->depth) + error = XCB_MATCH; + if (!error && (mask & ~XCB_GC_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && !XCopyGC(xcbCompatDisplay(c), src, mask, dst)) + error = XCB_VALUE; + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, errorValue, XCB_COPY_GC, checked); +} +xcb_void_cookie_t xcb_copy_gc(xcb_connection_t *c, + xcb_gcontext_t src, + xcb_gcontext_t dst, + uint32_t mask) +{ + return copyGc(c, src, dst, mask, 0); +} +xcb_void_cookie_t xcb_copy_gc_checked(xcb_connection_t *c, + xcb_gcontext_t src, + xcb_gcontext_t dst, + uint32_t mask) +{ + return copyGc(c, src, dst, mask, 1); +} + +static xcb_void_cookie_t freeGc(xcb_connection_t *c, + xcb_gcontext_t id, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + pthread_mutex_lock(&gcMutex); + GcEntry **link = &gcEntries; + while (*link && (*link)->id != id) + link = &(*link)->next; + uint8_t error = *link ? 0 : XCB_G_CONTEXT; + if (!error) { + GcEntry *entry = *link; + *link = entry->next; + XFreeGC(entry->display, entry->gc); + free(entry); + } + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, id, XCB_FREE_GC, checked); +} +xcb_void_cookie_t xcb_free_gc(xcb_connection_t *c, xcb_gcontext_t gc) +{ + return freeGc(c, gc, 0); +} +xcb_void_cookie_t xcb_free_gc_checked(xcb_connection_t *c, xcb_gcontext_t gc) +{ + return freeGc(c, gc, 1); +} + +static uint8_t lockDrawingResources(xcb_connection_t *c, + xcb_drawable_t drawable, + xcb_gcontext_t gc, + GC *xgc) +{ + *xgc = NULL; + if (!isDrawable(drawable)) + return XCB_DRAWABLE; + if (isInputOnlyDrawable(drawable)) + return XCB_MATCH; + pthread_mutex_lock(&gcMutex); + GcEntry *entry = findGcLocked(gc); + *xgc = entry ? entry->gc : NULL; + if (entry && entry->depth == drawableDepth(c, drawable)) + return 0; + if (entry) { + pthread_mutex_unlock(&gcMutex); + *xgc = NULL; + return XCB_MATCH; + } + pthread_mutex_unlock(&gcMutex); + return XCB_G_CONTEXT; +} + +static xcb_void_cookie_t clearArea(xcb_connection_t *c, + uint8_t exposures, + xcb_window_t window, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = isWindow(window) ? 0 : XCB_WINDOW; + if (!error) + XClearArea(xcbCompatDisplay(c), window, x, y, width, height, exposures); + return xcbCompatVoidCookie(c, error, window, XCB_CLEAR_AREA, checked); +} +#define CLEAR_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, uint8_t exposures, \ + xcb_window_t window, int16_t x, int16_t y, \ + uint16_t width, uint16_t height) \ + { \ + return clearArea(c, exposures, window, x, y, width, height, \ + checkedValue); \ + } +CLEAR_WRAPPER(xcb_clear_area, 0) +CLEAR_WRAPPER(xcb_clear_area_checked, 1) + +static xcb_void_cookie_t copyArea(xcb_connection_t *c, + xcb_drawable_t source, + xcb_drawable_t destination, + xcb_gcontext_t gc, + int16_t sourceX, + int16_t sourceY, + int16_t destinationX, + int16_t destinationY, + uint16_t width, + uint16_t height, + int checked, + int copyPlane, + uint32_t plane) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + GC xgc = NULL; + int sourceValid = isDrawable(source); + int destinationValid = isDrawable(destination); + uint8_t error = !sourceValid || !destinationValid + ? XCB_DRAWABLE + : lockDrawingResources(c, destination, gc, &xgc); + if (!error && isInputOnlyDrawable(source)) + error = XCB_MATCH; + uint32_t errorValue = !sourceValid ? source + : !destinationValid ? destination + : error == XCB_G_CONTEXT ? gc + : source; + if (!error && !copyPlane && + drawableDepth(c, source) != drawableDepth(c, destination)) { + error = XCB_MATCH; + errorValue = destination; + } + + /* CopyPlane names a single bit, and that bit has to exist in the source: a + * plane above the source depth selects nothing at all. A depth of 32 or + * more admits every bit, and shifting by the width of the type would be + * undefined, so that case skips the range test. + */ + if (!error && copyPlane) { + unsigned int sourceDepth = drawableDepth(c, source); + if (!plane || (plane & (plane - 1)) || + (sourceDepth < 32 && plane >= (UINT32_C(1) << sourceDepth))) { + error = XCB_VALUE; + errorValue = plane; + } + } + if (!error) { + if (copyPlane) + XCopyPlane(xcbCompatDisplay(c), source, destination, xgc, sourceX, + sourceY, width, height, destinationX, destinationY, + plane); + else + XCopyArea(xcbCompatDisplay(c), source, destination, xgc, sourceX, + sourceY, width, height, destinationX, destinationY); + } + if (xgc) + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, errorValue, + copyPlane ? XCB_COPY_PLANE : XCB_COPY_AREA, + checked); +} +#define COPY_AREA_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_drawable_t src, \ + xcb_drawable_t dst, xcb_gcontext_t gc, int16_t sx, \ + int16_t sy, int16_t dx, int16_t dy, uint16_t width, \ + uint16_t height) \ + { \ + return copyArea(c, src, dst, gc, sx, sy, dx, dy, width, height, \ + checkedValue, 0, 0); \ + } +COPY_AREA_WRAPPER(xcb_copy_area, 0) +COPY_AREA_WRAPPER(xcb_copy_area_checked, 1) + +#define COPY_PLANE_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_drawable_t src, \ + xcb_drawable_t dst, xcb_gcontext_t gc, int16_t sx, \ + int16_t sy, int16_t dx, int16_t dy, uint16_t width, \ + uint16_t height, uint32_t plane) \ + { \ + return copyArea(c, src, dst, gc, sx, sy, dx, dy, width, height, \ + checkedValue, 1, plane); \ + } +COPY_PLANE_WRAPPER(xcb_copy_plane, 0) +COPY_PLANE_WRAPPER(xcb_copy_plane_checked, 1) + +typedef enum { + DRAW_POINTS, + DRAW_LINES, + DRAW_SEGMENTS, + DRAW_RECTANGLES, + DRAW_ARCS +} DrawKind; + +static xcb_void_cookie_t drawPrimitives(xcb_connection_t *c, + xcb_drawable_t drawable, + xcb_gcontext_t gc, + uint8_t mode, + uint32_t count, + const void *items, + DrawKind kind, + uint8_t opcode, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + _Static_assert(sizeof(xcb_point_t) == sizeof(XPoint), "point ABI"); + _Static_assert(sizeof(xcb_segment_t) == sizeof(XSegment), "segment ABI"); + _Static_assert(sizeof(xcb_rectangle_t) == sizeof(XRectangle), + "rectangle ABI"); + _Static_assert(sizeof(xcb_arc_t) == sizeof(XArc), "arc ABI"); + size_t itemSize = kind == DRAW_POINTS || kind == DRAW_LINES + ? sizeof(xcb_point_t) + : kind == DRAW_SEGMENTS ? sizeof(xcb_segment_t) + : kind == DRAW_RECTANGLES ? sizeof(xcb_rectangle_t) + : sizeof(xcb_arc_t); + GC xgc = NULL; + uint8_t error = + requestPayloadFits(c, sizeof(xcb_poly_point_request_t), count, itemSize) + ? lockDrawingResources(c, drawable, gc, &xgc) + : XCB_LENGTH; + uint32_t errorValue = error == XCB_G_CONTEXT ? gc + : error == XCB_LENGTH ? count + : drawable; + if (!error && count && !items) { + error = XCB_VALUE; + errorValue = count; + } + if (!error && (kind == DRAW_POINTS || kind == DRAW_LINES) && + mode > XCB_COORD_MODE_PREVIOUS) { + error = XCB_VALUE; + errorValue = mode; + } + if (!error) { + switch (kind) { + case DRAW_POINTS: + XDrawPoints(xcbCompatDisplay(c), drawable, xgc, (XPoint *) items, + count, mode); + break; + case DRAW_LINES: + XDrawLines(xcbCompatDisplay(c), drawable, xgc, (XPoint *) items, + count, mode); + break; + case DRAW_SEGMENTS: + XDrawSegments(xcbCompatDisplay(c), drawable, xgc, + (XSegment *) items, count); + break; + case DRAW_RECTANGLES: + XDrawRectangles(xcbCompatDisplay(c), drawable, xgc, + (XRectangle *) items, count); + break; + case DRAW_ARCS: + XDrawArcs(xcbCompatDisplay(c), drawable, xgc, (XArc *) items, + count); + break; + } + } + if (xgc) + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, errorValue, opcode, checked); +} + +#define POINTS_WRAPPER(name, kindValue, opcodeValue, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, uint8_t mode, \ + xcb_drawable_t drawable, xcb_gcontext_t gc, \ + uint32_t count, const xcb_point_t *items) \ + { \ + return drawPrimitives(c, drawable, gc, mode, count, items, kindValue, \ + opcodeValue, checkedValue); \ + } +POINTS_WRAPPER(xcb_poly_point, DRAW_POINTS, XCB_POLY_POINT, 0) +POINTS_WRAPPER(xcb_poly_point_checked, DRAW_POINTS, XCB_POLY_POINT, 1) +POINTS_WRAPPER(xcb_poly_line, DRAW_LINES, XCB_POLY_LINE, 0) +POINTS_WRAPPER(xcb_poly_line_checked, DRAW_LINES, XCB_POLY_LINE, 1) + +#define FIXED_WRAPPER(name, itemType, kindValue, opcodeValue, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_drawable_t drawable, \ + xcb_gcontext_t gc, uint32_t count, \ + const itemType *items) \ + { \ + return drawPrimitives(c, drawable, gc, 0, count, items, kindValue, \ + opcodeValue, checkedValue); \ + } +FIXED_WRAPPER(xcb_poly_segment, + xcb_segment_t, + DRAW_SEGMENTS, + XCB_POLY_SEGMENT, + 0) +FIXED_WRAPPER(xcb_poly_segment_checked, + xcb_segment_t, + DRAW_SEGMENTS, + XCB_POLY_SEGMENT, + 1) +FIXED_WRAPPER(xcb_poly_rectangle, + xcb_rectangle_t, + DRAW_RECTANGLES, + XCB_POLY_RECTANGLE, + 0) +FIXED_WRAPPER(xcb_poly_rectangle_checked, + xcb_rectangle_t, + DRAW_RECTANGLES, + XCB_POLY_RECTANGLE, + 1) +FIXED_WRAPPER(xcb_poly_arc, xcb_arc_t, DRAW_ARCS, XCB_POLY_ARC, 0) +FIXED_WRAPPER(xcb_poly_arc_checked, xcb_arc_t, DRAW_ARCS, XCB_POLY_ARC, 1) + +/* ImageText8 and ImageText16 differ only in the character width, so one body + * takes both: wide selects the Xlib entry point, the string type and the + * opcode, the same way copyArea covers CopyPlane. + */ +static xcb_void_cookie_t imageText(xcb_connection_t *c, + uint8_t length, + xcb_drawable_t drawable, + xcb_gcontext_t gc, + int16_t x, + int16_t y, + const void *string, + int wide, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + _Static_assert(sizeof(xcb_char2b_t) == sizeof(XChar2b), "char2b ABI"); + GC xgc; + uint8_t error = lockDrawingResources(c, drawable, gc, &xgc); + if (!error && length && !string) + error = XCB_VALUE; + if (!error && wide) + XDrawImageString16(xcbCompatDisplay(c), drawable, xgc, x, y, + (XChar2b *) string, length); + else if (!error) + XDrawImageString(xcbCompatDisplay(c), drawable, xgc, x, y, string, + length); + if (xgc) + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, error == XCB_G_CONTEXT ? gc : drawable, + wide ? XCB_IMAGE_TEXT_16 : XCB_IMAGE_TEXT_8, + checked); +} + +#define TEXT_WRAPPER(name, stringType, wideValue, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, uint8_t length, \ + xcb_drawable_t drawable, xcb_gcontext_t gc, \ + int16_t x, int16_t y, stringType *string) \ + { \ + return imageText(c, length, drawable, gc, x, y, string, wideValue, \ + checkedValue); \ + } +TEXT_WRAPPER(xcb_image_text_8, const char, 0, 0) +TEXT_WRAPPER(xcb_image_text_8_checked, const char, 0, 1) +TEXT_WRAPPER(xcb_image_text_16, const xcb_char2b_t, 1, 0) +TEXT_WRAPPER(xcb_image_text_16_checked, const xcb_char2b_t, 1, 1) + +static int imagePayloadSize(uint16_t width, + uint16_t height, + uint8_t depth, + size_t *rowBytes, + size_t *totalBytes) +{ + unsigned int bitsPerPixel = depth <= 1 ? 1 + : depth <= 8 ? 8 + : depth <= 16 ? 16 + : depth <= 32 ? 32 + : 0; + if (!bitsPerPixel) + return 0; + uint64_t rowBits = (uint64_t) width * bitsPerPixel; + uint64_t row = ((rowBits + 31) & ~UINT64_C(31)) / 8; + uint64_t total = row * height; + if (row > SIZE_MAX || total > SIZE_MAX || total > UINT32_MAX) + return 0; + *rowBytes = (size_t) row; + *totalBytes = (size_t) total; + return 1; +} + +static xcb_void_cookie_t putImage(xcb_connection_t *c, + uint8_t format, + xcb_drawable_t drawable, + xcb_gcontext_t gc, + uint16_t width, + uint16_t height, + int16_t destinationX, + int16_t destinationY, + uint8_t leftPad, + uint8_t depth, + uint32_t dataLength, + const uint8_t *data, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + GC xgc; + uint8_t error = lockDrawingResources(c, drawable, gc, &xgc); + uint32_t errorValue = error == XCB_G_CONTEXT ? gc : drawable; + size_t rowBytes = 0, bytes = 0; + if (!error && format != XCB_IMAGE_FORMAT_Z_PIXMAP) { + error = XCB_VALUE; + errorValue = format; + } + if (!error && leftPad != 0) { + error = XCB_VALUE; + errorValue = leftPad; + } + if (!error && !width) { + error = XCB_VALUE; + errorValue = width; + } + if (!error && !height) { + error = XCB_VALUE; + errorValue = height; + } + if (!error && depth != drawableDepth(c, drawable)) { + error = XCB_MATCH; + errorValue = drawable; + } + if (!error && !imagePayloadSize(width, height, depth, &rowBytes, &bytes)) { + error = XCB_LENGTH; + errorValue = dataLength; + } + if (!error && !requestPayloadFits(c, sizeof(xcb_put_image_request_t), + dataLength, 1)) { + error = XCB_LENGTH; + errorValue = dataLength; + } + if (!error && dataLength < bytes) { + error = XCB_VALUE; + errorValue = dataLength; + } + if (!error && !data) { + error = XCB_VALUE; + errorValue = dataLength; + } + if (!error) { + char *copy = malloc(bytes); + if (!copy) + error = XCB_ALLOC; + else { + memcpy(copy, data, bytes); + XImage *image = XCreateImage( + xcbCompatDisplay(c), + DefaultVisual(xcbCompatDisplay(c), + DefaultScreen(xcbCompatDisplay(c))), + depth, ZPixmap, 0, copy, width, height, 32, (int) rowBytes); + if (!image) { + free(copy); + error = XCB_ALLOC; + } else { + XPutImage(xcbCompatDisplay(c), drawable, xgc, image, 0, 0, + destinationX, destinationY, width, height); + XDestroyImage(image); + } + } + } + if (xgc) + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, errorValue, XCB_PUT_IMAGE, checked); +} +#define PUT_IMAGE_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, uint8_t format, \ + xcb_drawable_t drawable, xcb_gcontext_t gc, \ + uint16_t width, uint16_t height, int16_t x, \ + int16_t y, uint8_t leftPad, uint8_t depth, \ + uint32_t dataLength, const uint8_t *data) \ + { \ + return putImage(c, format, drawable, gc, width, height, x, y, leftPad, \ + depth, dataLength, data, checkedValue); \ + } +PUT_IMAGE_WRAPPER(xcb_put_image, 0) +PUT_IMAGE_WRAPPER(xcb_put_image_checked, 1) + +static xcb_get_image_cookie_t getImage(xcb_connection_t *c, + uint8_t format, + xcb_drawable_t drawable, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint32_t planeMask, + int checked) +{ + REQUIRE_REQUEST(c, xcb_get_image_cookie_t); + xcb_get_image_cookie_t cookie = {nextSequence(c)}; + if (!isDrawable(drawable)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_DRAWABLE, drawable, + XCB_GET_IMAGE, checked); + return cookie; + } + if (isInputOnlyDrawable(drawable)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_MATCH, drawable, + XCB_GET_IMAGE, checked); + return cookie; + } + if (format != XCB_IMAGE_FORMAT_Z_PIXMAP) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_VALUE, format, + XCB_GET_IMAGE, checked); + return cookie; + } + if (!width || !height) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_VALUE, + !width ? width : height, XCB_GET_IMAGE, + checked); + return cookie; + } + + /* Core GetImage requires the rectangle to lie wholly inside the drawable. + * XGetImage zero-fills whatever hangs over the edge, so without this the + * client would get a successful reply full of invented pixels. + */ + Window root; + int drawableX, drawableY; + unsigned int drawableWidth, drawableHeight, borderWidth, depth; + if (!XGetGeometry(xcbCompatDisplay(c), drawable, &root, &drawableX, + &drawableY, &drawableWidth, &drawableHeight, &borderWidth, + &depth) || + x < 0 || y < 0 || (unsigned int) x + width > drawableWidth || + (unsigned int) y + height > drawableHeight) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_MATCH, drawable, + XCB_GET_IMAGE, checked); + return cookie; + } + XImage *image = XGetImage(xcbCompatDisplay(c), drawable, x, y, width, + height, planeMask, ZPixmap); + if (!image) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_MATCH, drawable, + XCB_GET_IMAGE, checked); + return cookie; + } + if (image->bytes_per_line < 0) { + XDestroyImage(image); + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return cookie; + } + uint64_t imageBytes = + (uint64_t) (unsigned int) image->bytes_per_line * height; + if (imageBytes > SIZE_MAX || imageBytes / 4 > UINT32_MAX) { + XDestroyImage(image); + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return cookie; + } + size_t bytes = 0; + xcb_get_image_reply_t *reply = + allocateReplyPayload(c, sizeof(*reply), imageBytes, 1, &bytes); + if (reply) { + reply->response_type = 1; + reply->depth = image->depth; + reply->visual = + isWindow(drawable) + ? XVisualIDFromVisual(DefaultVisual( + xcbCompatDisplay(c), DefaultScreen(xcbCompatDisplay(c)))) + : XCB_NONE; + reply->length = bytes / 4; + if (bytes) + memcpy(reply + 1, image->data, bytes); + } + XDestroyImage(image); + storeReply(c, cookie.sequence, reply); + return cookie; +} +xcb_get_image_cookie_t xcb_get_image(xcb_connection_t *c, + uint8_t format, + xcb_drawable_t drawable, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint32_t planeMask) +{ + return getImage(c, format, drawable, x, y, width, height, planeMask, 1); +} +xcb_get_image_cookie_t xcb_get_image_unchecked(xcb_connection_t *c, + uint8_t format, + xcb_drawable_t drawable, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint32_t planeMask) +{ + return getImage(c, format, drawable, x, y, width, height, planeMask, 0); +} +xcb_get_image_reply_t *xcb_get_image_reply(xcb_connection_t *c, + xcb_get_image_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} +int xcb_get_image_sizeof(const void *buffer) +{ + const xcb_get_image_reply_t *reply = buffer; + uint64_t bytes = reply ? (uint64_t) reply->length * 4u : 0; + return reply && bytes <= INT_MAX - sizeof(*reply) + ? (int) sizeof(*reply) + (int) bytes + : 0; +} +uint8_t *xcb_get_image_data(const xcb_get_image_reply_t *reply) +{ + return reply ? (uint8_t *) (reply + 1) : NULL; +} +int xcb_get_image_data_length(const xcb_get_image_reply_t *reply) +{ + uint64_t bytes = reply ? (uint64_t) reply->length * 4u : 0; + return bytes <= INT_MAX ? (int) bytes : 0; +} +xcb_generic_iterator_t xcb_get_image_data_end( + const xcb_get_image_reply_t *reply) +{ + xcb_generic_iterator_t iterator = {0}; + uint64_t bytes = reply ? (uint64_t) reply->length * 4u : 0; + if (reply && bytes <= INT_MAX - sizeof(*reply)) { + iterator.data = xcb_get_image_data(reply) + (size_t) bytes; + iterator.index = sizeof(*reply) + (int) bytes; + } + return iterator; +} diff --git a/docs/XCB-COVERAGE.md b/docs/XCB-COVERAGE.md index c3fa0f7d..d2e7a8e0 100644 --- a/docs/XCB-COVERAGE.md +++ b/docs/XCB-COVERAGE.md @@ -29,18 +29,32 @@ restages. `make install` ships the built copies. ## Implemented surface -Requests 1-4, 7-8, 10, 12 and 14-24: window lifecycle and configuration, -geometry and tree queries, properties and selections. Every other core opcode is -deferred and deliberately absent from the +Requests 1-4, 7-8, 10, 12, 14-24, 53-57, 60-68, 72-73, and 76-77: window +lifecycle and configuration, geometry and tree queries, properties, selections, +pixmaps, graphics contexts, drawing primitives, text and image transfer. The +exact callable ABI is `tests/xcb-symbols.txt`, enforced against the built +library by `scripts/check-xcb-symbols.py` on every `make XCB=1 check-unit`. + +Every other core opcode is deferred and deliberately absent from the export manifest, so a client fails at link time instead of receiving a cookie that silently never completes. +The staged headers are upstream's own and so declare the whole core protocol: +645 entry points against the 143 this layer exports. That gap is the deferral +made visible. A client that calls a deferred request fails to link, which is the +intended outcome. + ## Deliberate limits `xcb_get_file_descriptor()` returns -1: this backend has no transport socket, so callers must use the wait and poll entry points rather than integrating a connection FD into an external `poll(2)` set. +PutImage and GetImage accept `ZPixmap` only. XYBitmap and XYPixmap have a +different payload geometry and left-pad meaning, and the backing store keeps +pixels in Z order, so the other two formats answer `BadValue` rather than +guessing at a conversion. + RENDER, SHM, SHAPE, XFIXES and RANDR are separate extension libraries and are not part of this layer. DRI, Present, FD passing and authorization transport have no in-process equivalent at all. diff --git a/mk/tests.mk b/mk/tests.mk index e4884002..c90cc598 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -12,7 +12,9 @@ ifeq ($(XCB),1) CHECK_BINS += $(OUT)/tests/test-xcb-link CHECK_BINS += $(OUT)/tests/test-xcb-setup CHECK_BINS += $(OUT)/tests/test-xcb-window $(OUT)/tests/test-xcb-property +CHECK_BINS += $(OUT)/tests/test-xlib-xcb CHECK_BINS += $(OUT)/tests/test-xcb-events +CHECK_BINS += $(OUT)/tests/test-xcb-drawing endif # The GLX tests only exist when the optional GLX layer is built (GLX=1). # test-glx-link covers the no-provider degrade path; test-glx-provider drives the @@ -302,31 +304,27 @@ $(OUT)/tests/%: tests/%.c $(TARGET) $(LDLIBS) $(TEST_LDFLAGS) -o $@ ifeq ($(XCB),1) -$(OUT)/tests/test-xcb-link: tests/test-xcb-link.c $(XCB_COMPAT_TARGET) $(TARGET) - @mkdir -p $(dir $@) - @echo " CC $<" - $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ - -L$(OUT) -lxcb-compat $(TEST_LDFLAGS) -o $@ - $(OUT)/tests/test-xcb-setup: tests/test-xcb-setup.c $(XCB_COMPAT_OBJS) $(TARGET) @mkdir -p $(dir $@) @echo " CC $<" $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ $(XCB_COMPAT_OBJS) $(TARGET) $(LDLIBS) $(TEST_LDFLAGS) -o $@ -XCB_TESTS := test-xcb-window test-xcb-property -XLIB_XCB_TESTS := test-xcb-events +XCB_TESTS := test-xcb-link test-xcb-window test-xcb-property test-xcb-drawing \ + test-xlib-xcb test-xcb-events -$(addprefix $(OUT)/tests/,$(XCB_TESTS)): $(OUT)/tests/%: tests/%.c $(XCB_COMPAT_TARGET) $(TARGET) - @mkdir -p $(dir $@) - @echo " CC $<" - $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ - -L$(OUT) -lxcb-compat -lX11-compat $(TEST_LDFLAGS) -o $@ +# One recipe; each test names the libraries it needs and nothing more, so the +# link test keeps proving that libxcb-compat resolves on its own. +$(OUT)/tests/test-xcb-link: XCB_TEST_LIBS := -lxcb-compat +$(OUT)/tests/test-xcb-window $(OUT)/tests/test-xcb-property \ +$(OUT)/tests/test-xcb-drawing: XCB_TEST_LIBS := -lxcb-compat -lX11-compat +$(OUT)/tests/test-xlib-xcb $(OUT)/tests/test-xcb-events: \ + XCB_TEST_LIBS := -lX11-xcb-compat -lxcb-compat -lX11-compat -$(addprefix $(OUT)/tests/,$(XLIB_XCB_TESTS)): $(OUT)/tests/%: tests/%.c $(X11_XCB_COMPAT_TARGET) $(XCB_COMPAT_TARGET) $(TARGET) +$(addprefix $(OUT)/tests/,$(XCB_TESTS)): $(OUT)/tests/%: tests/%.c \ + $(X11_XCB_COMPAT_TARGET) $(XCB_COMPAT_TARGET) $(TARGET) @mkdir -p $(dir $@) @echo " CC $<" $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ - -L$(OUT) -lX11-xcb-compat -lxcb-compat -lX11-compat \ - $(TEST_LDFLAGS) -o $@ + -L$(OUT) $(XCB_TEST_LIBS) $(TEST_LDFLAGS) -o $@ endif diff --git a/src/drawing.h b/src/drawing.h index b73da34c..fdd3091f 100644 --- a/src/drawing.h +++ b/src/drawing.h @@ -418,4 +418,15 @@ static inline void unionRect(const SDL_Rect *a, out->h = clampToInt(y2 > y1 ? y2 - y1 : 0); } + +/* Create a pixmap bound to a caller-chosen resource id, for the XCB shim. The + * id is released here if creation fails. + */ +Pixmap libx11CompatCreatePixmapWithId(Display *display, + Pixmap pixmap, + Drawable drawable, + unsigned int width, + unsigned int height, + unsigned int depth); + #endif /* _DRAWING_H_ */ diff --git a/src/gc.c b/src/gc.c index e65ac55c..90d09fdc 100644 --- a/src/gc.c +++ b/src/gc.c @@ -33,21 +33,20 @@ int XFreeGC(Display *display, GC gc) return 1; } -GC XCreateGC(Display *display, - Drawable d, - unsigned long valuemask, - XGCValues *values) +GC libx11CompatCreateGCWithId(Display *display, + GContext contextId, + Drawable d, + unsigned long valuemask, + XGCValues *values) { - // https://tronche.com/gui/x/xlib/GC/XCreateGC.html - SET_X_SERVER_REQUEST(display, X_CreateGC); - TYPE_CHECK(d, DRAWABLE, display, NULL); - if (IS_TYPE(d, WINDOW) && IS_INPUT_ONLY(d)) { - handleError(0, display, d, 0, BadMatch, 0); + if (!IS_TYPE(d, DRAWABLE)) { + handleError(0, display, d, 0, BadDrawable, 0); + FREE_XID(contextId); return NULL; } - XID contextId = ALLOC_XID(); - if (contextId == None) { - handleOutOfMemory(0, display, 0, 0); + if (IS_TYPE(d, WINDOW) && IS_INPUT_ONLY(d)) { + handleError(0, display, d, 0, BadMatch, 0); + FREE_XID(contextId); return NULL; } GraphicContext *gc = malloc(sizeof(GraphicContext)); @@ -126,6 +125,21 @@ GC XCreateGC(Display *display, return graphicContextStruct; } +GC XCreateGC(Display *display, + Drawable d, + unsigned long valuemask, + XGCValues *values) +{ + // https://tronche.com/gui/x/xlib/GC/XCreateGC.html + SET_X_SERVER_REQUEST(display, X_CreateGC); + XID contextId = ALLOC_XID(); + if (contextId == None) { + handleOutOfMemory(0, display, 0, 0); + return NULL; + } + return libx11CompatCreateGCWithId(display, contextId, d, valuemask, values); +} + GContext XGContextFromGC(GC gc) { // https://tronche.com/gui/x/xlib/GC/XGContextFromGC.html diff --git a/src/gc.h b/src/gc.h index c4330bdb..219da5b8 100644 --- a/src/gc.h +++ b/src/gc.h @@ -25,6 +25,7 @@ typedef struct _GraphicContext { int clipOriginX; int clipOriginY; Pixmap clipMask; + /* Stored clip rectangles from XSetClipRectangles / XSetRegion. * clipRectanglesSet distinguishes an empty clip region from no rectangle * clip region. @@ -37,6 +38,7 @@ typedef struct _GraphicContext { // concatenated with itself. size_t numDashes; int arcMode; + /* Bumped on every mutation. Lets the draw path skip re-applying unchanged * SDL state by caching the (gc, generation) pair per drawable / renderer. */ @@ -68,4 +70,14 @@ typedef struct _GraphicContext { #define GET_GC(gc) GET_GC_FROM_XID(((struct _XGC *) (gc))->gid) #endif +/* Create a GC bound to a caller-chosen resource id. The XCB shim needs this + * because an XCB client names every resource itself; the id is released here if + * creation fails. + */ +GC libx11CompatCreateGCWithId(Display *display, + GContext contextId, + Drawable drawable, + unsigned long valueMask, + XGCValues *values); + #endif /* GC_H */ diff --git a/src/pixmap.c b/src/pixmap.c index 89103bf7..e80e94e3 100644 --- a/src/pixmap.c +++ b/src/pixmap.c @@ -98,30 +98,25 @@ static Pixmap createPixmapFromPixels(Display *display, return pixmap; } -Pixmap XCreatePixmap(Display *display, - Drawable drawable, - unsigned int width, - unsigned int height, - unsigned int depth) +Pixmap libx11CompatCreatePixmapWithId(Display *display, + Pixmap pixmap, + Drawable drawable, + unsigned int width, + unsigned int height, + unsigned int depth) { - // https://tronche.com/gui/x/xlib/pixmap-and-cursor/XCreatePixmap.html - SET_X_SERVER_REQUEST(display, X_CreatePixmap); (void) drawable; if (width == 0 || height == 0) { LOG("Width and/or height are 0 in XCreatePixmap: w = %u, h = %u\n", width, height); handleError(0, display, None, 0, BadValue, 0); + FREE_XID(pixmap); return None; } if (!isSupportedPixmapDepth(depth)) { LOG("Got unsupported depth (%u) in XCreatePixmap\n", depth); handleError(0, display, None, 0, BadValue, 0); - return None; - } - XID pixmap = ALLOC_XID(); - if (pixmap == None) { - LOG("Out of memory: Could not allocate XID in XCreatePixmap!\n"); - handleOutOfMemory(0, display, 0, 0); + FREE_XID(pixmap); return None; } PixmapStruct *pixmapStruct = malloc(sizeof(PixmapStruct)); @@ -162,6 +157,24 @@ Pixmap XCreatePixmap(Display *display, return pixmap; } +Pixmap XCreatePixmap(Display *display, + Drawable drawable, + unsigned int width, + unsigned int height, + unsigned int depth) +{ + // https://tronche.com/gui/x/xlib/pixmap-and-cursor/XCreatePixmap.html + SET_X_SERVER_REQUEST(display, X_CreatePixmap); + XID pixmap = ALLOC_XID(); + if (pixmap == None) { + LOG("Out of memory: Could not allocate XID in XCreatePixmap!\n"); + handleOutOfMemory(0, display, 0, 0); + return None; + } + return libx11CompatCreatePixmapWithId(display, pixmap, drawable, width, + height, depth); +} + int XFreePixmap(Display *display, Pixmap pixmap) { // https://tronche.com/gui/x/xlib/pixmap-and-cursor/XFreePixmap.html diff --git a/tests/shim-symbols.txt b/tests/shim-symbols.txt index 3e5f3dfa..7d4ac96a 100644 --- a/tests/shim-symbols.txt +++ b/tests/shim-symbols.txt @@ -12,6 +12,8 @@ libx11CompatAcceleratedPresentForcedForTest libx11CompatAcceleratedPresentUsable libx11CompatBeginXcbEventAccess libx11CompatConfigureLiveResizeLayer +libx11CompatCreateGCWithId +libx11CompatCreatePixmapWithId libx11CompatCreateWindowWithId libx11CompatEndXcbEventAccess libx11CompatFailAcceleratedPresentOnceForTest diff --git a/tests/test-xcb-drawing.c b/tests/test-xcb-drawing.c new file mode 100644 index 00000000..429ef0e9 --- /dev/null +++ b/tests/test-xcb-drawing.c @@ -0,0 +1,446 @@ +#include +#include +#include +#include + +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", m); \ + return 1; \ + } \ + } while (0) + +static uint64_t checksum(const xcb_get_image_reply_t *reply) +{ + const uint32_t *pixels = (const uint32_t *) xcb_get_image_data(reply); + size_t count = (size_t) xcb_get_image_data_length(reply) / 4; + uint64_t hash = UINT64_C(1469598103934665603); + for (size_t i = 0; i < count; i++) { + uint32_t pixel = pixels[i] & 0x00ffffff; + for (int byte = 0; byte < 3; byte++) { + hash ^= (pixel >> (byte * 8)) & 0xff; + hash *= UINT64_C(1099511628211); + } + } + return hash; +} + +int main(void) +{ + xcb_connection_t *c = xcb_connect(NULL, NULL); + CHECK(c && !xcb_connection_has_error(c), "connect"); + CHECK(!xcb_get_image_data(NULL) && !xcb_get_image_data_length(NULL) && + !xcb_get_image_data_end(NULL).data && !xcb_get_image_sizeof(NULL), + "NULL image accessor"); + xcb_screen_t *screen = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + xcb_pixmap_t source = xcb_generate_id(c), target = xcb_generate_id(c); + CHECK(!xcb_request_check( + c, xcb_create_pixmap_checked(c, screen->root_depth, source, + screen->root, 16, 16)), + "create source"); + CHECK(!xcb_request_check( + c, xcb_create_pixmap_checked(c, screen->root_depth, target, + screen->root, 16, 16)), + "create target"); + xcb_gcontext_t gc = xcb_generate_id(c), copied = xcb_generate_id(c); + uint32_t values[] = {0x00ffffff, 0x00000000}; + uint32_t mask = XCB_GC_FOREGROUND | XCB_GC_BACKGROUND; + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, gc, source, mask, values)), + "create gc"); + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, copied, target, mask, values)), + "create copied gc"); + xcb_point_t onePoint = {0, 0}; + xcb_generic_error_t *error = xcb_request_check( + c, xcb_poly_point_checked(c, XCB_COORD_MODE_ORIGIN, source, gc, 65536, + &onePoint)); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_POLY_POINT, + "oversized checked primitive request"); + free(error); + xcb_void_cookie_t uncheckedPrimitive = + xcb_poly_point(c, XCB_COORD_MODE_ORIGIN, source, gc, 65536, &onePoint); + error = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_POLY_POINT && + error->full_sequence == uncheckedPrimitive.sequence, + "oversized unchecked primitive request"); + free(error); + uint32_t onePixel = 0; + error = xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, source, gc, + UINT16_MAX, UINT16_MAX, 0, 0, 0, + screen->root_depth, UINT32_MAX, + (const uint8_t *) &onePixel)); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_PUT_IMAGE, + "put-image overflowing payload"); + free(error); + error = xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, source, gc, 1, 1, + 0, 0, 0, screen->root_depth, UINT32_MAX, + (const uint8_t *) &onePixel)); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_PUT_IMAGE, + "put-image oversized request"); + free(error); + error = xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, source, gc, 0, 1, + 0, 0, 0, screen->root_depth, 0, NULL)); + CHECK(error && error->error_code == XCB_VALUE && + error->major_code == XCB_PUT_IMAGE, + "put-image zero width"); + free(error); + uint32_t pixels[16 * 16]; + for (unsigned int y = 0; y < 16; y++) + for (unsigned int x = 0; x < 16; x++) + pixels[y * 16 + x] = ((x * 13) << 16) | ((y * 11) << 8) | (x + y); + CHECK(!xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, source, gc, + 16, 16, 0, 0, 0, screen->root_depth, + sizeof(pixels), (uint8_t *) pixels)), + "put image"); + xcb_copy_area(c, source, target, copied, 0, 0, 0, 0, 16, 16); + xcb_no_exposure_event_t *noExpose = + (xcb_no_exposure_event_t *) xcb_poll_for_queued_event(c); + CHECK(noExpose && noExpose->response_type == XCB_NO_EXPOSURE && + noExpose->drawable == target && + noExpose->major_opcode == XCB_COPY_AREA && + noExpose->minor_opcode == 0, + "copy-area no-exposure event"); + free(noExpose); + xcb_get_image_reply_t *image = + xcb_get_image_reply(c, + xcb_get_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, target, + 0, 0, 16, 16, UINT32_MAX), + NULL); + CHECK(image && xcb_get_image_data_length(image) == 16 * 16 * 4, + "get image"); + xcb_generic_iterator_t imageEnd = xcb_get_image_data_end(image); + CHECK(xcb_get_image_sizeof(image) == (int) (sizeof(*image) + 16 * 16 * 4) && + imageEnd.data == xcb_get_image_data(image) + + xcb_get_image_data_length(image) && + imageEnd.index == xcb_get_image_sizeof(image), + "image layout"); + CHECK(image->visual == XCB_NONE, "pixmap image visual"); + uint64_t hash = checksum(image); + CHECK(hash == UINT64_C(0x293005f544a302e3), "drawing checksum mismatch"); + printf("drawing-checksum=%016" PRIx64 "\n", hash); + free(image); + xcb_copy_area(c, source, target, copied, -2, 0, 0, 0, 4, 4); + xcb_graphics_exposure_event_t *graphicsExpose = + (xcb_graphics_exposure_event_t *) xcb_poll_for_queued_event(c); + CHECK(graphicsExpose && + graphicsExpose->response_type == XCB_GRAPHICS_EXPOSURE && + graphicsExpose->drawable == target && graphicsExpose->x == 0 && + graphicsExpose->y == 0 && graphicsExpose->width == 2 && + graphicsExpose->height == 4 && graphicsExpose->count == 0 && + graphicsExpose->major_opcode == XCB_COPY_AREA && + graphicsExpose->minor_opcode == 0, + "copy-area graphics-exposure event"); + free(graphicsExpose); + xcb_get_image_cookie_t uncheckedImage = + xcb_get_image_unchecked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, + UINT32_C(0xdeadbeef), 0, 0, 1, 1, UINT32_MAX); + error = NULL; + image = xcb_get_image_reply(c, uncheckedImage, &error); + CHECK(!image && !error, "unchecked image error bypasses reply API"); + xcb_generic_error_t *queuedError = + (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queuedError && queuedError->response_type == 0 && + queuedError->error_code == XCB_DRAWABLE && + queuedError->resource_id == UINT32_C(0xdeadbeef) && + queuedError->full_sequence == uncheckedImage.sequence, + "unchecked image error is queued"); + free(queuedError); + xcb_pixmap_t narrowPixmap = xcb_generate_id(c); + xcb_gcontext_t narrowGc = xcb_generate_id(c); + CHECK(!xcb_request_check(c, xcb_create_pixmap_checked(c, 16, narrowPixmap, + screen->root, 1, 1)), + "create 16-bit pixmap"); + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, narrowGc, narrowPixmap, 0, NULL)), + "create 16-bit pixmap GC"); + const uint8_t narrowPixels[4] = {0}; + CHECK(!xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, + narrowPixmap, narrowGc, 1, 1, 0, 0, 0, + 16, sizeof(narrowPixels), narrowPixels)), + "put padded 16-bit image"); + error = xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, narrowPixmap, + narrowGc, 1, 1, 0, 0, 0, 8, + sizeof(narrowPixels), narrowPixels)); + CHECK(error && error->error_code == XCB_MATCH && + error->resource_id == narrowPixmap && + error->major_code == XCB_PUT_IMAGE, + "put image depth BadMatch"); + free(error); + image = + xcb_get_image_reply(c, + xcb_get_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, + narrowPixmap, 0, 0, 1, 1, UINT32_MAX), + NULL); + CHECK(image && image->depth == 16 && image->visual == XCB_NONE && + xcb_get_image_data_length(image) == sizeof(narrowPixels), + "16-bit image wire stride"); + free(image); + xcb_get_image_reply_t oversizedImage = {.length = UINT32_MAX}; + CHECK(!xcb_get_image_sizeof(&oversizedImage) && + !xcb_get_image_data_length(&oversizedImage) && + !xcb_get_image_data_end(&oversizedImage).data, + "oversized image layout"); + error = NULL; + image = + xcb_get_image_reply(c, + xcb_get_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, + narrowPixmap, 0, 0, 0, 1, UINT32_MAX), + &error); + CHECK(!image && error && error->error_code == XCB_VALUE && + error->resource_id == 0 && error->major_code == XCB_GET_IMAGE, + "get image zero-width BadValue"); + free(error); + xcb_point_t narrowPoint = {0, 0}; + error = xcb_request_check( + c, xcb_poly_point_checked(c, XCB_COORD_MODE_ORIGIN, narrowPixmap, gc, 1, + &narrowPoint)); + CHECK(error && error->error_code == XCB_MATCH && + error->resource_id == narrowPixmap && + error->major_code == XCB_POLY_POINT, + "cross-depth drawing BadMatch"); + free(error); + error = xcb_request_check( + c, xcb_copy_gc_checked(c, narrowGc, gc, XCB_GC_FOREGROUND)); + CHECK(error && error->error_code == XCB_MATCH && error->resource_id == gc && + error->major_code == XCB_COPY_GC, + "cross-depth copy-GC BadMatch"); + free(error); + uint32_t narrowTile = narrowPixmap; + error = xcb_request_check( + c, xcb_change_gc_checked(c, gc, XCB_GC_TILE, &narrowTile)); + CHECK(error && error->error_code == XCB_MATCH && + error->resource_id == narrowPixmap && + error->major_code == XCB_CHANGE_GC, + "cross-depth GC tile BadMatch"); + free(error); + error = + xcb_request_check(c, xcb_copy_area_checked(c, source, narrowPixmap, + narrowGc, 0, 0, 0, 0, 1, 1)); + CHECK(error && error->error_code == XCB_MATCH && + error->resource_id == narrowPixmap && + error->major_code == XCB_COPY_AREA, + "cross-depth copy-area BadMatch"); + free(error); + xcb_free_gc(c, narrowGc); + xcb_free_pixmap(c, narrowPixmap); + xcb_point_t points[] = {{1, 1}, {2, 3}, {4, 2}}; + xcb_poly_point(c, XCB_COORD_MODE_ORIGIN, source, gc, 3, points); + xcb_poly_line(c, XCB_COORD_MODE_ORIGIN, source, gc, 3, points); + xcb_segment_t segment[] = {{0, 15, 15, 0}}; + xcb_poly_segment(c, source, gc, 1, segment); + xcb_rectangle_t rectangle[] = {{3, 3, 8, 7}}; + xcb_poly_rectangle(c, source, gc, 1, rectangle); + xcb_arc_t arc[] = {{2, 2, 10, 10, 0, 180 * 64}}; + xcb_poly_arc(c, source, gc, 1, arc); + xcb_copy_plane(c, source, target, copied, 0, 0, 0, 0, 8, 8, 1); + uint32_t red = 0x00ff0000; + xcb_change_gc(c, copied, XCB_GC_FOREGROUND, &red); + xcb_copy_gc(c, copied, gc, XCB_GC_FOREGROUND); + xcb_point_t redPoint = {15, 15}; + xcb_poly_point(c, XCB_COORD_MODE_ORIGIN, target, gc, 1, &redPoint); + xcb_image_text_8(c, 1, target, gc, 0, 10, "A"); + xcb_char2b_t letter = {0, 'B'}; + xcb_image_text_16(c, 1, target, gc, 6, 10, &letter); + xcb_window_t window = xcb_generate_id(c); + CHECK( + !xcb_request_check(c, xcb_create_window_checked( + c, screen->root_depth, window, screen->root, + 0, 0, 8, 8, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + screen->root_visual, 0, NULL)), + "create window"); + CHECK( + !xcb_request_check(c, xcb_clear_area_checked(c, 0, window, 0, 0, 0, 0)), + "clear area"); + error = + xcb_request_check(c, xcb_free_pixmap_checked(c, UINT32_C(0xdeadbeef))); + CHECK(error && error->error_code == XCB_PIXMAP, "BadPixmap"); + free(error); + xcb_pixmap_t invalidPixmap = xcb_generate_id(c); + error = xcb_request_check( + c, xcb_create_pixmap_checked(c, screen->root_depth, invalidPixmap, + screen->root, 0, 1)); + CHECK(error && error->error_code == XCB_VALUE && error->resource_id == 0 && + error->major_code == XCB_CREATE_PIXMAP, + "pixmap BadValue"); + free(error); + CHECK(!xcb_request_check( + c, xcb_create_pixmap_checked(c, screen->root_depth, invalidPixmap, + screen->root, 1, 1)), + "reuse pixmap ID after error"); + error = xcb_request_check( + c, xcb_create_pixmap_checked(c, screen->root_depth, invalidPixmap, + screen->root, 1, 1)); + CHECK(error && error->error_code == XCB_ID_CHOICE && + error->resource_id == invalidPixmap && + error->major_code == XCB_CREATE_PIXMAP, + "pixmap BadIDChoice metadata"); + free(error); + xcb_free_pixmap(c, invalidPixmap); + xcb_gcontext_t invalidGc = xcb_generate_id(c); + error = xcb_request_check( + c, xcb_create_gc_checked(c, invalidGc, UINT32_C(0xdeadbeef), 0, NULL)); + CHECK(error && error->error_code == XCB_DRAWABLE && + error->resource_id == UINT32_C(0xdeadbeef) && + error->major_code == XCB_CREATE_GC, + "GC BadDrawable"); + free(error); + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, invalidGc, target, 0, NULL)), + "reuse GC ID after error"); + error = xcb_request_check( + c, xcb_create_gc_checked(c, invalidGc, target, 0, NULL)); + CHECK(error && error->error_code == XCB_ID_CHOICE && + error->resource_id == invalidGc && + error->major_code == XCB_CREATE_GC, + "GC BadIDChoice resource"); + free(error); + xcb_free_gc(c, invalidGc); + xcb_gcontext_t invalidMaskGc = xcb_generate_id(c); + uint32_t invalidMaskValue = 0; + error = xcb_request_check( + c, xcb_create_gc_checked(c, invalidMaskGc, target, UINT32_C(1) << 31, + &invalidMaskValue)); + CHECK(error && error->error_code == XCB_VALUE && + error->major_code == XCB_CREATE_GC, + "create GC unknown mask"); + free(error); + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, invalidMaskGc, target, 0, NULL)), + "reuse GC ID after unknown mask"); + error = xcb_request_check( + c, xcb_change_gc_checked(c, invalidMaskGc, UINT32_C(1) << 31, + &invalidMaskValue)); + CHECK(error && error->error_code == XCB_VALUE && + error->resource_id == (UINT32_C(1) << 31) && + error->major_code == XCB_CHANGE_GC, + "change GC unknown mask"); + free(error); + uint32_t invalidFunction = 99; + error = xcb_request_check( + c, xcb_change_gc_checked(c, invalidMaskGc, XCB_GC_FUNCTION, + &invalidFunction)); + CHECK(error && error->error_code == XCB_VALUE && + error->resource_id == invalidFunction && + error->major_code == XCB_CHANGE_GC, + "change GC invalid function"); + free(error); + uint32_t invalidTile = screen->root; + error = xcb_request_check( + c, xcb_change_gc_checked(c, invalidMaskGc, XCB_GC_TILE, &invalidTile)); + CHECK(error && error->error_code == XCB_PIXMAP && + error->resource_id == screen->root && + error->major_code == XCB_CHANGE_GC, + "change GC tile BadPixmap"); + free(error); + uint32_t invalidFont = screen->root; + error = xcb_request_check( + c, xcb_change_gc_checked(c, invalidMaskGc, XCB_GC_FONT, &invalidFont)); + CHECK(error && error->error_code == XCB_FONT && + error->resource_id == screen->root && + error->major_code == XCB_CHANGE_GC, + "change GC BadFont"); + free(error); + error = xcb_request_check( + c, xcb_copy_area_checked(c, source, UINT32_C(0xdeadbeef), gc, 0, 0, 0, + 0, 1, 1)); + CHECK(error && error->error_code == XCB_DRAWABLE && + error->resource_id == UINT32_C(0xdeadbeef) && + error->major_code == XCB_COPY_AREA, + "copy-area destination BadDrawable metadata"); + free(error); + error = xcb_request_check( + c, xcb_copy_gc_checked(c, invalidMaskGc, gc, UINT32_C(1) << 31)); + CHECK(error && error->error_code == XCB_VALUE && + error->major_code == XCB_COPY_GC, + "copy GC unknown mask"); + free(error); + xcb_free_gc(c, invalidMaskGc); + error = xcb_request_check( + c, xcb_poly_point_checked(c, XCB_COORD_MODE_ORIGIN, target, + UINT32_C(0xdeadbeef), 1, &redPoint)); + CHECK(error && error->error_code == XCB_G_CONTEXT, "BadGC"); + free(error); + error = xcb_request_check( + c, xcb_poly_point_checked(c, XCB_COORD_MODE_ORIGIN, target, gc, + UINT32_MAX, &redPoint)); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_POLY_POINT, + "drawing oversized item count"); + free(error); + error = xcb_request_check( + c, xcb_copy_plane_checked(c, source, target, gc, 0, 0, 0, 0, 1, 1, 3)); + CHECK(error && error->error_code == XCB_VALUE, "copy-plane BadValue"); + free(error); + + /* A depth-16 source makes the out-of-range plane deterministic whatever the + * screen depth is. + */ + xcb_pixmap_t shallowSource = xcb_generate_id(c); + CHECK(!xcb_request_check(c, xcb_create_pixmap_checked(c, 16, shallowSource, + screen->root, 4, 4)), + "create shallow copy-plane source"); + error = xcb_request_check( + c, xcb_copy_plane_checked(c, shallowSource, target, gc, 0, 0, 0, 0, 1, + 1, UINT32_C(1) << 20)); + CHECK(error && error->error_code == XCB_VALUE && + error->major_code == XCB_COPY_PLANE, + "copy-plane above source depth"); + free(error); + CHECK(!xcb_request_check( + c, xcb_copy_plane_checked(c, shallowSource, target, gc, 0, 0, 0, + 0, 1, 1, UINT32_C(1) << 15)), + "copy-plane within source depth"); + xcb_free_pixmap(c, shallowSource); + xcb_pixmap_t oddDepthPixmap = xcb_generate_id(c); + error = xcb_request_check( + c, xcb_create_pixmap_checked(c, 7, oddDepthPixmap, screen->root, 1, 1)); + CHECK(error && error->error_code == XCB_VALUE && + error->major_code == XCB_CREATE_PIXMAP, + "create pixmap unsupported depth"); + free(error); + xcb_get_image_cookie_t outsideImage = xcb_get_image( + c, XCB_IMAGE_FORMAT_Z_PIXMAP, source, 8, 8, 16, 16, UINT32_MAX); + error = NULL; + CHECK(!xcb_get_image_reply(c, outsideImage, &error) && error && + error->error_code == XCB_MATCH, + "image outside drawable"); + free(error); + xcb_free_gc(c, copied); + xcb_free_gc(c, gc); + xcb_free_pixmap(c, target); + xcb_free_pixmap(c, source); + xcb_destroy_window(c, window); + + xcb_gcontext_t orphanGc = xcb_generate_id(c); + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, orphanGc, screen->root, 0, NULL)), + "create disconnect-owned GC"); + xcb_connection_t *survivor = xcb_connect(NULL, NULL); + CHECK(survivor && !xcb_connection_has_error(survivor), + "connect GC survivor"); + xcb_disconnect(c); + xcb_point_t survivorPoint = {0, 0}; + error = xcb_request_check( + survivor, + xcb_poly_point_checked( + survivor, XCB_COORD_MODE_ORIGIN, + xcb_setup_roots_iterator(xcb_get_setup(survivor)).data->root, + orphanGc, 1, &survivorPoint)); + CHECK(error && error->error_code == XCB_G_CONTEXT, + "disconnect removes owned GC entries"); + free(error); + xcb_disconnect(survivor); + return 0; +} diff --git a/tests/test-xcb-setup.c b/tests/test-xcb-setup.c index 6043f654..82a66ff3 100644 --- a/tests/test-xcb-setup.c +++ b/tests/test-xcb-setup.c @@ -64,6 +64,20 @@ static int failedConnectionChecks(void) XCB_ATOM_STRING, 8, 0, NULL) .sequence == 0, "failed property request returned a sequence"); + CHECK( + xcb_create_pixmap_checked(connection, 1, 1, XCB_NONE, 1, 1).sequence == + 0, + "failed pixmap request returned a sequence"); + CHECK(xcb_create_gc_checked(connection, 1, XCB_NONE, 0, NULL).sequence == 0, + "failed GC request returned a sequence"); + CHECK(xcb_poly_point_checked(connection, XCB_COORD_MODE_ORIGIN, XCB_NONE, 1, + 0, NULL) + .sequence == 0, + "failed drawing request returned a sequence"); + CHECK(xcb_get_image(connection, XCB_IMAGE_FORMAT_Z_PIXMAP, XCB_NONE, 0, 0, + 1, 1, UINT32_MAX) + .sequence == 0, + "failed image request returned a sequence"); CHECK(!xcb_request_check(connection, (xcb_void_cookie_t) {.sequence = 0}), "failed connection stored a checked request"); CHECK(xcb_connection_has_error(connection) == XCB_CONN_ERROR, @@ -144,6 +158,28 @@ int main(void) } CHECK(!formats.rem && xcb_format_end(formats).data == formats.data, "pixmap format iterator termination failed"); + /* The setup advertises the depths a client may ask for, so every one of + * them has to be creatable and anything else has to be refused. Without + * this the advertised set and the set src/pixmap.c accepts could drift. + */ + xcb_screen_t *formatScreen = xcb_setup_roots_iterator(setup).data; + for (xcb_format_iterator_t f = xcb_setup_pixmap_formats_iterator(setup); + f.rem > 0; xcb_format_next(&f)) { + xcb_pixmap_t pixmap = xcb_generate_id(connection); + CHECK(!xcb_request_check( + connection, + xcb_create_pixmap_checked(connection, f.data->depth, pixmap, + formatScreen->root, 2, 2)), + "advertised pixmap depth was refused"); + xcb_free_pixmap(connection, pixmap); + } + xcb_generic_error_t *depthError = xcb_request_check( + connection, + xcb_create_pixmap_checked(connection, 7, xcb_generate_id(connection), + formatScreen->root, 2, 2)); + CHECK(depthError && depthError->error_code == XCB_VALUE, + "unadvertised pixmap depth was accepted"); + free(depthError); xcb_screen_iterator_t screens = xcb_setup_roots_iterator(setup); CHECK(screens.rem == 1 && screens.data && screens.data->root, diff --git a/tests/test-xlib-xcb.c b/tests/test-xlib-xcb.c new file mode 100644 index 00000000..993f7e1a --- /dev/null +++ b/tests/test-xlib-xcb.c @@ -0,0 +1,81 @@ +#include +#include +#include +#include +#include + +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", m); \ + return 1; \ + } \ + } while (0) + +int main(void) +{ + Display *display = XOpenDisplay(NULL); + CHECK(display, "open display"); + xcb_connection_t *connection = XGetXCBConnection(display); + CHECK(connection && connection == XGetXCBConnection(display), + "one wrapper per display"); + Display *otherDisplay = XOpenDisplay(NULL); + CHECK(otherDisplay, "open second display"); + xcb_connection_t *otherConnection = XGetXCBConnection(otherDisplay); + CHECK(otherConnection && otherConnection != connection, + "distinct wrappers for two displays"); + Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 0, + 0, 16, 16, 0, 0, 0); + Atom property = XInternAtom(display, "X11_XCB_SHARED", False); + const char value[] = "shared"; + XChangeProperty(display, window, property, XA_STRING, 8, PropModeReplace, + (const unsigned char *) value, sizeof(value) - 1); + xcb_get_property_reply_t *reply = xcb_get_property_reply( + connection, + xcb_get_property(connection, 0, window, property, XCB_ATOM_STRING, 0, + sizeof(value)), + NULL); + CHECK(reply && xcb_get_property_value_length(reply) == sizeof(value) - 1 && + !memcmp(xcb_get_property_value(reply), value, sizeof(value) - 1), + "Xlib write visible to XCB"); + free(reply); + XSetEventQueueOwner(display, XCBOwnsEventQueue); + XSetEventQueueOwner(display, XlibOwnsEventQueue); + xcb_disconnect(otherConnection); + XCloseDisplay(otherDisplay); + xcb_disconnect(connection); + XDestroyWindow(display, window); + XCloseDisplay(display); + + display = XOpenDisplay(NULL); + CHECK(display, "reopen display"); + connection = XGetXCBConnection(display); + CHECK(connection, "second wrapper"); + xcb_screen_t *screen = + xcb_setup_roots_iterator(xcb_get_setup(connection)).data; + xcb_gcontext_t gc = xcb_generate_id(connection); + CHECK(!xcb_request_check( + connection, + xcb_create_gc_checked(connection, gc, screen->root, 0, NULL)), + "create shared-display GC"); + xcb_connection_t *survivor = xcb_connect(NULL, NULL); + CHECK(survivor && !xcb_connection_has_error(survivor), + "open close-hook survivor"); + XCloseDisplay(display); + CHECK(xcb_connection_has_error(connection) == XCB_CONN_ERROR, + "display-first close invalidates wrapper"); + xcb_point_t point = {0, 0}; + xcb_generic_error_t *error = xcb_request_check( + survivor, + xcb_poly_point_checked( + survivor, XCB_COORD_MODE_ORIGIN, + xcb_setup_roots_iterator(xcb_get_setup(survivor)).data->root, gc, 1, + &point)); + CHECK(error && error->error_code == XCB_G_CONTEXT, + "display close removes owned GC entries"); + free(error); + xcb_disconnect(connection); + xcb_disconnect(survivor); + puts("test-xlib-xcb: ok"); + return 0; +} diff --git a/tests/xcb-symbols.txt b/tests/xcb-symbols.txt index 9850e74f..46f59ac9 100644 --- a/tests/xcb-symbols.txt +++ b/tests/xcb-symbols.txt @@ -1,9 +1,13 @@ # Milestone-1 libxcb-compat ABI used by tests/probe-system-xcb.c. # Keep sorted; later phases add symbols only with implementation tests. +xcb_change_gc +xcb_change_gc_checked xcb_change_property xcb_change_property_checked xcb_change_window_attributes xcb_change_window_attributes_checked +xcb_clear_area +xcb_clear_area_checked xcb_configure_window xcb_configure_window_checked xcb_connect @@ -11,6 +15,16 @@ xcb_connect_to_display_with_auth_info xcb_connection_has_error xcb_convert_selection xcb_convert_selection_checked +xcb_copy_area +xcb_copy_area_checked +xcb_copy_gc +xcb_copy_gc_checked +xcb_copy_plane +xcb_copy_plane_checked +xcb_create_gc +xcb_create_gc_checked +xcb_create_pixmap +xcb_create_pixmap_checked xcb_create_window xcb_create_window_checked xcb_delete_property @@ -29,6 +43,10 @@ xcb_disconnect xcb_flush xcb_format_end xcb_format_next +xcb_free_gc +xcb_free_gc_checked +xcb_free_pixmap +xcb_free_pixmap_checked xcb_generate_id xcb_get_atom_name xcb_get_atom_name_name @@ -41,6 +59,13 @@ xcb_get_file_descriptor xcb_get_geometry xcb_get_geometry_reply xcb_get_geometry_unchecked +xcb_get_image +xcb_get_image_data +xcb_get_image_data_end +xcb_get_image_data_length +xcb_get_image_reply +xcb_get_image_sizeof +xcb_get_image_unchecked xcb_get_maximum_request_length xcb_get_property xcb_get_property_reply @@ -56,6 +81,10 @@ xcb_get_setup xcb_get_window_attributes xcb_get_window_attributes_reply xcb_get_window_attributes_unchecked +xcb_image_text_16 +xcb_image_text_16_checked +xcb_image_text_8 +xcb_image_text_8_checked xcb_intern_atom xcb_intern_atom_reply xcb_intern_atom_unchecked @@ -71,6 +100,18 @@ xcb_map_window_checked xcb_poll_for_event xcb_poll_for_queued_event xcb_poll_for_special_event +xcb_poly_arc +xcb_poly_arc_checked +xcb_poly_line +xcb_poly_line_checked +xcb_poly_point +xcb_poly_point_checked +xcb_poly_rectangle +xcb_poly_rectangle_checked +xcb_poly_segment +xcb_poly_segment_checked +xcb_put_image +xcb_put_image_checked xcb_query_tree xcb_query_tree_children xcb_query_tree_children_end From 86e183da2e1a567d6fad0b6c35d8e9ec106ba72f Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 31 Aug 2026 18:44:14 +0800 Subject: [PATCH 5/6] Add native XCB showcase programs The XCB tests drive the shim through assertions, which proves the return values but never renders a frame. These two programs are XCB clients with no Xlib calls at all, so they exercise the paths a real client takes: connection setup, a mapped window, a graphics context, image upload and the primitive calls, ending on the event loop. They build with the other clients under make examples rather than a target of their own, since XCB=1 already decides whether they exist and a second entry point only gives the same set two names. Both accept --smoke for a headless run under SDL's dummy video driver, so make check-xcb-showcases keeps them building and running rather than rotting as documentation. Smoke mode drains the event queue before exiting, because the drawing requests are unchecked and a run that failed every one of them would otherwise still exit successfully. --- README.md | 8 +- examples/README-xcb.md | 14 ++++ examples/xcb-kaleidoscope.c | 50 +++++++++++++ examples/xcb-mandelbrot.c | 47 ++++++++++++ examples/xcb-shared.h | 143 ++++++++++++++++++++++++++++++++++++ mk/examples.mk | 26 ++++++- 6 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 examples/README-xcb.md create mode 100644 examples/xcb-kaleidoscope.c create mode 100644 examples/xcb-mandelbrot.c create mode 100644 examples/xcb-shared.h diff --git a/README.md b/README.md index 7a0f2419..495a99d4 100644 --- a/README.md +++ b/README.md @@ -64,14 +64,20 @@ Clients link against it the same way they would link against the system `libX11. ## Examples -`examples/` bundles real Xlib clients built against the local `libX11-compat.so`: +`examples/` bundles real Xlib and XCB clients built against the local +compatibility libraries: ```sh make examples build/examples/2048 +make XCB=1 examples +build/examples/xcb-kaleidoscope ``` The bundle covers a 2048 game, a paint demo, Conway's Game of Life, an analog clock, an interactive Mandelbrot viewer, a single-runner Processing-style showcase, an SDL-backed clipboard probe, and the upstream X.Org `x11perf` benchmark. +The XCB shim is off by default, so `XCB=1` adds the two native XCB clients: a +geometric image/primitives demo and a CPU-rendered Mandelbrot client; see +[`examples/README-xcb.md`](examples/README-xcb.md). See [`docs/EXAMPLES.md`](docs/EXAMPLES.md) for the API each example exercises. The screenshot above is from the larger ViolaWWW port described in [Larger Workloads Under Investigation](#larger-workloads-under-investigation). diff --git a/examples/README-xcb.md b/examples/README-xcb.md new file mode 100644 index 00000000..d57760b7 --- /dev/null +++ b/examples/README-xcb.md @@ -0,0 +1,14 @@ +# XCB Showcases + +These native examples exercise the XCB compatibility library without using +Xlib client calls: + +- `xcb-kaleidoscope` combines image upload with XCB line and rectangle + primitives. +- `xcb-mandelbrot` renders a CPU-generated fractal through `xcb_put_image`. + +The XCB compatibility layer is off by default, so these two build with +`make XCB=1 examples` alongside the Xlib clients. Run +`build/examples/xcb-kaleidoscope` or `build/examples/xcb-mandelbrot` and press +any key to exit. Use `make XCB=1 check-xcb-showcases` for a headless, +non-interactive smoke test with SDL's dummy video driver. diff --git a/examples/xcb-kaleidoscope.c b/examples/xcb-kaleidoscope.c new file mode 100644 index 00000000..120a60bb --- /dev/null +++ b/examples/xcb-kaleidoscope.c @@ -0,0 +1,50 @@ +#include "xcb-shared.h" + +static int paint(xcb_connection_t *connection, + xcb_window_t window, + xcb_gcontext_t gc, + uint8_t depth) +{ + uint32_t *pixels = + malloc((size_t) SHOWCASE_WIDTH * SHOWCASE_HEIGHT * sizeof(*pixels)); + if (!pixels) + return 0; + for (unsigned int y = 0; y < SHOWCASE_HEIGHT; y++) { + for (unsigned int x = 0; x < SHOWCASE_WIDTH; x++) { + unsigned int dx = x > SHOWCASE_WIDTH / 2 ? x - SHOWCASE_WIDTH / 2 + : SHOWCASE_WIDTH / 2 - x; + unsigned int dy = y > SHOWCASE_HEIGHT / 2 ? y - SHOWCASE_HEIGHT / 2 + : SHOWCASE_HEIGHT / 2 - y; + unsigned int r = (dx * 3 + dy) & 0xff; + unsigned int g = (dx + dy * 3) & 0xff; + unsigned int b = ((dx ^ dy) * 2) & 0xff; + pixels[(size_t) y * SHOWCASE_WIDTH + x] = (r << 16) | (g << 8) | b; + } + } + showcasePutImage(connection, window, gc, depth, pixels); + free(pixels); + + const xcb_segment_t rays[] = { + {SHOWCASE_WIDTH / 2, SHOWCASE_HEIGHT / 2, 0, 0}, + {SHOWCASE_WIDTH / 2, SHOWCASE_HEIGHT / 2, SHOWCASE_WIDTH - 1, 0}, + {SHOWCASE_WIDTH / 2, SHOWCASE_HEIGHT / 2, SHOWCASE_WIDTH - 1, + SHOWCASE_HEIGHT - 1}, + {SHOWCASE_WIDTH / 2, SHOWCASE_HEIGHT / 2, 0, SHOWCASE_HEIGHT - 1}, + }; + const uint32_t white = 0x00ffffff; + xcb_change_gc(connection, gc, XCB_GC_FOREGROUND, &white); + xcb_poly_segment(connection, window, gc, sizeof(rays) / sizeof(rays[0]), + rays); + for (unsigned int inset = 24; inset < 220; inset += 24) { + xcb_rectangle_t rectangle = {inset, inset, SHOWCASE_WIDTH - 2 * inset, + SHOWCASE_HEIGHT - 2 * inset}; + xcb_poly_rectangle(connection, window, gc, 1, &rectangle); + } + xcb_flush(connection); + return 1; +} + +int main(int argc, char **argv) +{ + return showcaseRun(argc, argv, "xcb-kaleidoscope", paint); +} diff --git a/examples/xcb-mandelbrot.c b/examples/xcb-mandelbrot.c new file mode 100644 index 00000000..f04d51a5 --- /dev/null +++ b/examples/xcb-mandelbrot.c @@ -0,0 +1,47 @@ +#include "xcb-shared.h" + +enum { MAX_ITERATIONS = 96 }; + +static uint32_t color(unsigned int iteration) +{ + if (iteration == MAX_ITERATIONS) + return 0; + unsigned int shade = iteration * 255 / MAX_ITERATIONS; + return ((shade * 3 & 0xff) << 16) | ((shade * 7 & 0xff) << 8) | + (255 - shade); +} + +static int paint(xcb_connection_t *connection, + xcb_window_t window, + xcb_gcontext_t gc, + uint8_t depth) +{ + uint32_t *pixels = + malloc((size_t) SHOWCASE_WIDTH * SHOWCASE_HEIGHT * sizeof(*pixels)); + if (!pixels) + return 0; + for (unsigned int py = 0; py < SHOWCASE_HEIGHT; py++) { + double ci = (double) py / SHOWCASE_HEIGHT * 2.4 - 1.2; + for (unsigned int px = 0; px < SHOWCASE_WIDTH; px++) { + double cr = (double) px / SHOWCASE_WIDTH * 3.2 - 2.2; + double zr = 0.0, zi = 0.0; + unsigned int iteration = 0; + while (zr * zr + zi * zi <= 4.0 && iteration < MAX_ITERATIONS) { + double next = zr * zr - zi * zi + cr; + zi = 2.0 * zr * zi + ci; + zr = next; + iteration++; + } + pixels[(size_t) py * SHOWCASE_WIDTH + px] = color(iteration); + } + } + showcasePutImage(connection, window, gc, depth, pixels); + free(pixels); + xcb_flush(connection); + return 1; +} + +int main(int argc, char **argv) +{ + return showcaseRun(argc, argv, "xcb-mandelbrot", paint); +} diff --git a/examples/xcb-shared.h b/examples/xcb-shared.h new file mode 100644 index 00000000..123cf62f --- /dev/null +++ b/examples/xcb-shared.h @@ -0,0 +1,143 @@ +/* + * Scaffolding shared by the native XCB showcases: connection setup, a mapped + * window, a graphics context, the redraw loop and teardown. Each showcase then + * consists of its paint function alone. + * + * Header-only on purpose. The showcases are single-file programs built without + * a link step of their own, and the scaffolding is not a library anyone else + * consumes. + */ +#ifndef XCB_SHARED_H +#define XCB_SHARED_H + +#include +#include +#include +#include + +enum { SHOWCASE_WIDTH = 640, SHOWCASE_HEIGHT = 480 }; +enum { SHOWCASE_STRIPE_HEIGHT = 32 }; + +/* Returns nonzero on success. Called once before the loop and again on expose. + */ +typedef int (*ShowcasePaint)(xcb_connection_t *connection, + xcb_window_t window, + xcb_gcontext_t gc, + uint8_t depth); + +/* Upload a full frame of 32-bit pixels in horizontal stripes. */ +static inline void showcasePutImage(xcb_connection_t *connection, + xcb_window_t window, + xcb_gcontext_t gc, + uint8_t depth, + const uint32_t *pixels) +{ + for (unsigned int y = 0; y < SHOWCASE_HEIGHT; y += SHOWCASE_STRIPE_HEIGHT) { + unsigned int rows = SHOWCASE_HEIGHT - y < SHOWCASE_STRIPE_HEIGHT + ? SHOWCASE_HEIGHT - y + : SHOWCASE_STRIPE_HEIGHT; + xcb_put_image( + connection, XCB_IMAGE_FORMAT_Z_PIXMAP, window, gc, SHOWCASE_WIDTH, + rows, 0, y, 0, depth, + (uint32_t) ((size_t) SHOWCASE_WIDTH * rows * sizeof(*pixels)), + (const uint8_t *) (pixels + (size_t) y * SHOWCASE_WIDTH)); + } +} + +/* Drain the queue after a smoke run. The drawing requests are unchecked, so a + * run that failed every one of them would otherwise still exit successfully. + */ +static inline int showcaseDrainErrors(xcb_connection_t *connection, + const char *name) +{ + xcb_flush(connection); + xcb_generic_event_t *queued; + while ((queued = xcb_poll_for_event(connection))) { + int failed = (queued->response_type & 0x7f) == 0; + uint8_t code = ((xcb_generic_error_t *) queued)->error_code; + free(queued); + if (failed) { + fprintf(stderr, "%s: request failed (%u)\n", name, code); + return 1; + } + } + return 0; +} + +static inline int showcaseRun(int argc, + char **argv, + const char *name, + ShowcasePaint paint) +{ + int smoke = argc == 2 && strcmp(argv[1], "--smoke") == 0; + xcb_connection_t *connection = xcb_connect(NULL, NULL); + if (!connection || xcb_connection_has_error(connection)) { + fprintf(stderr, "%s: cannot connect\n", name); + xcb_disconnect(connection); + return 1; + } + xcb_screen_t *screen = + xcb_setup_roots_iterator(xcb_get_setup(connection)).data; + + /* The pixel buffers are packed 32 bits per pixel, so say so rather than + * uploading a mis-strided image on a screen that wants something else. + */ + if (screen->root_depth != 24 && screen->root_depth != 32) { + fprintf(stderr, "%s: needs a 24 or 32 bit screen, got %u\n", name, + screen->root_depth); + xcb_disconnect(connection); + return 1; + } + xcb_window_t window = xcb_generate_id(connection); + uint32_t windowValues[] = { + screen->black_pixel, + XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS}; + xcb_generic_error_t *error = xcb_request_check( + connection, xcb_create_window_checked( + connection, screen->root_depth, window, screen->root, 0, + 0, SHOWCASE_WIDTH, SHOWCASE_HEIGHT, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, + XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK, windowValues)); + if (error) { + fprintf(stderr, "%s: create failed (%u)\n", name, error->error_code); + free(error); + xcb_disconnect(connection); + return 1; + } + xcb_gcontext_t gc = xcb_generate_id(connection); + xcb_create_gc(connection, gc, window, 0, NULL); + xcb_map_window(connection, window); + if (!paint(connection, window, gc, screen->root_depth)) { + fprintf(stderr, "%s: image allocation failed\n", name); + xcb_disconnect(connection); + return 1; + } + + int status = smoke ? showcaseDrainErrors(connection, name) : 0; + while (!smoke) { + xcb_generic_event_t *event = xcb_wait_for_event(connection); + if (!event) { + fprintf(stderr, "%s: connection closed while waiting\n", name); + status = 1; + break; + } + if ((event->response_type & 0x7f) == XCB_KEY_PRESS) { + free(event); + break; + } + if ((event->response_type & 0x7f) == XCB_EXPOSE && + !paint(connection, window, gc, screen->root_depth)) { + fprintf(stderr, "%s: repaint failed\n", name); + free(event); + status = 1; + break; + } + free(event); + } + xcb_free_gc(connection, gc); + xcb_destroy_window(connection, window); + xcb_disconnect(connection); + return status; +} + +#endif diff --git a/mk/examples.mk b/mk/examples.mk index f05ea02d..4154b790 100644 --- a/mk/examples.mk +++ b/mk/examples.mk @@ -1,6 +1,11 @@ EXAMPLE_NAMES := 2048 paint life clock mandel processing clipboard catclock \ moire EXAMPLE_BINS := $(addprefix $(OUT)/examples/,$(EXAMPLE_NAMES)) +ifeq ($(XCB),1) + XCB_SHOWCASE_NAMES := xcb-kaleidoscope xcb-mandelbrot + XCB_SHOWCASE_BINS := $(addprefix $(OUT)/examples/,$(XCB_SHOWCASE_NAMES)) + EXAMPLE_BINS += $(XCB_SHOWCASE_BINS) +endif X11PERF_DIR := examples/x11perf X11PERF_SRCS := \ $(X11PERF_DIR)/bitmaps.c \ @@ -30,11 +35,20 @@ ifeq ($(UNAME_S),Darwin) EXAMPLE_LDFLAGS += -Wl,-rpath,$(abspath $(OUT)) endif -.PHONY: examples bench-x11perf +.PHONY: examples check-xcb-showcases bench-x11perf -## Build the bundled Xlib client examples (linked against libX11-compat.so) +## Build the bundled example clients (add XCB=1 for the native XCB ones) examples: $(EXAMPLE_BINS) $(X11PERF_BIN) +## Run the XCB showcases headless under SDL's dummy driver (needs XCB=1) +check-xcb-showcases: $(XCB_SHOWCASE_BINS) + @test "$(XCB)" = 1 || \ + { echo "Error: check-xcb-showcases needs XCB=1" >&2; exit 1; } + @set -e; for showcase in $(XCB_SHOWCASE_BINS); do \ + echo " RUN $$showcase --smoke"; \ + SDL_VIDEODRIVER=dummy $$showcase --smoke; \ + done + ## Run the bundled x11perf benchmark in short regression mode bench-x11perf: $(X11PERF_BIN) SDL_VIDEODRIVER=dummy $(X11PERF_BIN) $(X11PERF_BENCH_ARGS) @@ -45,6 +59,14 @@ $(OUT)/examples/%: examples/%.c $(TARGET) $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(CFLAGS_EXTRA) $< $(TARGET) \ $(LDLIBS) $(EXAMPLE_LDFLAGS) -o $@ +ifeq ($(XCB),1) +$(XCB_SHOWCASE_BINS): $(OUT)/examples/%: examples/%.c $(XCB_COMPAT_TARGET) $(TARGET) + @mkdir -p $(dir $@) + @echo " CC $<" + $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(CFLAGS_EXTRA) $< \ + $(XCB_COMPAT_TARGET) $(TARGET) $(LDLIBS) $(EXAMPLE_LDFLAGS) -o $@ +endif + $(X11PERF_BIN): $(X11PERF_SRCS) $(TARGET) @mkdir -p $(dir $@) @echo " CC examples/x11perf" From eea742ab11ba7b0bb840fe8267f44b781090b656 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 31 Aug 2026 23:55:13 +0800 Subject: [PATCH 6/6] Consolidate and harden the wasm paint check Three WebKitGTK paint jobs failed once each on a single pull request and passed on re-run with no code change: two with "driver request timeout" and one with a blank first frame. The harness had no tolerance for a loaded runner. Every driver request carried a hardcoded five second budget, and a run that never got a frame failed outright. Give the per-request budget its own knob at fifteen seconds, and retry a failed attempt once. Only a transient outcome is retried: losing the driver, or never painting at all, says nothing about the build. A run that painted and then failed an input, layout or visual assertion is a real result and is reported on the first attempt, so this cannot mask a regression. The four app recipes ran the same node smoke plus paint check with slightly different wrappers, and had drifted: xnedit skipped the paint check silently when node was missing rather than honoring WASM_PAINT_REQUIRED, so a runner without node passed it while the other three failed. One macro now covers all four, and xnedit gains the gate. --- mk/wasm-deps.mk | 21 +++++++++++ mk/wasm-mosaic.mk | 9 +---- mk/wasm-xcircuit.mk | 10 +----- mk/wasm-xnedit.mk | 4 +-- mk/wasm-xwpe.mk | 9 +---- scripts/wasm-paint-check.mjs | 68 +++++++++++++++++++++++++++++------- 6 files changed, 80 insertions(+), 41 deletions(-) diff --git a/mk/wasm-deps.mk b/mk/wasm-deps.mk index 27973362..abbc08fa 100644 --- a/mk/wasm-deps.mk +++ b/mk/wasm-deps.mk @@ -439,6 +439,27 @@ check-wasm: # the result is a symbol-complete wasm module. Separate from check-wasm so the # cheap example smoke does not pull in the libXt build. Skips without emcc. .PHONY: check-wasm-libxt + +# The four wasm app checks all end the same way: smoke the module under node, +# then drive it through scripts/wasm-paint-check.mjs. Keeping that in one place +# stops the four copies drifting; xnedit had already lost the +# WASM_PAINT_REQUIRED gate, so a CI host without node skipped its paint check +# silently while the others failed. +# $(1) app basename, $(2) non-empty to also run the node module smoke +# The body ends without a continuation so endef stays visible to make; call +# sites that continue a shell line add their own trailing backslash. +define wasm_paint_check + if command -v node >/dev/null 2>&1; then \ + $(if $(2),node scripts/wasm-node-smoke.mjs $(OUT)/$(1).wasm || exit 1; \ + ,)node scripts/wasm-paint-check.mjs $(OUT) $(1) || exit 1; \ + elif [ "$${WASM_PAINT_REQUIRED:-0}" = 1 ]; then \ + echo "check-wasm-$(1): node missing for required paint check" >&2; \ + exit 1; \ + else \ + echo " WARN check-wasm-$(1): node missing, skipped paint check" >&2; \ + fi; +endef + ## Link-smoke the wasm libXt archive (skips when emcc is unavailable) check-wasm-libxt: $(Q)if ! command -v emcc >/dev/null 2>&1; then \ diff --git a/mk/wasm-mosaic.mk b/mk/wasm-mosaic.mk index 478ef9d3..ee0c7819 100644 --- a/mk/wasm-mosaic.mk +++ b/mk/wasm-mosaic.mk @@ -182,12 +182,5 @@ check-wasm-mosaic: done; \ case `file -b $(OUT)/mosaic.wasm` in *WebAssembly*) ;; \ *) echo "check-wasm-mosaic: not a wasm module" >&2; exit 1;; esac; \ - if command -v node >/dev/null 2>&1; then \ - node scripts/wasm-node-smoke.mjs $(OUT)/mosaic.wasm || exit 1; \ - node scripts/wasm-paint-check.mjs $(OUT) mosaic || exit 1; \ - else \ - if [ "$${WASM_PAINT_REQUIRED:-0}" = 1 ]; then \ - echo "check-wasm-mosaic: node missing for required paint check" >&2; exit 1; fi; \ - echo " WARN check-wasm-mosaic: node missing, skipped module smoke" >&2; \ - fi; \ + $(call wasm_paint_check,mosaic,smoke) \ echo " OK check-wasm-mosaic (Mosaic cross-compiled to a wasm module)" diff --git a/mk/wasm-xcircuit.mk b/mk/wasm-xcircuit.mk index 1e6e0a5e..94ae8bd8 100644 --- a/mk/wasm-xcircuit.mk +++ b/mk/wasm-xcircuit.mk @@ -244,13 +244,5 @@ check-wasm-xcircuit: *WebAssembly*) ;; \ *) echo "check-wasm-xcircuit: not a wasm module" >&2; exit 1;; \ esac; \ - if command -v node >/dev/null 2>&1; then \ - node scripts/wasm-node-smoke.mjs $(OUT)/xcircuit.wasm || exit 1; \ - node scripts/wasm-paint-check.mjs $(OUT) xcircuit || exit 1; \ - else \ - if [ "$${WASM_PAINT_REQUIRED:-0}" = 1 ]; then \ - echo "check-wasm-xcircuit: node missing for required paint check" >&2; exit 1; \ - fi; \ - echo " WARN check-wasm-xcircuit: node missing, skipped module smoke" >&2; \ - fi; \ + $(call wasm_paint_check,xcircuit,smoke) \ echo " OK check-wasm-xcircuit (xcircuit cross-compiled to a wasm module)" diff --git a/mk/wasm-xnedit.mk b/mk/wasm-xnedit.mk index fd829fb2..743c2d51 100644 --- a/mk/wasm-xnedit.mk +++ b/mk/wasm-xnedit.mk @@ -174,7 +174,5 @@ check-wasm-xnedit: *WebAssembly*) ;; \ *) echo "check-wasm-xnedit: not a wasm module" >&2; exit 1;; \ esac; \ - if command -v node >/dev/null 2>&1; then \ - node scripts/wasm-paint-check.mjs $(OUT) xnedit || exit 1; \ - fi; \ + $(call wasm_paint_check,xnedit) \ echo " OK check-wasm-xnedit (xnedit cross-compiled to a wasm module)" diff --git a/mk/wasm-xwpe.mk b/mk/wasm-xwpe.mk index 6e6ca486..4c42b3ab 100644 --- a/mk/wasm-xwpe.mk +++ b/mk/wasm-xwpe.mk @@ -173,12 +173,5 @@ check-wasm-xwpe: done; \ case `file -b $(OUT)/xwpe.wasm` in *WebAssembly*) ;; \ *) echo "check-wasm-xwpe: not a wasm module" >&2; exit 1;; esac; \ - if command -v node >/dev/null 2>&1; then \ - node scripts/wasm-node-smoke.mjs $(OUT)/xwpe.wasm || exit 1; \ - node scripts/wasm-paint-check.mjs $(OUT) xwpe || exit 1; \ - else \ - if [ "$${WASM_PAINT_REQUIRED:-0}" = 1 ]; then \ - echo "check-wasm-xwpe: node missing for required paint check" >&2; exit 1; fi; \ - echo " WARN check-wasm-xwpe: node missing, skipped module smoke" >&2; \ - fi; \ + $(call wasm_paint_check,xwpe,smoke) \ echo " OK check-wasm-xwpe (xwpe cross-compiled to a wasm module)" diff --git a/scripts/wasm-paint-check.mjs b/scripts/wasm-paint-check.mjs index 70c671d8..aade63cc 100644 --- a/scripts/wasm-paint-check.mjs +++ b/scripts/wasm-paint-check.mjs @@ -23,7 +23,9 @@ // .js and, if present, .data must sit in build-dir. // Env: WASM_PAINT_TIMEOUT_MS (default 20000), WASM_PAINT_CALIBRATE=1 (report // only, never fail), WASM_PAINT_STRICT_INPUT=0 (downgrade xwpe input -// failure to a warning), WASM_PAINT_WEBDRIVER (driver binary path). +// failure to a warning), WASM_PAINT_WEBDRIVER (driver binary path), +// WASM_PAINT_REQUEST_TIMEOUT_MS (default 15000, per driver request), +// WASM_PAINT_ATTEMPTS (default 2, retries of a transient failure). import { createServer } from "node:http"; import { readFile, access } from "node:fs/promises"; @@ -38,7 +40,22 @@ if (!buildDir || appArgs.length === 0) { process.exit(2); } const apps = appArgs.map((a) => a.replace(/\.(html|js)$/, "")); -const TIMEOUT_MS = Number(process.env.WASM_PAINT_TIMEOUT_MS || 20000); +// A malformed budget must not silently disable a check or spin forever, so +// anything that is not a finite positive integer falls back to the documented +// default rather than propagating NaN or Infinity into a loop bound. +function positiveInt(value, fallback) { + const parsed = Number(value); + return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; +} +const TIMEOUT_MS = positiveInt(process.env.WASM_PAINT_TIMEOUT_MS, 20000); +// A probe or action that outlives this is treated as a stuck driver. The +// budget is generous because a loaded CI runner can stall a WebKit request for +// several seconds without anything being wrong. +const REQUEST_TIMEOUT_MS = + positiveInt(process.env.WASM_PAINT_REQUEST_TIMEOUT_MS, 15000); +// Retries cover only what a loaded runner can do before anything renders. Once +// a frame exists, every later outcome is a real result: see the verdict below. +const ATTEMPTS = positiveInt(process.env.WASM_PAINT_ATTEMPTS, 2); const CALIBRATE = process.env.WASM_PAINT_CALIBRATE === "1"; const KEEP_BROWSER = process.env.WASM_PAINT_KEEP_BROWSER === "1"; const STRICT_INPUT = process.env.WASM_PAINT_STRICT_INPUT !== "0"; @@ -316,7 +333,8 @@ function drvReq(method, path, body, timeoutMs) { // A driver that accepts the socket then hangs must not stall the whole run. // Navigation blocks until the page load event, so it gets the paint budget; // ordinary probe/action calls keep the short default. - req.setTimeout(timeoutMs || 5000, () => req.destroy(new Error("driver request timeout"))); + req.setTimeout(timeoutMs || REQUEST_TIMEOUT_MS, + () => req.destroy(new Error("driver request timeout"))); req.on("error", reject); req.end(data); }); @@ -376,6 +394,10 @@ for (const name of apps) { continue; } + let attempt = 0; + let transient = ""; + for (;;) { + attempt++; let sessionId = null; let best = { found: false, w: 0, h: 0, nonFirstPixels: -1 }; let launchErr = null; @@ -541,18 +563,38 @@ for (const name of apps) { const painted = best.w > 0 && best.h > 0 && best.nonFirstPixels > 16; const detail = `canvas=${best.w}x${best.h} nonFirstPixels=${best.nonFirstPixels}`; const inputOk = inputChanged && (!inputErr || !STRICT_INPUT); - if (painted && inputOk && !layoutErr && !visualErr) { - console.log(` OK ${name} (${detail}${inputDetail})`); + if (painted && inputOk && !layoutErr && !visualErr && !launchErr) { + console.log(` OK ${name} (${detail}${inputDetail}${ + attempt > 1 ? ` attempt=${attempt}` : ""})`); if (inputErr && !STRICT_INPUT) console.error(` WARN input: ${inputErr}`); - } else { - console.error(` FAIL ${name} (${detail})`); - if (best.err) console.error(` printErr: ${String(best.err).slice(0, 200)}`); - if (inputErr) console.error(` input: ${inputErr}`); - if (layoutErr) console.error(` layout: ${layoutErr}`); - if (visualErr) console.error(` visual: ${visualErr}`); - if (launchErr) console.error(` driver: ${String(launchErr).slice(0, 200)}`); - failed++; + break; + } + + /* Retry only a run that never produced a frame: that is what a loaded runner + * does to a healthy build. Once the canvas has painted, every later outcome + * is a real result and is reported on this attempt, including a driver + * request that hung during an input action. Keying this on the error alone + * would let a post-paint hang be retried away by a luckier attempt. + */ + transient = painted ? "" + : launchErr ? `driver: ${String(launchErr).slice(0, 120)}` + : `no frame (${detail})`; + if (transient && attempt < ATTEMPTS) { + console.error(` RETRY ${name} (${transient}; attempt ${attempt}/${ATTEMPTS})`); + await sleep(1000); + continue; + } + + console.error(` FAIL ${name} (${detail}${ + attempt > 1 ? ` after ${attempt} attempts` : ""})`); + if (best.err) console.error(` printErr: ${String(best.err).slice(0, 200)}`); + if (inputErr) console.error(` input: ${inputErr}`); + if (layoutErr) console.error(` layout: ${layoutErr}`); + if (visualErr) console.error(` visual: ${visualErr}`); + if (launchErr) console.error(` driver: ${String(launchErr).slice(0, 200)}`); + failed++; + break; } }