summaryrefslogtreecommitdiffstats
path: root/src/locale
diff options
context:
space:
mode:
Diffstat (limited to 'src/locale')
-rw-r--r--src/locale/kbd-model-map72
-rw-r--r--src/locale/language-fallback-map13
-rw-r--r--src/locale/localectl.c540
-rw-r--r--src/locale/localed-util.c846
-rw-r--r--src/locale/localed-util.h45
-rw-r--r--src/locale/localed.c815
-rw-r--r--src/locale/meson.build36
-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.c191
11 files changed, 2643 insertions, 0 deletions
diff --git a/src/locale/kbd-model-map b/src/locale/kbd-model-map
new file mode 100644
index 0000000..fd90f3c
--- /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
+hu101 hu pc105 qwerty 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
+hu hu pc105 - 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 - terminate:ctrl_alt_bksp,qwerty
+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..c23f1fa
--- /dev/null
+++ b/src/locale/localectl.c
@@ -0,0 +1,540 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <stdbool.h>
+
+#include "sd-bus.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 = { .mtime = USEC_INFINITY };
+
+ 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("key", "value");
+ if (!table)
+ return log_oom();
+
+ assert_se(cell = table_get_cell(table, 0, 0));
+ (void) table_set_ellipsize_percent(table, cell, 100);
+ (void) table_set_align_percent(table, cell, 100);
+
+ table_set_header(table, false);
+
+ 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_STRING, "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_STRING, "System Locale:",
+ TABLE_STRV, i->locale,
+ TABLE_STRING, "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_STRING, "VC Toggle Keymap:",
+ TABLE_STRING, i->vconsole_keymap_toggle);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_many(table,
+ TABLE_STRING, "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_STRING, "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_STRING, "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_STRING, "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 *l, *w;
+
+ r = read_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;
+
+ l = strstrip(line);
+
+ if (isempty(l))
+ continue;
+
+ if (l[0] == '!') {
+ if (startswith(l, "! model"))
+ state = MODELS;
+ else if (startswith(l, "! layout"))
+ state = LAYOUTS;
+ else if (startswith(l, "! variant"))
+ state = VARIANTS;
+ else if (startswith(l, "! option"))
+ state = OPTIONS;
+ else
+ state = NONE;
+
+ continue;
+ }
+
+ if (state != look_for)
+ continue;
+
+ w = l + strcspn(l, 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;
+
+ r = strv_extend(&list, l);
+ if (r < 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, false, &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..4d021fb
--- /dev/null
+++ b/src/locale/localed-util.c
@@ -0,0 +1,846 @@
+/* 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 "string-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+
+static bool startswith_comma(const char *s, const char *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;
+}
+
+static void context_free_x11(Context *c) {
+ c->x11_layout = mfree(c->x11_layout);
+ c->x11_options = mfree(c->x11_options);
+ c->x11_model = mfree(c->x11_model);
+ c->x11_variant = mfree(c->x11_variant);
+}
+
+static void context_free_vconsole(Context *c) {
+ c->vc_keymap = mfree(c->vc_keymap);
+ c->vc_keymap_toggle = mfree(c->vc_keymap_toggle);
+}
+
+void context_clear(Context *c) {
+ locale_context_clear(&c->locale_context);
+ context_free_x11(c);
+ context_free_vconsole(c);
+
+ sd_bus_message_unref(c->locale_cache);
+ sd_bus_message_unref(c->x11_cache);
+ sd_bus_message_unref(c->vc_cache);
+
+ bus_verify_polkit_async_registry_free(c->polkit_registry);
+};
+
+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) {
+ struct stat st;
+ usec_t t;
+
+ /* 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);
+ }
+
+ if (stat("/etc/vconsole.conf", &st) < 0) {
+ if (errno != ENOENT)
+ return -errno;
+
+ c->vc_mtime = USEC_INFINITY;
+ context_free_vconsole(c);
+ return 0;
+ }
+
+ /* If mtime is not changed, then we do not need to re-read */
+ t = timespec_load(&st.st_mtim);
+ if (c->vc_mtime != USEC_INFINITY && t == c->vc_mtime)
+ return 0;
+
+ c->vc_mtime = t;
+ context_free_vconsole(c);
+
+ return parse_env_file(NULL, "/etc/vconsole.conf",
+ "KEYMAP", &c->vc_keymap,
+ "KEYMAP_TOGGLE", &c->vc_keymap_toggle);
+}
+
+int x11_read_data(Context *c, sd_bus_message *m) {
+ _cleanup_fclose_ FILE *f = NULL;
+ bool in_section = false;
+ struct stat st;
+ usec_t t;
+ int r;
+
+ /* 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);
+ }
+
+ if (stat("/etc/X11/xorg.conf.d/00-keyboard.conf", &st) < 0) {
+ if (errno != ENOENT)
+ return -errno;
+
+ c->x11_mtime = USEC_INFINITY;
+ context_free_x11(c);
+ return 0;
+ }
+
+ /* If mtime is not changed, then we do not need to re-read */
+ t = timespec_load(&st.st_mtim);
+ if (c->x11_mtime != USEC_INFINITY && t == c->x11_mtime)
+ return 0;
+
+ c->x11_mtime = t;
+ context_free_x11(c);
+
+ f = fopen("/etc/X11/xorg.conf.d/00-keyboard.conf", "re");
+ if (!f)
+ return -errno;
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+ char *l;
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ l = strstrip(line);
+ if (IN_SET(l[0], 0, '#'))
+ continue;
+
+ if (in_section && first_word(l, "Option")) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ r = strv_split_full(&a, l, WHITESPACE, EXTRACT_UNQUOTE);
+ if (r < 0)
+ return r;
+
+ if (strv_length(a) == 3) {
+ char **p = NULL;
+
+ if (streq(a[1], "XkbLayout"))
+ p = &c->x11_layout;
+ else if (streq(a[1], "XkbModel"))
+ p = &c->x11_model;
+ else if (streq(a[1], "XkbVariant"))
+ p = &c->x11_variant;
+ else if (streq(a[1], "XkbOptions"))
+ p = &c->x11_options;
+
+ if (p)
+ free_and_replace(*p, a[2]);
+ }
+
+ } else if (!in_section && first_word(l, "Section")) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ r = strv_split_full(&a, l, 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(l, "EndSection"))
+ in_section = false;
+ }
+
+ return 0;
+}
+
+int vconsole_write_data(Context *c) {
+ _cleanup_strv_free_ char **l = NULL;
+ struct stat st;
+ int r;
+
+ 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_keymap_toggle));
+ if (r < 0)
+ return r;
+
+ if (strv_isempty(l)) {
+ if (unlink("/etc/vconsole.conf") < 0)
+ return errno == ENOENT ? 0 : -errno;
+
+ c->vc_mtime = USEC_INFINITY;
+ return 0;
+ }
+
+ r = write_env_file_label("/etc/vconsole.conf", l);
+ if (r < 0)
+ return r;
+
+ if (stat("/etc/vconsole.conf", &st) >= 0)
+ c->vc_mtime = timespec_load(&st.st_mtim);
+
+ return 0;
+}
+
+int x11_write_data(Context *c) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *temp_path = NULL;
+ struct stat st;
+ int r;
+
+ if (isempty(c->x11_layout) &&
+ isempty(c->x11_model) &&
+ isempty(c->x11_variant) &&
+ isempty(c->x11_options)) {
+
+ if (unlink("/etc/X11/xorg.conf.d/00-keyboard.conf") < 0)
+ return errno == ENOENT ? 0 : -errno;
+
+ c->vc_mtime = USEC_INFINITY;
+ 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"
+ "# instruct systemd-localed to update it.\n"
+ "Section \"InputClass\"\n"
+ " Identifier \"system-keyboard\"\n"
+ " MatchIsKeyboard \"on\"\n", f);
+
+ if (!isempty(c->x11_layout))
+ fprintf(f, " Option \"XkbLayout\" \"%s\"\n", c->x11_layout);
+
+ if (!isempty(c->x11_model))
+ fprintf(f, " Option \"XkbModel\" \"%s\"\n", c->x11_model);
+
+ if (!isempty(c->x11_variant))
+ fprintf(f, " Option \"XkbVariant\" \"%s\"\n", c->x11_variant);
+
+ if (!isempty(c->x11_options))
+ fprintf(f, " Option \"XkbOptions\" \"%s\"\n", c->x11_options);
+
+ fputs("EndSection\n", f);
+
+ r = fflush_sync_and_check(f);
+ if (r < 0)
+ goto fail;
+
+ if (rename(temp_path, "/etc/X11/xorg.conf.d/00-keyboard.conf") < 0) {
+ r = -errno;
+ goto fail;
+ }
+
+ if (stat("/etc/X11/xorg.conf.d/00-keyboard.conf", &st) >= 0)
+ c->x11_mtime = timespec_load(&st.st_mtim);
+
+ return 0;
+
+fail:
+ if (temp_path)
+ (void) unlink(temp_path);
+
+ return r;
+}
+
+static int read_next_mapping(const char* filename,
+ unsigned min_fields, unsigned max_fields,
+ FILE *f, unsigned *n, char ***a) {
+ assert(f);
+ assert(n);
+ assert(a);
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+ size_t length;
+ char *l, **b;
+ int r;
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ (*n)++;
+
+ l = strstrip(line);
+ if (IN_SET(l[0], 0, '#'))
+ continue;
+
+ r = strv_split_full(&b, l, WHITESPACE, EXTRACT_UNQUOTE);
+ if (r < 0)
+ return r;
+
+ length = strv_length(b);
+ if (length < min_fields || length > max_fields) {
+ log_error("Invalid line %s:%u, ignoring.", filename, *n);
+ strv_free(b);
+ continue;
+
+ }
+
+ *a = b;
+ return 1;
+ }
+
+ return 0;
+}
+
+int vconsole_convert_to_x11(Context *c) {
+ const char *map;
+ int modified = -1;
+
+ map = systemd_kbd_model_map();
+
+ if (isempty(c->vc_keymap)) {
+ modified =
+ !isempty(c->x11_layout) ||
+ !isempty(c->x11_model) ||
+ !isempty(c->x11_variant) ||
+ !isempty(c->x11_options);
+
+ context_free_x11(c);
+ } else {
+ _cleanup_fclose_ FILE *f = NULL;
+ unsigned n = 0;
+
+ f = fopen(map, "re");
+ if (!f)
+ return -errno;
+
+ for (;;) {
+ _cleanup_strv_free_ char **a = NULL;
+ int r;
+
+ r = read_next_mapping(map, 5, UINT_MAX, f, &n, &a);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ if (!streq(c->vc_keymap, a[0]))
+ continue;
+
+ if (!streq_ptr(c->x11_layout, empty_or_dash_to_null(a[1])) ||
+ !streq_ptr(c->x11_model, empty_or_dash_to_null(a[2])) ||
+ !streq_ptr(c->x11_variant, empty_or_dash_to_null(a[3])) ||
+ !streq_ptr(c->x11_options, empty_or_dash_to_null(a[4]))) {
+
+ if (free_and_strdup(&c->x11_layout, empty_or_dash_to_null(a[1])) < 0 ||
+ free_and_strdup(&c->x11_model, empty_or_dash_to_null(a[2])) < 0 ||
+ free_and_strdup(&c->x11_variant, empty_or_dash_to_null(a[3])) < 0 ||
+ free_and_strdup(&c->x11_options, empty_or_dash_to_null(a[4])) < 0)
+ return -ENOMEM;
+
+ modified = true;
+ }
+
+ break;
+ }
+ }
+
+ if (modified > 0)
+ log_info("Changing X11 keyboard layout to '%s' model '%s' variant '%s' options '%s'",
+ strempty(c->x11_layout),
+ strempty(c->x11_model),
+ strempty(c->x11_variant),
+ strempty(c->x11_options));
+ else if (modified < 0)
+ log_notice("X11 keyboard layout was not modified: no conversion found for \"%s\".",
+ c->vc_keymap);
+ else
+ log_debug("X11 keyboard layout did not need to be modified.");
+
+ return modified > 0;
+}
+
+int find_converted_keymap(const char *x11_layout, const char *x11_variant, char **new_keymap) {
+ const char *dir;
+ _cleanup_free_ char *n = NULL;
+
+ if (x11_variant)
+ n = strjoin(x11_layout, "-", x11_variant);
+ else
+ n = strdup(x11_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);
+
+ *new_keymap = TAKE_PTR(n);
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+int find_legacy_keymap(Context *c, char **ret) {
+ const char *map;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *new_keymap = NULL;
+ unsigned n = 0;
+ unsigned best_matching = 0;
+ int r;
+
+ assert(!isempty(c->x11_layout));
+
+ map = systemd_kbd_model_map();
+
+ f = fopen(map, "re");
+ if (!f)
+ return -errno;
+
+ for (;;) {
+ _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(c->x11_layout, a[1]))
+ /* If we got an exact match, this is best */
+ matching = 10;
+ 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(c->x11_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 = strndup(a[1], strcspn(a[1], ","));
+ if (startswith_comma(c->x11_layout, x))
+ matching = 1;
+ }
+ }
+
+ if (matching > 0) {
+ if (isempty(c->x11_model) || streq_ptr(c->x11_model, a[2])) {
+ matching++;
+
+ if (streq_ptr(c->x11_variant, a[3])) {
+ matching++;
+
+ if (streq_ptr(c->x11_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 < 10 && c->x11_layout) {
+ /* 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.
+ */
+ char *l, *v = NULL, *converted;
+
+ l = strndupa_safe(c->x11_layout, strcspn(c->x11_layout, ","));
+ if (c->x11_variant)
+ v = strndupa_safe(c->x11_variant,
+ strcspn(c->x11_variant, ","));
+ r = find_converted_keymap(l, v, &converted);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ free_and_replace(new_keymap, converted);
+ }
+
+ *ret = TAKE_PTR(new_keymap);
+ return (bool) *ret;
+}
+
+int find_language_fallback(const char *lang, char **language) {
+ const char *map;
+ _cleanup_fclose_ FILE *f = NULL;
+ unsigned n = 0;
+
+ assert(lang);
+ assert(language);
+
+ map = systemd_language_fallback_map();
+
+ f = fopen(map, "re");
+ if (!f)
+ return -errno;
+
+ for (;;) {
+ _cleanup_strv_free_ char **a = NULL;
+ int r;
+
+ 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);
+ *language = TAKE_PTR(a[1]);
+ return 1;
+ }
+ }
+
+ assert_not_reached();
+}
+
+int x11_convert_to_vconsole(Context *c) {
+ bool modified = false;
+
+ if (isempty(c->x11_layout)) {
+ modified =
+ !isempty(c->vc_keymap) ||
+ !isempty(c->vc_keymap_toggle);
+
+ context_free_vconsole(c);
+ } else {
+ _cleanup_free_ char *new_keymap = NULL;
+ int r;
+
+ r = find_converted_keymap(c->x11_layout, c->x11_variant, &new_keymap);
+ if (r < 0)
+ return r;
+ else if (r == 0) {
+ r = find_legacy_keymap(c, &new_keymap);
+ if (r < 0)
+ return r;
+ }
+ if (r == 0)
+ /* 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\".",
+ c->x11_layout);
+
+ if (!streq_ptr(c->vc_keymap, new_keymap)) {
+ free_and_replace(c->vc_keymap, new_keymap);
+ c->vc_keymap_toggle = mfree(c->vc_keymap_toggle);
+ modified = true;
+ }
+ }
+
+ if (modified)
+ log_info("Changing virtual console keymap to '%s' toggle '%s'",
+ strempty(c->vc_keymap), strempty(c->vc_keymap_toggle));
+ else
+ log_debug("Virtual console keymap was not modified.");
+
+ return modified;
+}
+
+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;
+ char *l;
+
+ r = read_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;
+
+ l = strstrip(line);
+ if (strcaseeq_ptr(l, 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), fileno(fw), 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..5470d1b
--- /dev/null
+++ b/src/locale/localed-util.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "hashmap.h"
+#include "locale-setup.h"
+#include "time-util.h"
+
+typedef struct Context {
+ sd_bus_message *locale_cache;
+ LocaleContext locale_context;
+
+ sd_bus_message *x11_cache;
+ usec_t x11_mtime;
+ char *x11_layout;
+ char *x11_model;
+ char *x11_variant;
+ char *x11_options;
+
+ sd_bus_message *vc_cache;
+ usec_t vc_mtime;
+ char *vc_keymap;
+ char *vc_keymap_toggle;
+
+ Hashmap *polkit_registry;
+} Context;
+
+int find_converted_keymap(const char *x11_layout, const char *x11_variant, char **new_keymap);
+int find_legacy_keymap(Context *c, char **new_keymap);
+int find_language_fallback(const char *lang, char **language);
+
+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(Context *c);
+int vconsole_write_data(Context *c);
+int x11_convert_to_vconsole(Context *c);
+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..8b1f0de
--- /dev/null
+++ b/src/locale/localed.c
@@ -0,0 +1,815 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#if HAVE_XKBCOMMON
+#include <xkbcommon/xkbcommon.h>
+#include <dlfcn.h>
+#endif
+
+#include "sd-bus.h"
+
+#include "alloc-util.h"
+#include "bus-error.h"
+#include "bus-log-control-api.h"
+#include "bus-message.h"
+#include "bus-polkit.h"
+#include "def.h"
+#include "dlfcn-util.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 reload_system_manager(sd_bus *bus) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(bus);
+
+ r = sd_bus_message_new_method_call(bus, &m,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ "Reload");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, 0, &error, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to reload system manager: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+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 = sd_bus_call_method(bus,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ "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 vconsole_convert_to_x11_and_emit(Context *c, sd_bus_message *m) {
+ int r;
+
+ assert(m);
+
+ r = x11_read_data(c, m);
+ if (r < 0)
+ return r;
+
+ r = vconsole_convert_to_x11(c);
+ if (r <= 0)
+ return r;
+
+ /* modified */
+ r = x11_write_data(c);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write X11 keyboard layout: %m");
+
+ sd_bus_emit_properties_changed(sd_bus_message_get_bus(m),
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ "X11Layout", "X11Model", "X11Variant", "X11Options", NULL);
+
+ return 1;
+}
+
+static int x11_convert_to_vconsole_and_emit(Context *c, sd_bus_message *m) {
+ int r;
+
+ assert(m);
+
+ r = vconsole_read_data(c, m);
+ if (r < 0)
+ return r;
+
+ r = x11_convert_to_vconsole(c);
+ if (r <= 0)
+ return r;
+
+ /* modified */
+ r = vconsole_write_data(c);
+ if (r < 0)
+ log_error_errno(r, "Failed to save virtual console keymap: %m");
+
+ sd_bus_emit_properties_changed(sd_bus_message_get_bus(m),
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ "VConsoleKeymap", "VConsoleKeymapToggle", NULL);
+
+ return vconsole_reload(sd_bus_message_get_bus(m));
+}
+
+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 = 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 = userdata;
+ int r;
+
+ 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);
+ else if (streq(property, "VConsoleKeymapToggle"))
+ return sd_bus_message_append_basic(reply, 's', c->vc_keymap_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 = userdata;
+ int r;
+
+ r = x11_read_data(c, reply);
+ if (r < 0)
+ return r;
+
+ if (streq(property, "X11Layout"))
+ return sd_bus_message_append_basic(reply, 's', c->x11_layout);
+ else if (streq(property, "X11Model"))
+ return sd_bus_message_append_basic(reply, 's', c->x11_model);
+ else if (streq(property, "X11Variant"))
+ return sd_bus_message_append_basic(reply, 's', c->x11_variant);
+ else if (streq(property, "X11Options"))
+ return sd_bus_message_append_basic(reply, 's', c->x11_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]);
+ } else 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,
+ "Can not enable locale generation for invalid locale: %s",
+ new_locale[p]);
+ } else 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 r;
+
+ r = sd_bus_message_read_basic(m, 'b', &interactive);
+ if (r < 0)
+ return 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 -ENOMEM;
+
+ 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 r;
+
+ 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) reload_system_manager(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) {
+ Context *c = ASSERT_PTR(userdata);
+ const char *keymap, *keymap_toggle;
+ int convert, interactive, r;
+
+ assert(m);
+
+ r = sd_bus_message_read(m, "ssbb", &keymap, &keymap_toggle, &convert, &interactive);
+ if (r < 0)
+ return r;
+
+ keymap = empty_to_null(keymap);
+ keymap_toggle = empty_to_null(keymap_toggle);
+
+ 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");
+ }
+
+ FOREACH_STRING(name, keymap ?: keymap_toggle, keymap ? keymap_toggle : NULL) {
+ r = keymap_exists(name); /* This also verifies that the keymap name is kosher. */
+ if (r < 0) {
+ log_error_errno(r, "Failed to check keymap %s: %m", name);
+ return sd_bus_error_set_errnof(error, r, "Failed to check keymap %s: %m", name);
+ }
+ if (r == 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_FAILED, "Keymap %s is not installed.", name);
+ }
+
+ if (streq_ptr(keymap, c->vc_keymap) &&
+ streq_ptr(keymap_toggle, c->vc_keymap_toggle))
+ 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 */
+
+ if (free_and_strdup(&c->vc_keymap, keymap) < 0 ||
+ free_and_strdup(&c->vc_keymap_toggle, keymap_toggle) < 0)
+ return -ENOMEM;
+
+ r = vconsole_write_data(c);
+ if (r < 0) {
+ log_error_errno(r, "Failed to set virtual console keymap: %m");
+ return sd_bus_error_set_errnof(error, r, "Failed to set virtual console keymap: %m");
+ }
+
+ log_info("Changed virtual console keymap to '%s' toggle '%s'",
+ strempty(c->vc_keymap), strempty(c->vc_keymap_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", NULL);
+
+ if (convert) {
+ r = vconsole_convert_to_x11_and_emit(c, m);
+ if (r < 0)
+ log_error_errno(r, "Failed to convert keymap data: %m");
+ }
+
+ return sd_bus_reply_method_return(m, NULL);
+}
+
+#if HAVE_XKBCOMMON
+
+_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 LOAD_SYMBOL(symbol, dl, name) \
+ ({ \
+ (symbol) = (typeof(symbol)) dlvsym((dl), (name), "V_0.5.0"); \
+ (symbol) ? 0 : -EOPNOTSUPP; \
+ })
+
+static int verify_xkb_rmlvo(const char *model, const char *layout, const char *variant, const char *options) {
+
+ /* We dlopen() the library in order to make the dependency soft. The library (and what it pulls in) is huge
+ * after all, hence let's support XKB maps when the library is around, and refuse otherwise. The function
+ * pointers to the shared library are below: */
+
+ struct xkb_context* (*symbol_xkb_context_new)(enum xkb_context_flags flags) = NULL;
+ void (*symbol_xkb_context_unref)(struct xkb_context *context) = NULL;
+ void (*symbol_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)) = NULL;
+ struct xkb_keymap* (*symbol_xkb_keymap_new_from_names)(struct xkb_context *context, const struct xkb_rule_names *names, enum xkb_keymap_compile_flags flags) = NULL;
+ void (*symbol_xkb_keymap_unref)(struct xkb_keymap *keymap) = NULL;
+
+ const struct xkb_rule_names rmlvo = {
+ .model = model,
+ .layout = layout,
+ .variant = variant,
+ .options = options,
+ };
+ struct xkb_context *ctx = NULL;
+ struct xkb_keymap *km = NULL;
+ _cleanup_(dlclosep) void *dl = NULL;
+ int r;
+
+ /* Compile keymap from RMLVO information to check out its validity */
+
+ dl = dlopen("libxkbcommon.so.0", RTLD_LAZY);
+ if (!dl)
+ return -EOPNOTSUPP;
+
+ r = LOAD_SYMBOL(symbol_xkb_context_new, dl, "xkb_context_new");
+ if (r < 0)
+ goto finish;
+
+ r = LOAD_SYMBOL(symbol_xkb_context_unref, dl, "xkb_context_unref");
+ if (r < 0)
+ goto finish;
+
+ r = LOAD_SYMBOL(symbol_xkb_context_set_log_fn, dl, "xkb_context_set_log_fn");
+ if (r < 0)
+ goto finish;
+
+ r = LOAD_SYMBOL(symbol_xkb_keymap_new_from_names, dl, "xkb_keymap_new_from_names");
+ if (r < 0)
+ goto finish;
+
+ r = LOAD_SYMBOL(symbol_xkb_keymap_unref, dl, "xkb_keymap_unref");
+ if (r < 0)
+ goto finish;
+
+ ctx = symbol_xkb_context_new(XKB_CONTEXT_NO_ENVIRONMENT_NAMES);
+ if (!ctx) {
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ symbol_xkb_context_set_log_fn(ctx, log_xkb);
+
+ km = symbol_xkb_keymap_new_from_names(ctx, &rmlvo, XKB_KEYMAP_COMPILE_NO_FLAGS);
+ if (!km) {
+ r = -EINVAL;
+ goto finish;
+ }
+
+ r = 0;
+
+finish:
+ if (symbol_xkb_keymap_unref && km)
+ symbol_xkb_keymap_unref(km);
+
+ if (symbol_xkb_context_unref && ctx)
+ symbol_xkb_context_unref(ctx);
+
+ return r;
+}
+
+#else
+
+static int verify_xkb_rmlvo(const char *model, const char *layout, const char *variant, const char *options) {
+ return 0;
+}
+
+#endif
+
+static int method_set_x11_keyboard(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+ Context *c = ASSERT_PTR(userdata);
+ const char *layout, *model, *variant, *options;
+ int convert, interactive, r;
+
+ assert(m);
+
+ r = sd_bus_message_read(m, "ssssbb", &layout, &model, &variant, &options, &convert, &interactive);
+ if (r < 0)
+ return r;
+
+ layout = empty_to_null(layout);
+ model = empty_to_null(model);
+ variant = empty_to_null(variant);
+ options = empty_to_null(options);
+
+ 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 (streq_ptr(layout, c->x11_layout) &&
+ streq_ptr(model, c->x11_model) &&
+ streq_ptr(variant, c->x11_variant) &&
+ streq_ptr(options, c->x11_options))
+ return sd_bus_reply_method_return(m, NULL);
+
+ if ((layout && !string_is_safe(layout)) ||
+ (model && !string_is_safe(model)) ||
+ (variant && !string_is_safe(variant)) ||
+ (options && !string_is_safe(options)))
+ return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Received invalid keyboard data");
+
+ r = verify_xkb_rmlvo(model, layout, variant, options);
+ if (r < 0) {
+ log_error_errno(r, "Cannot compile XKB keymap for new x11 keyboard layout ('%s' / '%s' / '%s' / '%s'): %m",
+ strempty(model), strempty(layout), strempty(variant), strempty(options));
+
+ if (r == -EOPNOTSUPP)
+ return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Local keyboard configuration not supported on this system.");
+
+ return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Specified keymap cannot be compiled, refusing as invalid.");
+ }
+
+ 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 */
+
+ if (free_and_strdup(&c->x11_layout, layout) < 0 ||
+ free_and_strdup(&c->x11_model, model) < 0 ||
+ free_and_strdup(&c->x11_variant, variant) < 0 ||
+ free_and_strdup(&c->x11_options, options) < 0)
+ return -ENOMEM;
+
+ r = x11_write_data(c);
+ if (r < 0) {
+ log_error_errno(r, "Failed to set X11 keyboard layout: %m");
+ return sd_bus_error_set_errnof(error, r, "Failed to set X11 keyboard layout: %m");
+ }
+
+ log_info("Changed X11 keyboard layout to '%s' model '%s' variant '%s' options '%s'",
+ strempty(c->x11_layout),
+ strempty(c->x11_model),
+ strempty(c->x11_variant),
+ strempty(c->x11_options));
+
+ (void) sd_bus_emit_properties_changed(
+ sd_bus_message_get_bus(m),
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ "X11Layout", "X11Model", "X11Variant", "X11Options", NULL);
+
+ if (convert) {
+ r = x11_convert_to_vconsole_and_emit(c, m);
+ if (r < 0)
+ log_error_errno(r, "Failed to convert keymap data: %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_NAMES("SetLocale",
+ "asb",
+ SD_BUS_PARAM(locale)
+ SD_BUS_PARAM(interactive),
+ NULL,,
+ method_set_locale,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_NAMES("SetVConsoleKeyboard",
+ "ssbb",
+ SD_BUS_PARAM(keymap)
+ SD_BUS_PARAM(keymap_toggle)
+ SD_BUS_PARAM(convert)
+ SD_BUS_PARAM(interactive),
+ NULL,,
+ method_set_vc_keyboard,
+ SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_NAMES("SetX11Keyboard",
+ "ssssbb",
+ SD_BUS_PARAM(layout)
+ SD_BUS_PARAM(model)
+ SD_BUS_PARAM(variant)
+ SD_BUS_PARAM(options)
+ SD_BUS_PARAM(convert)
+ SD_BUS_PARAM(interactive),
+ NULL,,
+ 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 = {
+ .locale_context.mtime = USEC_INFINITY,
+ .vc_mtime = USEC_INFINITY,
+ .x11_mtime = USEC_INFINITY,
+ };
+ _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_selinux_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..c1ae340
--- /dev/null
+++ b/src/locale/meson.build
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+systemd_localed_sources = files(
+ 'localed-util.c',
+ 'localed-util.h',
+ 'localed.c',
+)
+
+localectl_sources = files('localectl.c')
+
+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)
+endif
+
+# 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('kbd-model-map',
+ 'language-fallback-map',
+ install_dir : pkgdatadir)
+endif
+
+tests += [
+ [files('test-localed-util.c',
+ 'localed-util.c',
+ 'localed-util.h')],
+]
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..b48fc9f
--- /dev/null
+++ b/src/locale/test-localed-util.c
@@ -0,0 +1,191 @@
+/* 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("pl", "foobar", &ans) == 0);
+ assert_se(ans == NULL);
+
+ r = find_converted_keymap("pl", NULL, &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"));
+
+ assert_se(find_converted_keymap("pl", "dvorak", &ans2) == 1);
+ assert_se(streq(ans2, "pl-dvorak"));
+}
+
+TEST(find_legacy_keymap) {
+ Context c = {};
+ _cleanup_free_ char *ans = NULL, *ans2 = NULL;
+
+ c.x11_layout = (char*) "foobar";
+ assert_se(find_legacy_keymap(&c, &ans) == 0);
+ assert_se(ans == NULL);
+
+ c.x11_layout = (char*) "pl";
+ assert_se(find_legacy_keymap(&c, &ans) == 1);
+ assert_se(streq(ans, "pl2"));
+
+ c.x11_layout = (char*) "pl,ru";
+ assert_se(find_legacy_keymap(&c, &ans2) == 1);
+ assert_se(streq(ans, "pl2"));
+}
+
+TEST(vconsole_convert_to_x11) {
+ _cleanup_(context_clear) Context c = {};
+
+ log_info("/* test emptying first (:) */");
+ assert_se(free_and_strdup(&c.x11_layout, "foo") >= 0);
+ assert_se(free_and_strdup(&c.x11_variant, "bar") >= 0);
+ assert_se(vconsole_convert_to_x11(&c) == 1);
+ assert_se(c.x11_layout == NULL);
+ assert_se(c.x11_variant == NULL);
+
+ log_info("/* test emptying second (:) */");
+
+ assert_se(vconsole_convert_to_x11(&c) == 0);
+ assert_se(c.x11_layout == NULL);
+ assert_se(c.x11_variant == NULL);
+
+ log_info("/* test without variant, new mapping (es:) */");
+ assert_se(free_and_strdup(&c.vc_keymap, "es") >= 0);
+
+ assert_se(vconsole_convert_to_x11(&c) == 1);
+ assert_se(streq(c.x11_layout, "es"));
+ assert_se(c.x11_variant == NULL);
+
+ log_info("/* test with known variant, new mapping (es:dvorak) */");
+ assert_se(free_and_strdup(&c.vc_keymap, "es-dvorak") >= 0);
+
+ assert_se(vconsole_convert_to_x11(&c) == 1);
+ assert_se(streq(c.x11_layout, "es"));
+ assert_se(streq(c.x11_variant, "dvorak"));
+
+ log_info("/* test with old mapping (fr:latin9) */");
+ assert_se(free_and_strdup(&c.vc_keymap, "fr-latin9") >= 0);
+
+ assert_se(vconsole_convert_to_x11(&c) == 1);
+ assert_se(streq(c.x11_layout, "fr"));
+ assert_se(streq(c.x11_variant, "latin9"));
+
+ log_info("/* test with a compound mapping (ru,us) */");
+ assert_se(free_and_strdup(&c.vc_keymap, "ru") >= 0);
+
+ assert_se(vconsole_convert_to_x11(&c) == 1);
+ assert_se(streq(c.x11_layout, "ru,us"));
+ assert_se(c.x11_variant == NULL);
+
+ log_info("/* test with a simple mapping (us) */");
+ assert_se(free_and_strdup(&c.vc_keymap, "us") >= 0);
+
+ assert_se(vconsole_convert_to_x11(&c) == 1);
+ assert_se(streq(c.x11_layout, "us"));
+ assert_se(c.x11_variant == NULL);
+}
+
+TEST(x11_convert_to_vconsole) {
+ _cleanup_(context_clear) Context c = {};
+ int r;
+
+ log_info("/* test emptying first (:) */");
+ assert_se(free_and_strdup(&c.vc_keymap, "foobar") >= 0);
+ assert_se(x11_convert_to_vconsole(&c) == 1);
+ assert_se(c.vc_keymap == NULL);
+
+ log_info("/* test emptying second (:) */");
+
+ assert_se(x11_convert_to_vconsole(&c) == 0);
+ assert_se(c.vc_keymap == NULL);
+
+ log_info("/* test without variant, new mapping (es:) */");
+ assert_se(free_and_strdup(&c.x11_layout, "es") >= 0);
+
+ assert_se(x11_convert_to_vconsole(&c) == 1);
+ assert_se(streq(c.vc_keymap, "es"));
+
+ log_info("/* test with unknown variant, new mapping (es:foobar) */");
+ assert_se(free_and_strdup(&c.x11_variant, "foobar") >= 0);
+
+ assert_se(x11_convert_to_vconsole(&c) == 0);
+ assert_se(streq(c.vc_keymap, "es"));
+
+ log_info("/* test with known variant, new mapping (es:dvorak) */");
+ assert_se(free_and_strdup(&c.x11_variant, "dvorak") >= 0);
+
+ r = x11_convert_to_vconsole(&c);
+ if (r == 0) {
+ log_info("Skipping rest of %s: keymaps are not installed", __func__);
+ return;
+ }
+
+ assert_se(r == 1);
+ assert_se(streq(c.vc_keymap, "es-dvorak"));
+
+ log_info("/* test with old mapping (fr:latin9) */");
+ assert_se(free_and_strdup(&c.x11_layout, "fr") >= 0);
+ assert_se(free_and_strdup(&c.x11_variant, "latin9") >= 0);
+
+ assert_se(x11_convert_to_vconsole(&c) == 1);
+ assert_se(streq(c.vc_keymap, "fr-latin9"));
+
+ log_info("/* test with a compound mapping (us,ru:) */");
+ assert_se(free_and_strdup(&c.x11_layout, "us,ru") >= 0);
+ assert_se(free_and_strdup(&c.x11_variant, NULL) >= 0);
+
+ assert_se(x11_convert_to_vconsole(&c) == 1);
+ assert_se(streq(c.vc_keymap, "us"));
+
+ log_info("/* test with a compound mapping (ru,us:) */");
+ assert_se(free_and_strdup(&c.x11_layout, "ru,us") >= 0);
+ assert_se(free_and_strdup(&c.x11_variant, NULL) >= 0);
+
+ assert_se(x11_convert_to_vconsole(&c) == 1);
+ assert_se(streq(c.vc_keymap, "ru"));
+
+ /* https://bugzilla.redhat.com/show_bug.cgi?id=1333998 */
+ log_info("/* test with a simple new mapping (ru:) */");
+ assert_se(free_and_strdup(&c.x11_layout, "ru") >= 0);
+ assert_se(free_and_strdup(&c.x11_variant, NULL) >= 0);
+
+ assert_se(x11_convert_to_vconsole(&c) == 0);
+ assert_se(streq(c.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);