Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
nixos/xserver: Properly validate XKB options
Checking the keyboard layout has been a long set of hurdles so far, with several attempts. Originally, the checking was introduced by @lheckemann in #23709. The initial implementation just was trying to check whether the symbols/ directory contained the layout name. Unfortunately, that wasn't enough and keyboard variants weren't recognized, so if you set layout to eg. "dvorak" it will fail with an error (#25526). So my improvement on that was to use sed to filter rules/base.lst and match the layout against that. I fucked up twice with this, first because layout can be a comma-separated list which I didn't account for and second because I ran into a Nix issue (NixOS/nix#1426). After fixing this, it still wasn't enough (and this is btw. what localectl also does), because we were *only* matching rules but not symbols, so using "eu" as a layout won't work either. I decided now it's the time to actually use libxkbcommon to try compiling the keyboard options and see whether it succeeds. This comes in the form of a helper tool called xkbvalidate. IMHO this approach is a lot less error-prone and we can be sure that we don't forget about anything because that's what the X server itself uses to compile the keymap. Another advantage of this is that we now validate the full set of XKB options rather than just the layout. Tested this against a variety of wrong and correct keyboard configurations and against the "keymap" NixOS VM tests. Signed-off-by: aszlig <aszlig@redmoonstudios.org> Cc: @lheckemann, @peti, @7c6f434c, @tohl, @vcunat, @lluchs Fixes: #27597
- Loading branch information
Showing
4 changed files
with
156 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ lib, runCommandCC, libxkbcommon }: | ||
|
||
runCommandCC "xkbvalidate" { | ||
buildInputs = [ libxkbcommon ]; | ||
meta = { | ||
description = "NixOS tool to validate X keyboard configuration"; | ||
license = lib.licenses.mit; | ||
platforms = lib.platforms.linux; | ||
maintainers = [ lib.maintainers.aszlig ]; | ||
}; | ||
} '' | ||
mkdir -p "$out/bin" | ||
gcc -std=gnu11 -Wall -pedantic -lxkbcommon ${./xkbvalidate.c} \ | ||
-o "$out/bin/validate" | ||
'' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
#define _GNU_SOURCE | ||
#include <stdarg.h> | ||
#include <stdbool.h> | ||
#include <stdio.h> | ||
#include <stdlib.h> | ||
#include <string.h> | ||
|
||
#include <xkbcommon/xkbcommon.h> | ||
|
||
static char **log_buffer = NULL; | ||
static int log_buffer_size = 0; | ||
static bool log_alloc_success = true; | ||
|
||
static void add_log(struct xkb_context *ctx, enum xkb_log_level level, | ||
const char *fmt, va_list args) | ||
{ | ||
log_buffer_size++; | ||
|
||
if (log_buffer == NULL) | ||
log_buffer = malloc(sizeof(char *)); | ||
else | ||
log_buffer = realloc(log_buffer, sizeof(char *) * log_buffer_size); | ||
|
||
if (log_buffer == NULL) { | ||
perror("buffer alloc"); | ||
log_alloc_success = false; | ||
log_buffer_size--; | ||
return; | ||
} | ||
|
||
if (vasprintf(&log_buffer[log_buffer_size - 1], fmt, args) == -1) { | ||
perror("log line alloc"); | ||
log_alloc_success = false; | ||
return; | ||
} | ||
} | ||
|
||
static void print_logs(void) | ||
{ | ||
for (int i = 0; i < log_buffer_size; ++i) | ||
fprintf(stderr, " %s", log_buffer[i]); | ||
} | ||
|
||
static void free_logs(void) | ||
{ | ||
if (log_buffer == NULL) | ||
return; | ||
for (int i = 0; i < log_buffer_size; ++i) | ||
free(log_buffer[i]); | ||
free(log_buffer); | ||
log_buffer = NULL; | ||
log_buffer_size = 0; | ||
} | ||
|
||
static bool try_keymap(struct xkb_context *ctx, struct xkb_rule_names *rdef) | ||
{ | ||
struct xkb_keymap *keymap; | ||
bool result = true; | ||
|
||
if ((keymap = xkb_keymap_new_from_names(ctx, rdef, 0)) == NULL) | ||
result = false; | ||
else | ||
xkb_keymap_unref(keymap); | ||
|
||
return result; | ||
} | ||
|
||
static void print_error(const char *name, const char *value, | ||
const char *nixos_option) | ||
{ | ||
fprintf(stderr, "\nThe value `%s' for keyboard %s is invalid.\n\n" | ||
"Please check the definition in `services.xserver.%s'.\n", | ||
value, name, nixos_option); | ||
fputs("\nDetailed XKB compiler errors:\n\n", stderr); | ||
print_logs(); | ||
putc('\n', stderr); | ||
} | ||
|
||
#define TRY_KEYMAP(name, value, nixos_option) \ | ||
*rdef = (struct xkb_rule_names) {0}; \ | ||
free_logs(); \ | ||
rdef->name = value; \ | ||
result = try_keymap(ctx, rdef); \ | ||
if (!log_alloc_success) \ | ||
goto out; \ | ||
if (!result) { \ | ||
print_error(#name, value, nixos_option); \ | ||
exit_code = EXIT_FAILURE; \ | ||
goto out; \ | ||
} | ||
|
||
int main(int argc, char **argv) | ||
{ | ||
int exit_code = EXIT_SUCCESS; | ||
bool result; | ||
struct xkb_context *ctx; | ||
struct xkb_rule_names *rdef; | ||
|
||
if (argc != 5) { | ||
fprintf(stderr, "Usage: %s model layout variant options\n", argv[0]); | ||
return EXIT_FAILURE; | ||
} | ||
|
||
ctx = xkb_context_new(XKB_CONTEXT_NO_ENVIRONMENT_NAMES); | ||
xkb_context_set_log_fn(ctx, add_log); | ||
|
||
rdef = malloc(sizeof(struct xkb_rule_names)); | ||
|
||
TRY_KEYMAP(model, argv[1], "xkbModel"); | ||
TRY_KEYMAP(layout, argv[2], "layout"); | ||
TRY_KEYMAP(variant, argv[3], "xkbVariant"); | ||
TRY_KEYMAP(options, argv[4], "xkbOptions"); | ||
|
||
free_logs(); | ||
rdef->model = argv[1]; | ||
rdef->layout = argv[2]; | ||
rdef->variant = argv[3]; | ||
rdef->options = argv[4]; | ||
|
||
result = try_keymap(ctx, rdef); | ||
if (!log_alloc_success) | ||
goto out; | ||
|
||
if (!result) { | ||
fputs("The XKB keyboard definition failed to compile:\n", stderr); | ||
print_logs(); | ||
exit_code = EXIT_FAILURE; | ||
} | ||
|
||
out: | ||
free_logs(); | ||
free(rdef); | ||
xkb_context_unref(ctx); | ||
return exit_code; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6e5d2f8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, the amount of NixOS-specific work just to validate keyboard config :-)
6e5d2f8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it isn't so NixOS-specific after all, other distros pretty much validate it at runtime by "starting the xserver".
6e5d2f8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We didn't do that?
6e5d2f8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vcunat: Sure, but I think it's better to bail out at build time if the keyboard config is wrong rather than having a broken X =)
6e5d2f8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Certainly, I didn't mean to doubt that.
6e5d2f8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was only broken in the sense that the keyboard layout was wrong, but it was certainly confusing. FWIW, I like this solution, it feels like The Right Way™. Thanks @aszlig!