diff --git a/Makefile b/Makefile index b2a1a6e..9e7c623 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ include mk/config.mk include mk/sources.mk include mk/common.mk include mk/library.mk +include mk/libxt.mk include mk/tests.mk include mk/examples.mk include mk/upstream-headers.mk diff --git a/include/X11/ICE/ICElib.h b/include/X11/ICE/ICElib.h new file mode 100644 index 0000000..8c274f5 --- /dev/null +++ b/include/X11/ICE/ICElib.h @@ -0,0 +1,31 @@ +/* Stub X11/ICE/ICElib.h for libx11-compat's libXt build + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ + +/* libXt's Shell.c includes this header at the top of the file even when + * XT_NO_SM keeps every ICE/SM call out of the compiled translation unit. + * Provide an opaque IceConn type plus the small set of helpers and + * constants the in-tree compatibility surface references so the header + * resolves without dragging libICE into the build closure. + */ +#ifndef _LIBX11_COMPAT_ICELIB_H_ +#define _LIBX11_COMPAT_ICELIB_H_ + +typedef struct _IceConn *IceConn; + +/* Real libICE defines IceProcessMessagesStatus as an enum too, but the + * values themselves are commonly probed via macros in some downstream + * code. Mirror with #ifndef so any later #include of the real header + * (e.g. through a Motif build pulling system X11 headers) does not + * collide on the enum tag. */ +#ifndef IceProcessMessagesSuccess +typedef enum { + IceProcessMessagesSuccess = 0, + IceProcessMessagesIOError = 1, + IceProcessMessagesConnectionClosed = 2 +} IceProcessMessagesStatus; +#endif + +#endif /* _LIBX11_COMPAT_ICELIB_H_ */ diff --git a/include/X11/SM/SMlib.h b/include/X11/SM/SMlib.h new file mode 100644 index 0000000..ac425b4 --- /dev/null +++ b/include/X11/SM/SMlib.h @@ -0,0 +1,115 @@ +/* Stub X11/SM/SMlib.h for libx11-compat's libXt build + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ + +/* libXt's generated Shell.h unconditionally includes this header even + * when session management is compiled out via XT_NO_SM. ShellP.h also + * declares a SmcConn slot in the WidgetClass record so the struct layout + * stays binary-compatible. libx11-compat ships no libSM and runs in a + * single process with no XSMP peer, so the type lives only as an opaque + * pointer here and the few constants that downstream code probes are + * synthesized as enums. + */ +#ifndef _LIBX11_COMPAT_SMLIB_H_ +#define _LIBX11_COMPAT_SMLIB_H_ + +#include +#include + +typedef struct _SmcConn *SmcConn; +typedef struct _SmsConn *SmsConn; +typedef void *SmPointer; + +typedef struct { + int length; + SmPointer value; +} SmPropValue; + +typedef struct { + char *name; + char *type; + int num_vals; + SmPropValue *vals; +} SmProp; + +typedef int SmcCallbacks; + +/* Real libSM ships these as #define macros, so we mirror the same shape + * with #ifndef guards. Using `enum { SmProtoMajor = 1, ... }` here would + * conflict at preprocess time if any consumer also pulled in the system + * , because its `#define SmProtoMajor 1` would expand + * inside our enum declaration to `1 = 1`. */ +#ifndef SmProtoMajor +#define SmProtoMajor 1 +#endif +#ifndef SmProtoMinor +#define SmProtoMinor 0 +#endif +#ifndef SmDialogError +#define SmDialogError 0 +#endif +#ifndef SmDialogNormal +#define SmDialogNormal 1 +#endif +#ifndef SmInteractStyleNone +#define SmInteractStyleNone 0 +#endif +#ifndef SmInteractStyleErrors +#define SmInteractStyleErrors 1 +#endif +#ifndef SmInteractStyleAny +#define SmInteractStyleAny 2 +#endif +#ifndef SmSaveGlobal +#define SmSaveGlobal 0 +#endif +#ifndef SmSaveLocal +#define SmSaveLocal 1 +#endif +#ifndef SmSaveBoth +#define SmSaveBoth 2 +#endif +#ifndef SmRestartIfRunning +#define SmRestartIfRunning 0 +#endif +#ifndef SmRestartAnyway +#define SmRestartAnyway 1 +#endif +#ifndef SmRestartImmediately +#define SmRestartImmediately 2 +#endif +#ifndef SmRestartNever +#define SmRestartNever 3 +#endif +#ifndef SmcSaveYourselfProcMask +#define SmcSaveYourselfProcMask 1 +#endif +#ifndef SmcDieProcMask +#define SmcDieProcMask 2 +#endif +#ifndef SmcSaveCompleteProcMask +#define SmcSaveCompleteProcMask 4 +#endif +#ifndef SmcShutdownCancelledProcMask +#define SmcShutdownCancelledProcMask 8 +#endif + +#define SmCloneCommand "CloneCommand" +#define SmCurrentDirectory "CurrentDirectory" +#define SmDiscardCommand "DiscardCommand" +#define SmEnvironment "Environment" +#define SmProcessID "ProcessID" +#define SmProgram "Program" +#define SmResignCommand "ResignCommand" +#define SmRestartCommand "RestartCommand" +#define SmRestartStyleHint "RestartStyleHint" +#define SmShutdownCommand "ShutdownCommand" +#define SmUserID "UserID" + +#define SmARRAY8 "ARRAY8" +#define SmCARD8 "CARD8" +#define SmLISTofARRAY8 "LISTofARRAY8" + +#endif /* _LIBX11_COMPAT_SMLIB_H_ */ diff --git a/include/X11/Xmu/Editres.h b/include/X11/Xmu/Editres.h new file mode 100644 index 0000000..fb18bcd --- /dev/null +++ b/include/X11/Xmu/Editres.h @@ -0,0 +1,35 @@ +/* Stub Xmu/Editres.h for libx11-compat's libXt build + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ + +/* The real Xmu/Editres.h declares the editres-protocol hook that libXt's + * Shell.c registers so an external editres client can introspect a live + * widget tree. libx11-compat ships no Xmu, no editres client targets this + * in-process display, and Shell.c's only use is the function-pointer + * registration. A no-op _XEditResCheckMessages keeps Shell.c compilable + * and link-safe without dragging libXmu into the build. + */ +#ifndef _XMU_EDITRES_H_ +#define _XMU_EDITRES_H_ + +#include + +#define EDITRES_NAME "Editres" +#define EDITRES_COMMAND_ATOM "Comm" +#define EDITRES_PROTOCOL_ATOM "EditresProtocol" +#define EDITRES_VERSION 5 + +static inline void _XEditResCheckMessages(Widget widget, + XtPointer data, + XEvent *event, + Boolean *cont) +{ + (void) widget; + (void) data; + (void) event; + (void) cont; +} + +#endif /* _XMU_EDITRES_H_ */ diff --git a/include/libxt-build/config.h b/include/libxt-build/config.h new file mode 100644 index 0000000..209990c --- /dev/null +++ b/include/libxt-build/config.h @@ -0,0 +1,55 @@ +/* Synthesized config.h for the libXt build inside libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ + +/* libXt expects an autoconf-generated config.h to define a handful of + * platform knobs. We are not running autoconf, so the knobs are pinned + * to values that match the modern macOS / Linux environments libx11-compat + * targets: poll(2) is always available, threads are enabled, and the + * session-management surface is disabled so libICE/libSM stay out of the + * dependency closure (see mk/libxt.mk). + * + * This header lives under include/libxt-build/ rather than the X11/ + * namespace because the basename ``config.h'' is too generic to share an + * include path with anything else. mk/libxt.mk adds the parent directory + * to the include path *only* for the libXt translation units. + */ + +#ifndef LIBX11_COMPAT_LIBXT_CONFIG_H +#define LIBX11_COMPAT_LIBXT_CONFIG_H + +#define PACKAGE_NAME "libXt" +#define PACKAGE_VERSION "1.3.1" +#define PACKAGE_STRING "libXt 1.3.1" +#define PACKAGE_BUGREPORT "https://github.com/sysprog21/libx11-compat/issues" +#define VERSION PACKAGE_VERSION + +/* poll() is on every libx11-compat-supported platform. The select() fallback + * path in NextEvent.c is bit-set-sized and predates large fd numbers; we + * never want it. */ +#define USE_POLL 1 + +/* Multi-threaded toolkit. Wires Threads.c's mutex hooks into the XtAppLock + * machinery so XtAppContexts can be shared across threads. */ +#define XTHREADS 1 + +/* Disable session-management hooks in Shell.c so libICE / libSM stay out + * of the link line. SDL has no notion of XSMP and the in-process display + * has no peer to negotiate with. */ +#define XT_NO_SM 1 + +/* HAVE_REALLOCARRAY left undefined deliberately. reallocarray() exists in + * Apple's libSystem and glibc >= 2.26, but the macOS SDK does not declare + * it in even with _DARWIN_C_SOURCE, so the implicit-declaration + * error kills the build before the linker would have found it. libXt's + * Alloc.c falls back to Xrealloc + an explicit overflow check when this + * macro is absent, which costs nothing in performance and works on every + * supported host. */ + +/* asprintf is on every glibc and Apple libc. Initialize.c uses it for + * defaults-file path assembly. */ +#define HAVE_ASPRINTF 1 + +#endif /* LIBX11_COMPAT_LIBXT_CONFIG_H */ diff --git a/mk/libxt.mk b/mk/libxt.mk new file mode 100644 index 0000000..ad81098 --- /dev/null +++ b/mk/libxt.mk @@ -0,0 +1,190 @@ +# Build libXt-compat.so from the pinned libXt-1.3.1 source tree staged +# under $(OUT)/upstream/src-libXt/ (see scripts/sync-upstream-headers.py). +# +# libXt is a pure Xlib client: every symbol it calls outside libc reaches +# libX11-compat.so. Compile its 53 .c files against the staged upstream +# headers + a hand-synthesized config.h, generate StringDefs.c from +# string.list via the upstream makestrs helper compiled as a host tool, +# and link the result as build/libXt-compat.so. +# +# The build is intentionally always-on. The dependency closure adds no +# new system packages -- only header staging and a host-side helper that +# is rebuilt from upstream source on every fetch. + +LIBXT_SRC_DIR := $(OUT)/upstream/src-libXt +LIBXT_TOPDIR := $(OUT)/upstream/topdir-libXt +LIBXT_UTIL_DIR := $(LIBXT_TOPDIR)/util +LIBXT_GEN_DIR := $(OUT)/libxt-gen +LIBXT_OBJ_DIR := $(OUT)/libxt +LIBXT_HOST_DIR := $(OUT)/host +LIBXT_TARGET := $(OUT)/libXt-compat.so + +# mk/libxt.mk is included before mk/upstream-headers.mk, but its rules need +# this stamp while make parses prerequisites. Keep the value aligned with the +# authoritative upstream fragment. +UPSTREAM_HEADERS_DIR ?= $(OUT)/upstream/include +UPSTREAM_HEADERS_STAMP ?= $(UPSTREAM_HEADERS_DIR)/.upstream-stamp + +LIBXT_HOST_MAKESTRS := $(LIBXT_HOST_DIR)/makestrs +LIBXT_STRING_LIST := $(LIBXT_UTIL_DIR)/string.list +LIBXT_TEMPLATES := \ + $(LIBXT_UTIL_DIR)/StrDefs.ct \ + $(LIBXT_UTIL_DIR)/StrDefs.ht \ + $(LIBXT_UTIL_DIR)/Shell.ht + +LIBXT_GEN_STAMP := $(LIBXT_GEN_DIR)/.stringdefs-stamp +LIBXT_GEN_C := $(LIBXT_GEN_DIR)/StringDefs.c +LIBXT_GEN_HEADERS := $(LIBXT_GEN_DIR)/StringDefs.h $(LIBXT_GEN_DIR)/Shell.h +LIBXT_STAGED_H := \ + $(OUT)/upstream/include/X11/StringDefs.h \ + $(OUT)/upstream/include/X11/Shell.h + +# Enumerated rather than wildcarded: $(wildcard) evaluates at parse time, +# before the first sync has created the directory. The list comes from the +# libXt-1.3.1 src/ tree (53 files). Keep alphabetized so version bumps that +# add or remove a file produce a small, readable diff. +LIBXT_SRC_BASES := \ + ActionHook.c Alloc.c ArgList.c Callback.c ClickTime.c Composite.c \ + Constraint.c Convert.c Converters.c Core.c Create.c Destroy.c \ + Display.c Error.c Event.c EventUtil.c Functions.c GCManager.c \ + Geometry.c GetActKey.c GetResList.c GetValues.c HookObj.c Hooks.c \ + Initialize.c Intrinsic.c Keyboard.c Manage.c NextEvent.c Object.c \ + PassivGrab.c Pointer.c Popup.c PopupCB.c RectObj.c ResConfig.c \ + Resources.c Selection.c SetSens.c SetValues.c SetWMCW.c Shell.c \ + TMaction.c TMgrab.c TMkey.c TMparse.c TMprint.c TMstate.c Threads.c \ + VarCreate.c VarGet.c Varargs.c Vendor.c +LIBXT_SRCS := $(addprefix $(LIBXT_SRC_DIR)/,$(LIBXT_SRC_BASES)) +LIBXT_OBJS := $(patsubst $(LIBXT_SRC_DIR)/%.c,$(LIBXT_OBJ_DIR)/%.o,$(LIBXT_SRCS)) \ + $(LIBXT_OBJ_DIR)/StringDefs.o + +# libXt include path layering: +# include/libxt-build - hand-synthesized config.h (libXt only) +# $(LIBXT_GEN_DIR) - generated StringDefs.h / Shell.h (quoted form) +# $(OUT)/upstream/... - libX11 + xorgproto + libXt headers (already +# covered by the global -iquote setup in +# mk/config.mk, repeated here for clarity) +# include - tracked headers (X11/Xmu/Editres.h stub) +LIBXT_CPPFLAGS := \ + -DHAVE_CONFIG_H -DLIBXT_COMPILATION \ + -DXFILESEARCHPATHDEFAULT='"/usr/share/X11/app-defaults/%N%C"' \ + -DERRORDB='"/usr/share/X11/XtErrorDB"' \ + -Iinclude/libxt-build \ + -I$(LIBXT_GEN_DIR) \ + -I$(OUT)/upstream/include \ + -Iinclude \ + -iquote $(LIBXT_GEN_DIR) \ + -iquote include/X11 \ + -iquote $(OUT)/upstream/include/X11 \ + $(if $(SDL2_PREFIX),-I$(SDL2_PREFIX)/include) \ + -D_GNU_SOURCE -D_DARWIN_C_SOURCE + +# Upstream libXt was written before -Wall / strict-prototypes were common, +# so silence the noise specific to that tree without weakening our own +# warning set on libx11-compat sources. -Wno-incompatible-pointer-types- +# discards-qualifiers is the clang spelling for the gcc-only +# -Wno-discarded-qualifiers and covers the cases where libXt drops a +# `const` while assigning to a non-const callback argument. +LIBXT_CFLAGS := -std=c99 -Wall -fPIC \ + -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function \ + -Wno-missing-braces -Wno-sign-compare -Wno-deprecated-declarations \ + -Wno-incompatible-pointer-types-discards-qualifiers \ + -Wno-incompatible-pointer-types + +# Host compiler for makestrs. We do not cross-compile today, so reuse CC. +HOST_CC ?= $(CC) + +$(LIBXT_HOST_DIR) $(LIBXT_GEN_DIR) $(LIBXT_OBJ_DIR): + @mkdir -p $@ + +# Compile makestrs as a host-side tool. It is a single-file standalone +# generator that depends only on libc; no need for any libx11-compat +# include paths. +$(LIBXT_HOST_MAKESTRS): $(UPSTREAM_HEADERS_STAMP) | $(LIBXT_HOST_DIR) + @echo " HOSTCC $(LIBXT_UTIL_DIR)/makestrs.c" + $(Q)$(HOST_CC) -O2 -o $@ $(LIBXT_UTIL_DIR)/makestrs.c + +# The upstream sync stages libXt sources and util files as side effects. +# Declare them so a clean build knows how to fetch these normal +# prerequisites before trying to build libXt objects or the host generator. +$(LIBXT_SRCS) $(LIBXT_UTIL_DIR)/makestrs.c $(LIBXT_STRING_LIST) $(LIBXT_TEMPLATES): $(UPSTREAM_HEADERS_STAMP) + +# Run makestrs from the topdir so the "util/StrDefs.ct" / "util/StrDefs.ht" +# paths embedded in string.list resolve against cwd. The generated headers +# land in cwd alongside StringDefs.c, then move to LIBXT_GEN_DIR. GNU Make +# 3.81 does not support grouped targets, so a portable stamp ensures the +# multi-output generator runs once under parallel make. +$(LIBXT_GEN_STAMP): $(LIBXT_HOST_MAKESTRS) $(UPSTREAM_HEADERS_STAMP) | $(LIBXT_GEN_DIR) + @echo " GEN StringDefs.{c,h} Shell.h" + $(Q)rm -f $(LIBXT_GEN_C) $(LIBXT_GEN_HEADERS) + $(Q)cd $(LIBXT_TOPDIR) && \ + $(abspath $(LIBXT_HOST_MAKESTRS)) < util/string.list \ + > $(abspath $(LIBXT_GEN_C)) + $(Q)mv $(LIBXT_TOPDIR)/StringDefs.h $(LIBXT_GEN_DIR)/StringDefs.h + $(Q)mv $(LIBXT_TOPDIR)/Shell.h $(LIBXT_GEN_DIR)/Shell.h + $(Q)touch $@ + +$(LIBXT_GEN_C) $(LIBXT_GEN_HEADERS): $(LIBXT_GEN_STAMP) + $(Q)test -f $@ || { \ + echo " ERROR $@ missing despite stamp present; remove $(LIBXT_GEN_STAMP) and retry"; \ + exit 1; \ + } + +# Stage StringDefs.h and Shell.h into the upstream X11/ include tree so +# consumers that say `#include ` (e.g. Motif's lib/Xm/*.c) +# resolve them through the standard include path, not just the libXt +# build's private generation dir. +$(OUT)/upstream/include/X11/StringDefs.h: $(LIBXT_GEN_DIR)/StringDefs.h + $(Q)cp $< $@ +$(OUT)/upstream/include/X11/Shell.h: $(LIBXT_GEN_DIR)/Shell.h + $(Q)cp $< $@ + +# Compile libXt translation units. The upstream stamp guarantees the .c +# files have been staged before the recipe reads them; the explicit +# dependency on LIBXT_GEN_HEADERS ensures StringDefs.h is available before +# any unit that quotes it compiles. +$(LIBXT_OBJ_DIR)/%.o: $(UPSTREAM_HEADERS_STAMP) $(LIBXT_GEN_HEADERS) $(LIBXT_STAGED_H) | \ + $(LIBXT_OBJ_DIR) + @echo " CC $(LIBXT_SRC_DIR)/$*.c" + $(Q)$(CC) $(LIBXT_CPPFLAGS) $(CFLAGS) $(LIBXT_CFLAGS) $(CFLAGS_EXTRA) \ + -MMD -MP -MF $(@:.o=.d) -c $(LIBXT_SRC_DIR)/$*.c -o $@ + +# StringDefs.c lives in $(LIBXT_GEN_DIR), not LIBXT_SRC_DIR. +$(LIBXT_OBJ_DIR)/StringDefs.o: $(LIBXT_GEN_C) $(LIBXT_GEN_HEADERS) $(LIBXT_STAGED_H) | \ + $(LIBXT_OBJ_DIR) $(UPSTREAM_HEADERS_STAMP) + @echo " CC $<" + $(Q)$(CC) $(LIBXT_CPPFLAGS) $(CFLAGS) $(LIBXT_CFLAGS) $(CFLAGS_EXTRA) \ + -MMD -MP -MF $(@:.o=.d) -c $< -o $@ + +# Link as a shared library. libXt's own dependency closure is just +# libX11-compat (which provides Xlib, Xrm, atoms, ...) and libc; SDL2 is +# pulled in transitively through libX11-compat.so. -lm and -pthread match +# what libX11-compat uses so XTHREADS-enabled mutex code links cleanly. +# +# rpath=$$ORIGIN tells the Linux dynamic linker to look in the same +# directory as libXt-compat.so for its DT_NEEDED entries -- without it, +# libXt-compat.so's reference to libX11-compat.so (recorded by basename +# from the -lX11-compat link) is unresolvable at runtime because the +# loader does not search the original link directory. macOS encodes a +# relative install_name (build/libX11-compat.so) so cwd-from-repo-root +# already works there; gate the flag on Linux only. +LIBXT_LDFLAGS := +ifeq ($(UNAME_S),Linux) + LIBXT_LDFLAGS += -Wl,-rpath,'$$ORIGIN' +endif + +$(LIBXT_TARGET): $(LIBXT_OBJS) $(TARGET) | $(OUT) + @echo " LD $@" + $(Q)$(CC) $(LDFLAGS) $(LIBXT_LDFLAGS) -shared -o $@ $(LIBXT_OBJS) \ + -L$(OUT) -lX11-compat -lm -pthread + +.PHONY: libxt +## Build the libXt compatibility shared library +libxt: $(LIBXT_TARGET) + +all: $(LIBXT_TARGET) + +# Include the per-object header-dependency files generated by -MMD above. +# Without this, edits to staged libXt headers or the synthesized +# config.h / StringDefs.h do not trigger rebuilds of the libXt .o files +# and incremental builds can ship stale objects. +-include $(LIBXT_OBJS:.o=.d) diff --git a/mk/tests.mk b/mk/tests.mk index e7eb77e..79ced36 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -1,4 +1,5 @@ -CHECK_BINS := $(OUT)/tests/check $(OUT)/tests/symbol-coverage +CHECK_BINS := $(OUT)/tests/check $(OUT)/tests/symbol-coverage \ + $(OUT)/tests/test-libxt-link $(OUT)/tests/test-libxt-micro BENCH_BINS := $(OUT)/tests/bench-paths .PHONY: check symbol-coverage api-symbol-coverage bench-paths @@ -22,6 +23,25 @@ api-symbol-coverage: $(TARGET) tests/api-symbols.txt tests/check-api-symbols.py bench-paths: $(BENCH_BINS) SDL_VIDEODRIVER=dummy $(OUT)/tests/bench-paths +# libXt-aware tests need both the upstream libXt headers (staged under +# $(OUT)/upstream/include) and a link line that pulls in libXt-compat.so +# *and* libX11-compat.so. They are compiled with the libxt-build config.h +# the same way the libXt translation units are, so XT_NO_SM stays defined +# and the public Xt headers see consistent feature flags. -rpath-link +# silences a Linux-only warning where ld walks libXt-compat.so's +# DT_NEEDED entries while linking the test and cannot find the sibling +# libX11-compat.so on a default -L path. +LIBXT_TEST_LDFLAGS := +ifeq ($(UNAME_S),Linux) + LIBXT_TEST_LDFLAGS += -Wl,-rpath-link,$(OUT) +endif + +$(OUT)/tests/test-libxt-%: tests/test-libxt-%.c $(LIBXT_TARGET) $(TARGET) + @mkdir -p $(dir $@) + @echo " CC $<" + $(Q)$(CC) $(LIBXT_CPPFLAGS) $(CFLAGS) $(CFLAGS_EXTRA) $< \ + $(LIBXT_TARGET) $(TARGET) $(LDLIBS) $(LIBXT_TEST_LDFLAGS) -o $@ + $(OUT)/tests/%: tests/%.c $(TARGET) @mkdir -p $(dir $@) @echo " CC $<" diff --git a/scripts/sync-upstream-headers.py b/scripts/sync-upstream-headers.py index a807009..e4b5a12 100644 --- a/scripts/sync-upstream-headers.py +++ b/scripts/sync-upstream-headers.py @@ -61,6 +61,28 @@ "url": "https://www.x.org/releases/individual/proto/xorgproto-2025.1.tar.xz", "sha256": "56898c716c0578df8a2d828c9c3e5c528277705c0484381a81960fe1a67668e8", }, + { + "name": "libXt", + "version": "libXt-1.3.1", + "url": "https://www.x.org/releases/individual/lib/libXt-1.3.1.tar.xz", + "sha256": "e0a774b33324f4d4c05b199ea45050f87206586d81655f8bef4dba434d931288", + # libXt's src/ goes to its own staging dir so it does not collide + # with the libX11 src/ slice (different headers, different build + # flags). All .c files in src/ are taken so the Makefile picks them + # up without a hand-maintained whitelist. + "src_subdir": "src-libXt", + "src_take_all": True, + # makestrs (compiled host-side) and string.list together regenerate + # StringDefs.c / StringDefs.h at build time. The .ct/.ht templates + # live alongside string.list and are consumed by makestrs. The + # nested ``util'' segment matches the path strings hard-coded in + # string.list (``#ctmpl util/StrDefs.ct'', etc.) so makestrs can + # find the templates with the topdir-libXt directory as its base. + "util_files": frozenset( + {"makestrs.c", "string.list", "StrDefs.ct", "StrDefs.ht", "Shell.ht"} + ), + "util_subdir": "topdir-libXt/util", + }, ] # Build-system noise that we never want to extract. @@ -150,6 +172,47 @@ "#define xmalloc(s) Xmalloc(s)\n#define xfree(s) Xfree(s)\n", ), ), + # libXt 1.3.1's SessionSetValues closes its `#ifndef XT_NO_SM` block + # halfway through the function, then keeps using the cw/nw widget + # casts declared inside that block. When we build with -DXT_NO_SM the + # declarations vanish and the tail-end SM_CLIENT_ID property update + # references undeclared identifiers. Extend the conditional to the + # function's `return False;` so the whole body becomes a no-op without + # session management. Anchors carry enough surrounding text (the + # StopManagingSession call and the strlen line that hands the session + # id to XChangeProperty) to remain unique within Shell.c. + "Shell.c": ( + ( + " StopManagingSession(nw, nw->session.connection);\n" + "#endif /* !XT_NO_SM */\n" + "\n" + " if (cw->wm.client_leader != nw->wm.client_leader ||\n", + " StopManagingSession(nw, nw->session.connection);\n" + "\n" + " if (cw->wm.client_leader != nw->wm.client_leader ||\n", + ), + ( + " (unsigned char *) nw->session.session_id,\n" + " (int) strlen(nw->session.session_id));\n" + " }\n" + " }\n" + " return False;\n" + "}\n" + "\n" + "void\n" + "_XtShellGetCoordinates(Widget widget, Position *x, Position *y)\n", + " (unsigned char *) nw->session.session_id,\n" + " (int) strlen(nw->session.session_id));\n" + " }\n" + " }\n" + "#endif /* !XT_NO_SM */\n" + " return False;\n" + "}\n" + "\n" + "void\n" + "_XtShellGetCoordinates(Widget widget, Position *x, Position *y)\n", + ), + ), } # Guard against typos when bumping versions: every URL must contain its tag. @@ -244,11 +307,14 @@ def relevant_member(member: tarfile.TarInfo) -> str | None: return rel -def relevant_src_member( - member: tarfile.TarInfo, whitelist: frozenset[str] -) -> str | None: - """Return the basename if ``member`` is a whitelisted libX11 src/ file.""" - if not member.isreg() or not whitelist: +def _direct_child_basename(member: tarfile.TarInfo, dir_name: str) -> str | None: + """Return the basename if ``member`` is a regular file at + ``//`` (no deeper nesting), else None. + + Rejects absolute paths and ``..`` segments so a malicious tar cannot + escape the staging directory. + """ + if not member.isreg(): return None posix = PurePosixPath(member.name) if posix.is_absolute(): @@ -256,12 +322,46 @@ def relevant_src_member( parts = posix.parts if not _is_safe(parts): return None - # Expected layout: /src/ (we only take direct children - # of src/ to avoid pulling in nested helper directories like src/xcms/). - if len(parts) != 3 or parts[1] != "src": + if len(parts) != 3 or parts[1] != dir_name: return None - base = parts[-1] - if base not in whitelist: + return parts[-1] + + +def relevant_src_member( + member: tarfile.TarInfo, + whitelist: frozenset[str], + take_all: bool = False, +) -> str | None: + """Return the basename if ``member`` is a wanted src/ file. + + Only direct children of src/ are returned, to avoid pulling in nested + helper directories like src/xcms/. When ``take_all`` is true (libXt), + every .c child is returned regardless of whitelist contents; otherwise + the whitelist is consulted in the original libX11 sense. + """ + base = _direct_child_basename(member, "src") + if base is None: + return None + if take_all: + return base if base.endswith(".c") else None + if not whitelist or base not in whitelist: + return None + return base + + +def relevant_util_member( + member: tarfile.TarInfo, whitelist: frozenset[str] +) -> str | None: + """Return the basename if ``member`` is a wanted util/ file. + + libXt's util/ holds the host-side makestrs generator plus the + string.list / .ct / .ht templates it consumes. The Makefile compiles + makestrs and runs it to emit StringDefs.c before the library link. + """ + if not whitelist: + return None + base = _direct_child_basename(member, "util") + if base is None or base not in whitelist: return None return base @@ -285,31 +385,73 @@ def upstream_index() -> dict[str, tuple[str, bytes]]: return index -def upstream_src_index() -> dict[str, tuple[str, bytes]]: - """Return ``{basename: (source_name, content)}`` for whitelisted src files. +def upstream_src_index() -> dict[tuple[str, str], tuple[str, bytes]]: + """Return ``{(subdir, basename): (source_name, content)}`` for src/ files. Iterates per-source so a file's whitelist entry only matches inside the - tarball it was registered under (currently libX11 only). + tarball it was registered under. libX11 uses the SRC_WHITELIST set; + libXt sets ``src_take_all`` and pulls every direct .c child of src/. + Keying by ``(subdir, basename)`` keeps libX11 and libXt staged into + separate directories under ``$(OUT)/upstream/`` so each library builds + against its own include-path layering without name collisions. """ - index: dict[str, tuple[str, bytes]] = {} + index: dict[tuple[str, str], tuple[str, bytes]] = {} for source in SOURCES: + take_all = source.get("src_take_all", False) whitelist = SRC_WHITELIST.get(source["name"], frozenset()) + if not take_all and not whitelist: + continue + subdir = source.get("src_subdir", "src") + tarball = download(source["url"], source["sha256"]) + seen: set[str] = set() + with tarfile.open(tarball, "r:xz") as tar: + for member in tar: + rel = relevant_src_member(member, whitelist, take_all) + if rel is None or rel in seen: + continue + fh = tar.extractfile(member) + if fh is None: + continue + seen.add(rel) + index[(subdir, rel)] = (source["name"], fh.read()) + if not take_all: + missing = whitelist - seen + if missing: + raise SystemExit( + f"{source['name']}: whitelisted src/ files missing " + f"from tarball: " + ", ".join(sorted(missing)) + ) + return index + + +def upstream_util_index() -> dict[tuple[str, str], tuple[str, bytes]]: + """Return ``{(subdir, basename): (source_name, content)}`` for util/ files. + + Currently only libXt declares util files (makestrs.c and the string + templates). Other sources skip this step. + """ + index: dict[tuple[str, str], tuple[str, bytes]] = {} + for source in SOURCES: + whitelist = source.get("util_files") if not whitelist: continue + subdir = source.get("util_subdir", "util") tarball = download(source["url"], source["sha256"]) + seen: set[str] = set() with tarfile.open(tarball, "r:xz") as tar: for member in tar: - rel = relevant_src_member(member, whitelist) - if rel is None or rel in index: + rel = relevant_util_member(member, whitelist) + if rel is None or rel in seen: continue fh = tar.extractfile(member) if fh is None: continue - index[rel] = (source["name"], fh.read()) - missing = whitelist - set(index) + seen.add(rel) + index[(subdir, rel)] = (source["name"], fh.read()) + missing = whitelist - seen if missing: raise SystemExit( - f"{source['name']}: whitelisted src/ files missing from tarball: " + f"{source['name']}: util/ files missing from tarball: " + ", ".join(sorted(missing)) ) return index @@ -332,12 +474,21 @@ def _apply_patch(rel: str, content: bytes) -> bytes: def stamp_token() -> str: lines = [ - "stamp-format=4", + "stamp-format=5", f"sync-script-sha256={sha256_of(Path(__file__).resolve())}", ] lines.extend(f"{src['name']}={src['version']}#{src['sha256']}" for src in SOURCES) for src_name, basenames in sorted(SRC_WHITELIST.items()): lines.append(f"{src_name}-src={','.join(sorted(basenames))}") + for src in SOURCES: + if src.get("src_take_all"): + lines.append(f"{src['name']}-src=*->{src.get('src_subdir', 'src')}") + util = src.get("util_files") + if util: + lines.append( + f"{src['name']}-util=" + f"{','.join(sorted(util))}->{src.get('util_subdir', 'util')}" + ) return "\n".join(lines) + "\n" @@ -361,9 +512,43 @@ def _validate_dest(dest: Path) -> Path: return resolved +def _collect_staging_subdirs() -> list[str]: + """All staging subdirs that ``cmd_fetch`` may write under ``dest.parent``.""" + subs = {"src"} + for source in SOURCES: + if source.get("src_take_all") or SRC_WHITELIST.get(source["name"]): + subs.add(source.get("src_subdir", "src")) + if source.get("util_files"): + subs.add(source.get("util_subdir", "util")) + return sorted(subs) + + +def _stage_indexed( + staging_root: Path, + index: dict[tuple[str, str], tuple[str, bytes]], + patch: bool, +) -> dict[str, int]: + """Write each ``(subdir, rel) -> content`` entry under ``staging_root`` + and return a per-subdir count. Applies SRC_PATCHES when ``patch`` is + True; passes content through unchanged otherwise. + """ + counts: dict[str, int] = {} + for (subdir, rel), (_source, content) in index.items(): + sub_dir = staging_root / subdir + sub_dir.mkdir(parents=True, exist_ok=True) + out_path = sub_dir / rel + try: + out_path.resolve().relative_to(sub_dir) + except ValueError: + raise SystemExit(f"refusing unsafe tar path: {subdir}/{rel}") + out_path.write_bytes(_apply_patch(rel, content) if patch else content) + counts[subdir] = counts.get(subdir, 0) + 1 + return counts + + def cmd_fetch(args: argparse.Namespace) -> int: dest = _validate_dest(Path(args.dest)) - src_dest = _validate_dest(dest.parent / "src") + staging_root = _validate_dest(dest.parent) stamp = dest / STAMP_NAME token = stamp_token() if not args.force and stamp.exists() and stamp.read_text() == token: @@ -374,8 +559,10 @@ def cmd_fetch(args: argparse.Namespace) -> int: x11_root = dest / "X11" if x11_root.exists(): shutil.rmtree(x11_root) - if src_dest.exists(): - shutil.rmtree(src_dest) + for sub in _collect_staging_subdirs(): + sub_dir = staging_root / sub + if sub_dir.exists(): + shutil.rmtree(sub_dir) if stamp.exists(): stamp.unlink() dest.mkdir(parents=True, exist_ok=True) @@ -390,24 +577,21 @@ def cmd_fetch(args: argparse.Namespace) -> int: raise SystemExit(f"refusing unsafe tar path: {rel}") out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_bytes(content) - src_index = upstream_src_index() - if src_index: - src_dest.mkdir(parents=True, exist_ok=True) - for rel, (_source, content) in src_index.items(): - out_path = src_dest / rel - try: - out_path.resolve().relative_to(src_dest) - except ValueError: - raise SystemExit(f"refusing unsafe tar path: src/{rel}") - out_path.write_bytes(_apply_patch(rel, content)) + src_counts = _stage_indexed(staging_root, upstream_src_index(), patch=True) + util_counts = _stage_indexed(staging_root, upstream_util_index(), patch=False) stamp.write_text(token) print( f" STAGE {len(index)} upstream header(s) -> {dest}", file=sys.stderr, ) - if src_index: + for subdir, count in sorted(src_counts.items()): + print( + f" STAGE {count} upstream source(s) -> {staging_root / subdir}", + file=sys.stderr, + ) + for subdir, count in sorted(util_counts.items()): print( - f" STAGE {len(src_index)} upstream source(s) -> {src_dest}", + f" STAGE {count} upstream util file(s) -> {staging_root / subdir}", file=sys.stderr, ) return 0 diff --git a/src/keysym-case.c b/src/keysym-case.c new file mode 100644 index 0000000..cd9e309 --- /dev/null +++ b/src/keysym-case.c @@ -0,0 +1,478 @@ +/* KeySym case folding for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ + +/* UCSConvertCase and XConvertCase below are derived verbatim from libX11's + * src/KeyBind.c (Unicode Data version 4.0.0, updated through UD 14.0 for + * the Greek mapping tables). The original upstream code is: + * + * Copyright 1985, 1987, 1990, 1998 The Open Group + * Copyright 2004 Sun Microsystems, Inc. + * + * See the libX11 source tree's COPYING for the full notice; the MIT-style + * terms there are compatible with this project's own MIT license. libXt's + * translation tables and Motif's case-insensitive accelerators both reach + * for XConvertCase, so the surface needs the upstream-equivalent semantics + * rather than an ASCII-only shortcut. The surrounding KeyBind.c overlaps + * heavily with src/input.c (XLookupKeysym, XKeysymToKeycode, ...), so + * carving out just the case-fold portion avoids duplicate-symbol link + * errors against the SDL-backed key handling. + */ + +#include +#include + +static void UCSConvertCase(register unsigned code, KeySym *lower, KeySym *upper) +{ + /* Case conversion for UCS, as in Unicode Data version 4.0.0. */ + /* NB: Only converts simple one-to-one mappings. */ + + /* Tables are used where they take less space than */ + /* the code to work out the mappings. Zero values mean */ + /* undefined code points. */ + + static unsigned short const IPAExt_upper_mapping[] = { + /* part only */ + 0x0181, 0x0186, 0x0255, 0x0189, 0x018A, 0x0258, 0x018F, 0x025A, + 0x0190, 0x025C, 0x025D, 0x025E, 0x025F, 0x0193, 0x0261, 0x0262, + 0x0194, 0x0264, 0x0265, 0x0266, 0x0267, 0x0197, 0x0196, 0x026A, + 0x026B, 0x026C, 0x026D, 0x026E, 0x019C, 0x0270, 0x0271, 0x019D, + 0x0273, 0x0274, 0x019F, 0x0276, 0x0277, 0x0278, 0x0279, 0x027A, + 0x027B, 0x027C, 0x027D, 0x027E, 0x027F, 0x01A6, 0x0281, 0x0282, + 0x01A9, 0x0284, 0x0285, 0x0286, 0x0287, 0x01AE, 0x0289, 0x01B1, + 0x01B2, 0x028C, 0x028D, 0x028E, 0x028F, 0x0290, 0x0291, 0x01B7}; + + static unsigned short const LatinExtB_upper_mapping[] = { + /* first part only */ + 0x0180, 0x0181, 0x0182, 0x0182, 0x0184, 0x0184, 0x0186, 0x0187, 0x0187, + 0x0189, 0x018A, 0x018B, 0x018B, 0x018D, 0x018E, 0x018F, 0x0190, 0x0191, + 0x0191, 0x0193, 0x0194, 0x01F6, 0x0196, 0x0197, 0x0198, 0x0198, 0x019A, + 0x019B, 0x019C, 0x019D, 0x0220, 0x019F, 0x01A0, 0x01A0, 0x01A2, 0x01A2, + 0x01A4, 0x01A4, 0x01A6, 0x01A7, 0x01A7, 0x01A9, 0x01AA, 0x01AB, 0x01AC, + 0x01AC, 0x01AE, 0x01AF, 0x01AF, 0x01B1, 0x01B2, 0x01B3, 0x01B3, 0x01B5, + 0x01B5, 0x01B7, 0x01B8, 0x01B8, 0x01BA, 0x01BB, 0x01BC, 0x01BC, 0x01BE, + 0x01F7, 0x01C0, 0x01C1, 0x01C2, 0x01C3, 0x01C4, 0x01C4, 0x01C4, 0x01C7, + 0x01C7, 0x01C7, 0x01CA, 0x01CA, 0x01CA}; + + static unsigned short const LatinExtB_lower_mapping[] = { + /* first part only */ + 0x0180, 0x0253, 0x0183, 0x0183, 0x0185, 0x0185, 0x0254, 0x0188, 0x0188, + 0x0256, 0x0257, 0x018C, 0x018C, 0x018D, 0x01DD, 0x0259, 0x025B, 0x0192, + 0x0192, 0x0260, 0x0263, 0x0195, 0x0269, 0x0268, 0x0199, 0x0199, 0x019A, + 0x019B, 0x026F, 0x0272, 0x019E, 0x0275, 0x01A1, 0x01A1, 0x01A3, 0x01A3, + 0x01A5, 0x01A5, 0x0280, 0x01A8, 0x01A8, 0x0283, 0x01AA, 0x01AB, 0x01AD, + 0x01AD, 0x0288, 0x01B0, 0x01B0, 0x028A, 0x028B, 0x01B4, 0x01B4, 0x01B6, + 0x01B6, 0x0292, 0x01B9, 0x01B9, 0x01BA, 0x01BB, 0x01BD, 0x01BD, 0x01BE, + 0x01BF, 0x01C0, 0x01C1, 0x01C2, 0x01C3, 0x01C6, 0x01C6, 0x01C6, 0x01C9, + 0x01C9, 0x01C9, 0x01CC, 0x01CC, 0x01CC}; + + static unsigned short const Greek_upper_mapping[] = { + /* updated to UD 14.0 */ + 0x0370, 0x0370, 0x0372, 0x0372, 0x0374, 0x0375, 0x0376, 0x0376, 0x0000, + 0x0000, 0x037A, 0x03FD, 0x03FE, 0x03FF, 0x037E, 0x037F, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0384, 0x0385, 0x0386, 0x0387, 0x0388, 0x0389, 0x038A, + 0x0000, 0x038C, 0x0000, 0x038E, 0x038F, 0x0390, 0x0391, 0x0392, 0x0393, + 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, + 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x0000, 0x03A3, 0x03A4, 0x03A5, + 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x03AA, 0x03AB, 0x0386, 0x0388, 0x0389, + 0x038A, 0x03B0, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, + 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, + 0x03A1, 0x03A3, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, + 0x03AA, 0x03AB, 0x038C, 0x038E, 0x038F, 0x03CF, 0x0392, 0x0398, 0x03D2, + 0x03D3, 0x03D4, 0x03A6, 0x03A0, 0x03CF, 0x03D8, 0x03D8, 0x03DA, 0x03DA, + 0x03DC, 0x03DC, 0x03DE, 0x03DE, 0x03E0, 0x03E0, 0x03E2, 0x03E2, 0x03E4, + 0x03E4, 0x03E6, 0x03E6, 0x03E8, 0x03E8, 0x03EA, 0x03EA, 0x03EC, 0x03EC, + 0x03EE, 0x03EE, 0x039A, 0x03A1, 0x03F9, 0x037F, 0x03F4, 0x0395, 0x03F6, + 0x03F7, 0x03F7, 0x03F9, 0x03FA, 0x03FA, 0x03FC, 0x03FD, 0x03FE, 0x03FF}; + + static unsigned short const Greek_lower_mapping[] = { + /* updated to UD 14.0 */ + 0x0371, 0x0371, 0x0373, 0x0373, 0x0374, 0x0375, 0x0377, 0x0377, 0x0000, + 0x0000, 0x037A, 0x037B, 0x037C, 0x037D, 0x037E, 0x03F3, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0384, 0x0385, 0x03AC, 0x0387, 0x03AD, 0x03AE, 0x03AF, + 0x0000, 0x03CC, 0x0000, 0x03CD, 0x03CE, 0x0390, 0x03B1, 0x03B2, 0x03B3, + 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, + 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x0000, 0x03C3, 0x03C4, 0x03C5, + 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x03CA, 0x03CB, 0x03AC, 0x03AD, 0x03AE, + 0x03AF, 0x03B0, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, + 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, + 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, + 0x03CA, 0x03CB, 0x03CC, 0x03CD, 0x03CE, 0x03D7, 0x03D0, 0x03D1, 0x03D2, + 0x03D3, 0x03D4, 0x03D5, 0x03D6, 0x03D7, 0x03D9, 0x03D9, 0x03DB, 0x03DB, + 0x03DD, 0x03DD, 0x03DF, 0x03DF, 0x03E1, 0x03E1, 0x03E3, 0x03E3, 0x03E5, + 0x03E5, 0x03E7, 0x03E7, 0x03E9, 0x03E9, 0x03EB, 0x03EB, 0x03ED, 0x03ED, + 0x03EF, 0x03EF, 0x03F0, 0x03F1, 0x03F2, 0x03F3, 0x03B8, 0x03F5, 0x03F6, + 0x03F8, 0x03F8, 0x03F2, 0x03FB, 0x03FB, 0x03FC, 0x037B, 0x037C, 0x037D}; + + static unsigned short const GreekExt_lower_mapping[] = { + 0x1F00, 0x1F01, 0x1F02, 0x1F03, 0x1F04, 0x1F05, 0x1F06, 0x1F07, 0x1F00, + 0x1F01, 0x1F02, 0x1F03, 0x1F04, 0x1F05, 0x1F06, 0x1F07, 0x1F10, 0x1F11, + 0x1F12, 0x1F13, 0x1F14, 0x1F15, 0x0000, 0x0000, 0x1F10, 0x1F11, 0x1F12, + 0x1F13, 0x1F14, 0x1F15, 0x0000, 0x0000, 0x1F20, 0x1F21, 0x1F22, 0x1F23, + 0x1F24, 0x1F25, 0x1F26, 0x1F27, 0x1F20, 0x1F21, 0x1F22, 0x1F23, 0x1F24, + 0x1F25, 0x1F26, 0x1F27, 0x1F30, 0x1F31, 0x1F32, 0x1F33, 0x1F34, 0x1F35, + 0x1F36, 0x1F37, 0x1F30, 0x1F31, 0x1F32, 0x1F33, 0x1F34, 0x1F35, 0x1F36, + 0x1F37, 0x1F40, 0x1F41, 0x1F42, 0x1F43, 0x1F44, 0x1F45, 0x0000, 0x0000, + 0x1F40, 0x1F41, 0x1F42, 0x1F43, 0x1F44, 0x1F45, 0x0000, 0x0000, 0x1F50, + 0x1F51, 0x1F52, 0x1F53, 0x1F54, 0x1F55, 0x1F56, 0x1F57, 0x0000, 0x1F51, + 0x0000, 0x1F53, 0x0000, 0x1F55, 0x0000, 0x1F57, 0x1F60, 0x1F61, 0x1F62, + 0x1F63, 0x1F64, 0x1F65, 0x1F66, 0x1F67, 0x1F60, 0x1F61, 0x1F62, 0x1F63, + 0x1F64, 0x1F65, 0x1F66, 0x1F67, 0x1F70, 0x1F71, 0x1F72, 0x1F73, 0x1F74, + 0x1F75, 0x1F76, 0x1F77, 0x1F78, 0x1F79, 0x1F7A, 0x1F7B, 0x1F7C, 0x1F7D, + 0x0000, 0x0000, 0x1F80, 0x1F81, 0x1F82, 0x1F83, 0x1F84, 0x1F85, 0x1F86, + 0x1F87, 0x1F80, 0x1F81, 0x1F82, 0x1F83, 0x1F84, 0x1F85, 0x1F86, 0x1F87, + 0x1F90, 0x1F91, 0x1F92, 0x1F93, 0x1F94, 0x1F95, 0x1F96, 0x1F97, 0x1F90, + 0x1F91, 0x1F92, 0x1F93, 0x1F94, 0x1F95, 0x1F96, 0x1F97, 0x1FA0, 0x1FA1, + 0x1FA2, 0x1FA3, 0x1FA4, 0x1FA5, 0x1FA6, 0x1FA7, 0x1FA0, 0x1FA1, 0x1FA2, + 0x1FA3, 0x1FA4, 0x1FA5, 0x1FA6, 0x1FA7, 0x1FB0, 0x1FB1, 0x1FB2, 0x1FB3, + 0x1FB4, 0x0000, 0x1FB6, 0x1FB7, 0x1FB0, 0x1FB1, 0x1F70, 0x1F71, 0x1FB3, + 0x1FBD, 0x1FBE, 0x1FBF, 0x1FC0, 0x1FC1, 0x1FC2, 0x1FC3, 0x1FC4, 0x0000, + 0x1FC6, 0x1FC7, 0x1F72, 0x1F73, 0x1F74, 0x1F75, 0x1FC3, 0x1FCD, 0x1FCE, + 0x1FCF, 0x1FD0, 0x1FD1, 0x1FD2, 0x1FD3, 0x0000, 0x0000, 0x1FD6, 0x1FD7, + 0x1FD0, 0x1FD1, 0x1F76, 0x1F77, 0x0000, 0x1FDD, 0x1FDE, 0x1FDF, 0x1FE0, + 0x1FE1, 0x1FE2, 0x1FE3, 0x1FE4, 0x1FE5, 0x1FE6, 0x1FE7, 0x1FE0, 0x1FE1, + 0x1F7A, 0x1F7B, 0x1FE5, 0x1FED, 0x1FEE, 0x1FEF, 0x0000, 0x0000, 0x1FF2, + 0x1FF3, 0x1FF4, 0x0000, 0x1FF6, 0x1FF7, 0x1F78, 0x1F79, 0x1F7C, 0x1F7D, + 0x1FF3, 0x1FFD, 0x1FFE, 0x0000}; + + static unsigned short const GreekExt_upper_mapping[] = { + 0x1F08, 0x1F09, 0x1F0A, 0x1F0B, 0x1F0C, 0x1F0D, 0x1F0E, 0x1F0F, 0x1F08, + 0x1F09, 0x1F0A, 0x1F0B, 0x1F0C, 0x1F0D, 0x1F0E, 0x1F0F, 0x1F18, 0x1F19, + 0x1F1A, 0x1F1B, 0x1F1C, 0x1F1D, 0x0000, 0x0000, 0x1F18, 0x1F19, 0x1F1A, + 0x1F1B, 0x1F1C, 0x1F1D, 0x0000, 0x0000, 0x1F28, 0x1F29, 0x1F2A, 0x1F2B, + 0x1F2C, 0x1F2D, 0x1F2E, 0x1F2F, 0x1F28, 0x1F29, 0x1F2A, 0x1F2B, 0x1F2C, + 0x1F2D, 0x1F2E, 0x1F2F, 0x1F38, 0x1F39, 0x1F3A, 0x1F3B, 0x1F3C, 0x1F3D, + 0x1F3E, 0x1F3F, 0x1F38, 0x1F39, 0x1F3A, 0x1F3B, 0x1F3C, 0x1F3D, 0x1F3E, + 0x1F3F, 0x1F48, 0x1F49, 0x1F4A, 0x1F4B, 0x1F4C, 0x1F4D, 0x0000, 0x0000, + 0x1F48, 0x1F49, 0x1F4A, 0x1F4B, 0x1F4C, 0x1F4D, 0x0000, 0x0000, 0x1F50, + 0x1F59, 0x1F52, 0x1F5B, 0x1F54, 0x1F5D, 0x1F56, 0x1F5F, 0x0000, 0x1F59, + 0x0000, 0x1F5B, 0x0000, 0x1F5D, 0x0000, 0x1F5F, 0x1F68, 0x1F69, 0x1F6A, + 0x1F6B, 0x1F6C, 0x1F6D, 0x1F6E, 0x1F6F, 0x1F68, 0x1F69, 0x1F6A, 0x1F6B, + 0x1F6C, 0x1F6D, 0x1F6E, 0x1F6F, 0x1FBA, 0x1FBB, 0x1FC8, 0x1FC9, 0x1FCA, + 0x1FCB, 0x1FDA, 0x1FDB, 0x1FF8, 0x1FF9, 0x1FEA, 0x1FEB, 0x1FFA, 0x1FFB, + 0x0000, 0x0000, 0x1F88, 0x1F89, 0x1F8A, 0x1F8B, 0x1F8C, 0x1F8D, 0x1F8E, + 0x1F8F, 0x1F88, 0x1F89, 0x1F8A, 0x1F8B, 0x1F8C, 0x1F8D, 0x1F8E, 0x1F8F, + 0x1F98, 0x1F99, 0x1F9A, 0x1F9B, 0x1F9C, 0x1F9D, 0x1F9E, 0x1F9F, 0x1F98, + 0x1F99, 0x1F9A, 0x1F9B, 0x1F9C, 0x1F9D, 0x1F9E, 0x1F9F, 0x1FA8, 0x1FA9, + 0x1FAA, 0x1FAB, 0x1FAC, 0x1FAD, 0x1FAE, 0x1FAF, 0x1FA8, 0x1FA9, 0x1FAA, + 0x1FAB, 0x1FAC, 0x1FAD, 0x1FAE, 0x1FAF, 0x1FB8, 0x1FB9, 0x1FB2, 0x1FBC, + 0x1FB4, 0x0000, 0x1FB6, 0x1FB7, 0x1FB8, 0x1FB9, 0x1FBA, 0x1FBB, 0x1FBC, + 0x1FBD, 0x0399, 0x1FBF, 0x1FC0, 0x1FC1, 0x1FC2, 0x1FCC, 0x1FC4, 0x0000, + 0x1FC6, 0x1FC7, 0x1FC8, 0x1FC9, 0x1FCA, 0x1FCB, 0x1FCC, 0x1FCD, 0x1FCE, + 0x1FCF, 0x1FD8, 0x1FD9, 0x1FD2, 0x1FD3, 0x0000, 0x0000, 0x1FD6, 0x1FD7, + 0x1FD8, 0x1FD9, 0x1FDA, 0x1FDB, 0x0000, 0x1FDD, 0x1FDE, 0x1FDF, 0x1FE8, + 0x1FE9, 0x1FE2, 0x1FE3, 0x1FE4, 0x1FEC, 0x1FE6, 0x1FE7, 0x1FE8, 0x1FE9, + 0x1FEA, 0x1FEB, 0x1FEC, 0x1FED, 0x1FEE, 0x1FEF, 0x0000, 0x0000, 0x1FF2, + 0x1FFC, 0x1FF4, 0x0000, 0x1FF6, 0x1FF7, 0x1FF8, 0x1FF9, 0x1FFA, 0x1FFB, + 0x1FFC, 0x1FFD, 0x1FFE, 0x0000}; + + *lower = code; + *upper = code; + + /* Basic Latin and Latin-1 Supplement, U+0000 to U+00FF */ + if (code <= 0x00ff) { + if (code >= 0x0041 && code <= 0x005a) /* A-Z */ + *lower += 0x20; + else if (code >= 0x0061 && code <= 0x007a) /* a-z */ + *upper -= 0x20; + else if ((code >= 0x00c0 && code <= 0x00d6) || + (code >= 0x00d8 && code <= 0x00de)) + *lower += 0x20; + else if ((code >= 0x00e0 && code <= 0x00f6) || + (code >= 0x00f8 && code <= 0x00fe)) + *upper -= 0x20; + else if (code == 0x00ff) /* y with diaeresis */ + *upper = 0x0178; + else if (code == 0x00b5) /* micro sign */ + *upper = 0x039c; + else if (code == 0x00df) /* ssharp */ + *upper = 0x1e9e; + return; + } + + /* Latin Extended-A, U+0100 to U+017F */ + if (code <= 0x017f) { + if ((code >= 0x0100 && code <= 0x012f) || + (code >= 0x0132 && code <= 0x0137) || + (code >= 0x014a && code <= 0x0177)) { + *upper = code & ~1; + *lower = code | 1; + } else if ((code >= 0x0139 && code <= 0x0148) || + (code >= 0x0179 && code <= 0x017e)) { + if (code & 1) + *lower += 1; + else + *upper -= 1; + } else if (code == 0x0130) + *lower = 0x0069; + else if (code == 0x0131) + *upper = 0x0049; + else if (code == 0x0178) + *lower = 0x00ff; + else if (code == 0x017f) + *upper = 0x0053; + return; + } + + /* Latin Extended-B, U+0180 to U+024F */ + if (code <= 0x024f) { + if (code >= 0x0180 && code <= 0x01cc) { + *lower = LatinExtB_lower_mapping[code - 0x0180]; + *upper = LatinExtB_upper_mapping[code - 0x0180]; + } else if (code >= 0x01cd && code <= 0x01dc) { + if (code & 1) + *lower += 1; + else + *upper -= 1; + } else if (code == 0x01dd) + *upper = 0x018e; + else if ((code >= 0x01de && code <= 0x01ef) || + (code >= 0x01f4 && code <= 0x01f5) || + (code >= 0x01f8 && code <= 0x021f) || + (code >= 0x0222 && code <= 0x0233)) { + *lower |= 1; + *upper &= ~1; + } else if (code == 0x01f1 || code == 0x01f2) { + *lower = 0x01f3; + *upper = 0x01f1; + } else if (code == 0x01f3) + *upper = 0x01f1; + else if (code == 0x01f6) + *lower = 0x0195; + else if (code == 0x01f7) + *lower = 0x01bf; + else if (code == 0x0220) + *lower = 0x019e; + return; + } + + /* IPA Extensions, U+0250 to U+02AF */ + if (code >= 0x0253 && code <= 0x0292) { + *upper = IPAExt_upper_mapping[code - 0x0253]; + return; + } + + /* Combining Diacritical Marks, U+0300 to U+036F */ + if (code == 0x0345) { + *upper = 0x0399; + return; + } + + /* Greek and Coptic, U+0370 to U+03FF */ + if (code >= 0x0370 && code <= 0x03ff) { + *lower = Greek_lower_mapping[code - 0x0370]; + *upper = Greek_upper_mapping[code - 0x0370]; + if (*upper == 0) + *upper = code; + if (*lower == 0) + *lower = code; + return; + } + + /* Cyrillic and Cyrillic Supplementary, U+0400 to U+052F */ + if ((code >= 0x0400 && code <= 0x052f)) { + if (code >= 0x0400 && code <= 0x040f) + *lower += 0x50; + else if (code >= 0x0410 && code <= 0x042f) + *lower += 0x20; + else if (code >= 0x0430 && code <= 0x044f) + *upper -= 0x20; + else if (code >= 0x0450 && code <= 0x045f) + *upper -= 0x50; + else if ((code >= 0x0460 && code <= 0x0481) || + (code >= 0x048a && code <= 0x04bf) || + (code >= 0x04d0 && code <= 0x04f5) || + (code >= 0x04f8 && code <= 0x04f9) || + (code >= 0x0500 && code <= 0x050f)) { + *upper &= ~1; + *lower |= 1; + } else if (code >= 0x04c1 && code <= 0x04ce) { + if (code & 1) + *lower += 1; + else + *upper -= 1; + } + return; + } + + /* Armenian, U+0530 to U+058F */ + if (code >= 0x0530 && code <= 0x058f) { + if (code >= 0x0531 && code <= 0x0556) + *lower += 0x30; + else if (code >= 0x0561 && code <= 0x0586) + *upper -= 0x30; + return; + } + + /* Latin Extended Additional, U+1E00 to U+1EFF */ + if (code >= 0x1e00 && code <= 0x1eff) { + if ((code >= 0x1e00 && code <= 0x1e95) || + (code >= 0x1ea0 && code <= 0x1ef9)) { + *upper &= ~1; + *lower |= 1; + } else if (code == 0x1e9b) + *upper = 0x1e60; + else if (code == 0x1e9e) + *lower = 0x00df; /* ssharp */ + return; + } + + /* Greek Extended, U+1F00 to U+1FFF */ + if (code >= 0x1f00 && code <= 0x1fff) { + *lower = GreekExt_lower_mapping[code - 0x1f00]; + *upper = GreekExt_upper_mapping[code - 0x1f00]; + if (*upper == 0) + *upper = code; + if (*lower == 0) + *lower = code; + return; + } + + /* Letterlike Symbols, U+2100 to U+214F */ + if (code >= 0x2100 && code <= 0x214f) { + switch (code) { + case 0x2126: + *lower = 0x03c9; + break; + case 0x212a: + *lower = 0x006b; + break; + case 0x212b: + *lower = 0x00e5; + break; + } + } + /* Number Forms, U+2150 to U+218F */ + else if (code >= 0x2160 && code <= 0x216f) + *lower += 0x10; + else if (code >= 0x2170 && code <= 0x217f) + *upper -= 0x10; + /* Enclosed Alphanumerics, U+2460 to U+24FF */ + else if (code >= 0x24b6 && code <= 0x24cf) + *lower += 0x1a; + else if (code >= 0x24d0 && code <= 0x24e9) + *upper -= 0x1a; + /* Halfwidth and Fullwidth Forms, U+FF00 to U+FFEF */ + else if (code >= 0xff21 && code <= 0xff3a) + *lower += 0x20; + else if (code >= 0xff41 && code <= 0xff5a) + *upper -= 0x20; + /* Deseret, U+10400 to U+104FF */ + else if (code >= 0x10400 && code <= 0x10427) + *lower += 0x28; + else if (code >= 0x10428 && code <= 0x1044f) + *upper -= 0x28; +} + +void XConvertCase(register KeySym sym, KeySym *lower, KeySym *upper) +{ + /* Latin 1 keysym */ + if (sym < 0x100) { + UCSConvertCase(sym, lower, upper); + return; + } + + /* Unicode keysym */ + if ((sym & 0xff000000) == 0x01000000) { + UCSConvertCase((sym & 0x00ffffff), lower, upper); + *upper |= 0x01000000; + *lower |= 0x01000000; + return; + } + + /* Legacy keysym */ + *lower = sym; + *upper = sym; + + switch (sym >> 8) { + case 1: /* Latin 2 */ + /* Assume the KeySym is a legal value (ignore discontinuities) */ + if (sym == XK_Aogonek) + *lower = XK_aogonek; + else if (sym >= XK_Lstroke && sym <= XK_Sacute) + *lower += (XK_lstroke - XK_Lstroke); + else if (sym >= XK_Scaron && sym <= XK_Zacute) + *lower += (XK_scaron - XK_Scaron); + else if (sym >= XK_Zcaron && sym <= XK_Zabovedot) + *lower += (XK_zcaron - XK_Zcaron); + else if (sym == XK_aogonek) + *upper = XK_Aogonek; + else if (sym >= XK_lstroke && sym <= XK_sacute) + *upper -= (XK_lstroke - XK_Lstroke); + else if (sym >= XK_scaron && sym <= XK_zacute) + *upper -= (XK_scaron - XK_Scaron); + else if (sym >= XK_zcaron && sym <= XK_zabovedot) + *upper -= (XK_zcaron - XK_Zcaron); + else if (sym >= XK_Racute && sym <= XK_Tcedilla) + *lower += (XK_racute - XK_Racute); + else if (sym >= XK_racute && sym <= XK_tcedilla) + *upper -= (XK_racute - XK_Racute); + break; + case 2: /* Latin 3 */ + /* Assume the KeySym is a legal value (ignore discontinuities) */ + if (sym >= XK_Hstroke && sym <= XK_Hcircumflex) + *lower += (XK_hstroke - XK_Hstroke); + else if (sym >= XK_Gbreve && sym <= XK_Jcircumflex) + *lower += (XK_gbreve - XK_Gbreve); + else if (sym >= XK_hstroke && sym <= XK_hcircumflex) + *upper -= (XK_hstroke - XK_Hstroke); + else if (sym >= XK_gbreve && sym <= XK_jcircumflex) + *upper -= (XK_gbreve - XK_Gbreve); + else if (sym >= XK_Cabovedot && sym <= XK_Scircumflex) + *lower += (XK_cabovedot - XK_Cabovedot); + else if (sym >= XK_cabovedot && sym <= XK_scircumflex) + *upper -= (XK_cabovedot - XK_Cabovedot); + break; + case 3: /* Latin 4 */ + /* Assume the KeySym is a legal value (ignore discontinuities) */ + if (sym >= XK_Rcedilla && sym <= XK_Tslash) + *lower += (XK_rcedilla - XK_Rcedilla); + else if (sym >= XK_rcedilla && sym <= XK_tslash) + *upper -= (XK_rcedilla - XK_Rcedilla); + else if (sym == XK_ENG) + *lower = XK_eng; + else if (sym == XK_eng) + *upper = XK_ENG; + else if (sym >= XK_Amacron && sym <= XK_Umacron) + *lower += (XK_amacron - XK_Amacron); + else if (sym >= XK_amacron && sym <= XK_umacron) + *upper -= (XK_amacron - XK_Amacron); + break; + case 6: /* Cyrillic */ + /* Assume the KeySym is a legal value (ignore discontinuities) */ + if (sym >= XK_Serbian_DJE && sym <= XK_Serbian_DZE) + *lower -= (XK_Serbian_DJE - XK_Serbian_dje); + else if (sym >= XK_Serbian_dje && sym <= XK_Serbian_dze) + *upper += (XK_Serbian_DJE - XK_Serbian_dje); + else if (sym >= XK_Cyrillic_YU && sym <= XK_Cyrillic_HARDSIGN) + *lower -= (XK_Cyrillic_YU - XK_Cyrillic_yu); + else if (sym >= XK_Cyrillic_yu && sym <= XK_Cyrillic_hardsign) + *upper += (XK_Cyrillic_YU - XK_Cyrillic_yu); + break; + case 7: /* Greek */ + /* Assume the KeySym is a legal value (ignore discontinuities) */ + if (sym >= XK_Greek_ALPHAaccent && sym <= XK_Greek_OMEGAaccent) + *lower += (XK_Greek_alphaaccent - XK_Greek_ALPHAaccent); + else if (sym >= XK_Greek_alphaaccent && sym <= XK_Greek_omegaaccent && + sym != XK_Greek_iotaaccentdieresis && + sym != XK_Greek_upsilonaccentdieresis) + *upper -= (XK_Greek_alphaaccent - XK_Greek_ALPHAaccent); + else if (sym >= XK_Greek_ALPHA && sym <= XK_Greek_OMEGA) + *lower += (XK_Greek_alpha - XK_Greek_ALPHA); + else if (sym == XK_Greek_finalsmallsigma) + *upper = XK_Greek_SIGMA; + else if (sym >= XK_Greek_alpha && sym <= XK_Greek_omega) + *upper -= (XK_Greek_alpha - XK_Greek_ALPHA); + break; + case 0x13: /* Latin 9 */ + if (sym == XK_OE) + *lower = XK_oe; + else if (sym == XK_oe) + *upper = XK_OE; + else if (sym == XK_Ydiaeresis) + *lower = XK_ydiaeresis; + break; + } +} diff --git a/src/missing.c b/src/missing.c index c83f66c..d7c5753 100644 --- a/src/missing.c +++ b/src/missing.c @@ -2795,3 +2795,45 @@ XModifierKeymap *XDeleteModifiermapEntry(XModifierKeymap *map, { return NULL; } + +/* Internal-connection plumbing used by libXt and IM clients to fold extra + * descriptors into the Xlib event loop. The SDL-backed display owns no + * auxiliary sockets, so there is nothing to watch and nothing to drain: + * report an empty fd set and acknowledge watcher registration without + * recording the callback. */ +Status XInternalConnectionNumbers(Display *dpy, + int **fd_return, + int *count_return) +{ + (void) dpy; + if (fd_return) + *fd_return = NULL; + if (count_return) + *count_return = 0; + return 1; +} + +void XProcessInternalConnection(Display *dpy, int fd) +{ + (void) dpy; + (void) fd; +} + +Status XAddConnectionWatch(Display *dpy, + XConnectionWatchProc proc, + XPointer data) +{ + (void) dpy; + (void) proc; + (void) data; + return 1; +} + +void XRemoveConnectionWatch(Display *dpy, + XConnectionWatchProc proc, + XPointer data) +{ + (void) dpy; + (void) proc; + (void) data; +} diff --git a/src/xlibint-stubs.c b/src/xlibint-stubs.c index 0519ca6..f0a58e7 100644 --- a/src/xlibint-stubs.c +++ b/src/xlibint-stubs.c @@ -1,7 +1,7 @@ /* Definitions for symbols upstream libX11 expects from XlibInt.c and * the lcWrap/lcConv i18n helpers, which libx11-compat does not compile. * - * Copyright 2025 libx11-compat contributors + * Copyright 2026 libx11-compat contributors * SPDX-License-Identifier: MIT */ diff --git a/src/xrm.c b/src/xrm.c index 8b86813..73d0a40 100644 --- a/src/xrm.c +++ b/src/xrm.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -594,6 +595,7 @@ void XrmParseCommand(XrmDatabase *pdb, prefix = ""; int kept = 1; /* argv[0] always survives. */ + int original_argc = *argc; for (int i = 1; i < *argc;) { char *arg = argv[i]; int matched = 0; @@ -665,7 +667,13 @@ void XrmParseCommand(XrmDatabase *pdb, } } *argc = kept; - argv[kept] = NULL; + /* Mirror upstream libX11 ParseCmd.c: only NULL-terminate when + * compression actually freed a slot. libXt's _XtAppInit passes a + * heap-allocated argv sized exactly to *argc, so writing argv[*argc] + * unconditionally smashes the byte past the allocation (caught by + * AddressSanitizer in CI). */ + if (kept < original_argc) + argv[kept] = NULL; } const char *XrmLocaleOfDatabase(XrmDatabase db) @@ -674,8 +682,83 @@ const char *XrmLocaleOfDatabase(XrmDatabase db) return "C"; } -/* Stubs kept harmless until their bucket-based callers actually need - * traversal. */ +/* The bucket-based quark API (XrmQGetSearchList, XrmQGetSearchResource, + * XrmQGetResource) is libXt's primary path into the resource database -- + * every widget Set/GetValues, every _XtDisplayInitialize boot probe, and + * Motif's giant resource cascade all funnel through it. The local + * database is a flat linked list of (pattern, type, value) strings rather + * than the bucketed quark tree libX11 ships, so we bridge by encoding + * the database pointer *and* the widget prefix arrays into the search + * list slots, then reassembling the full name/class path inside + * XrmQGetSearchResource. Skipping the prefix and looking up the leaf + * alone misses every hierarchical Motif resource and can false-hit + * unrelated same-leaf entries, so the encoding is load-bearing. + * + * Search-list layout: + * + * [0] -> (XrmHashTable) db + * [1] -> n (number of prefix components) + * [2 .. 2 + n - 1] -> name quarks + * [2 + n .. 2 + 2n -1] -> class quarks + * [2 + 2n] -> NULL terminator + * + * The caller-supplied list_length must hold 3 + 2 * n slots or we return + * False so libXt's doubling loop in _XtDisplayInitialize widens the + * buffer and retries. */ + +/* Xlib documents resource paths up to 100 components; round to 128 so we + * accept the entire spec range without forcing libXt's _XtDisplayInitialize + * doubling loop to give up on a legitimate deep widget tree. */ +#define XRM_PREFIX_MAX 128 + +static char *quarkToCString(XrmQuark q) +{ + /* XrmQuarkToString returns NULL for NULLQUARK. Callers prefer an + * empty string for "wildcard / unspecified" since XrmGetResource + * handles a "" class as "any class". The String typedef is libXt's, + * not libX11's, so spell out char * here. */ + char *s = XrmQuarkToString(q); + return s ? s : (char *) ""; +} + +static char *quarkListToCString(const XrmQuark *quarks) +{ + if (!quarks) { + char *empty = malloc(1); + if (empty) + empty[0] = '\0'; + return empty; + } + + /* Two-pass: first measure the joined length (with overflow guards on + * each addition so a pathological quark table cannot wrap size_t), + * then allocate and fill. */ + size_t total = 1; + for (int i = 0; quarks[i] != NULLQUARK; i++) { + const char *segment = quarkToCString(quarks[i]); + size_t len = strlen(segment); + size_t sep = (i > 0) ? 1 : 0; + if (sep > SIZE_MAX - total || len > SIZE_MAX - total - sep) + return NULL; + total += sep + len; + } + + char *path = malloc(total); + if (!path) + return NULL; + + size_t off = 0; + for (int i = 0; quarks[i] != NULLQUARK; i++) { + const char *segment = quarkToCString(quarks[i]); + size_t len = strlen(segment); + if (i > 0) + path[off++] = '.'; + memcpy(path + off, segment, len); + off += len; + } + path[off] = '\0'; + return path; +} Bool XrmQGetResource(XrmDatabase db, XrmNameList quark_name, @@ -683,16 +766,46 @@ Bool XrmQGetResource(XrmDatabase db, XrmRepresentation *quark_type_return, XrmValuePtr value_return) { - (void) db; - (void) quark_name; - (void) quark_class; if (quark_type_return) *quark_type_return = 0; if (value_return) { value_return->addr = NULL; value_return->size = 0; } - return False; + if (!db || !quark_name) + return False; + + /* Concatenate the quark lists back into the dotted strings the + * pattern matcher in XrmGetResource expects. Resource paths and Xt + * widget names are not byte-limited, so size these buffers from the + * quark strings rather than imposing a fixed stack cap. */ + char *name_buf = quarkListToCString(quark_name); + char *class_buf = quarkListToCString(quark_class); + if (!name_buf || !class_buf) { + free(name_buf); + free(class_buf); + return False; + } + + char *type_str = NULL; + Bool found = + XrmGetResource(db, name_buf, class_buf, &type_str, value_return); + free(name_buf); + free(class_buf); + if (!found) + return False; + if (quark_type_return) + *quark_type_return = type_str ? XrmStringToQuark(type_str) : 0; + return True; +} + +static int countQuarkList(const XrmQuark *q) +{ + int n = 0; + if (q) + while (q[n] != NULLQUARK) + n++; + return n; } Bool XrmQGetSearchList(XrmDatabase db, @@ -701,12 +814,34 @@ Bool XrmQGetSearchList(XrmDatabase db, XrmSearchList list_return, int list_length) { - (void) db; - (void) names; - (void) classes; - (void) list_return; - (void) list_length; - return False; + /* libXt callers interpret False as "buffer too small" and retry with + * larger storage. Pack the database pointer plus the prefix arrays + * into the caller's slots so XrmQGetSearchResource can reconstruct + * the full path; the layout is documented above. For unsupported + * over-deep prefixes, return a valid empty list so callers stop + * retrying and the follow-up resource lookup simply fails. */ + if (!list_return || list_length <= 0) + return False; + int n_names = countQuarkList(names); + int n_classes = countQuarkList(classes); + int n = n_names > n_classes ? n_names : n_classes; + if (n > XRM_PREFIX_MAX) { + list_return[0] = NULL; + return True; + } + int needed = 3 + 2 * n; + if (list_length < needed) + return False; + list_return[0] = (XrmHashTable) db; + list_return[1] = (XrmHashTable) (uintptr_t) n; + for (int i = 0; i < n; i++) { + XrmQuark nq = (i < n_names) ? names[i] : NULLQUARK; + XrmQuark cq = (i < n_classes) ? classes[i] : NULLQUARK; + list_return[2 + i] = (XrmHashTable) (uintptr_t) nq; + list_return[2 + n + i] = (XrmHashTable) (uintptr_t) cq; + } + list_return[2 + 2 * n] = NULL; + return True; } Bool XrmQGetSearchResource(XrmSearchList searchList, @@ -715,16 +850,36 @@ Bool XrmQGetSearchResource(XrmSearchList searchList, XrmRepresentation *pType, XrmValue *pValue) { - (void) searchList; - (void) name; - (void) class; if (pType) *pType = 0; if (pValue) { pValue->addr = NULL; pValue->size = 0; } - return False; + if (!searchList || !searchList[0]) + return False; + XrmDatabase db = (XrmDatabase) searchList[0]; + int n = (int) (uintptr_t) searchList[1]; + if (n < 0 || n > XRM_PREFIX_MAX) + return False; + if (name == NULLQUARK) + return False; + + /* Rebuild the full path: + leaf name + NULLQUARK, + * matched by + leaf class + NULLQUARK. +2 for + * the leaf slot plus the terminator. */ + XrmQuark full_names[XRM_PREFIX_MAX + 2]; + XrmQuark full_classes[XRM_PREFIX_MAX + 2]; + for (int i = 0; i < n; i++) { + full_names[i] = (XrmQuark) (uintptr_t) searchList[2 + i]; + full_classes[i] = (XrmQuark) (uintptr_t) searchList[2 + n + i]; + } + full_names[n] = name; + full_classes[n] = class; + full_names[n + 1] = NULLQUARK; + full_classes[n + 1] = NULLQUARK; + + return XrmQGetResource(db, full_names, full_classes, pType, pValue); } void XrmQPutResource(XrmDatabase *pdb, diff --git a/tests/api-symbols.txt b/tests/api-symbols.txt index acc1391..2e2a4be 100644 --- a/tests/api-symbols.txt +++ b/tests/api-symbols.txt @@ -2,6 +2,7 @@ # Keep this manifest in sync with intentional API surface changes. XActivateScreenSaver +XAddConnectionWatch XAddExtension XAddHost XAddHosts @@ -51,6 +52,7 @@ XCloseIM XConfigureWindow XConnectionNumber XContextDependentDrawing +XConvertCase XConvertSelection XCopyArea XCopyColormapAndFree @@ -236,6 +238,7 @@ XInsertModifiermapEntry XInstallColormap XInternAtom XInternAtoms +XInternalConnectionNumbers XIntersectRegion XKeycodeToKeysym XKeysymToKeycode @@ -283,6 +286,7 @@ XPending XPlanesOfScreen XPointInRegion XPolygonRegion +XProcessInternalConnection XProtocolRevision XProtocolVersion XPutBackEvent @@ -311,6 +315,7 @@ XReconfigureWMWindow XRectInRegion XRefreshKeyboardMapping XRegisterIMInstantiateCallback +XRemoveConnectionWatch XRemoveFromSaveSet XRemoveHost XRemoveHosts diff --git a/tests/check.c b/tests/check.c index 6381a43..68ec62a 100644 --- a/tests/check.c +++ b/tests/check.c @@ -3147,6 +3147,57 @@ static int test_xrm(Display *display) CHECK(value.addr != NULL && strcmp((char *) value.addr, "Hello") == 0, "XrmGetResource returned wrong value"); + char longName[301]; + memset(longName, 'a', sizeof(longName) - 1); + longName[sizeof(longName) - 1] = '\0'; + char longSpec[sizeof("App.") + sizeof(longName) + sizeof(": QLong")]; + snprintf(longSpec, sizeof(longSpec), "App.%s: QLong", longName); + XrmPutLineResource(&db, longSpec); + XrmName qLongNames[] = { + XrmStringToQuark("App"), + XrmStringToQuark(longName), + NULLQUARK, + }; + XrmClass qLongClasses[] = { + XrmStringToQuark("Application"), + XrmStringToQuark("LongName"), + NULLQUARK, + }; + XrmRepresentation qLongType = 0; + XrmValue qLongValue = {.size = 0, .addr = NULL}; + CHECK( + XrmQGetResource(db, qLongNames, qLongClasses, &qLongType, &qLongValue), + "XrmQGetResource missed long quark resource path"); + CHECK(qLongValue.addr != NULL && + strcmp((char *) qLongValue.addr, "QLong") == 0, + "XrmQGetResource returned wrong value for long path"); + + XrmName deepNames[66]; + XrmClass deepClasses[66]; + for (size_t i = 0; i < ARRAY_LENGTH(deepNames) - 1; i++) { + deepNames[i] = XrmStringToQuark("deepName"); + deepClasses[i] = XrmStringToQuark("DeepClass"); + } + deepNames[ARRAY_LENGTH(deepNames) - 1] = NULLQUARK; + deepClasses[ARRAY_LENGTH(deepClasses) - 1] = NULLQUARK; + /* The prefix-encoding bridge needs 3 + 2 * n slots (db + count + name + * quarks + class quarks + NULL terminator). A list_length too small for + * that layout must return False so libXt's _XtDisplayInitialize doubling + * loop can enlarge the buffer and retry; an adequate buffer encodes the + * prefix and returns True. Verify both branches so neither alloca- + * smashes the stack (always-False regression) nor lies about contents + * (always-True regression). */ + XrmHashTable tinySearch[1]; + CHECK(!XrmQGetSearchList(db, deepNames, deepClasses, tinySearch, 1), + "XrmQGetSearchList should signal buffer-too-small with False"); + XrmHashTable deepSearch[200]; + CHECK(XrmQGetSearchList(db, deepNames, deepClasses, deepSearch, 200), + "XrmQGetSearchList should encode 65-component prefix in 200 slots"); + CHECK(!XrmQGetSearchResource(deepSearch, XrmStringToQuark("leaf"), + XrmStringToQuark("Leaf"), &qLongType, + &qLongValue), + "deep no-match search list should not resolve resources"); + /* Loose binding via '*'. */ value.addr = NULL; CHECK(XrmGetResource(db, "App.window.background", "App.Window.Background", diff --git a/tests/test-libxt-link.c b/tests/test-libxt-link.c new file mode 100644 index 0000000..9e3d379 --- /dev/null +++ b/tests/test-libxt-link.c @@ -0,0 +1,96 @@ +/* libXt smoke test for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ + +/* Exercise the path that proves libXt sits cleanly on libx11-compat: + * + * 1. XtToolkitInitialize - one-shot Xt init, exits dirty if XtMalloc / + * quark table machinery is broken. + * 2. XtCreateApplicationContext + XtOpenDisplay - prove the Xt application + * context can drive XOpenDisplay under SDL_VIDEODRIVER=dummy. + * 3. XtAppCreateShell - top-level widget creation + * hits the widget class machinery, resource manager, and translation + * table compiler. If any of those see the wrong Display layout, this + * either segfaults or returns NULL. + * 4. XtRealizeWidget + XtUnrealizeWidget - drives the realize path + * (XCreateWindow + class methods) without entering the event loop. + * 5. Teardown is XtDestroyApplicationContext. + * + * The test does not enter XtAppMainLoop; the goal is link/init coverage, not + * a steady-state UI session. Anything that returns NULL or 0 where success + * is expected is fatal so make check fails loudly. + */ +#include +#include +#include + +#include +#include +#include + +int main(int argc, char *argv[]) +{ + XtAppContext app; + Widget shell; + Display *display; + int local_argc = 1; + char *local_argv[] = {(char *) "test_libxt_link", NULL}; + + if (!getenv("SDL_VIDEODRIVER")) { + /* tests/check defaults to dummy via the make target. When a developer + * runs the binary directly without that wrapper, force dummy so we do + * not pop a real window. */ + setenv("SDL_VIDEODRIVER", "dummy", 1); + } + + XtToolkitInitialize(); + app = XtCreateApplicationContext(); + if (!app) { + fprintf(stderr, "XtCreateApplicationContext returned NULL\n"); + return 1; + } + + display = XtOpenDisplay(app, NULL, "smoke", "Smoke", NULL, 0, &local_argc, + local_argv); + if (!display) { + fprintf(stderr, "XtOpenDisplay returned NULL\n"); + XtDestroyApplicationContext(app); + return 1; + } + (void) argc; + (void) argv; + + shell = XtAppCreateShell("smoke", "Smoke", applicationShellWidgetClass, + display, NULL, 0); + if (!shell) { + fprintf(stderr, "XtAppCreateShell returned NULL\n"); + XtCloseDisplay(display); + XtDestroyApplicationContext(app); + return 1; + } + + /* Pin a size before realize so the shell does not refuse to map. + * The sentinel for XtVaSetValues is a literal NULL, not a cast: the + * cast hides the sentinel attribute from -Wsentinel and the va_list + * walker reads past the end. */ + XtVaSetValues(shell, XtNwidth, 64, XtNheight, 64, NULL); + + XtRealizeWidget(shell); + if (!XtIsRealized(shell)) { + fprintf(stderr, "Shell did not realize\n"); + XtDestroyWidget(shell); + XtCloseDisplay(display); + XtDestroyApplicationContext(app); + return 1; + } + + XtUnrealizeWidget(shell); + XtDestroyWidget(shell); + XtCloseDisplay(display); + XtDestroyApplicationContext(app); + + printf("test_libxt_link: ok\n"); + return 0; +} diff --git a/tests/test-libxt-micro.c b/tests/test-libxt-micro.c new file mode 100644 index 0000000..f63ea1a --- /dev/null +++ b/tests/test-libxt-micro.c @@ -0,0 +1,270 @@ +/* libXt Tier 1 micro-tests for libx11-compat + * + * Copyright 2026 libx11-compat contributors + * SPDX-License-Identifier: MIT + */ + +/* Exercise the chokepoints Motif depends on, one per Xt subsystem. Each + * subtest is independent and stops the binary at the first failure with + * a specific diagnostic so make check fails loudly. The order matters: + * subsystems with no Display dependency run first (keysyms), then init + * happens once, then everything that needs an app context or widget runs + * against the shared shell. + * + * keysyms - XConvertCase across ASCII, Latin-1, and function keys. + * events - XtAppMainLoop drains an XtAppAddTimeOut without spin. + * callbacks - XtAddCallback/XtCallCallbacks/XtRemoveCallback round-trip. + * gc - XtAllocateGC returns a non-NULL GC and tolerates release. + * resources - XtVaGetValues against an XtAppSetFallbackResources entry. + * + * The resources path drives the full XrmQGetSearchResource -> + * XrmGetResource bridge introduced in src/xrm.c. If that bridge ever + * regresses, the resources subtest is what catches it first. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#define OK(name) \ + do { \ + puts("ok " name); \ + fflush(stdout); \ + } while (0) +#define FAIL(name, ...) \ + do { \ + fprintf(stderr, "FAIL " name ": "); \ + fprintf(stderr, __VA_ARGS__); \ + fputc('\n', stderr); \ + exit(1); \ + } while (0) +#define MUST(cond, name, ...) \ + do { \ + if (!(cond)) \ + FAIL(name, __VA_ARGS__); \ + } while (0) + +/* --------------------------------------------------------------- keysyms */ + +static void test_keysyms(void) +{ + KeySym lo, up; + + /* ASCII A: lower-cases to a, upper-cases to itself. */ + XConvertCase(XK_A, &lo, &up); + MUST(lo == XK_a && up == XK_A, "keysyms", + "XK_A -> lo=0x%lx up=0x%lx expected (a, A)", lo, up); + + /* ASCII z: upper-cases to Z. */ + XConvertCase(XK_z, &lo, &up); + MUST(lo == XK_z && up == XK_Z, "keysyms", + "XK_z -> lo=0x%lx up=0x%lx expected (z, Z)", lo, up); + + /* Function key has no case form. */ + XConvertCase(XK_F1, &lo, &up); + MUST(lo == XK_F1 && up == XK_F1, "keysyms", + "XK_F1 changed: lo=0x%lx up=0x%lx", lo, up); + + /* Latin-1: e-acute pairs with E-acute. */ + XConvertCase(XK_eacute, &lo, &up); + MUST(lo == XK_eacute && up == XK_Eacute, "keysyms", + "Latin-1 eacute -> lo=0x%lx up=0x%lx", lo, up); + + /* Greek capital alpha pairs with lower alpha. */ + XConvertCase(XK_Greek_ALPHA, &lo, &up); + MUST(lo == XK_Greek_alpha && up == XK_Greek_ALPHA, "keysyms", + "Greek_ALPHA -> lo=0x%lx up=0x%lx", lo, up); + + OK("keysyms"); +} + +/* ---------------------------------------------------------------- events */ + +static int tick_count = 0; +static XtAppContext tick_app = NULL; + +static void tick_proc(XtPointer baton, XtIntervalId *id) +{ + (void) baton; + (void) id; + tick_count++; + /* Setting the exit flag is checked by XtAppMainLoop after the current + * callback returns, so this drops us out cleanly with no further + * event processing. */ + XtAppSetExitFlag(tick_app); +} + +static void test_events(XtAppContext app) +{ + tick_count = 0; + tick_app = app; + /* 10ms is short enough to keep make check fast but long enough that + * the SDL dummy backend has a real interval to wait through. */ + XtAppAddTimeOut(app, 10, tick_proc, NULL); + XtAppMainLoop(app); + MUST(tick_count == 1, "events", + "timeout fired %d times, expected exactly 1", tick_count); + /* The exit flag is sticky inside XtAppMainLoop but does not reset; a + * follow-up loop would return immediately. Confirm by asking. */ + MUST(XtAppGetExitFlag(app), "events", + "XtAppGetExitFlag is False after XtAppSetExitFlag in callback"); + OK("events"); +} + +/* ------------------------------------------------------------- callbacks */ + +static int cb_calls = 0; +static XtPointer cb_last_call_data = (XtPointer) (-1); + +static void cb_proc(Widget w, XtPointer client_data, XtPointer call_data) +{ + (void) w; + (void) client_data; + cb_calls++; + cb_last_call_data = call_data; +} + +static void test_callbacks(Widget shell) +{ + cb_calls = 0; + cb_last_call_data = (XtPointer) (-1); + + XtAddCallback(shell, XtNdestroyCallback, cb_proc, (XtPointer) 0x1234); + XtCallCallbacks(shell, XtNdestroyCallback, (XtPointer) 0xBEEF); + MUST(cb_calls == 1, "callbacks", "first call fired %d times, expected 1", + cb_calls); + MUST(cb_last_call_data == (XtPointer) 0xBEEF, "callbacks", + "call_data passthrough: got %p expected 0xBEEF", cb_last_call_data); + + /* Removing the same (proc, client_data) pair makes a follow-up + * CallCallbacks a no-op. */ + XtRemoveCallback(shell, XtNdestroyCallback, cb_proc, (XtPointer) 0x1234); + XtCallCallbacks(shell, XtNdestroyCallback, NULL); + MUST(cb_calls == 1, "callbacks", "callback fired after remove (now %d)", + cb_calls); + + OK("callbacks"); +} + +/* -------------------------------------------------------------------- gc */ + +static void test_gc(Widget shell) +{ + XGCValues values; + memset(&values, 0, sizeof(values)); + values.foreground = 0x000000; + values.background = 0xFFFFFF; + + GC gc1 = XtAllocateGC(shell, 0, GCForeground | GCBackground, &values, 0, 0); + MUST(gc1 != NULL, "gc", "XtAllocateGC returned NULL"); + + /* Same widget + same value mask + same values should hit the shared + * GC cache. We do not strictly require pointer equality (libXt is + * allowed to return a fresh GC), but the allocation must succeed. */ + GC gc2 = XtAllocateGC(shell, 0, GCForeground | GCBackground, &values, 0, 0); + MUST(gc2 != NULL, "gc", "second XtAllocateGC returned NULL"); + + /* XtReleaseGC must accept GCs allocated via XtAllocateGC without + * crashing the GC cache bookkeeping. */ + XtReleaseGC(shell, gc1); + XtReleaseGC(shell, gc2); + + OK("gc"); +} + +/* --------------------------------------------------------- resources */ + +/* Fallback resources are merged into the per-screen database the first + * time XtScreenDatabase runs, then cached. Set them before XtOpenDisplay + * in main() (not here) so the database we query is the one that includes + * them. The class name "MicroLibxtTier1" is unique enough that no real + * X11 app-defaults file is going to shadow it. */ +static String fallback_resources[] = { + "*microLibxtTier1.smokeProbeValue: ResourceBridgeOk", + NULL, +}; + +static void test_resources(Widget shell) +{ + /* Look up the fallback through the screen-level database the same way + * any Xt widget initialization would. This is the path Motif uses for + * every widget resource and the one that crashes hardest when the + * XrmQGetSearchResource bridge regresses. */ + XrmDatabase db = XtScreenDatabase(XtScreen(shell)); + MUST(db != NULL, "resources", "XtScreenDatabase NULL"); + + char *type = NULL; + XrmValue value; + memset(&value, 0, sizeof(value)); + Bool found = + XrmGetResource(db, "microLibxtTier1.smokeProbeValue", + "MicroLibxtTier1.SmokeProbeValue", &type, &value); + MUST(found, "resources", + "fallback resource not found through XtScreenDatabase"); + MUST(value.addr != NULL && value.size > 0, "resources", + "fallback value pointer/size empty"); + MUST( + strncmp((const char *) value.addr, "ResourceBridgeOk", value.size) == 0, + "resources", "fallback value mismatch: got %.*s", (int) value.size, + (const char *) value.addr); + + OK("resources"); +} + +/* ----------------------------------------------------------------- main */ + +int main(int argc, char *argv[]) +{ + if (!getenv("SDL_VIDEODRIVER")) + setenv("SDL_VIDEODRIVER", "dummy", 1); + + /* Subsystems below this point all need a live application context. */ + XtAppContext app; + int local_argc = 1; + char *local_argv[] = {(char *) "test_libxt_micro", NULL}; + + XtToolkitInitialize(); + app = XtCreateApplicationContext(); + MUST(app != NULL, "init", "XtCreateApplicationContext returned NULL"); + + /* Fallbacks must be set before the first XtScreenDatabase build, which + * happens transitively through XtAppCreateShell. Otherwise the cached + * per-screen database skips the fallback merge entirely. */ + XtAppSetFallbackResources(app, fallback_resources); + + Display *dpy = + XtOpenDisplay(app, NULL, "microLibxtTier1", "MicroLibxtTier1", NULL, 0, + &local_argc, local_argv); + MUST(dpy != NULL, "init", "XtOpenDisplay returned NULL"); + + Widget shell = XtAppCreateShell("microLibxtTier1", "MicroLibxtTier1", + applicationShellWidgetClass, dpy, NULL, 0); + MUST(shell != NULL, "init", "XtAppCreateShell returned NULL"); + + XtVaSetValues(shell, XtNwidth, 64, XtNheight, 64, NULL); + XtRealizeWidget(shell); + MUST(XtIsRealized(shell), "init", "shell did not realize"); + + test_keysyms(); + test_events(app); + test_callbacks(shell); + test_gc(shell); + test_resources(shell); + + /* Hold off destroy until the shell-using tests finish so a callback + * accidentally triggered during teardown does not corrupt cb_calls. */ + XtDestroyWidget(shell); + XtCloseDisplay(dpy); + XtDestroyApplicationContext(app); + + puts("test_libxt_micro: all subtests ok"); + (void) argc; + (void) argv; + return 0; +}