]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/locale/keymap-util.c
tree-wide: use TAKE_PTR() and TAKE_FD() macros
[thirdparty/systemd.git] / src / locale / keymap-util.c
CommitLineData
53e1b683 1/* SPDX-License-Identifier: LGPL-2.1+ */
4897d1dc
ZJS
2/***
3 This file is part of systemd.
4
5 Copyright 2011 Lennart Poettering
6 Copyright 2013 Kay Sievers
7
8 systemd is free software; you can redistribute it and/or modify it
9 under the terms of the GNU Lesser General Public License as published by
10 the Free Software Foundation; either version 2.1 of the License, or
11 (at your option) any later version.
12
13 systemd is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public License
19 along with systemd; If not, see <http://www.gnu.org/licenses/>.
20***/
21
22#include <errno.h>
0d536673 23#include <stdio_ext.h>
4897d1dc
ZJS
24#include <string.h>
25#include <unistd.h>
26
27#include "def.h"
28#include "env-util.h"
29#include "fd-util.h"
30#include "fileio-label.h"
31#include "fileio.h"
32#include "keymap-util.h"
33#include "locale-util.h"
34#include "macro.h"
35#include "mkdir.h"
36#include "string-util.h"
37#include "strv.h"
38
39static bool startswith_comma(const char *s, const char *prefix) {
5ad327dd
ZJS
40 s = startswith(s, prefix);
41 if (!s)
42 return false;
4897d1dc 43
4c701096 44 return IN_SET(*s, ',', '\0');
4897d1dc
ZJS
45}
46
47static const char* strnulldash(const char *s) {
48 return isempty(s) || streq(s, "-") ? NULL : s;
49}
50
cabffaf8
ZJS
51static const char* systemd_kbd_model_map(void) {
52 const char* s;
53
54 s = getenv("SYSTEMD_KBD_MODEL_MAP");
55 if (s)
56 return s;
57
58 return SYSTEMD_KBD_MODEL_MAP;
59}
60
61static const char* systemd_language_fallback_map(void) {
62 const char* s;
63
64 s = getenv("SYSTEMD_LANGUAGE_FALLBACK_MAP");
65 if (s)
66 return s;
67
68 return SYSTEMD_LANGUAGE_FALLBACK_MAP;
69}
70
4897d1dc
ZJS
71static void context_free_x11(Context *c) {
72 c->x11_layout = mfree(c->x11_layout);
73 c->x11_options = mfree(c->x11_options);
74 c->x11_model = mfree(c->x11_model);
75 c->x11_variant = mfree(c->x11_variant);
76}
77
78static void context_free_vconsole(Context *c) {
79 c->vc_keymap = mfree(c->vc_keymap);
80 c->vc_keymap_toggle = mfree(c->vc_keymap_toggle);
81}
82
83static void context_free_locale(Context *c) {
84 int p;
85
86 for (p = 0; p < _VARIABLE_LC_MAX; p++)
87 c->locale[p] = mfree(c->locale[p]);
88}
89
90void context_free(Context *c) {
91 context_free_locale(c);
92 context_free_x11(c);
93 context_free_vconsole(c);
94};
95
96void locale_simplify(Context *c) {
97 int p;
98
99 for (p = VARIABLE_LANG+1; p < _VARIABLE_LC_MAX; p++)
100 if (isempty(c->locale[p]) || streq_ptr(c->locale[VARIABLE_LANG], c->locale[p]))
101 c->locale[p] = mfree(c->locale[p]);
102}
103
104static int locale_read_data(Context *c) {
105 int r;
106
107 context_free_locale(c);
108
109 r = parse_env_file("/etc/locale.conf", NEWLINE,
110 "LANG", &c->locale[VARIABLE_LANG],
111 "LANGUAGE", &c->locale[VARIABLE_LANGUAGE],
112 "LC_CTYPE", &c->locale[VARIABLE_LC_CTYPE],
113 "LC_NUMERIC", &c->locale[VARIABLE_LC_NUMERIC],
114 "LC_TIME", &c->locale[VARIABLE_LC_TIME],
115 "LC_COLLATE", &c->locale[VARIABLE_LC_COLLATE],
116 "LC_MONETARY", &c->locale[VARIABLE_LC_MONETARY],
117 "LC_MESSAGES", &c->locale[VARIABLE_LC_MESSAGES],
118 "LC_PAPER", &c->locale[VARIABLE_LC_PAPER],
119 "LC_NAME", &c->locale[VARIABLE_LC_NAME],
120 "LC_ADDRESS", &c->locale[VARIABLE_LC_ADDRESS],
121 "LC_TELEPHONE", &c->locale[VARIABLE_LC_TELEPHONE],
122 "LC_MEASUREMENT", &c->locale[VARIABLE_LC_MEASUREMENT],
123 "LC_IDENTIFICATION", &c->locale[VARIABLE_LC_IDENTIFICATION],
124 NULL);
125
126 if (r == -ENOENT) {
127 int p;
128
129 /* Fill in what we got passed from systemd. */
130 for (p = 0; p < _VARIABLE_LC_MAX; p++) {
131 const char *name;
132
133 name = locale_variable_to_string(p);
134 assert(name);
135
136 r = free_and_strdup(&c->locale[p], empty_to_null(getenv(name)));
137 if (r < 0)
138 return r;
139 }
140
141 r = 0;
142 }
143
144 locale_simplify(c);
145 return r;
146}
147
148static int vconsole_read_data(Context *c) {
149 int r;
150
151 context_free_vconsole(c);
152
153 r = parse_env_file("/etc/vconsole.conf", NEWLINE,
154 "KEYMAP", &c->vc_keymap,
155 "KEYMAP_TOGGLE", &c->vc_keymap_toggle,
156 NULL);
157
158 if (r < 0 && r != -ENOENT)
159 return r;
160
161 return 0;
162}
163
164static int x11_read_data(Context *c) {
165 _cleanup_fclose_ FILE *f;
166 char line[LINE_MAX];
167 bool in_section = false;
168 int r;
169
170 context_free_x11(c);
171
172 f = fopen("/etc/X11/xorg.conf.d/00-keyboard.conf", "re");
173 if (!f)
174 return errno == ENOENT ? 0 : -errno;
175
176 while (fgets(line, sizeof(line), f)) {
177 char *l;
178
179 char_array_0(line);
180 l = strstrip(line);
181
4c701096 182 if (IN_SET(l[0], 0, '#'))
4897d1dc
ZJS
183 continue;
184
185 if (in_section && first_word(l, "Option")) {
186 _cleanup_strv_free_ char **a = NULL;
187
188 r = strv_split_extract(&a, l, WHITESPACE, EXTRACT_QUOTES);
189 if (r < 0)
190 return r;
191
192 if (strv_length(a) == 3) {
193 char **p = NULL;
194
195 if (streq(a[1], "XkbLayout"))
196 p = &c->x11_layout;
197 else if (streq(a[1], "XkbModel"))
198 p = &c->x11_model;
199 else if (streq(a[1], "XkbVariant"))
200 p = &c->x11_variant;
201 else if (streq(a[1], "XkbOptions"))
202 p = &c->x11_options;
203
204 if (p) {
f9ecfd3b 205 free_and_replace(*p, a[2]);
4897d1dc
ZJS
206 }
207 }
208
209 } else if (!in_section && first_word(l, "Section")) {
210 _cleanup_strv_free_ char **a = NULL;
211
212 r = strv_split_extract(&a, l, WHITESPACE, EXTRACT_QUOTES);
213 if (r < 0)
214 return -ENOMEM;
215
216 if (strv_length(a) == 2 && streq(a[1], "InputClass"))
217 in_section = true;
218
219 } else if (in_section && first_word(l, "EndSection"))
220 in_section = false;
221 }
222
223 return 0;
224}
225
226int context_read_data(Context *c) {
227 int r, q, p;
228
229 r = locale_read_data(c);
230 q = vconsole_read_data(c);
231 p = x11_read_data(c);
232
233 return r < 0 ? r : q < 0 ? q : p;
234}
235
236int locale_write_data(Context *c, char ***settings) {
237 int r, p;
238 _cleanup_strv_free_ char **l = NULL;
239
240 /* Set values will be returned as strv in *settings on success. */
241
242 r = load_env_file(NULL, "/etc/locale.conf", NULL, &l);
243 if (r < 0 && r != -ENOENT)
244 return r;
245
246 for (p = 0; p < _VARIABLE_LC_MAX; p++) {
247 _cleanup_free_ char *t = NULL;
248 char **u;
249 const char *name;
250
251 name = locale_variable_to_string(p);
252 assert(name);
253
254 if (isempty(c->locale[p])) {
255 l = strv_env_unset(l, name);
256 continue;
257 }
258
259 if (asprintf(&t, "%s=%s", name, c->locale[p]) < 0)
260 return -ENOMEM;
261
262 u = strv_env_set(l, t);
263 if (!u)
264 return -ENOMEM;
265
266 strv_free(l);
267 l = u;
268 }
269
270 if (strv_isempty(l)) {
271 if (unlink("/etc/locale.conf") < 0)
272 return errno == ENOENT ? 0 : -errno;
273
274 return 0;
275 }
276
277 r = write_env_file_label("/etc/locale.conf", l);
278 if (r < 0)
279 return r;
280
ae2a15bc 281 *settings = TAKE_PTR(l);
4897d1dc
ZJS
282 return 0;
283}
284
285int vconsole_write_data(Context *c) {
286 int r;
287 _cleanup_strv_free_ char **l = NULL;
288
289 r = load_env_file(NULL, "/etc/vconsole.conf", NULL, &l);
290 if (r < 0 && r != -ENOENT)
291 return r;
292
293 if (isempty(c->vc_keymap))
294 l = strv_env_unset(l, "KEYMAP");
295 else {
296 _cleanup_free_ char *s = NULL;
297 char **u;
298
299 s = strappend("KEYMAP=", c->vc_keymap);
300 if (!s)
301 return -ENOMEM;
302
303 u = strv_env_set(l, s);
304 if (!u)
305 return -ENOMEM;
306
307 strv_free(l);
308 l = u;
309 }
310
311 if (isempty(c->vc_keymap_toggle))
312 l = strv_env_unset(l, "KEYMAP_TOGGLE");
313 else {
314 _cleanup_free_ char *s = NULL;
315 char **u;
316
317 s = strappend("KEYMAP_TOGGLE=", c->vc_keymap_toggle);
318 if (!s)
319 return -ENOMEM;
320
321 u = strv_env_set(l, s);
322 if (!u)
323 return -ENOMEM;
324
325 strv_free(l);
326 l = u;
327 }
328
329 if (strv_isempty(l)) {
330 if (unlink("/etc/vconsole.conf") < 0)
331 return errno == ENOENT ? 0 : -errno;
332
333 return 0;
334 }
335
336 return write_env_file_label("/etc/vconsole.conf", l);
337}
338
339int x11_write_data(Context *c) {
340 _cleanup_fclose_ FILE *f = NULL;
341 _cleanup_free_ char *temp_path = NULL;
342 int r;
343
344 if (isempty(c->x11_layout) &&
345 isempty(c->x11_model) &&
346 isempty(c->x11_variant) &&
347 isempty(c->x11_options)) {
348
349 if (unlink("/etc/X11/xorg.conf.d/00-keyboard.conf") < 0)
350 return errno == ENOENT ? 0 : -errno;
351
352 return 0;
353 }
354
355 mkdir_p_label("/etc/X11/xorg.conf.d", 0755);
356
357 r = fopen_temporary("/etc/X11/xorg.conf.d/00-keyboard.conf", &f, &temp_path);
358 if (r < 0)
359 return r;
360
0d536673
LP
361 (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
362 (void) fchmod(fileno(f), 0644);
4897d1dc 363
0d536673
LP
364 fputs("# Written by systemd-localed(8), read by systemd-localed and Xorg. It's\n"
365 "# probably wise not to edit this file manually. Use localectl(1) to\n"
366 "# instruct systemd-localed to update it.\n"
367 "Section \"InputClass\"\n"
368 " Identifier \"system-keyboard\"\n"
369 " MatchIsKeyboard \"on\"\n", f);
4897d1dc
ZJS
370
371 if (!isempty(c->x11_layout))
372 fprintf(f, " Option \"XkbLayout\" \"%s\"\n", c->x11_layout);
373
374 if (!isempty(c->x11_model))
375 fprintf(f, " Option \"XkbModel\" \"%s\"\n", c->x11_model);
376
377 if (!isempty(c->x11_variant))
378 fprintf(f, " Option \"XkbVariant\" \"%s\"\n", c->x11_variant);
379
380 if (!isempty(c->x11_options))
381 fprintf(f, " Option \"XkbOptions\" \"%s\"\n", c->x11_options);
382
0d536673 383 fputs("EndSection\n", f);
4897d1dc 384
0675e94a 385 r = fflush_sync_and_check(f);
4897d1dc
ZJS
386 if (r < 0)
387 goto fail;
388
389 if (rename(temp_path, "/etc/X11/xorg.conf.d/00-keyboard.conf") < 0) {
390 r = -errno;
391 goto fail;
392 }
393
394 return 0;
395
396fail:
4897d1dc
ZJS
397 if (temp_path)
398 (void) unlink(temp_path);
399
400 return r;
401}
402
403static int read_next_mapping(const char* filename,
404 unsigned min_fields, unsigned max_fields,
405 FILE *f, unsigned *n, char ***a) {
406 assert(f);
407 assert(n);
408 assert(a);
409
410 for (;;) {
411 char line[LINE_MAX];
412 char *l, **b;
413 int r;
414 size_t length;
415
416 errno = 0;
417 if (!fgets(line, sizeof(line), f)) {
418
419 if (ferror(f))
420 return errno > 0 ? -errno : -EIO;
421
422 return 0;
423 }
424
425 (*n)++;
426
427 l = strstrip(line);
4c701096 428 if (IN_SET(l[0], 0, '#'))
4897d1dc
ZJS
429 continue;
430
431 r = strv_split_extract(&b, l, WHITESPACE, EXTRACT_QUOTES);
432 if (r < 0)
433 return r;
434
435 length = strv_length(b);
436 if (length < min_fields || length > max_fields) {
437 log_error("Invalid line %s:%u, ignoring.", filename, *n);
438 strv_free(b);
439 continue;
440
441 }
442
443 *a = b;
444 return 1;
445 }
446}
447
448int vconsole_convert_to_x11(Context *c) {
cabffaf8 449 const char *map;
6f3287b3 450 int modified = -1;
4897d1dc 451
cabffaf8
ZJS
452 map = systemd_kbd_model_map();
453
4897d1dc 454 if (isempty(c->vc_keymap)) {
4897d1dc
ZJS
455 modified =
456 !isempty(c->x11_layout) ||
457 !isempty(c->x11_model) ||
458 !isempty(c->x11_variant) ||
459 !isempty(c->x11_options);
460
461 context_free_x11(c);
462 } else {
463 _cleanup_fclose_ FILE *f = NULL;
464 unsigned n = 0;
465
cabffaf8 466 f = fopen(map, "re");
4897d1dc
ZJS
467 if (!f)
468 return -errno;
469
470 for (;;) {
471 _cleanup_strv_free_ char **a = NULL;
472 int r;
473
cabffaf8 474 r = read_next_mapping(map, 5, UINT_MAX, f, &n, &a);
4897d1dc
ZJS
475 if (r < 0)
476 return r;
477 if (r == 0)
478 break;
479
480 if (!streq(c->vc_keymap, a[0]))
481 continue;
482
483 if (!streq_ptr(c->x11_layout, strnulldash(a[1])) ||
484 !streq_ptr(c->x11_model, strnulldash(a[2])) ||
485 !streq_ptr(c->x11_variant, strnulldash(a[3])) ||
486 !streq_ptr(c->x11_options, strnulldash(a[4]))) {
487
488 if (free_and_strdup(&c->x11_layout, strnulldash(a[1])) < 0 ||
489 free_and_strdup(&c->x11_model, strnulldash(a[2])) < 0 ||
490 free_and_strdup(&c->x11_variant, strnulldash(a[3])) < 0 ||
491 free_and_strdup(&c->x11_options, strnulldash(a[4])) < 0)
492 return -ENOMEM;
493
494 modified = true;
495 }
496
497 break;
498 }
499 }
500
6f3287b3 501 if (modified > 0)
4897d1dc
ZJS
502 log_info("Changing X11 keyboard layout to '%s' model '%s' variant '%s' options '%s'",
503 strempty(c->x11_layout),
504 strempty(c->x11_model),
505 strempty(c->x11_variant),
506 strempty(c->x11_options));
6f3287b3
ZJS
507 else if (modified < 0)
508 log_notice("X11 keyboard layout was not modified: no conversion found for \"%s\".",
509 c->vc_keymap);
4897d1dc 510 else
6f3287b3 511 log_debug("X11 keyboard layout did not need to be modified.");
4897d1dc 512
6f3287b3 513 return modified > 0;
4897d1dc
ZJS
514}
515
516int find_converted_keymap(const char *x11_layout, const char *x11_variant, char **new_keymap) {
517 const char *dir;
518 _cleanup_free_ char *n;
519
520 if (x11_variant)
605405c6 521 n = strjoin(x11_layout, "-", x11_variant);
4897d1dc
ZJS
522 else
523 n = strdup(x11_layout);
524 if (!n)
525 return -ENOMEM;
526
527 NULSTR_FOREACH(dir, KBD_KEYMAP_DIRS) {
528 _cleanup_free_ char *p = NULL, *pz = NULL;
529 bool uncompressed;
530
605405c6
ZJS
531 p = strjoin(dir, "xkb/", n, ".map");
532 pz = strjoin(dir, "xkb/", n, ".map.gz");
4897d1dc
ZJS
533 if (!p || !pz)
534 return -ENOMEM;
535
536 uncompressed = access(p, F_OK) == 0;
537 if (uncompressed || access(pz, F_OK) == 0) {
538 log_debug("Found converted keymap %s at %s",
539 n, uncompressed ? p : pz);
540
ae2a15bc 541 *new_keymap = TAKE_PTR(n);
4897d1dc
ZJS
542 return 1;
543 }
544 }
545
546 return 0;
547}
548
aa63b56f 549int find_legacy_keymap(Context *c, char **new_keymap) {
cabffaf8
ZJS
550 const char *map;
551 _cleanup_fclose_ FILE *f = NULL;
4897d1dc
ZJS
552 unsigned n = 0;
553 unsigned best_matching = 0;
554 int r;
555
5ad327dd
ZJS
556 assert(!isempty(c->x11_layout));
557
cabffaf8
ZJS
558 map = systemd_kbd_model_map();
559
560 f = fopen(map, "re");
4897d1dc
ZJS
561 if (!f)
562 return -errno;
563
564 for (;;) {
565 _cleanup_strv_free_ char **a = NULL;
566 unsigned matching = 0;
567
cabffaf8 568 r = read_next_mapping(map, 5, UINT_MAX, f, &n, &a);
4897d1dc
ZJS
569 if (r < 0)
570 return r;
571 if (r == 0)
572 break;
573
574 /* Determine how well matching this entry is */
5ad327dd 575 if (streq(c->x11_layout, a[1]))
4897d1dc
ZJS
576 /* If we got an exact match, this is best */
577 matching = 10;
578 else {
579 /* We have multiple X layouts, look for an
580 * entry that matches our key with everything
581 * but the first layout stripped off. */
582 if (startswith_comma(c->x11_layout, a[1]))
583 matching = 5;
584 else {
585 char *x;
586
587 /* If that didn't work, strip off the
588 * other layouts from the entry, too */
589 x = strndupa(a[1], strcspn(a[1], ","));
590 if (startswith_comma(c->x11_layout, x))
591 matching = 1;
592 }
593 }
594
595 if (matching > 0) {
596 if (isempty(c->x11_model) || streq_ptr(c->x11_model, a[2])) {
597 matching++;
598
599 if (streq_ptr(c->x11_variant, a[3])) {
600 matching++;
601
602 if (streq_ptr(c->x11_options, a[4]))
603 matching++;
604 }
605 }
606 }
607
608 /* The best matching entry so far, then let's save that */
609 if (matching >= MAX(best_matching, 1u)) {
610 log_debug("Found legacy keymap %s with score %u",
611 a[0], matching);
612
613 if (matching > best_matching) {
614 best_matching = matching;
615
616 r = free_and_strdup(new_keymap, a[0]);
617 if (r < 0)
618 return r;
619 }
620 }
621 }
622
623 if (best_matching < 10 && c->x11_layout) {
624 /* The best match is only the first part of the X11
625 * keymap. Check if we have a converted map which
626 * matches just the first layout.
627 */
628 char *l, *v = NULL, *converted;
629
630 l = strndupa(c->x11_layout, strcspn(c->x11_layout, ","));
631 if (c->x11_variant)
632 v = strndupa(c->x11_variant, strcspn(c->x11_variant, ","));
633 r = find_converted_keymap(l, v, &converted);
634 if (r < 0)
635 return r;
636 if (r > 0) {
637 free(*new_keymap);
638 *new_keymap = converted;
639 }
640 }
641
5ad327dd 642 return (bool) *new_keymap;
4897d1dc
ZJS
643}
644
645int find_language_fallback(const char *lang, char **language) {
cabffaf8 646 const char *map;
4897d1dc
ZJS
647 _cleanup_fclose_ FILE *f = NULL;
648 unsigned n = 0;
649
aa63b56f 650 assert(lang);
4897d1dc
ZJS
651 assert(language);
652
cabffaf8
ZJS
653 map = systemd_language_fallback_map();
654
655 f = fopen(map, "re");
4897d1dc
ZJS
656 if (!f)
657 return -errno;
658
659 for (;;) {
660 _cleanup_strv_free_ char **a = NULL;
661 int r;
662
cabffaf8 663 r = read_next_mapping(map, 2, 2, f, &n, &a);
4897d1dc
ZJS
664 if (r <= 0)
665 return r;
666
667 if (streq(lang, a[0])) {
668 assert(strv_length(a) == 2);
1cc6c93a 669 *language = TAKE_PTR(a[1]);
4897d1dc
ZJS
670 return 1;
671 }
672 }
673
674 assert_not_reached("should not be here");
675}
676
677int x11_convert_to_vconsole(Context *c) {
678 bool modified = false;
679
680 if (isempty(c->x11_layout)) {
4897d1dc
ZJS
681 modified =
682 !isempty(c->vc_keymap) ||
683 !isempty(c->vc_keymap_toggle);
684
aa63b56f 685 context_free_vconsole(c);
4897d1dc
ZJS
686 } else {
687 char *new_keymap = NULL;
688 int r;
689
690 r = find_converted_keymap(c->x11_layout, c->x11_variant, &new_keymap);
691 if (r < 0)
692 return r;
693 else if (r == 0) {
694 r = find_legacy_keymap(c, &new_keymap);
695 if (r < 0)
696 return r;
697 }
5ad327dd
ZJS
698 if (r == 0)
699 /* We search for layout-variant match first, but then we also look
700 * for anything which matches just the layout. So it's accurate to say
701 * that we couldn't find anything which matches the layout. */
702 log_notice("No conversion to virtual console map found for \"%s\".",
703 c->x11_layout);
4897d1dc
ZJS
704
705 if (!streq_ptr(c->vc_keymap, new_keymap)) {
706 free(c->vc_keymap);
707 c->vc_keymap = new_keymap;
708 c->vc_keymap_toggle = mfree(c->vc_keymap_toggle);
709 modified = true;
710 } else
711 free(new_keymap);
712 }
713
714 if (modified)
715 log_info("Changing virtual console keymap to '%s' toggle '%s'",
716 strempty(c->vc_keymap), strempty(c->vc_keymap_toggle));
717 else
718 log_debug("Virtual console keymap was not modified.");
719
720 return modified;
721}