summaryrefslogtreecommitdiffstats
path: root/src/locale
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/locale/kbd-model-map72
-rw-r--r--src/locale/language-fallback-map13
-rw-r--r--src/locale/localectl.c535
-rw-r--r--src/locale/localed-util.c1161
-rw-r--r--src/locale/localed-util.h80
-rw-r--r--src/locale/localed.c680
-rw-r--r--src/locale/meson.build62
-rw-r--r--src/locale/org.freedesktop.locale1.conf29
-rw-r--r--src/locale/org.freedesktop.locale1.policy42
-rw-r--r--src/locale/org.freedesktop.locale1.service14
-rw-r--r--src/locale/test-localed-util.c236
-rw-r--r--src/locale/xkbcommon-util.c80
-rw-r--r--src/locale/xkbcommon-util.h30
13 files changed, 3034 insertions, 0 deletions
diff --git a/src/locale/kbd-model-map b/src/locale/kbd-model-map
new file mode 100644
index 0000000..279d1a3
--- /dev/null
+++ b/src/locale/kbd-model-map
@@ -0,0 +1,72 @@
+# Originally generated from system-config-keyboard's model list.
+# consolelayout xlayout xmodel xvariant xoptions
+sg ch pc105 de_nodeadkeys terminate:ctrl_alt_bksp
+nl nl pc105 - terminate:ctrl_alt_bksp
+mk-utf mk,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
+trq tr pc105 - terminate:ctrl_alt_bksp
+uk gb pc105 - terminate:ctrl_alt_bksp
+is-latin1 is pc105 - terminate:ctrl_alt_bksp
+de de pc105 - terminate:ctrl_alt_bksp
+la-latin1 latam pc105 - terminate:ctrl_alt_bksp
+us us pc105+inet - terminate:ctrl_alt_bksp
+ko kr pc105 - terminate:ctrl_alt_bksp
+ro-std ro pc105 std terminate:ctrl_alt_bksp
+de-latin1 de pc105 - terminate:ctrl_alt_bksp
+slovene si pc105 - terminate:ctrl_alt_bksp
+hu hu pc105 - terminate:ctrl_alt_bksp
+jp106 jp jp106 - terminate:ctrl_alt_bksp
+croat hr pc105 - terminate:ctrl_alt_bksp
+it2 it pc105 - terminate:ctrl_alt_bksp
+hu101 hu pc105 qwerty terminate:ctrl_alt_bksp
+sr-latin rs pc105 latin terminate:ctrl_alt_bksp
+fi fi pc105 - terminate:ctrl_alt_bksp
+fr_CH ch pc105 fr terminate:ctrl_alt_bksp
+dk-latin1 dk pc105 - terminate:ctrl_alt_bksp
+fr fr pc105 - terminate:ctrl_alt_bksp
+it it pc105 - terminate:ctrl_alt_bksp
+ua-utf ua,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
+fr-latin1 fr pc105 - terminate:ctrl_alt_bksp
+sg-latin1 ch pc105 de_nodeadkeys terminate:ctrl_alt_bksp
+be-latin1 be pc105 - terminate:ctrl_alt_bksp
+dk dk pc105 - terminate:ctrl_alt_bksp
+fr-pc fr pc105 - terminate:ctrl_alt_bksp
+bg_pho-utf8 bg,us pc105 ,phonetic terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
+it-ibm it pc105 - terminate:ctrl_alt_bksp
+cz-us-qwertz cz,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
+cz-qwerty cz,us pc105 qwerty, terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
+br-abnt2 br abnt2 - terminate:ctrl_alt_bksp
+ro ro pc105 - terminate:ctrl_alt_bksp
+us-acentos us pc105 intl terminate:ctrl_alt_bksp
+pt-latin1 pt pc105 - terminate:ctrl_alt_bksp
+ro-std-cedilla ro pc105 std_cedilla terminate:ctrl_alt_bksp
+tj_alt-UTF8 tj pc105 - terminate:ctrl_alt_bksp
+de-latin1-nodeadkeys de pc105 nodeadkeys terminate:ctrl_alt_bksp
+no no pc105 - terminate:ctrl_alt_bksp
+bg_bds-utf8 bg,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
+dvorak us pc105 dvorak terminate:ctrl_alt_bksp
+dvorak us pc105 dvorak-alt-intl terminate:ctrl_alt_bksp
+ru ru,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
+cz-lat2 cz pc105 qwerty terminate:ctrl_alt_bksp
+pl2 pl pc105 - terminate:ctrl_alt_bksp
+es es pc105 - terminate:ctrl_alt_bksp
+ro-cedilla ro pc105 cedilla terminate:ctrl_alt_bksp
+ie ie pc105 - terminate:ctrl_alt_bksp
+et ee pc105 - terminate:ctrl_alt_bksp
+sk-qwerty sk pc105 qwerty terminate:ctrl_alt_bksp
+sk-qwertz sk pc105 - terminate:ctrl_alt_bksp
+fr-latin9 fr pc105 latin9 terminate:ctrl_alt_bksp
+fr_CH-latin1 ch pc105 fr terminate:ctrl_alt_bksp
+cf ca pc105 - terminate:ctrl_alt_bksp
+sv-latin1 se pc105 - terminate:ctrl_alt_bksp
+sr-cy rs pc105 - terminate:ctrl_alt_bksp
+gr gr,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
+by by,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
+il il pc105 - terminate:ctrl_alt_bksp
+kazakh kz,us pc105 - terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
+lt.baltic lt pc105 - terminate:ctrl_alt_bksp
+lt.l4 lt pc105 - terminate:ctrl_alt_bksp
+lt lt pc105 - terminate:ctrl_alt_bksp
+khmer kh,us pc105 - terminate:ctrl_alt_bksp
+es-dvorak es microsoftpro dvorak terminate:ctrl_alt_bksp
+lv lv pc105 apostrophe terminate:ctrl_alt_bksp
+lv-tilde lv pc105 tilde terminate:ctrl_alt_bksp
diff --git a/src/locale/language-fallback-map b/src/locale/language-fallback-map
new file mode 100644
index 0000000..d0b02a6
--- /dev/null
+++ b/src/locale/language-fallback-map
@@ -0,0 +1,13 @@
+csb_PL csb:pl
+en_AU en_AU:en_GB
+en_IE en_IE:en_GB
+en_NZ en_NZ:en_GB
+en_ZA en_ZA:en_GB
+fr_BE fr_BE:fr_FR
+fr_CA fr_CA:fr_FR
+fr_CH fr_CH:fr_FR
+fr_LU fr_LU:fr_FR
+it_CH it_CH:it_IT
+mai_IN mai:hi
+nds_DE nds:de
+szl_PL szl:pl
diff --git a/src/locale/localectl.c b/src/locale/localectl.c
new file mode 100644
index 0000000..3235402
--- /dev/null
+++ b/src/locale/localectl.c
@@ -0,0 +1,535 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <stdbool.h>
+
+#include "sd-bus.h"
+
+#include "build.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "bus-map-properties.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-table.h"
+#include "kbd-util.h"
+#include "locale-setup.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "pager.h"
+#include "pretty-print.h"
+#include "proc-cmdline.h"
+#include "set.h"
+#include "spawn-polkit-agent.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "verbs.h"
+#include "virt.h"
+
+/* Enough time for locale-gen to finish server-side (in case it is in use) */
+#define LOCALE_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE)
+
+static PagerFlags arg_pager_flags = 0;
+static bool arg_ask_password = true;
+static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
+static const char *arg_host = NULL;
+static bool arg_convert = true;
+
+typedef struct StatusInfo {
+ char **locale;
+ const char *vconsole_keymap;
+ const char *vconsole_keymap_toggle;
+ const char *x11_layout;
+ const char *x11_model;
+ const char *x11_variant;
+ const char *x11_options;
+} StatusInfo;
+
+static void status_info_clear(StatusInfo *info) {
+ if (info) {
+ strv_free(info->locale);
+ zero(*info);
+ }
+}
+
+static int print_status_info(StatusInfo *i) {
+ _cleanup_strv_free_ char **kernel_locale = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ TableCell *cell;
+ int r;
+
+ assert(i);
+
+ if (arg_transport == BUS_TRANSPORT_LOCAL) {
+ _cleanup_(locale_context_clear) LocaleContext c = {};
+
+ r = locale_context_load(&c, LOCALE_LOAD_PROC_CMDLINE);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read /proc/cmdline: %m");
+
+ r = locale_context_build_env(&c, &kernel_locale, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build locale settings from kernel command line: %m");
+ }
+
+ table = table_new_vertical();
+ if (!table)
+ return log_oom();
+
+ assert_se(cell = table_get_cell(table, 0, 0));
+ (void) table_set_ellipsize_percent(table, cell, 100);
+
+ table_set_ersatz_string(table, TABLE_ERSATZ_UNSET);
+
+ if (!strv_isempty(kernel_locale)) {
+ log_warning("Warning: Settings on kernel command line override system locale settings in /etc/locale.conf.");
+ r = table_add_many(table,
+ TABLE_FIELD, "Command Line",
+ TABLE_SET_COLOR, ansi_highlight_yellow(),
+ TABLE_STRV, kernel_locale,
+ TABLE_SET_COLOR, ansi_highlight_yellow());
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_many(table,
+ TABLE_FIELD, "System Locale",
+ TABLE_STRV, i->locale,
+ TABLE_FIELD, "VC Keymap",
+ TABLE_STRING, i->vconsole_keymap);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (!isempty(i->vconsole_keymap_toggle)) {
+ r = table_add_many(table,
+ TABLE_FIELD, "VC Toggle Keymap",
+ TABLE_STRING, i->vconsole_keymap_toggle);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_many(table,
+ TABLE_FIELD, "X11 Layout",
+ TABLE_STRING, i->x11_layout);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (!isempty(i->x11_model)) {
+ r = table_add_many(table,
+ TABLE_FIELD, "X11 Model",
+ TABLE_STRING, i->x11_model);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (!isempty(i->x11_variant)) {
+ r = table_add_many(table,
+ TABLE_FIELD, "X11 Variant",
+ TABLE_STRING, i->x11_variant);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (!isempty(i->x11_options)) {
+ r = table_add_many(table,
+ TABLE_FIELD, "X11 Options",
+ TABLE_STRING, i->x11_options);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return table_log_print_error(r);
+
+ return 0;
+}
+
+static int show_status(int argc, char **argv, void *userdata) {
+ _cleanup_(status_info_clear) StatusInfo info = {};
+ static const struct bus_properties_map map[] = {
+ { "VConsoleKeymap", "s", NULL, offsetof(StatusInfo, vconsole_keymap) },
+ { "VConsoleKeymapToggle", "s", NULL, offsetof(StatusInfo, vconsole_keymap_toggle) },
+ { "X11Layout", "s", NULL, offsetof(StatusInfo, x11_layout) },
+ { "X11Model", "s", NULL, offsetof(StatusInfo, x11_model) },
+ { "X11Variant", "s", NULL, offsetof(StatusInfo, x11_variant) },
+ { "X11Options", "s", NULL, offsetof(StatusInfo, x11_options) },
+ { "Locale", "as", NULL, offsetof(StatusInfo, locale) },
+ {}
+ };
+
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ sd_bus *bus = ASSERT_PTR(userdata);
+ int r;
+
+ r = bus_map_all_properties(bus,
+ "org.freedesktop.locale1",
+ "/org/freedesktop/locale1",
+ map,
+ 0,
+ &error,
+ &m,
+ &info);
+ if (r < 0)
+ return log_error_errno(r, "Could not get properties: %s", bus_error_message(&error, r));
+
+ return print_status_info(&info);
+}
+
+static int set_locale(int argc, char **argv, void *userdata) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ sd_bus *bus = ASSERT_PTR(userdata);
+ int r;
+
+ polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ r = bus_message_new_method_call(bus, &m, bus_locale, "SetLocale");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_strv(m, argv + 1);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "b", arg_ask_password);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ /* We use a longer timeout for the method call in case localed is running locale-gen */
+ r = sd_bus_call(bus, m, LOCALE_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to issue method call: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int list_locales(int argc, char **argv, void *userdata) {
+ _cleanup_strv_free_ char **l = NULL;
+ int r;
+
+ r = get_locales(&l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read list of locales: %m");
+
+ pager_open(arg_pager_flags);
+ strv_print(l);
+
+ return 0;
+}
+
+static int set_vconsole_keymap(int argc, char **argv, void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ const char *map, *toggle_map;
+ sd_bus *bus = ASSERT_PTR(userdata);
+ int r;
+
+ polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ map = argv[1];
+ toggle_map = argc > 2 ? argv[2] : "";
+
+ r = bus_call_method(
+ bus,
+ bus_locale,
+ "SetVConsoleKeyboard",
+ &error,
+ NULL,
+ "ssbb", map, toggle_map, arg_convert, arg_ask_password);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set keymap: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int list_vconsole_keymaps(int argc, char **argv, void *userdata) {
+ _cleanup_strv_free_ char **l = NULL;
+ int r;
+
+ r = get_keymaps(&l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read list of keymaps: %m");
+
+ pager_open(arg_pager_flags);
+
+ strv_print(l);
+
+ return 0;
+}
+
+static int set_x11_keymap(int argc, char **argv, void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ const char *layout, *model, *variant, *options;
+ sd_bus *bus = userdata;
+ int r;
+
+ polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ layout = argv[1];
+ model = argc > 2 ? argv[2] : "";
+ variant = argc > 3 ? argv[3] : "";
+ options = argc > 4 ? argv[4] : "";
+
+ r = bus_call_method(
+ bus,
+ bus_locale,
+ "SetX11Keyboard",
+ &error,
+ NULL,
+ "ssssbb", layout, model, variant, options,
+ arg_convert, arg_ask_password);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set keymap: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int list_x11_keymaps(int argc, char **argv, void *userdata) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_strv_free_ char **list = NULL;
+ enum {
+ NONE,
+ MODELS,
+ LAYOUTS,
+ VARIANTS,
+ OPTIONS
+ } state = NONE, look_for;
+ int r;
+
+ f = fopen("/usr/share/X11/xkb/rules/base.lst", "re");
+ if (!f)
+ return log_error_errno(errno, "Failed to open keyboard mapping list. %m");
+
+ if (streq(argv[0], "list-x11-keymap-models"))
+ look_for = MODELS;
+ else if (streq(argv[0], "list-x11-keymap-layouts"))
+ look_for = LAYOUTS;
+ else if (streq(argv[0], "list-x11-keymap-variants"))
+ look_for = VARIANTS;
+ else if (streq(argv[0], "list-x11-keymap-options"))
+ look_for = OPTIONS;
+ else
+ assert_not_reached();
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+ char *w;
+
+ r = read_stripped_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read keyboard mapping list: %m");
+ if (r == 0)
+ break;
+
+ if (isempty(line))
+ continue;
+
+ if (line[0] == '!') {
+ if (startswith(line, "! model"))
+ state = MODELS;
+ else if (startswith(line, "! layout"))
+ state = LAYOUTS;
+ else if (startswith(line, "! variant"))
+ state = VARIANTS;
+ else if (startswith(line, "! option"))
+ state = OPTIONS;
+ else
+ state = NONE;
+
+ continue;
+ }
+
+ if (state != look_for)
+ continue;
+
+ w = line + strcspn(line, WHITESPACE);
+
+ if (argc > 1) {
+ char *e;
+
+ if (*w == 0)
+ continue;
+
+ *w = 0;
+ w++;
+ w += strspn(w, WHITESPACE);
+
+ e = strchr(w, ':');
+ if (!e)
+ continue;
+
+ *e = 0;
+
+ if (!streq(w, argv[1]))
+ continue;
+ } else
+ *w = 0;
+
+ if (strv_consume(&list, TAKE_PTR(line)) < 0)
+ return log_oom();
+ }
+
+ if (strv_isempty(list))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+ "Couldn't find any entries.");
+
+ strv_sort(list);
+ strv_uniq(list);
+
+ pager_open(arg_pager_flags);
+
+ strv_print(list);
+ return 0;
+}
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("localectl", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...] COMMAND ...\n\n"
+ "%sQuery or change system locale and keyboard settings.%s\n"
+ "\nCommands:\n"
+ " status Show current locale settings\n"
+ " set-locale LOCALE... Set system locale\n"
+ " list-locales Show known locales\n"
+ " set-keymap MAP [MAP] Set console and X11 keyboard mappings\n"
+ " list-keymaps Show known virtual console keyboard mappings\n"
+ " set-x11-keymap LAYOUT [MODEL [VARIANT [OPTIONS]]]\n"
+ " Set X11 and console keyboard mappings\n"
+ " list-x11-keymap-models Show known X11 keyboard mapping models\n"
+ " list-x11-keymap-layouts Show known X11 keyboard mapping layouts\n"
+ " list-x11-keymap-variants [LAYOUT]\n"
+ " Show known X11 keyboard mapping variants\n"
+ " list-x11-keymap-options Show known X11 keyboard mapping options\n"
+ "\nOptions:\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-ask-password Do not prompt for password\n"
+ " -H --host=[USER@]HOST Operate on remote host\n"
+ " -M --machine=CONTAINER Operate on local container\n"
+ " --no-convert Don't convert keyboard mappings\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ ansi_highlight(),
+ ansi_normal(),
+ link);
+
+ return 0;
+}
+
+static int verb_help(int argc, char **argv, void *userdata) {
+ return help();
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_CONVERT,
+ ARG_NO_ASK_PASSWORD
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "host", required_argument, NULL, 'H' },
+ { "machine", required_argument, NULL, 'M' },
+ { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD },
+ { "no-convert", no_argument, NULL, ARG_NO_CONVERT },
+ {}
+ };
+
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "hH:M:", options, NULL)) >= 0)
+
+ switch (c) {
+
+ case 'h':
+ return help();
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_CONVERT:
+ arg_convert = false;
+ break;
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_NO_ASK_PASSWORD:
+ arg_ask_password = false;
+ break;
+
+ case 'H':
+ arg_transport = BUS_TRANSPORT_REMOTE;
+ arg_host = optarg;
+ break;
+
+ case 'M':
+ arg_transport = BUS_TRANSPORT_MACHINE;
+ arg_host = optarg;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int localectl_main(sd_bus *bus, int argc, char *argv[]) {
+
+ static const Verb verbs[] = {
+ { "status", VERB_ANY, 1, VERB_DEFAULT, show_status },
+ { "set-locale", 2, VERB_ANY, 0, set_locale },
+ { "list-locales", VERB_ANY, 1, 0, list_locales },
+ { "set-keymap", 2, 3, 0, set_vconsole_keymap },
+ { "list-keymaps", VERB_ANY, 1, 0, list_vconsole_keymaps },
+ { "set-x11-keymap", 2, 5, 0, set_x11_keymap },
+ { "list-x11-keymap-models", VERB_ANY, 1, 0, list_x11_keymaps },
+ { "list-x11-keymap-layouts", VERB_ANY, 1, 0, list_x11_keymaps },
+ { "list-x11-keymap-variants", VERB_ANY, 2, 0, list_x11_keymaps },
+ { "list-x11-keymap-options", VERB_ANY, 1, 0, list_x11_keymaps },
+ { "help", VERB_ANY, VERB_ANY, 0, verb_help }, /* Not documented, but supported since it is created. */
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, bus);
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ setlocale(LC_ALL, "");
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ r = bus_connect_transport(arg_transport, arg_host, RUNTIME_SCOPE_SYSTEM, &bus);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ return localectl_main(bus, argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/locale/localed-util.c b/src/locale/localed-util.c
new file mode 100644
index 0000000..e4e57a0
--- /dev/null
+++ b/src/locale/localed-util.c
@@ -0,0 +1,1161 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "bus-polkit.h"
+#include "copy.h"
+#include "env-file-label.h"
+#include "env-file.h"
+#include "env-util.h"
+#include "fd-util.h"
+#include "fileio-label.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "kbd-util.h"
+#include "localed-util.h"
+#include "macro.h"
+#include "mkdir-label.h"
+#include "nulstr-util.h"
+#include "process-util.h"
+#include "stat-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+#include "xkbcommon-util.h"
+
+static bool startswith_comma(const char *s, const char *prefix) {
+ assert(s);
+ assert(prefix);
+
+ s = startswith(s, prefix);
+ if (!s)
+ return false;
+
+ return IN_SET(*s, ',', '\0');
+}
+
+static const char* systemd_kbd_model_map(void) {
+ const char* s;
+
+ s = getenv("SYSTEMD_KBD_MODEL_MAP");
+ if (s)
+ return s;
+
+ return SYSTEMD_KBD_MODEL_MAP;
+}
+
+static const char* systemd_language_fallback_map(void) {
+ const char* s;
+
+ s = getenv("SYSTEMD_LANGUAGE_FALLBACK_MAP");
+ if (s)
+ return s;
+
+ return SYSTEMD_LANGUAGE_FALLBACK_MAP;
+}
+
+void x11_context_clear(X11Context *xc) {
+ assert(xc);
+
+ xc->layout = mfree(xc->layout);
+ xc->options = mfree(xc->options);
+ xc->model = mfree(xc->model);
+ xc->variant = mfree(xc->variant);
+}
+
+void x11_context_replace(X11Context *dest, X11Context *src) {
+ assert(dest);
+ assert(src);
+
+ x11_context_clear(dest);
+ *dest = TAKE_STRUCT(*src);
+}
+
+bool x11_context_isempty(const X11Context *xc) {
+ assert(xc);
+
+ return
+ isempty(xc->layout) &&
+ isempty(xc->model) &&
+ isempty(xc->variant) &&
+ isempty(xc->options);
+}
+
+void x11_context_empty_to_null(X11Context *xc) {
+ assert(xc);
+
+ /* Do not call x11_context_clear() for the passed object. */
+
+ xc->layout = empty_to_null(xc->layout);
+ xc->model = empty_to_null(xc->model);
+ xc->variant = empty_to_null(xc->variant);
+ xc->options = empty_to_null(xc->options);
+}
+
+bool x11_context_is_safe(const X11Context *xc) {
+ assert(xc);
+
+ return
+ (!xc->layout || string_is_safe(xc->layout)) &&
+ (!xc->model || string_is_safe(xc->model)) &&
+ (!xc->variant || string_is_safe(xc->variant)) &&
+ (!xc->options || string_is_safe(xc->options));
+}
+
+bool x11_context_equal(const X11Context *a, const X11Context *b) {
+ assert(a);
+ assert(b);
+
+ return
+ streq_ptr(a->layout, b->layout) &&
+ streq_ptr(a->model, b->model) &&
+ streq_ptr(a->variant, b->variant) &&
+ streq_ptr(a->options, b->options);
+}
+
+int x11_context_copy(X11Context *dest, const X11Context *src) {
+ bool modified;
+ int r;
+
+ assert(dest);
+
+ if (dest == src)
+ return 0;
+
+ if (!src) {
+ modified = !x11_context_isempty(dest);
+ x11_context_clear(dest);
+ return modified;
+ }
+
+ r = free_and_strdup(&dest->layout, src->layout);
+ if (r < 0)
+ return r;
+ modified = r > 0;
+
+ r = free_and_strdup(&dest->model, src->model);
+ if (r < 0)
+ return r;
+ modified = modified || r > 0;
+
+ r = free_and_strdup(&dest->variant, src->variant);
+ if (r < 0)
+ return r;
+ modified = modified || r > 0;
+
+ r = free_and_strdup(&dest->options, src->options);
+ if (r < 0)
+ return r;
+ modified = modified || r > 0;
+
+ return modified;
+}
+
+int x11_context_verify_and_warn(const X11Context *xc, int log_level, sd_bus_error *error) {
+ int r;
+
+ assert(xc);
+
+ if (!x11_context_is_safe(xc)) {
+ if (error)
+ sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid X11 keyboard layout.");
+ return log_full_errno(log_level, SYNTHETIC_ERRNO(EINVAL), "Invalid X11 keyboard layout.");
+ }
+
+ r = verify_xkb_rmlvo(xc->model, xc->layout, xc->variant, xc->options);
+ if (r == -EOPNOTSUPP) {
+ log_full_errno(MAX(log_level, LOG_NOTICE), r,
+ "Cannot verify if new keymap is correct, libxkbcommon.so unavailable.");
+ return 0;
+ }
+ if (r < 0) {
+ if (error)
+ sd_bus_error_set_errnof(error, r, "Specified keymap cannot be compiled, refusing as invalid.");
+ return log_full_errno(log_level, r,
+ "Cannot compile XKB keymap for x11 keyboard layout "
+ "(model='%s' / layout='%s' / variant='%s' / options='%s'): %m",
+ strempty(xc->model), strempty(xc->layout), strempty(xc->variant), strempty(xc->options));
+ }
+
+ return 0;
+}
+
+void vc_context_clear(VCContext *vc) {
+ assert(vc);
+
+ vc->keymap = mfree(vc->keymap);
+ vc->toggle = mfree(vc->toggle);
+}
+
+void vc_context_replace(VCContext *dest, VCContext *src) {
+ assert(dest);
+ assert(src);
+
+ vc_context_clear(dest);
+ *dest = TAKE_STRUCT(*src);
+}
+
+bool vc_context_isempty(const VCContext *vc) {
+ assert(vc);
+
+ return
+ isempty(vc->keymap) &&
+ isempty(vc->toggle);
+}
+
+void vc_context_empty_to_null(VCContext *vc) {
+ assert(vc);
+
+ /* Do not call vc_context_clear() for the passed object. */
+
+ vc->keymap = empty_to_null(vc->keymap);
+ vc->toggle = empty_to_null(vc->toggle);
+}
+
+bool vc_context_equal(const VCContext *a, const VCContext *b) {
+ assert(a);
+ assert(b);
+
+ return
+ streq_ptr(a->keymap, b->keymap) &&
+ streq_ptr(a->toggle, b->toggle);
+}
+
+int vc_context_copy(VCContext *dest, const VCContext *src) {
+ bool modified;
+ int r;
+
+ assert(dest);
+
+ if (dest == src)
+ return 0;
+
+ if (!src) {
+ modified = !vc_context_isempty(dest);
+ vc_context_clear(dest);
+ return modified;
+ }
+
+ r = free_and_strdup(&dest->keymap, src->keymap);
+ if (r < 0)
+ return r;
+ modified = r > 0;
+
+ r = free_and_strdup(&dest->toggle, src->toggle);
+ if (r < 0)
+ return r;
+ modified = modified || r > 0;
+
+ return modified;
+}
+
+static int verify_keymap(const char *keymap, int log_level, sd_bus_error *error) {
+ int r;
+
+ assert(keymap);
+
+ r = keymap_exists(keymap); /* This also verifies that the keymap name is kosher. */
+ if (r < 0) {
+ if (error)
+ sd_bus_error_set_errnof(error, r, "Failed to check keymap %s: %m", keymap);
+ return log_full_errno(log_level, r, "Failed to check keymap %s: %m", keymap);
+ }
+ if (r == 0) {
+ if (error)
+ sd_bus_error_setf(error, SD_BUS_ERROR_FAILED, "Keymap %s is not installed.", keymap);
+ return log_full_errno(log_level, SYNTHETIC_ERRNO(ENOENT), "Keymap %s is not installed.", keymap);
+ }
+
+ return 0;
+}
+
+int vc_context_verify_and_warn(const VCContext *vc, int log_level, sd_bus_error *error) {
+ int r;
+
+ assert(vc);
+
+ if (vc->keymap) {
+ r = verify_keymap(vc->keymap, log_level, error);
+ if (r < 0)
+ return r;
+ }
+
+ if (vc->toggle) {
+ r = verify_keymap(vc->toggle, log_level, error);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+void context_clear(Context *c) {
+ assert(c);
+
+ locale_context_clear(&c->locale_context);
+ x11_context_clear(&c->x11_from_xorg);
+ x11_context_clear(&c->x11_from_vc);
+ vc_context_clear(&c->vc);
+
+ c->locale_cache = sd_bus_message_unref(c->locale_cache);
+ c->x11_cache = sd_bus_message_unref(c->x11_cache);
+ c->vc_cache = sd_bus_message_unref(c->vc_cache);
+
+ c->polkit_registry = bus_verify_polkit_async_registry_free(c->polkit_registry);
+};
+
+X11Context *context_get_x11_context(Context *c) {
+ assert(c);
+
+ if (!x11_context_isempty(&c->x11_from_vc))
+ return &c->x11_from_vc;
+
+ if (!x11_context_isempty(&c->x11_from_xorg))
+ return &c->x11_from_xorg;
+
+ return &c->x11_from_vc;
+}
+
+int locale_read_data(Context *c, sd_bus_message *m) {
+ assert(c);
+
+ /* Do not try to re-read the file within single bus operation. */
+ if (m) {
+ if (m == c->locale_cache)
+ return 0;
+
+ sd_bus_message_unref(c->locale_cache);
+ c->locale_cache = sd_bus_message_ref(m);
+ }
+
+ return locale_context_load(&c->locale_context, LOCALE_LOAD_LOCALE_CONF | LOCALE_LOAD_ENVIRONMENT | LOCALE_LOAD_SIMPLIFY);
+}
+
+int vconsole_read_data(Context *c, sd_bus_message *m) {
+ _cleanup_close_ int fd = -EBADF;
+ struct stat st;
+ int r;
+
+ assert(c);
+
+ /* Do not try to re-read the file within single bus operation. */
+ if (m) {
+ if (m == c->vc_cache)
+ return 0;
+
+ sd_bus_message_unref(c->vc_cache);
+ c->vc_cache = sd_bus_message_ref(m);
+ }
+
+ fd = RET_NERRNO(open("/etc/vconsole.conf", O_CLOEXEC | O_PATH));
+ if (fd == -ENOENT) {
+ c->vc_stat = (struct stat) {};
+ vc_context_clear(&c->vc);
+ x11_context_clear(&c->x11_from_vc);
+ return 0;
+ }
+ if (fd < 0)
+ return fd;
+
+ if (fstat(fd, &st) < 0)
+ return -errno;
+
+ /* If the file is not changed, then we do not need to re-read */
+ if (stat_inode_unmodified(&c->vc_stat, &st))
+ return 0;
+
+ c->vc_stat = st;
+ vc_context_clear(&c->vc);
+ x11_context_clear(&c->x11_from_vc);
+
+ r = parse_env_file_fd(
+ fd, "/etc/vconsole.conf",
+ "KEYMAP", &c->vc.keymap,
+ "KEYMAP_TOGGLE", &c->vc.toggle,
+ "XKBLAYOUT", &c->x11_from_vc.layout,
+ "XKBMODEL", &c->x11_from_vc.model,
+ "XKBVARIANT", &c->x11_from_vc.variant,
+ "XKBOPTIONS", &c->x11_from_vc.options);
+ if (r < 0)
+ return r;
+
+ if (vc_context_verify(&c->vc) < 0)
+ vc_context_clear(&c->vc);
+
+ if (x11_context_verify(&c->x11_from_vc) < 0)
+ x11_context_clear(&c->x11_from_vc);
+
+ return 0;
+}
+
+int x11_read_data(Context *c, sd_bus_message *m) {
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_fclose_ FILE *f = NULL;
+ bool in_section = false;
+ struct stat st;
+ int r;
+
+ assert(c);
+
+ /* Do not try to re-read the file within single bus operation. */
+ if (m) {
+ if (m == c->x11_cache)
+ return 0;
+
+ sd_bus_message_unref(c->x11_cache);
+ c->x11_cache = sd_bus_message_ref(m);
+ }
+
+ fd = RET_NERRNO(open("/etc/X11/xorg.conf.d/00-keyboard.conf", O_CLOEXEC | O_PATH));
+ if (fd == -ENOENT) {
+ c->x11_stat = (struct stat) {};
+ x11_context_clear(&c->x11_from_xorg);
+ return 0;
+ }
+ if (fd < 0)
+ return fd;
+
+ if (fstat(fd, &st) < 0)
+ return -errno;
+
+ /* If the file is not changed, then we do not need to re-read */
+ if (stat_inode_unmodified(&c->x11_stat, &st))
+ return 0;
+
+ c->x11_stat = st;
+ x11_context_clear(&c->x11_from_xorg);
+
+ r = fdopen_independent(fd, "re", &f);
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+
+ r = read_stripped_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ if (IN_SET(line[0], 0, '#'))
+ continue;
+
+ if (in_section && first_word(line, "Option")) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ r = strv_split_full(&a, line, WHITESPACE, EXTRACT_UNQUOTE);
+ if (r < 0)
+ return r;
+
+ if (strv_length(a) == 3) {
+ char **p = NULL;
+
+ if (streq(a[1], "XkbLayout"))
+ p = &c->x11_from_xorg.layout;
+ else if (streq(a[1], "XkbModel"))
+ p = &c->x11_from_xorg.model;
+ else if (streq(a[1], "XkbVariant"))
+ p = &c->x11_from_xorg.variant;
+ else if (streq(a[1], "XkbOptions"))
+ p = &c->x11_from_xorg.options;
+
+ if (p)
+ free_and_replace(*p, a[2]);
+ }
+
+ } else if (!in_section && first_word(line, "Section")) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ r = strv_split_full(&a, line, WHITESPACE, EXTRACT_UNQUOTE);
+ if (r < 0)
+ return -ENOMEM;
+
+ if (strv_length(a) == 2 && streq(a[1], "InputClass"))
+ in_section = true;
+
+ } else if (in_section && first_word(line, "EndSection"))
+ in_section = false;
+ }
+
+ if (x11_context_verify(&c->x11_from_xorg) < 0)
+ x11_context_clear(&c->x11_from_xorg);
+
+ return 0;
+}
+
+int vconsole_write_data(Context *c) {
+ _cleanup_strv_free_ char **l = NULL;
+ const X11Context *xc;
+ int r;
+
+ assert(c);
+
+ xc = context_get_x11_context(c);
+
+ r = load_env_file(NULL, "/etc/vconsole.conf", &l);
+ if (r < 0 && r != -ENOENT)
+ return r;
+
+ r = strv_env_assign(&l, "KEYMAP", empty_to_null(c->vc.keymap));
+ if (r < 0)
+ return r;
+
+ r = strv_env_assign(&l, "KEYMAP_TOGGLE", empty_to_null(c->vc.toggle));
+ if (r < 0)
+ return r;
+
+ r = strv_env_assign(&l, "XKBLAYOUT", empty_to_null(xc->layout));
+ if (r < 0)
+ return r;
+
+ r = strv_env_assign(&l, "XKBMODEL", empty_to_null(xc->model));
+ if (r < 0)
+ return r;
+
+ r = strv_env_assign(&l, "XKBVARIANT", empty_to_null(xc->variant));
+ if (r < 0)
+ return r;
+
+ r = strv_env_assign(&l, "XKBOPTIONS", empty_to_null(xc->options));
+ if (r < 0)
+ return r;
+
+ if (strv_isempty(l)) {
+ if (unlink("/etc/vconsole.conf") < 0)
+ return errno == ENOENT ? 0 : -errno;
+
+ c->vc_stat = (struct stat) {};
+ return 0;
+ }
+
+ r = write_vconsole_conf_label(l);
+ if (r < 0)
+ return r;
+
+ if (stat("/etc/vconsole.conf", &c->vc_stat) < 0)
+ return -errno;
+
+ return 0;
+}
+
+int x11_write_data(Context *c) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_(unlink_and_freep) char *temp_path = NULL;
+ const X11Context *xc;
+ int r;
+
+ assert(c);
+
+ xc = context_get_x11_context(c);
+ if (x11_context_isempty(xc)) {
+ if (unlink("/etc/X11/xorg.conf.d/00-keyboard.conf") < 0)
+ return errno == ENOENT ? 0 : -errno;
+
+ c->x11_stat = (struct stat) {};
+ return 0;
+ }
+
+ (void) mkdir_p_label("/etc/X11/xorg.conf.d", 0755);
+ r = fopen_temporary("/etc/X11/xorg.conf.d/00-keyboard.conf", &f, &temp_path);
+ if (r < 0)
+ return r;
+
+ (void) fchmod(fileno(f), 0644);
+
+ fputs("# Written by systemd-localed(8), read by systemd-localed and Xorg. It's\n"
+ "# probably wise not to edit this file manually. Use localectl(1) to\n"
+ "# update this file.\n"
+ "Section \"InputClass\"\n"
+ " Identifier \"system-keyboard\"\n"
+ " MatchIsKeyboard \"on\"\n", f);
+
+ if (!isempty(xc->layout))
+ fprintf(f, " Option \"XkbLayout\" \"%s\"\n", xc->layout);
+
+ if (!isempty(xc->model))
+ fprintf(f, " Option \"XkbModel\" \"%s\"\n", xc->model);
+
+ if (!isempty(xc->variant))
+ fprintf(f, " Option \"XkbVariant\" \"%s\"\n", xc->variant);
+
+ if (!isempty(xc->options))
+ fprintf(f, " Option \"XkbOptions\" \"%s\"\n", xc->options);
+
+ fputs("EndSection\n", f);
+
+ r = fflush_sync_and_check(f);
+ if (r < 0)
+ return r;
+
+ if (rename(temp_path, "/etc/X11/xorg.conf.d/00-keyboard.conf") < 0)
+ return -errno;
+
+ if (stat("/etc/X11/xorg.conf.d/00-keyboard.conf", &c->x11_stat) < 0)
+ return -errno;
+
+ return 0;
+}
+
+static int read_next_mapping(
+ const char *filename,
+ unsigned min_fields,
+ unsigned max_fields,
+ FILE *f,
+ unsigned *n,
+ char ***ret) {
+
+ assert(f);
+ assert(n);
+ assert(ret);
+
+ for (;;) {
+ _cleanup_strv_free_ char **b = NULL;
+ _cleanup_free_ char *line = NULL;
+ size_t length;
+ int r;
+
+ r = read_stripped_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ (*n)++;
+
+ if (IN_SET(line[0], 0, '#'))
+ continue;
+
+ r = strv_split_full(&b, line, WHITESPACE, EXTRACT_UNQUOTE);
+ if (r < 0)
+ return r;
+
+ length = strv_length(b);
+ if (length < min_fields || length > max_fields) {
+ log_debug("Invalid line %s:%u, ignoring.", strna(filename), *n);
+ continue;
+
+ }
+
+ *ret = TAKE_PTR(b);
+ return 1;
+ }
+
+ *ret = NULL;
+ return 0;
+}
+
+int vconsole_convert_to_x11(const VCContext *vc, X11Context *ret) {
+ _cleanup_fclose_ FILE *f = NULL;
+ const char *map;
+ X11Context xc;
+ int r;
+
+ assert(vc);
+ assert(ret);
+
+ if (isempty(vc->keymap)) {
+ *ret = (X11Context) {};
+ return 0;
+ }
+
+ map = systemd_kbd_model_map();
+ f = fopen(map, "re");
+ if (!f)
+ return -errno;
+
+ for (unsigned n = 0;;) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ r = read_next_mapping(map, 5, UINT_MAX, f, &n, &a);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ if (!streq(vc->keymap, a[0]))
+ continue;
+
+ xc = (X11Context) {
+ .layout = empty_or_dash_to_null(a[1]),
+ .model = empty_or_dash_to_null(a[2]),
+ .variant = empty_or_dash_to_null(a[3]),
+ .options = empty_or_dash_to_null(a[4]),
+ };
+
+ if (x11_context_verify(&xc) < 0)
+ continue;
+
+ return x11_context_copy(ret, &xc);
+ }
+
+ /* No custom mapping has been found, see if the keymap is a converted one. In such case deducing the
+ * corresponding x11 layout is easy. */
+ _cleanup_free_ char *xlayout = NULL, *converted = NULL;
+ char *xvariant;
+
+ xlayout = strdup(vc->keymap);
+ if (!xlayout)
+ return -ENOMEM;
+ xvariant = strchr(xlayout, '-');
+ if (xvariant) {
+ xvariant[0] = '\0';
+ xvariant++;
+ }
+
+ /* Note: by default we use keyboard model "microsoftpro" which should be equivalent to "pc105" but
+ * with the internet/media key mapping added. */
+ xc = (X11Context) {
+ .layout = xlayout,
+ .model = (char*) "microsoftpro",
+ .variant = xvariant,
+ .options = (char*) "terminate:ctrl_alt_bksp",
+ };
+
+ /* This sanity check seems redundant with the verification of the X11 layout done on the next
+ * step. However xkbcommon is an optional dependency hence the verification might be a NOP. */
+ r = find_converted_keymap(&xc, &converted);
+ if (r == 0 && xc.variant) {
+ /* If we still haven't find a match, try with no variant, it's still better than nothing. */
+ xc.variant = NULL;
+ r = find_converted_keymap(&xc, &converted);
+ }
+ if (r < 0)
+ return r;
+
+ if (r == 0 || x11_context_verify(&xc) < 0) {
+ *ret = (X11Context) {};
+ return 0;
+ }
+
+ return x11_context_copy(ret, &xc);
+}
+
+int find_converted_keymap(const X11Context *xc, char **ret) {
+ _cleanup_free_ char *n = NULL;
+
+ assert(xc);
+ assert(!isempty(xc->layout));
+ assert(ret);
+
+ if (xc->variant)
+ n = strjoin(xc->layout, "-", xc->variant);
+ else
+ n = strdup(xc->layout);
+ if (!n)
+ return -ENOMEM;
+
+ NULSTR_FOREACH(dir, KBD_KEYMAP_DIRS) {
+ _cleanup_free_ char *p = NULL, *pz = NULL;
+ bool uncompressed;
+
+ p = strjoin(dir, "xkb/", n, ".map");
+ pz = strjoin(dir, "xkb/", n, ".map.gz");
+ if (!p || !pz)
+ return -ENOMEM;
+
+ uncompressed = access(p, F_OK) == 0;
+ if (uncompressed || access(pz, F_OK) == 0) {
+ log_debug("Found converted keymap %s at %s", n, uncompressed ? p : pz);
+ *ret = TAKE_PTR(n);
+ return 1;
+ }
+ }
+
+ *ret = NULL;
+ return 0;
+}
+
+int find_legacy_keymap(const X11Context *xc, char **ret) {
+ const char *map;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *new_keymap = NULL;
+ unsigned best_matching = 0;
+ int r;
+
+ assert(xc);
+ assert(!isempty(xc->layout));
+
+ map = systemd_kbd_model_map();
+ f = fopen(map, "re");
+ if (!f)
+ return -errno;
+
+ for (unsigned n = 0;;) {
+ _cleanup_strv_free_ char **a = NULL;
+ unsigned matching = 0;
+
+ r = read_next_mapping(map, 5, UINT_MAX, f, &n, &a);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ /* Determine how well matching this entry is */
+ if (streq(xc->layout, a[1]))
+ /* If we got an exact match, this is the best */
+ matching = 10;
+ else {
+ /* see if we get an exact match with the order reversed */
+ _cleanup_strv_free_ char **b = NULL;
+ _cleanup_free_ char *c = NULL;
+ r = strv_split_full(&b, a[1], ",", 0);
+ if (r < 0)
+ return r;
+ strv_reverse(b);
+ c = strv_join(b, ",");
+ if (!c)
+ return log_oom();
+ if (streq(xc->layout, c))
+ matching = 9;
+ else {
+ /* We have multiple X layouts, look for an
+ * entry that matches our key with everything
+ * but the first layout stripped off. */
+ if (startswith_comma(xc->layout, a[1]))
+ matching = 5;
+ else {
+ _cleanup_free_ char *x = NULL;
+
+ /* If that didn't work, strip off the
+ * other layouts from the entry, too */
+ x = strdupcspn(a[1], ",");
+ if (!x)
+ return -ENOMEM;
+ if (startswith_comma(xc->layout, x))
+ matching = 1;
+ }
+ }
+ }
+
+ if (matching > 0) {
+ if (isempty(xc->model) || streq_ptr(xc->model, a[2])) {
+ matching++;
+
+ if (streq_ptr(xc->variant, a[3]) || ((isempty(xc->variant) || streq_skip_trailing_chars(xc->variant, "", ",")) && streq(a[3], "-"))) {
+ matching++;
+
+ if (streq_ptr(xc->options, a[4]))
+ matching++;
+ }
+ }
+ }
+
+ /* The best matching entry so far, then let's save that */
+ if (matching >= MAX(best_matching, 1u)) {
+ log_debug("Found legacy keymap %s with score %u", a[0], matching);
+
+ if (matching > best_matching) {
+ best_matching = matching;
+
+ r = free_and_strdup(&new_keymap, a[0]);
+ if (r < 0)
+ return r;
+ }
+ }
+ }
+
+ if (best_matching < 9 && !isempty(xc->layout)) {
+ _cleanup_free_ char *l = NULL, *v = NULL, *converted = NULL;
+
+ /* The best match is only the first part of the X11
+ * keymap. Check if we have a converted map which
+ * matches just the first layout.
+ */
+
+ l = strdupcspn(xc->layout, ",");
+ if (!l)
+ return -ENOMEM;
+
+ if (!isempty(xc->variant)) {
+ v = strdupcspn(xc->variant, ",");
+ if (!v)
+ return -ENOMEM;
+ }
+
+ r = find_converted_keymap(
+ &(X11Context) {
+ .layout = l,
+ .variant = v,
+ },
+ &converted);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ free_and_replace(new_keymap, converted);
+ }
+
+ *ret = TAKE_PTR(new_keymap);
+ return !!*ret;
+}
+
+int x11_convert_to_vconsole(const X11Context *xc, VCContext *ret) {
+ _cleanup_free_ char *keymap = NULL;
+ int r;
+
+ assert(xc);
+ assert(ret);
+
+ if (isempty(xc->layout)) {
+ *ret = (VCContext) {};
+ return 0;
+ }
+
+ r = find_converted_keymap(xc, &keymap);
+ if (r == 0) {
+ r = find_legacy_keymap(xc, &keymap);
+ if (r == 0 && xc->variant)
+ /* If we still haven't find a match, try with no variant, it's still better than
+ * nothing. */
+ r = find_converted_keymap(
+ &(X11Context) {
+ .layout = xc->layout,
+ },
+ &keymap);
+ }
+ if (r < 0)
+ return r;
+
+ *ret = (VCContext) {
+ .keymap = TAKE_PTR(keymap),
+ };
+ return 0;
+}
+
+int find_language_fallback(const char *lang, char **ret) {
+ const char *map;
+ _cleanup_fclose_ FILE *f = NULL;
+ unsigned n = 0;
+ int r;
+
+ assert(lang);
+ assert(ret);
+
+ map = systemd_language_fallback_map();
+ f = fopen(map, "re");
+ if (!f)
+ return -errno;
+
+ for (;;) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ r = read_next_mapping(map, 2, 2, f, &n, &a);
+ if (r <= 0)
+ return r;
+
+ if (streq(lang, a[0])) {
+ assert(strv_length(a) == 2);
+ *ret = TAKE_PTR(a[1]);
+ return 1;
+ }
+ }
+}
+
+bool locale_gen_check_available(void) {
+#if HAVE_LOCALEGEN
+ if (access(LOCALEGEN_PATH, X_OK) < 0) {
+ if (errno != ENOENT)
+ log_warning_errno(errno, "Unable to determine whether " LOCALEGEN_PATH " exists and is executable, assuming it is not: %m");
+ return false;
+ }
+ if (access("/etc/locale.gen", F_OK) < 0) {
+ if (errno != ENOENT)
+ log_warning_errno(errno, "Unable to determine whether /etc/locale.gen exists, assuming it does not: %m");
+ return false;
+ }
+ return true;
+#else
+ return false;
+#endif
+}
+
+#if HAVE_LOCALEGEN
+static bool locale_encoding_is_utf8_or_unspecified(const char *locale) {
+ const char *c = strchr(locale, '.');
+ return !c || strcaseeq(c, ".UTF-8") || strcasestr(locale, ".UTF-8@");
+}
+
+static int locale_gen_locale_supported(const char *locale_entry) {
+ /* Returns an error valus <= 0 if the locale-gen entry is invalid or unsupported,
+ * 1 in case the locale entry is valid, and -EOPNOTSUPP specifically in case
+ * the distributor has not provided us with a SUPPORTED file to check
+ * locale for validity. */
+
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ assert(locale_entry);
+
+ /* Locale templates without country code are never supported */
+ if (!strstr(locale_entry, "_"))
+ return -EINVAL;
+
+ f = fopen("/usr/share/i18n/SUPPORTED", "re");
+ if (!f) {
+ if (errno == ENOENT)
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Unable to check validity of locale entry %s: /usr/share/i18n/SUPPORTED does not exist",
+ locale_entry);
+ return -errno;
+ }
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+
+ r = read_stripped_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to read /usr/share/i18n/SUPPORTED: %m");
+ if (r == 0)
+ return 0;
+
+ if (strcaseeq_ptr(line, locale_entry))
+ return 1;
+ }
+}
+#endif
+
+int locale_gen_enable_locale(const char *locale) {
+#if HAVE_LOCALEGEN
+ _cleanup_fclose_ FILE *fr = NULL, *fw = NULL;
+ _cleanup_(unlink_and_freep) char *temp_path = NULL;
+ _cleanup_free_ char *locale_entry = NULL;
+ bool locale_enabled = false, first_line = false;
+ bool write_new = false;
+ int r;
+
+ if (isempty(locale))
+ return 0;
+
+ if (locale_encoding_is_utf8_or_unspecified(locale)) {
+ locale_entry = strjoin(locale, " UTF-8");
+ if (!locale_entry)
+ return -ENOMEM;
+ } else
+ return -ENOEXEC; /* We do not process non-UTF-8 locale */
+
+ r = locale_gen_locale_supported(locale_entry);
+ if (r == 0)
+ return -EINVAL;
+ if (r < 0 && r != -EOPNOTSUPP)
+ return r;
+
+ fr = fopen("/etc/locale.gen", "re");
+ if (!fr) {
+ if (errno != ENOENT)
+ return -errno;
+ write_new = true;
+ }
+
+ r = fopen_temporary("/etc/locale.gen", &fw, &temp_path);
+ if (r < 0)
+ return r;
+
+ if (write_new)
+ (void) fchmod(fileno(fw), 0644);
+ else {
+ /* apply mode & xattrs of the original file to new file */
+ r = copy_access(fileno(fr), fileno(fw));
+ if (r < 0)
+ return r;
+ r = copy_xattr(fileno(fr), NULL, fileno(fw), NULL, COPY_ALL_XATTRS);
+ if (r < 0)
+ log_debug_errno(r, "Failed to copy all xattrs from old to new /etc/locale.gen file, ignoring: %m");
+ }
+
+ if (!write_new) {
+ /* The config file ends with a line break, which we do not want to include before potentially appending a new locale
+ * instead of uncommenting an existing line. By prepending linebreaks, we can avoid buffering this file but can still write
+ * a nice config file without empty lines */
+ first_line = true;
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+ char *line_locale;
+
+ r = read_line(fr, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ if (locale_enabled) {
+ /* Just complete writing the file if the new locale was already enabled */
+ if (!first_line)
+ fputc('\n', fw);
+ fputs(line, fw);
+ first_line = false;
+ continue;
+ }
+
+ line_locale = strstrip(line);
+ if (isempty(line_locale)) {
+ fputc('\n', fw);
+ first_line = false;
+ continue;
+ }
+
+ if (line_locale[0] == '#')
+ line_locale = strstrip(line_locale + 1);
+ else if (strcaseeq_ptr(line_locale, locale_entry))
+ return 0; /* the file already had our locale activated, so skip updating it */
+
+ if (strcaseeq_ptr(line_locale, locale_entry)) {
+ /* Uncomment existing line for new locale */
+ if (!first_line)
+ fputc('\n', fw);
+ fputs(locale_entry, fw);
+ locale_enabled = true;
+ first_line = false;
+ continue;
+ }
+
+ /* The line was not for the locale we want to enable, just copy it */
+ if (!first_line)
+ fputc('\n', fw);
+ fputs(line, fw);
+ first_line = false;
+ }
+ }
+
+ /* Add locale to enable to the end of the file if it was not found as commented line */
+ if (!locale_enabled) {
+ if (!write_new)
+ fputc('\n', fw);
+ fputs(locale_entry, fw);
+ }
+ fputc('\n', fw);
+
+ r = fflush_sync_and_check(fw);
+ if (r < 0)
+ return r;
+
+ if (rename(temp_path, "/etc/locale.gen") < 0)
+ return -errno;
+ temp_path = mfree(temp_path);
+
+ return 0;
+#else
+ return -EOPNOTSUPP;
+#endif
+}
+
+int locale_gen_run(void) {
+#if HAVE_LOCALEGEN
+ pid_t pid;
+ int r;
+
+ r = safe_fork("(sd-localegen)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_CLOSE_ALL_FDS|FORK_LOG|FORK_WAIT, &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ execl(LOCALEGEN_PATH, LOCALEGEN_PATH, NULL);
+ _exit(EXIT_FAILURE);
+ }
+
+ return 0;
+#else
+ return -EOPNOTSUPP;
+#endif
+}
diff --git a/src/locale/localed-util.h b/src/locale/localed-util.h
new file mode 100644
index 0000000..0c68f29
--- /dev/null
+++ b/src/locale/localed-util.h
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/stat.h>
+
+#include "sd-bus.h"
+
+#include "hashmap.h"
+#include "locale-setup.h"
+
+typedef struct X11Context {
+ char *layout;
+ char *model;
+ char *variant;
+ char *options;
+} X11Context;
+
+typedef struct VCContext {
+ char *keymap;
+ char *toggle;
+} VCContext;
+
+typedef struct Context {
+ sd_bus_message *locale_cache;
+ LocaleContext locale_context;
+
+ sd_bus_message *x11_cache;
+ struct stat x11_stat;
+ X11Context x11_from_xorg;
+ X11Context x11_from_vc;
+
+ sd_bus_message *vc_cache;
+ struct stat vc_stat;
+ VCContext vc;
+
+ Hashmap *polkit_registry;
+} Context;
+
+void x11_context_clear(X11Context *xc);
+void x11_context_replace(X11Context *dest, X11Context *src);
+bool x11_context_isempty(const X11Context *xc);
+void x11_context_empty_to_null(X11Context *xc);
+bool x11_context_is_safe(const X11Context *xc);
+bool x11_context_equal(const X11Context *a, const X11Context *b);
+int x11_context_copy(X11Context *dest, const X11Context *src);
+int x11_context_verify_and_warn(const X11Context *xc, int log_level, sd_bus_error *error);
+static inline int x11_context_verify(const X11Context *xc) {
+ return x11_context_verify_and_warn(xc, LOG_DEBUG, NULL);
+}
+
+X11Context *context_get_x11_context(Context *c);
+
+void vc_context_clear(VCContext *vc);
+void vc_context_replace(VCContext *dest, VCContext *src);
+bool vc_context_isempty(const VCContext *vc);
+void vc_context_empty_to_null(VCContext *vc);
+bool vc_context_equal(const VCContext *a, const VCContext *b);
+int vc_context_copy(VCContext *dest, const VCContext *src);
+int vc_context_verify_and_warn(const VCContext *vc, int log_level, sd_bus_error *error);
+static inline int vc_context_verify(const VCContext *vc) {
+ return vc_context_verify_and_warn(vc, LOG_DEBUG, NULL);
+}
+
+int find_converted_keymap(const X11Context *xc, char **ret);
+int find_legacy_keymap(const X11Context *xc, char **ret);
+int find_language_fallback(const char *lang, char **ret);
+
+int locale_read_data(Context *c, sd_bus_message *m);
+int vconsole_read_data(Context *c, sd_bus_message *m);
+int x11_read_data(Context *c, sd_bus_message *m);
+
+void context_clear(Context *c);
+int vconsole_convert_to_x11(const VCContext *vc, X11Context *ret);
+int vconsole_write_data(Context *c);
+int x11_convert_to_vconsole(const X11Context *xc, VCContext *ret);
+int x11_write_data(Context *c);
+
+bool locale_gen_check_available(void);
+int locale_gen_enable_locale(const char *locale);
+int locale_gen_run(void);
diff --git a/src/locale/localed.c b/src/locale/localed.c
new file mode 100644
index 0000000..5d96237
--- /dev/null
+++ b/src/locale/localed.c
@@ -0,0 +1,680 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "sd-bus.h"
+
+#include "alloc-util.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "bus-log-control-api.h"
+#include "bus-message.h"
+#include "bus-polkit.h"
+#include "bus-unit-util.h"
+#include "constants.h"
+#include "kbd-util.h"
+#include "localed-util.h"
+#include "macro.h"
+#include "main-func.h"
+#include "missing_capability.h"
+#include "path-util.h"
+#include "selinux-util.h"
+#include "service-util.h"
+#include "signal-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "user-util.h"
+
+static int vconsole_reload(sd_bus *bus) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(bus);
+
+ r = bus_call_method(bus, bus_systemd_mgr, "RestartUnit", &error, NULL, "ss", "systemd-vconsole-setup.service", "replace");
+ if (r < 0)
+ return log_error_errno(r, "Failed to issue method call: %s", bus_error_message(&error, r));
+ return 0;
+}
+
+static int property_get_locale(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Context *c = ASSERT_PTR(userdata);
+ _cleanup_strv_free_ char **l = NULL;
+ int r;
+
+ r = locale_read_data(c, reply);
+ if (r < 0)
+ return r;
+
+ r = locale_context_build_env(&c->locale_context, &l, NULL);
+ if (r < 0)
+ return r;
+
+ return sd_bus_message_append_strv(reply, l);
+}
+
+static int property_get_vconsole(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Context *c = ASSERT_PTR(userdata);
+ int r;
+
+ assert(property);
+
+ r = vconsole_read_data(c, reply);
+ if (r < 0)
+ return r;
+
+ if (streq(property, "VConsoleKeymap"))
+ return sd_bus_message_append_basic(reply, 's', c->vc.keymap);
+ if (streq(property, "VConsoleKeymapToggle"))
+ return sd_bus_message_append_basic(reply, 's', c->vc.toggle);
+
+ return -EINVAL;
+}
+
+static int property_get_xkb(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Context *c = ASSERT_PTR(userdata);
+ const X11Context *xc;
+ int r;
+
+ assert(property);
+
+ r = vconsole_read_data(c, reply);
+ if (r < 0)
+ return r;
+
+ r = x11_read_data(c, reply);
+ if (r < 0)
+ return r;
+
+ xc = context_get_x11_context(c);
+
+ if (streq(property, "X11Layout"))
+ return sd_bus_message_append_basic(reply, 's', xc->layout);
+ if (streq(property, "X11Model"))
+ return sd_bus_message_append_basic(reply, 's', xc->model);
+ if (streq(property, "X11Variant"))
+ return sd_bus_message_append_basic(reply, 's', xc->variant);
+ if (streq(property, "X11Options"))
+ return sd_bus_message_append_basic(reply, 's', xc->options);
+
+ return -EINVAL;
+}
+
+static int process_locale_list_item(
+ const char *assignment,
+ char *new_locale[static _VARIABLE_LC_MAX],
+ bool use_localegen,
+ sd_bus_error *error) {
+
+ assert(assignment);
+ assert(new_locale);
+
+ for (LocaleVariable p = 0; p < _VARIABLE_LC_MAX; p++) {
+ const char *name, *e;
+
+ assert_se(name = locale_variable_to_string(p));
+
+ e = startswith(assignment, name);
+ if (!e)
+ continue;
+
+ if (*e != '=')
+ continue;
+
+ e++;
+
+ if (!locale_is_valid(e))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Locale %s is not valid, refusing.", e);
+ if (!use_localegen && locale_is_installed(e) <= 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Locale %s not installed, refusing.", e);
+ if (new_locale[p])
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Locale variable %s set twice, refusing.", name);
+
+ new_locale[p] = strdup(e);
+ if (!new_locale[p])
+ return -ENOMEM;
+
+ return 0;
+ }
+
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Locale assignment %s not valid, refusing.", assignment);
+}
+
+static int locale_gen_process_locale(char *new_locale[static _VARIABLE_LC_MAX], sd_bus_error *error) {
+ int r;
+
+ assert(new_locale);
+
+ for (LocaleVariable p = 0; p < _VARIABLE_LC_MAX; p++) {
+ if (p == VARIABLE_LANGUAGE)
+ continue;
+ if (isempty(new_locale[p]))
+ continue;
+ if (locale_is_installed(new_locale[p]))
+ continue;
+
+ r = locale_gen_enable_locale(new_locale[p]);
+ if (r == -ENOEXEC) {
+ log_error_errno(r, "Refused to enable locale for generation: %m");
+ return sd_bus_error_setf(error,
+ SD_BUS_ERROR_INVALID_ARGS,
+ "Specified locale is not installed and non-UTF-8 locale will not be auto-generated: %s",
+ new_locale[p]);
+ }
+ if (r == -EINVAL) {
+ log_error_errno(r, "Failed to enable invalid locale %s for generation.", new_locale[p]);
+ return sd_bus_error_setf(error,
+ SD_BUS_ERROR_INVALID_ARGS,
+ "Cannot enable locale generation for invalid locale: %s",
+ new_locale[p]);
+ }
+ if (r < 0) {
+ log_error_errno(r, "Failed to enable locale for generation: %m");
+ return sd_bus_error_set_errnof(error, r, "Failed to enable locale generation: %m");
+ }
+
+ r = locale_gen_run();
+ if (r < 0) {
+ log_error_errno(r, "Failed to generate locale: %m");
+ return sd_bus_error_set_errnof(error, r, "Failed to generate locale: %m");
+ }
+ }
+
+ return 0;
+}
+
+static int method_set_locale(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+ _cleanup_(locale_variables_freep) char *new_locale[_VARIABLE_LC_MAX] = {};
+ _cleanup_strv_free_ char **l = NULL, **l_set = NULL, **l_unset = NULL;
+ Context *c = ASSERT_PTR(userdata);
+ int interactive, r;
+ bool use_localegen;
+
+ assert(m);
+
+ r = sd_bus_message_read_strv(m, &l);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_read_basic(m, 'b', &interactive);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ use_localegen = locale_gen_check_available();
+
+ /* If single locale without variable name is provided, then we assume it is LANG=. */
+ if (strv_length(l) == 1 && !strchr(l[0], '=')) {
+ if (!locale_is_valid(l[0]))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid locale specification: %s", l[0]);
+ if (!use_localegen && locale_is_installed(l[0]) <= 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Specified locale is not installed: %s", l[0]);
+
+ new_locale[VARIABLE_LANG] = strdup(l[0]);
+ if (!new_locale[VARIABLE_LANG])
+ return log_oom();
+
+ l = strv_free(l);
+ }
+
+ /* Check whether a variable is valid */
+ STRV_FOREACH(i, l) {
+ r = process_locale_list_item(*i, new_locale, use_localegen, error);
+ if (r < 0)
+ return r;
+ }
+
+ /* If LANG was specified, but not LANGUAGE, check if we should
+ * set it based on the language fallback table. */
+ if (!isempty(new_locale[VARIABLE_LANG]) &&
+ isempty(new_locale[VARIABLE_LANGUAGE])) {
+ _cleanup_free_ char *language = NULL;
+
+ (void) find_language_fallback(new_locale[VARIABLE_LANG], &language);
+ if (language) {
+ log_debug("Converted LANG=%s to LANGUAGE=%s", new_locale[VARIABLE_LANG], language);
+ free_and_replace(new_locale[VARIABLE_LANGUAGE], language);
+ }
+ }
+
+ r = locale_read_data(c, m);
+ if (r < 0) {
+ log_error_errno(r, "Failed to read locale data: %m");
+ return sd_bus_error_set(error, SD_BUS_ERROR_FAILED, "Failed to read locale data");
+ }
+
+ /* Merge with the current settings */
+ r = locale_context_merge(&c->locale_context, new_locale);
+ if (r < 0)
+ return log_oom();
+
+ locale_variables_simplify(new_locale);
+
+ if (locale_context_equal(&c->locale_context, new_locale)) {
+ log_debug("Locale settings were not modified.");
+ return sd_bus_reply_method_return(m, NULL);
+ }
+
+ r = bus_verify_polkit_async(
+ m,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.locale1.set-locale",
+ NULL,
+ interactive,
+ UID_INVALID,
+ &c->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
+
+ /* Generate locale in case it is missing and the system is using locale-gen */
+ if (use_localegen) {
+ r = locale_gen_process_locale(new_locale, error);
+ if (r < 0)
+ return r;
+ }
+
+ locale_context_take(&c->locale_context, new_locale);
+
+ /* Write locale configuration */
+ r = locale_context_save(&c->locale_context, &l_set, &l_unset);
+ if (r < 0) {
+ log_error_errno(r, "Failed to set locale: %m");
+ return sd_bus_error_set_errnof(error, r, "Failed to set locale: %m");
+ }
+
+ /* Since we just updated the locale configuration file, ask the system manager to read it again to
+ * update its default locale settings. It's important to not use UnsetAndSetEnvironment or a similar
+ * method because in this case unsetting variables means restoring them to PID1 default values, which
+ * may be outdated, since locale.conf has just changed and PID1 hasn't read it */
+ (void) bus_service_manager_reload(sd_bus_message_get_bus(m));
+
+ if (!strv_isempty(l_set)) {
+ _cleanup_free_ char *line = NULL;
+
+ line = strv_join(l_set, ", ");
+ log_info("Changed locale to %s.", strnull(line));
+ } else
+ log_info("Changed locale to unset.");
+
+ (void) sd_bus_emit_properties_changed(
+ sd_bus_message_get_bus(m),
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ "Locale", NULL);
+
+ return sd_bus_reply_method_return(m, NULL);
+}
+
+static int method_set_vc_keyboard(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+ _cleanup_(x11_context_clear) X11Context converted = {};
+ Context *c = ASSERT_PTR(userdata);
+ int convert, interactive, r;
+ bool x_needs_update;
+ VCContext in;
+
+ assert(m);
+
+ r = sd_bus_message_read(m, "ssbb", &in.keymap, &in.toggle, &convert, &interactive);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ vc_context_empty_to_null(&in);
+
+ r = vc_context_verify_and_warn(&in, LOG_ERR, error);
+ if (r < 0)
+ return r;
+
+ r = vconsole_read_data(c, m);
+ if (r < 0) {
+ log_error_errno(r, "Failed to read virtual console keymap data: %m");
+ return sd_bus_error_set_errnof(error, r, "Failed to read virtual console keymap data: %m");
+ }
+
+ r = x11_read_data(c, m);
+ if (r < 0) {
+ log_error_errno(r, "Failed to read X11 keyboard layout data: %m");
+ return sd_bus_error_set_errnof(error, r, "Failed to read X11 keyboard layout data: %m");
+ }
+
+ if (convert) {
+ r = vconsole_convert_to_x11(&in, &converted);
+ if (r < 0) {
+ log_error_errno(r, "Failed to convert keymap data: %m");
+ return sd_bus_error_set_errnof(error, r, "Failed to convert keymap data: %m");
+ }
+
+ if (x11_context_isempty(&converted))
+ log_notice("No conversion found for virtual console keymap \"%s\".", strempty(in.keymap));
+ else
+ log_info("The virtual console keymap '%s' is converted to X11 keyboard layout '%s' model '%s' variant '%s' options '%s'",
+ in.keymap, strempty(converted.layout), strempty(converted.model), strempty(converted.variant), strempty(converted.options));
+
+ /* save the result of conversion to emit changed properties later. */
+ x_needs_update = !x11_context_equal(&c->x11_from_vc, &converted) || !x11_context_equal(&c->x11_from_xorg, &converted);
+ } else
+ x_needs_update = !x11_context_equal(&c->x11_from_vc, &c->x11_from_xorg);
+
+ if (vc_context_equal(&c->vc, &in) && !x_needs_update)
+ return sd_bus_reply_method_return(m, NULL);
+
+ r = bus_verify_polkit_async(
+ m,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.locale1.set-keyboard",
+ NULL,
+ interactive,
+ UID_INVALID,
+ &c->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
+
+ r = vc_context_copy(&c->vc, &in);
+ if (r < 0)
+ return log_oom();
+
+ if (x_needs_update) {
+ if (convert) {
+ r = x11_context_copy(&c->x11_from_vc, &converted);
+ if (r < 0)
+ return log_oom();
+ x11_context_replace(&c->x11_from_xorg, &converted);
+ } else {
+ const X11Context *xc = context_get_x11_context(c);
+
+ /* Even if the conversion is not requested, sync the two X11 contexts. */
+ r = x11_context_copy(&c->x11_from_vc, xc);
+ if (r < 0)
+ return log_oom();
+
+ r = x11_context_copy(&c->x11_from_xorg, xc);
+ if (r < 0)
+ return log_oom();
+ }
+ }
+
+ r = vconsole_write_data(c);
+ if (r < 0)
+ log_warning_errno(r, "Failed to write virtual console keymap, ignoring: %m");
+
+ if (x_needs_update) {
+ r = x11_write_data(c);
+ if (r < 0)
+ log_warning_errno(r, "Failed to write X11 keyboard layout, ignoring: %m");
+ }
+
+ log_info("Changed virtual console keymap to '%s' toggle '%s'",
+ strempty(c->vc.keymap), strempty(c->vc.toggle));
+
+ (void) vconsole_reload(sd_bus_message_get_bus(m));
+
+ (void) sd_bus_emit_properties_changed(
+ sd_bus_message_get_bus(m),
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ "VConsoleKeymap", "VConsoleKeymapToggle",
+ x_needs_update ? "X11Layout" : NULL,
+ x_needs_update ? "X11Model" : NULL,
+ x_needs_update ? "X11Variant" : NULL,
+ x_needs_update ? "X11Options" : NULL,
+ NULL);
+
+ return sd_bus_reply_method_return(m, NULL);
+}
+
+static int method_set_x11_keyboard(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+ _cleanup_(vc_context_clear) VCContext converted = {};
+ Context *c = ASSERT_PTR(userdata);
+ int convert, interactive, r;
+ X11Context in;
+
+ assert(m);
+
+ r = sd_bus_message_read(m, "ssssbb", &in.layout, &in.model, &in.variant, &in.options, &convert, &interactive);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ x11_context_empty_to_null(&in);
+
+ r = x11_context_verify_and_warn(&in, LOG_ERR, error);
+ if (r < 0)
+ return r;
+
+ r = vconsole_read_data(c, m);
+ if (r < 0) {
+ log_error_errno(r, "Failed to read virtual console keymap data: %m");
+ return sd_bus_error_set_errnof(error, r, "Failed to read virtual console keymap data: %m");
+ }
+
+ r = x11_read_data(c, m);
+ if (r < 0) {
+ log_error_errno(r, "Failed to read x11 keyboard layout data: %m");
+ return sd_bus_error_set(error, SD_BUS_ERROR_FAILED, "Failed to read x11 keyboard layout data");
+ }
+
+ if (convert) {
+ r = x11_convert_to_vconsole(&in, &converted);
+ if (r < 0) {
+ log_error_errno(r, "Failed to convert keymap data: %m");
+ return sd_bus_error_set_errnof(error, r, "Failed to convert keymap data: %m");
+ }
+
+ if (vc_context_isempty(&converted))
+ /* We search for layout-variant match first, but then we also look
+ * for anything which matches just the layout. So it's accurate to say
+ * that we couldn't find anything which matches the layout. */
+ log_notice("No conversion to virtual console map found for \"%s\".", strempty(in.layout));
+ else
+ log_info("The X11 keyboard layout '%s' is converted to virtual console keymap '%s'",
+ in.layout, converted.keymap);
+
+ /* save the result of conversion to emit changed properties later. */
+ convert = !vc_context_equal(&c->vc, &converted);
+ }
+
+ if (x11_context_equal(&c->x11_from_vc, &in) && x11_context_equal(&c->x11_from_xorg, &in) && !convert)
+ return sd_bus_reply_method_return(m, NULL);
+
+ r = bus_verify_polkit_async(
+ m,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.locale1.set-keyboard",
+ NULL,
+ interactive,
+ UID_INVALID,
+ &c->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
+
+ r = x11_context_copy(&c->x11_from_vc, &in);
+ if (r < 0)
+ return log_oom();
+
+ r = x11_context_copy(&c->x11_from_xorg, &in);
+ if (r < 0)
+ return log_oom();
+
+ if (convert)
+ vc_context_replace(&c->vc, &converted);
+
+ r = vconsole_write_data(c);
+ if (r < 0)
+ log_warning_errno(r, "Failed to update vconsole.conf, ignoring: %m");
+
+ r = x11_write_data(c);
+ if (r < 0)
+ log_warning_errno(r, "Failed to write X11 keyboard layout, ignoring: %m");
+
+ log_info("Changed X11 keyboard layout to '%s' model '%s' variant '%s' options '%s'",
+ strempty(in.layout),
+ strempty(in.model),
+ strempty(in.variant),
+ strempty(in.options));
+
+ (void) sd_bus_emit_properties_changed(
+ sd_bus_message_get_bus(m),
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ "X11Layout", "X11Model", "X11Variant", "X11Options",
+ convert ? "VConsoleKeymap" : NULL,
+ convert ? "VConsoleKeymapToggle" : NULL,
+ NULL);
+
+ if (convert)
+ (void) vconsole_reload(sd_bus_message_get_bus(m));
+
+ return sd_bus_reply_method_return(m, NULL);
+}
+
+static const sd_bus_vtable locale_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+ SD_BUS_PROPERTY("Locale", "as", property_get_locale, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("X11Layout", "s", property_get_xkb, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("X11Model", "s", property_get_xkb, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("X11Variant", "s", property_get_xkb, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("X11Options", "s", property_get_xkb, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("VConsoleKeymap", "s", property_get_vconsole, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("VConsoleKeymapToggle", "s", property_get_vconsole, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+
+ SD_BUS_METHOD_WITH_ARGS("SetLocale",
+ SD_BUS_ARGS("as", locale, "b", interactive),
+ SD_BUS_NO_RESULT,
+ method_set_locale,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetVConsoleKeyboard",
+ SD_BUS_ARGS("s", keymap, "s", keymap_toggle, "b", convert, "b", interactive),
+ SD_BUS_NO_RESULT,
+ method_set_vc_keyboard,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetX11Keyboard",
+ SD_BUS_ARGS("s", layout, "s", model, "s", variant, "s", options, "b", convert, "b", interactive),
+ SD_BUS_NO_RESULT,
+ method_set_x11_keyboard,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+
+ SD_BUS_VTABLE_END
+};
+
+static const BusObjectImplementation manager_object = {
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ .vtables = BUS_VTABLES(locale_vtable),
+};
+
+static int connect_bus(Context *c, sd_event *event, sd_bus **_bus) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ assert(c);
+ assert(event);
+ assert(_bus);
+
+ r = sd_bus_default_system(&bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get system bus connection: %m");
+
+ r = bus_add_implementation(bus, &manager_object, c);
+ if (r < 0)
+ return r;
+
+ r = bus_log_control_api_register(bus);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_request_name_async(bus, NULL, "org.freedesktop.locale1", 0, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request name: %m");
+
+ r = sd_bus_attach_event(bus, event, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach bus to event loop: %m");
+
+ *_bus = TAKE_PTR(bus);
+
+ return 0;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(context_clear) Context context = {};
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ log_setup();
+
+ r = service_parse_argv("systemd-localed.service",
+ "Manage system locale settings and key mappings.",
+ BUS_IMPLEMENTATIONS(&manager_object,
+ &log_control_object),
+ argc, argv);
+ if (r <= 0)
+ return r;
+
+ umask(0022);
+
+ r = mac_init();
+ if (r < 0)
+ return r;
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0);
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ (void) sd_event_set_watchdog(event, true);
+
+ r = sd_event_add_signal(event, NULL, SIGINT, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to install SIGINT handler: %m");
+
+ r = sd_event_add_signal(event, NULL, SIGTERM, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to install SIGTERM handler: %m");
+
+ r = connect_bus(&context, event, &bus);
+ if (r < 0)
+ return r;
+
+ r = bus_event_loop_with_idle(event, bus, "org.freedesktop.locale1", DEFAULT_EXIT_USEC, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/locale/meson.build b/src/locale/meson.build
new file mode 100644
index 0000000..3d3aa58
--- /dev/null
+++ b/src/locale/meson.build
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+systemd_localed_sources = files(
+ 'localed-util.c',
+ 'localed.c',
+ 'xkbcommon-util.c',
+)
+
+localectl_sources = files('localectl.c')
+
+# logind will load libxkbcommon.so dynamically on its own, but we still need to
+# specify where the headers are.
+if conf.get('HAVE_XKBCOMMON') == 1
+ libxkbcommon_deps = [
+ libdl,
+ libxkbcommon.partial_dependency(compile_args: true),
+ ]
+else
+ libxkbcommon_deps = []
+endif
+
+executables += [
+ libexec_template + {
+ 'name' : 'systemd-localed',
+ 'dbus' : true,
+ 'conditions' : ['ENABLE_LOCALED'],
+ 'sources' : systemd_localed_sources,
+ 'dependencies' : libxkbcommon_deps,
+ },
+ executable_template + {
+ 'name' : 'localectl',
+ 'public' : true,
+ 'conditions' : ['ENABLE_LOCALED'],
+ 'sources' : files('localectl.c'),
+ },
+ test_template + {
+ 'sources' : files(
+ 'test-localed-util.c',
+ 'localed-util.c',
+ 'xkbcommon-util.c',
+ ),
+ 'dependencies' : libxkbcommon_deps,
+ },
+]
+
+# If you know a way that allows the same variables to be used
+# in sources list and concatenated to a string for test_env,
+# let me know.
+kbd_model_map = meson.current_source_dir() / 'kbd-model-map'
+language_fallback_map = meson.current_source_dir() / 'language-fallback-map'
+
+if conf.get('ENABLE_LOCALED') == 1
+ install_data('org.freedesktop.locale1.conf',
+ install_dir : dbuspolicydir)
+ install_data('org.freedesktop.locale1.service',
+ install_dir : dbussystemservicedir)
+ install_data('org.freedesktop.locale1.policy',
+ install_dir : polkitpolicydir)
+ install_data('kbd-model-map',
+ 'language-fallback-map',
+ install_dir : pkgdatadir)
+endif
diff --git a/src/locale/org.freedesktop.locale1.conf b/src/locale/org.freedesktop.locale1.conf
new file mode 100644
index 0000000..04813b2
--- /dev/null
+++ b/src/locale/org.freedesktop.locale1.conf
@@ -0,0 +1,29 @@
+<?xml version="1.0"?> <!--*-nxml-*-->
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+
+<!--
+ SPDX-License-Identifier: LGPL-2.1-or-later
+
+ This file is part of systemd.
+
+ systemd is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+-->
+
+<busconfig>
+
+ <policy user="root">
+ <allow own="org.freedesktop.locale1"/>
+ <allow send_destination="org.freedesktop.locale1"/>
+ <allow receive_sender="org.freedesktop.locale1"/>
+ </policy>
+
+ <policy context="default">
+ <allow send_destination="org.freedesktop.locale1"/>
+ <allow receive_sender="org.freedesktop.locale1"/>
+ </policy>
+
+</busconfig>
diff --git a/src/locale/org.freedesktop.locale1.policy b/src/locale/org.freedesktop.locale1.policy
new file mode 100644
index 0000000..553e31d
--- /dev/null
+++ b/src/locale/org.freedesktop.locale1.policy
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
+<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+
+<!--
+ SPDX-License-Identifier: LGPL-2.1-or-later
+
+ This file is part of systemd.
+
+ systemd is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+-->
+
+<policyconfig>
+
+ <vendor>The systemd Project</vendor>
+ <vendor_url>https://systemd.io</vendor_url>
+
+ <action id="org.freedesktop.locale1.set-locale">
+ <description gettext-domain="systemd">Set system locale</description>
+ <message gettext-domain="systemd">Authentication is required to set the system locale.</message>
+ <defaults>
+ <allow_any>auth_admin_keep</allow_any>
+ <allow_inactive>auth_admin_keep</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.imply">org.freedesktop.locale1.set-keyboard</annotate>
+ </action>
+
+ <action id="org.freedesktop.locale1.set-keyboard">
+ <description gettext-domain="systemd">Set system keyboard settings</description>
+ <message gettext-domain="systemd">Authentication is required to set the system keyboard settings.</message>
+ <defaults>
+ <allow_any>auth_admin_keep</allow_any>
+ <allow_inactive>auth_admin_keep</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ </action>
+
+</policyconfig>
diff --git a/src/locale/org.freedesktop.locale1.service b/src/locale/org.freedesktop.locale1.service
new file mode 100644
index 0000000..2d812cb
--- /dev/null
+++ b/src/locale/org.freedesktop.locale1.service
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[D-BUS Service]
+Name=org.freedesktop.locale1
+Exec=/bin/false
+User=root
+SystemdService=dbus-org.freedesktop.locale1.service
diff --git a/src/locale/test-localed-util.c b/src/locale/test-localed-util.c
new file mode 100644
index 0000000..e92c178
--- /dev/null
+++ b/src/locale/test-localed-util.c
@@ -0,0 +1,236 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "localed-util.h"
+#include "log.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(find_language_fallback) {
+ _cleanup_free_ char *ans = NULL, *ans2 = NULL;
+
+ assert_se(find_language_fallback("foobar", &ans) == 0);
+ assert_se(ans == NULL);
+
+ assert_se(find_language_fallback("csb", &ans) == 0);
+ assert_se(ans == NULL);
+
+ assert_se(find_language_fallback("csb_PL", &ans) == 1);
+ assert_se(streq(ans, "csb:pl"));
+
+ assert_se(find_language_fallback("szl_PL", &ans2) == 1);
+ assert_se(streq(ans2, "szl:pl"));
+}
+
+TEST(find_converted_keymap) {
+ _cleanup_free_ char *ans = NULL, *ans2 = NULL;
+ int r;
+
+ assert_se(find_converted_keymap(
+ &(X11Context) {
+ .layout = (char*) "pl",
+ .variant = (char*) "foobar",
+ }, &ans) == 0);
+ assert_se(ans == NULL);
+
+ r = find_converted_keymap(
+ &(X11Context) {
+ .layout = (char*) "pl",
+ }, &ans);
+ if (r == 0) {
+ log_info("Skipping rest of %s: keymaps are not installed", __func__);
+ return;
+ }
+
+ assert_se(r == 1);
+ assert_se(streq(ans, "pl"));
+ ans = mfree(ans);
+
+ assert_se(find_converted_keymap(
+ &(X11Context) {
+ .layout = (char*) "pl",
+ .variant = (char*) "dvorak",
+ }, &ans2) == 1);
+ assert_se(streq(ans2, "pl-dvorak"));
+}
+
+TEST(find_legacy_keymap) {
+ X11Context xc = {};
+ _cleanup_free_ char *ans = NULL, *ans2 = NULL;
+
+ xc.layout = (char*) "foobar";
+ assert_se(find_legacy_keymap(&xc, &ans) == 0);
+ assert_se(ans == NULL);
+
+ xc.layout = (char*) "pl";
+ assert_se(find_legacy_keymap(&xc, &ans) == 1);
+ assert_se(streq(ans, "pl2"));
+
+ xc.layout = (char*) "pl,ru";
+ assert_se(find_legacy_keymap(&xc, &ans2) == 1);
+ assert_se(streq(ans, "pl2"));
+}
+
+TEST(vconsole_convert_to_x11) {
+ _cleanup_(x11_context_clear) X11Context xc = {};
+ _cleanup_(vc_context_clear) VCContext vc = {};
+ int r;
+
+ log_info("/* test empty keymap */");
+ assert_se(vconsole_convert_to_x11(&vc, &xc) >= 0);
+ assert_se(x11_context_isempty(&xc));
+
+ log_info("/* test without variant, new mapping (es:) */");
+ assert_se(free_and_strdup(&vc.keymap, "es") >= 0);
+ assert_se(vconsole_convert_to_x11(&vc, &xc) >= 0);
+ assert_se(streq(xc.layout, "es"));
+ assert_se(xc.variant == NULL);
+ x11_context_clear(&xc);
+
+ log_info("/* test with known variant, new mapping (es:dvorak) */");
+ assert_se(free_and_strdup(&vc.keymap, "es-dvorak") >= 0);
+ assert_se(vconsole_convert_to_x11(&vc, &xc) >= 0);
+ assert_se(streq(xc.layout, "es"));
+ assert_se(streq(xc.variant, "dvorak"));
+ x11_context_clear(&xc);
+
+ log_info("/* test with old mapping (fr:latin9) */");
+ assert_se(free_and_strdup(&vc.keymap, "fr-latin9") >= 0);
+ assert_se(vconsole_convert_to_x11(&vc, &xc) >= 0);
+ assert_se(streq(xc.layout, "fr"));
+ assert_se(streq(xc.variant, "latin9"));
+ x11_context_clear(&xc);
+
+ log_info("/* test with a compound mapping (ru,us) */");
+ assert_se(free_and_strdup(&vc.keymap, "ru") >= 0);
+ assert_se(vconsole_convert_to_x11(&vc, &xc) >= 0);
+ assert_se(streq(xc.layout, "ru,us"));
+ assert_se(xc.variant == NULL);
+ x11_context_clear(&xc);
+
+ log_info("/* test with a simple mapping (us) */");
+ assert_se(free_and_strdup(&vc.keymap, "us") >= 0);
+ assert_se(vconsole_convert_to_x11(&vc, &xc) >= 0);
+ assert_se(streq(xc.layout, "us"));
+ assert_se(xc.variant == NULL);
+ x11_context_clear(&xc);
+
+ /* "gh" has no mapping in kbd-model-map and kbd provides a converted keymap for this layout. */
+ log_info("/* test with a converted keymap (gh:) */");
+ assert_se(free_and_strdup(&vc.keymap, "gh") >= 0);
+ r = vconsole_convert_to_x11(&vc, &xc);
+ if (r == 0) {
+ log_info("Skipping rest of %s: keymaps are not installed", __func__);
+ return;
+ }
+ assert_se(r > 0);
+ assert_se(streq(xc.layout, "gh"));
+ assert_se(xc.variant == NULL);
+ x11_context_clear(&xc);
+
+ log_info("/* test with converted keymap and with a known variant (gh:ewe) */");
+ assert_se(free_and_strdup(&vc.keymap, "gh-ewe") >= 0);
+ assert_se(vconsole_convert_to_x11(&vc, &xc) > 0);
+ assert_se(streq(xc.layout, "gh"));
+ assert_se(streq(xc.variant, "ewe"));
+ x11_context_clear(&xc);
+
+ log_info("/* test with converted keymap and with an unknown variant (gh:ewe) */");
+ assert_se(free_and_strdup(&vc.keymap, "gh-foobar") > 0);
+ assert_se(vconsole_convert_to_x11(&vc, &xc) > 0);
+ assert_se(streq(xc.layout, "gh"));
+ assert_se(xc.variant == NULL);
+ x11_context_clear(&xc);
+}
+
+TEST(x11_convert_to_vconsole) {
+ _cleanup_(x11_context_clear) X11Context xc = {};
+ _cleanup_(vc_context_clear) VCContext vc = {};
+
+ log_info("/* test empty layout (:) */");
+ assert_se(x11_convert_to_vconsole(&xc, &vc) >= 0);
+ assert_se(vc_context_isempty(&vc));
+
+ log_info("/* test without variant, new mapping (es:) */");
+ assert_se(free_and_strdup(&xc.layout, "es") >= 0);
+ assert_se(x11_convert_to_vconsole(&xc, &vc) >= 0);
+ assert_se(streq(vc.keymap, "es"));
+ vc_context_clear(&vc);
+
+ log_info("/* test with unknown variant, new mapping (es:foobar) */");
+ assert_se(free_and_strdup(&xc.variant, "foobar") >= 0);
+ assert_se(x11_convert_to_vconsole(&xc, &vc) >= 0);
+ assert_se(streq(vc.keymap, "es"));
+ vc_context_clear(&vc);
+
+ log_info("/* test with known variant, new mapping (es:dvorak) */");
+ assert_se(free_and_strdup(&xc.variant, "dvorak") >= 0);
+ assert_se(x11_convert_to_vconsole(&xc, &vc) >= 0);
+ if (vc_context_isempty(&vc)) {
+ log_info("Skipping rest of %s: keymaps are not installed", __func__);
+ return;
+ }
+ assert_se(streq(vc.keymap, "es-dvorak"));
+ vc_context_clear(&vc);
+
+ /* es no-variant test is not very good as the desired match
+ comes first in the list so will win if both candidates score
+ the same. in this case the desired match comes second so will
+ not win unless we correctly give the no-variant match a bonus
+ */
+ log_info("/* test without variant, desired match second (bg,us:) */");
+ assert_se(free_and_strdup(&xc.layout, "bg,us") >= 0);
+ assert_se(free_and_strdup(&xc.variant, NULL) >= 0);
+ assert_se(x11_convert_to_vconsole(&xc, &vc) >= 0);
+ assert_se(streq(vc.keymap, "bg_bds-utf8"));
+ vc_context_clear(&vc);
+
+ /* same, but with variant specified as "," */
+ log_info("/* test with variant as ',', desired match second (bg,us:) */");
+ assert_se(free_and_strdup(&xc.variant, ",") >= 0);
+ assert_se(x11_convert_to_vconsole(&xc, &vc) >= 0);
+ assert_se(streq(vc.keymap, "bg_bds-utf8"));
+ vc_context_clear(&vc);
+
+ log_info("/* test with old mapping (fr:latin9) */");
+ assert_se(free_and_strdup(&xc.layout, "fr") >= 0);
+ assert_se(free_and_strdup(&xc.variant, "latin9") >= 0);
+ assert_se(x11_convert_to_vconsole(&xc, &vc) >= 0);
+ assert_se(streq(vc.keymap, "fr-latin9"));
+ vc_context_clear(&vc);
+
+ /* https://bugzilla.redhat.com/show_bug.cgi?id=1039185 */
+ /* us,ru is the x config users want, but they still want ru
+ as the console layout in this case */
+ log_info("/* test with a compound mapping (us,ru:) */");
+ assert_se(free_and_strdup(&xc.layout, "us,ru") >= 0);
+ assert_se(free_and_strdup(&xc.variant, NULL) >= 0);
+ assert_se(x11_convert_to_vconsole(&xc, &vc) >= 0);
+ assert_se(streq(vc.keymap, "ru"));
+ vc_context_clear(&vc);
+
+ log_info("/* test with a compound mapping (ru,us:) */");
+ assert_se(free_and_strdup(&xc.layout, "ru,us") >= 0);
+ assert_se(free_and_strdup(&xc.variant, NULL) >= 0);
+ assert_se(x11_convert_to_vconsole(&xc, &vc) >= 0);
+ assert_se(streq(vc.keymap, "ru"));
+ vc_context_clear(&vc);
+
+ /* https://bugzilla.redhat.com/show_bug.cgi?id=1333998 */
+ log_info("/* test with a simple new mapping (ru:) */");
+ assert_se(free_and_strdup(&xc.layout, "ru") >= 0);
+ assert_se(free_and_strdup(&xc.variant, NULL) >= 0);
+ assert_se(x11_convert_to_vconsole(&xc, &vc) >= 0);
+ assert_se(streq(vc.keymap, "ru"));
+}
+
+static int intro(void) {
+ _cleanup_free_ char *map = NULL;
+
+ assert_se(get_testdata_dir("test-keymap-util/kbd-model-map", &map) >= 0);
+ assert_se(setenv("SYSTEMD_KBD_MODEL_MAP", map, 1) == 0);
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
diff --git a/src/locale/xkbcommon-util.c b/src/locale/xkbcommon-util.c
new file mode 100644
index 0000000..295ac8a
--- /dev/null
+++ b/src/locale/xkbcommon-util.c
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "dlfcn-util.h"
+#include "log.h"
+#include "macro.h"
+#include "string-util.h"
+#include "xkbcommon-util.h"
+
+#if HAVE_XKBCOMMON
+static void *xkbcommon_dl = NULL;
+
+struct xkb_context* (*sym_xkb_context_new)(enum xkb_context_flags flags);
+void (*sym_xkb_context_unref)(struct xkb_context *context);
+void (*sym_xkb_context_set_log_fn)(
+ struct xkb_context *context,
+ void (*log_fn)(
+ struct xkb_context *context,
+ enum xkb_log_level level,
+ const char *format,
+ va_list args));
+struct xkb_keymap* (*sym_xkb_keymap_new_from_names)(
+ struct xkb_context *context,
+ const struct xkb_rule_names *names,
+ enum xkb_keymap_compile_flags flags);
+void (*sym_xkb_keymap_unref)(struct xkb_keymap *keymap);
+
+static int dlopen_xkbcommon(void) {
+ return dlopen_many_sym_or_warn(
+ &xkbcommon_dl, "libxkbcommon.so.0", LOG_DEBUG,
+ DLSYM_ARG(xkb_context_new),
+ DLSYM_ARG(xkb_context_unref),
+ DLSYM_ARG(xkb_context_set_log_fn),
+ DLSYM_ARG(xkb_keymap_new_from_names),
+ DLSYM_ARG(xkb_keymap_unref));
+}
+
+_printf_(3, 0)
+static void log_xkb(struct xkb_context *ctx, enum xkb_log_level lvl, const char *format, va_list args) {
+ const char *fmt;
+
+ fmt = strjoina("libxkbcommon: ", format);
+ DISABLE_WARNING_FORMAT_NONLITERAL;
+ log_internalv(LOG_DEBUG, 0, PROJECT_FILE, __LINE__, __func__, fmt, args);
+ REENABLE_WARNING;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct xkb_context *, sym_xkb_context_unref, NULL);
+DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(struct xkb_keymap *, sym_xkb_keymap_unref, NULL);
+
+int verify_xkb_rmlvo(const char *model, const char *layout, const char *variant, const char *options) {
+ _cleanup_(sym_xkb_context_unrefp) struct xkb_context *ctx = NULL;
+ _cleanup_(sym_xkb_keymap_unrefp) struct xkb_keymap *km = NULL;
+ const struct xkb_rule_names rmlvo = {
+ .model = model,
+ .layout = layout,
+ .variant = variant,
+ .options = options,
+ };
+ int r;
+
+ /* Compile keymap from RMLVO information to check out its validity */
+
+ r = dlopen_xkbcommon();
+ if (r < 0)
+ return r;
+
+ ctx = sym_xkb_context_new(XKB_CONTEXT_NO_ENVIRONMENT_NAMES);
+ if (!ctx)
+ return -ENOMEM;
+
+ sym_xkb_context_set_log_fn(ctx, log_xkb);
+
+ km = sym_xkb_keymap_new_from_names(ctx, &rmlvo, XKB_KEYMAP_COMPILE_NO_FLAGS);
+ if (!km)
+ return -EINVAL;
+
+ return 0;
+}
+
+#endif
diff --git a/src/locale/xkbcommon-util.h b/src/locale/xkbcommon-util.h
new file mode 100644
index 0000000..e99c2d7
--- /dev/null
+++ b/src/locale/xkbcommon-util.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#if HAVE_XKBCOMMON
+#include <xkbcommon/xkbcommon.h>
+
+extern struct xkb_context* (*sym_xkb_context_new)(enum xkb_context_flags flags);
+extern void (*sym_xkb_context_unref)(struct xkb_context *context);
+extern void (*sym_xkb_context_set_log_fn)(
+ struct xkb_context *context,
+ void (*log_fn)(
+ struct xkb_context *context,
+ enum xkb_log_level level,
+ const char *format,
+ va_list args));
+extern struct xkb_keymap* (*sym_xkb_keymap_new_from_names)(
+ struct xkb_context *context,
+ const struct xkb_rule_names *names,
+ enum xkb_keymap_compile_flags flags);
+extern void (*sym_xkb_keymap_unref)(struct xkb_keymap *keymap);
+
+int verify_xkb_rmlvo(const char *model, const char *layout, const char *variant, const char *options);
+
+#else
+
+static inline int verify_xkb_rmlvo(const char *model, const char *layout, const char *variant, const char *options) {
+ return 0;
+}
+
+#endif