1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
8 #include "bus-polkit.h"
10 #include "env-file-label.h"
14 #include "fileio-label.h"
18 #include "keymap-util.h"
19 #include "locale-util.h"
21 #include "mkdir-label.h"
22 #include "nulstr-util.h"
23 #include "process-util.h"
24 #include "string-util.h"
26 #include "tmpfile-util.h"
28 static bool startswith_comma(const char *s
, const char *prefix
) {
29 s
= startswith(s
, prefix
);
33 return IN_SET(*s
, ',', '\0');
36 static const char* systemd_kbd_model_map(void) {
39 s
= getenv("SYSTEMD_KBD_MODEL_MAP");
43 return SYSTEMD_KBD_MODEL_MAP
;
46 static const char* systemd_language_fallback_map(void) {
49 s
= getenv("SYSTEMD_LANGUAGE_FALLBACK_MAP");
53 return SYSTEMD_LANGUAGE_FALLBACK_MAP
;
56 static void context_free_x11(Context
*c
) {
57 c
->x11_layout
= mfree(c
->x11_layout
);
58 c
->x11_options
= mfree(c
->x11_options
);
59 c
->x11_model
= mfree(c
->x11_model
);
60 c
->x11_variant
= mfree(c
->x11_variant
);
63 static void context_free_vconsole(Context
*c
) {
64 c
->vc_keymap
= mfree(c
->vc_keymap
);
65 c
->vc_keymap_toggle
= mfree(c
->vc_keymap_toggle
);
68 static void context_free_locale(Context
*c
) {
69 for (LocaleVariable p
= 0; p
< _VARIABLE_LC_MAX
; p
++)
70 c
->locale
[p
] = mfree(c
->locale
[p
]);
73 void context_clear(Context
*c
) {
74 context_free_locale(c
);
76 context_free_vconsole(c
);
78 sd_bus_message_unref(c
->locale_cache
);
79 sd_bus_message_unref(c
->x11_cache
);
80 sd_bus_message_unref(c
->vc_cache
);
82 bus_verify_polkit_async_registry_free(c
->polkit_registry
);
85 void locale_simplify(char *locale
[_VARIABLE_LC_MAX
]) {
86 for (LocaleVariable p
= VARIABLE_LANG
+1; p
< _VARIABLE_LC_MAX
; p
++)
87 if (isempty(locale
[p
]) || streq_ptr(locale
[VARIABLE_LANG
], locale
[p
]))
88 locale
[p
] = mfree(locale
[p
]);
91 int locale_read_data(Context
*c
, sd_bus_message
*m
) {
95 /* Do not try to re-read the file within single bus operation. */
97 if (m
== c
->locale_cache
)
100 sd_bus_message_unref(c
->locale_cache
);
101 c
->locale_cache
= sd_bus_message_ref(m
);
104 r
= stat("/etc/locale.conf", &st
);
105 if (r
< 0 && errno
!= ENOENT
)
111 /* If mtime is not changed, then we do not need to re-read the file. */
112 t
= timespec_load(&st
.st_mtim
);
113 if (c
->locale_mtime
!= USEC_INFINITY
&& t
== c
->locale_mtime
)
117 context_free_locale(c
);
119 r
= parse_env_file(NULL
, "/etc/locale.conf",
120 "LANG", &c
->locale
[VARIABLE_LANG
],
121 "LANGUAGE", &c
->locale
[VARIABLE_LANGUAGE
],
122 "LC_CTYPE", &c
->locale
[VARIABLE_LC_CTYPE
],
123 "LC_NUMERIC", &c
->locale
[VARIABLE_LC_NUMERIC
],
124 "LC_TIME", &c
->locale
[VARIABLE_LC_TIME
],
125 "LC_COLLATE", &c
->locale
[VARIABLE_LC_COLLATE
],
126 "LC_MONETARY", &c
->locale
[VARIABLE_LC_MONETARY
],
127 "LC_MESSAGES", &c
->locale
[VARIABLE_LC_MESSAGES
],
128 "LC_PAPER", &c
->locale
[VARIABLE_LC_PAPER
],
129 "LC_NAME", &c
->locale
[VARIABLE_LC_NAME
],
130 "LC_ADDRESS", &c
->locale
[VARIABLE_LC_ADDRESS
],
131 "LC_TELEPHONE", &c
->locale
[VARIABLE_LC_TELEPHONE
],
132 "LC_MEASUREMENT", &c
->locale
[VARIABLE_LC_MEASUREMENT
],
133 "LC_IDENTIFICATION", &c
->locale
[VARIABLE_LC_IDENTIFICATION
]);
137 c
->locale_mtime
= USEC_INFINITY
;
138 context_free_locale(c
);
140 /* Fill in what we got passed from systemd. */
141 for (LocaleVariable p
= 0; p
< _VARIABLE_LC_MAX
; p
++) {
144 name
= locale_variable_to_string(p
);
147 r
= free_and_strdup(&c
->locale
[p
], empty_to_null(getenv(name
)));
153 locale_simplify(c
->locale
);
157 int vconsole_read_data(Context
*c
, sd_bus_message
*m
) {
162 /* Do not try to re-read the file within single bus operation. */
164 if (m
== c
->vc_cache
)
167 sd_bus_message_unref(c
->vc_cache
);
168 c
->vc_cache
= sd_bus_message_ref(m
);
171 if (stat("/etc/vconsole.conf", &st
) < 0) {
175 c
->vc_mtime
= USEC_INFINITY
;
176 context_free_vconsole(c
);
180 /* If mtime is not changed, then we do not need to re-read */
181 t
= timespec_load(&st
.st_mtim
);
182 if (c
->vc_mtime
!= USEC_INFINITY
&& t
== c
->vc_mtime
)
186 context_free_vconsole(c
);
188 r
= parse_env_file(NULL
, "/etc/vconsole.conf",
189 "KEYMAP", &c
->vc_keymap
,
190 "KEYMAP_TOGGLE", &c
->vc_keymap_toggle
);
197 int x11_read_data(Context
*c
, sd_bus_message
*m
) {
198 _cleanup_fclose_
FILE *f
= NULL
;
199 bool in_section
= false;
204 /* Do not try to re-read the file within single bus operation. */
206 if (m
== c
->x11_cache
)
209 sd_bus_message_unref(c
->x11_cache
);
210 c
->x11_cache
= sd_bus_message_ref(m
);
213 if (stat("/etc/X11/xorg.conf.d/00-keyboard.conf", &st
) < 0) {
217 c
->x11_mtime
= USEC_INFINITY
;
222 /* If mtime is not changed, then we do not need to re-read */
223 t
= timespec_load(&st
.st_mtim
);
224 if (c
->x11_mtime
!= USEC_INFINITY
&& t
== c
->x11_mtime
)
230 f
= fopen("/etc/X11/xorg.conf.d/00-keyboard.conf", "re");
235 _cleanup_free_
char *line
= NULL
;
238 r
= read_line(f
, LONG_LINE_MAX
, &line
);
245 if (IN_SET(l
[0], 0, '#'))
248 if (in_section
&& first_word(l
, "Option")) {
249 _cleanup_strv_free_
char **a
= NULL
;
251 r
= strv_split_full(&a
, l
, WHITESPACE
, EXTRACT_UNQUOTE
);
255 if (strv_length(a
) == 3) {
258 if (streq(a
[1], "XkbLayout"))
260 else if (streq(a
[1], "XkbModel"))
262 else if (streq(a
[1], "XkbVariant"))
264 else if (streq(a
[1], "XkbOptions"))
268 free_and_replace(*p
, a
[2]);
271 } else if (!in_section
&& first_word(l
, "Section")) {
272 _cleanup_strv_free_
char **a
= NULL
;
274 r
= strv_split_full(&a
, l
, WHITESPACE
, EXTRACT_UNQUOTE
);
278 if (strv_length(a
) == 2 && streq(a
[1], "InputClass"))
281 } else if (in_section
&& first_word(l
, "EndSection"))
288 int locale_write_data(Context
*c
, char ***settings
) {
289 _cleanup_strv_free_
char **l
= NULL
;
293 /* Set values will be returned as strv in *settings on success. */
295 for (LocaleVariable p
= 0; p
< _VARIABLE_LC_MAX
; p
++)
296 if (!isempty(c
->locale
[p
])) {
297 r
= strv_env_assign(&l
, locale_variable_to_string(p
), c
->locale
[p
]);
302 if (strv_isempty(l
)) {
303 if (unlink("/etc/locale.conf") < 0)
304 return errno
== ENOENT
? 0 : -errno
;
306 c
->locale_mtime
= USEC_INFINITY
;
310 r
= write_env_file_label("/etc/locale.conf", l
);
314 *settings
= TAKE_PTR(l
);
316 if (stat("/etc/locale.conf", &st
) >= 0)
317 c
->locale_mtime
= timespec_load(&st
.st_mtim
);
322 int vconsole_write_data(Context
*c
) {
323 _cleanup_strv_free_
char **l
= NULL
;
327 r
= load_env_file(NULL
, "/etc/vconsole.conf", &l
);
328 if (r
< 0 && r
!= -ENOENT
)
331 r
= strv_env_assign(&l
, "KEYMAP", empty_to_null(c
->vc_keymap
));
335 r
= strv_env_assign(&l
, "KEYMAP_TOGGLE", empty_to_null(c
->vc_keymap_toggle
));
339 if (strv_isempty(l
)) {
340 if (unlink("/etc/vconsole.conf") < 0)
341 return errno
== ENOENT
? 0 : -errno
;
343 c
->vc_mtime
= USEC_INFINITY
;
347 r
= write_env_file_label("/etc/vconsole.conf", l
);
351 if (stat("/etc/vconsole.conf", &st
) >= 0)
352 c
->vc_mtime
= timespec_load(&st
.st_mtim
);
357 int x11_write_data(Context
*c
) {
358 _cleanup_fclose_
FILE *f
= NULL
;
359 _cleanup_free_
char *temp_path
= NULL
;
363 if (isempty(c
->x11_layout
) &&
364 isempty(c
->x11_model
) &&
365 isempty(c
->x11_variant
) &&
366 isempty(c
->x11_options
)) {
368 if (unlink("/etc/X11/xorg.conf.d/00-keyboard.conf") < 0)
369 return errno
== ENOENT
? 0 : -errno
;
371 c
->vc_mtime
= USEC_INFINITY
;
375 (void) mkdir_p_label("/etc/X11/xorg.conf.d", 0755);
376 r
= fopen_temporary("/etc/X11/xorg.conf.d/00-keyboard.conf", &f
, &temp_path
);
380 (void) fchmod(fileno(f
), 0644);
382 fputs("# Written by systemd-localed(8), read by systemd-localed and Xorg. It's\n"
383 "# probably wise not to edit this file manually. Use localectl(1) to\n"
384 "# instruct systemd-localed to update it.\n"
385 "Section \"InputClass\"\n"
386 " Identifier \"system-keyboard\"\n"
387 " MatchIsKeyboard \"on\"\n", f
);
389 if (!isempty(c
->x11_layout
))
390 fprintf(f
, " Option \"XkbLayout\" \"%s\"\n", c
->x11_layout
);
392 if (!isempty(c
->x11_model
))
393 fprintf(f
, " Option \"XkbModel\" \"%s\"\n", c
->x11_model
);
395 if (!isempty(c
->x11_variant
))
396 fprintf(f
, " Option \"XkbVariant\" \"%s\"\n", c
->x11_variant
);
398 if (!isempty(c
->x11_options
))
399 fprintf(f
, " Option \"XkbOptions\" \"%s\"\n", c
->x11_options
);
401 fputs("EndSection\n", f
);
403 r
= fflush_sync_and_check(f
);
407 if (rename(temp_path
, "/etc/X11/xorg.conf.d/00-keyboard.conf") < 0) {
412 if (stat("/etc/X11/xorg.conf.d/00-keyboard.conf", &st
) >= 0)
413 c
->x11_mtime
= timespec_load(&st
.st_mtim
);
419 (void) unlink(temp_path
);
424 static int read_next_mapping(const char* filename
,
425 unsigned min_fields
, unsigned max_fields
,
426 FILE *f
, unsigned *n
, char ***a
) {
432 _cleanup_free_
char *line
= NULL
;
437 r
= read_line(f
, LONG_LINE_MAX
, &line
);
446 if (IN_SET(l
[0], 0, '#'))
449 r
= strv_split_full(&b
, l
, WHITESPACE
, EXTRACT_UNQUOTE
);
453 length
= strv_length(b
);
454 if (length
< min_fields
|| length
> max_fields
) {
455 log_error("Invalid line %s:%u, ignoring.", filename
, *n
);
468 int vconsole_convert_to_x11(Context
*c
) {
472 map
= systemd_kbd_model_map();
474 if (isempty(c
->vc_keymap
)) {
476 !isempty(c
->x11_layout
) ||
477 !isempty(c
->x11_model
) ||
478 !isempty(c
->x11_variant
) ||
479 !isempty(c
->x11_options
);
483 _cleanup_fclose_
FILE *f
= NULL
;
486 f
= fopen(map
, "re");
491 _cleanup_strv_free_
char **a
= NULL
;
494 r
= read_next_mapping(map
, 5, UINT_MAX
, f
, &n
, &a
);
500 if (!streq(c
->vc_keymap
, a
[0]))
503 if (!streq_ptr(c
->x11_layout
, empty_or_dash_to_null(a
[1])) ||
504 !streq_ptr(c
->x11_model
, empty_or_dash_to_null(a
[2])) ||
505 !streq_ptr(c
->x11_variant
, empty_or_dash_to_null(a
[3])) ||
506 !streq_ptr(c
->x11_options
, empty_or_dash_to_null(a
[4]))) {
508 if (free_and_strdup(&c
->x11_layout
, empty_or_dash_to_null(a
[1])) < 0 ||
509 free_and_strdup(&c
->x11_model
, empty_or_dash_to_null(a
[2])) < 0 ||
510 free_and_strdup(&c
->x11_variant
, empty_or_dash_to_null(a
[3])) < 0 ||
511 free_and_strdup(&c
->x11_options
, empty_or_dash_to_null(a
[4])) < 0)
522 log_info("Changing X11 keyboard layout to '%s' model '%s' variant '%s' options '%s'",
523 strempty(c
->x11_layout
),
524 strempty(c
->x11_model
),
525 strempty(c
->x11_variant
),
526 strempty(c
->x11_options
));
527 else if (modified
< 0)
528 log_notice("X11 keyboard layout was not modified: no conversion found for \"%s\".",
531 log_debug("X11 keyboard layout did not need to be modified.");
536 int find_converted_keymap(const char *x11_layout
, const char *x11_variant
, char **new_keymap
) {
538 _cleanup_free_
char *n
= NULL
;
541 n
= strjoin(x11_layout
, "-", x11_variant
);
543 n
= strdup(x11_layout
);
547 NULSTR_FOREACH(dir
, KBD_KEYMAP_DIRS
) {
548 _cleanup_free_
char *p
= NULL
, *pz
= NULL
;
551 p
= strjoin(dir
, "xkb/", n
, ".map");
552 pz
= strjoin(dir
, "xkb/", n
, ".map.gz");
556 uncompressed
= access(p
, F_OK
) == 0;
557 if (uncompressed
|| access(pz
, F_OK
) == 0) {
558 log_debug("Found converted keymap %s at %s",
559 n
, uncompressed
? p
: pz
);
561 *new_keymap
= TAKE_PTR(n
);
569 int find_legacy_keymap(Context
*c
, char **ret
) {
571 _cleanup_fclose_
FILE *f
= NULL
;
572 _cleanup_free_
char *new_keymap
= NULL
;
574 unsigned best_matching
= 0;
577 assert(!isempty(c
->x11_layout
));
579 map
= systemd_kbd_model_map();
581 f
= fopen(map
, "re");
586 _cleanup_strv_free_
char **a
= NULL
;
587 unsigned matching
= 0;
589 r
= read_next_mapping(map
, 5, UINT_MAX
, f
, &n
, &a
);
595 /* Determine how well matching this entry is */
596 if (streq(c
->x11_layout
, a
[1]))
597 /* If we got an exact match, this is best */
600 /* We have multiple X layouts, look for an
601 * entry that matches our key with everything
602 * but the first layout stripped off. */
603 if (startswith_comma(c
->x11_layout
, a
[1]))
606 _cleanup_free_
char *x
= NULL
;
608 /* If that didn't work, strip off the
609 * other layouts from the entry, too */
610 x
= strndup(a
[1], strcspn(a
[1], ","));
611 if (startswith_comma(c
->x11_layout
, x
))
617 if (isempty(c
->x11_model
) || streq_ptr(c
->x11_model
, a
[2])) {
620 if (streq_ptr(c
->x11_variant
, a
[3])) {
623 if (streq_ptr(c
->x11_options
, a
[4]))
629 /* The best matching entry so far, then let's save that */
630 if (matching
>= MAX(best_matching
, 1u)) {
631 log_debug("Found legacy keymap %s with score %u",
634 if (matching
> best_matching
) {
635 best_matching
= matching
;
637 r
= free_and_strdup(&new_keymap
, a
[0]);
644 if (best_matching
< 10 && c
->x11_layout
) {
645 /* The best match is only the first part of the X11
646 * keymap. Check if we have a converted map which
647 * matches just the first layout.
649 char *l
, *v
= NULL
, *converted
;
651 l
= strndupa_safe(c
->x11_layout
, strcspn(c
->x11_layout
, ","));
653 v
= strndupa_safe(c
->x11_variant
,
654 strcspn(c
->x11_variant
, ","));
655 r
= find_converted_keymap(l
, v
, &converted
);
659 free_and_replace(new_keymap
, converted
);
662 *ret
= TAKE_PTR(new_keymap
);
666 int find_language_fallback(const char *lang
, char **language
) {
668 _cleanup_fclose_
FILE *f
= NULL
;
674 map
= systemd_language_fallback_map();
676 f
= fopen(map
, "re");
681 _cleanup_strv_free_
char **a
= NULL
;
684 r
= read_next_mapping(map
, 2, 2, f
, &n
, &a
);
688 if (streq(lang
, a
[0])) {
689 assert(strv_length(a
) == 2);
690 *language
= TAKE_PTR(a
[1]);
695 assert_not_reached();
698 int x11_convert_to_vconsole(Context
*c
) {
699 bool modified
= false;
701 if (isempty(c
->x11_layout
)) {
703 !isempty(c
->vc_keymap
) ||
704 !isempty(c
->vc_keymap_toggle
);
706 context_free_vconsole(c
);
708 _cleanup_free_
char *new_keymap
= NULL
;
711 r
= find_converted_keymap(c
->x11_layout
, c
->x11_variant
, &new_keymap
);
715 r
= find_legacy_keymap(c
, &new_keymap
);
720 /* We search for layout-variant match first, but then we also look
721 * for anything which matches just the layout. So it's accurate to say
722 * that we couldn't find anything which matches the layout. */
723 log_notice("No conversion to virtual console map found for \"%s\".",
726 if (!streq_ptr(c
->vc_keymap
, new_keymap
)) {
727 free_and_replace(c
->vc_keymap
, new_keymap
);
728 c
->vc_keymap_toggle
= mfree(c
->vc_keymap_toggle
);
734 log_info("Changing virtual console keymap to '%s' toggle '%s'",
735 strempty(c
->vc_keymap
), strempty(c
->vc_keymap_toggle
));
737 log_debug("Virtual console keymap was not modified.");
742 bool locale_gen_check_available(void) {
744 if (access(LOCALEGEN_PATH
, X_OK
) < 0) {
746 log_warning_errno(errno
, "Unable to determine whether " LOCALEGEN_PATH
" exists and is executable, assuming it is not: %m");
749 if (access("/etc/locale.gen", F_OK
) < 0) {
751 log_warning_errno(errno
, "Unable to determine whether /etc/locale.gen exists, assuming it does not: %m");
761 static bool locale_encoding_is_utf8_or_unspecified(const char *locale
) {
762 const char *c
= strchr(locale
, '.');
763 return !c
|| strcaseeq(c
, ".UTF-8") || strcasestr(locale
, ".UTF-8@");
766 static int locale_gen_locale_supported(const char *locale_entry
) {
767 /* Returns an error valus <= 0 if the locale-gen entry is invalid or unsupported,
768 * 1 in case the locale entry is valid, and -EOPNOTSUPP specifically in case
769 * the distributor has not provided us with a SUPPORTED file to check
770 * locale for validity. */
772 _cleanup_fclose_
FILE *f
= NULL
;
775 assert(locale_entry
);
777 /* Locale templates without country code are never supported */
778 if (!strstr(locale_entry
, "_"))
781 f
= fopen("/usr/share/i18n/SUPPORTED", "re");
784 return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP
),
785 "Unable to check validity of locale entry %s: /usr/share/i18n/SUPPORTED does not exist",
791 _cleanup_free_
char *line
= NULL
;
793 r
= read_line(f
, LONG_LINE_MAX
, &line
);
795 return log_debug_errno(r
, "Failed to read /usr/share/i18n/SUPPORTED: %m");
799 line
= strstrip(line
);
800 if (strcaseeq_ptr(line
, locale_entry
))
806 int locale_gen_enable_locale(const char *locale
) {
808 _cleanup_fclose_
FILE *fr
= NULL
, *fw
= NULL
;
809 _cleanup_(unlink_and_freep
) char *temp_path
= NULL
;
810 _cleanup_free_
char *locale_entry
= NULL
;
811 bool locale_enabled
= false, first_line
= false;
812 bool write_new
= false;
818 if (locale_encoding_is_utf8_or_unspecified(locale
)) {
819 locale_entry
= strjoin(locale
, " UTF-8");
823 return -ENOEXEC
; /* We do not process non-UTF-8 locale */
825 r
= locale_gen_locale_supported(locale_entry
);
828 if (r
< 0 && r
!= -EOPNOTSUPP
)
831 fr
= fopen("/etc/locale.gen", "re");
838 r
= fopen_temporary("/etc/locale.gen", &fw
, &temp_path
);
843 (void) fchmod(fileno(fw
), 0644);
845 /* apply mode & xattrs of the original file to new file */
846 r
= copy_access(fileno(fr
), fileno(fw
));
849 r
= copy_xattr(fileno(fr
), fileno(fw
), COPY_ALL_XATTRS
);
855 /* The config file ends with a line break, which we do not want to include before potentially appending a new locale
856 * instead of uncommenting an existing line. By prepending linebreaks, we can avoid buffering this file but can still write
857 * a nice config file without empty lines */
860 _cleanup_free_
char *line
= NULL
;
863 r
= read_line(fr
, LONG_LINE_MAX
, &line
);
869 if (locale_enabled
) {
870 /* Just complete writing the file if the new locale was already enabled */
878 line
= strstrip(line
);
886 if (line_locale
[0] == '#')
887 line_locale
= strstrip(line_locale
+ 1);
888 else if (strcaseeq_ptr(line_locale
, locale_entry
))
889 return 0; /* the file already had our locale activated, so skip updating it */
891 if (strcaseeq_ptr(line_locale
, locale_entry
)) {
892 /* Uncomment existing line for new locale */
895 fputs(locale_entry
, fw
);
896 locale_enabled
= true;
901 /* The line was not for the locale we want to enable, just copy it */
909 /* Add locale to enable to the end of the file if it was not found as commented line */
910 if (!locale_enabled
) {
913 fputs(locale_entry
, fw
);
917 r
= fflush_sync_and_check(fw
);
921 if (rename(temp_path
, "/etc/locale.gen") < 0)
923 temp_path
= mfree(temp_path
);
931 int locale_gen_run(void) {
936 r
= safe_fork("(sd-localegen)", FORK_RESET_SIGNALS
|FORK_RLIMIT_NOFILE_SAFE
|FORK_CLOSE_ALL_FDS
|FORK_LOG
|FORK_WAIT
, &pid
);
940 execl(LOCALEGEN_PATH
, LOCALEGEN_PATH
, NULL
);