]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/locale/keymap-util.c
macro: introduce TAKE_PTR() macro
[thirdparty/systemd.git] / src / locale / keymap-util.c
1 /* SPDX-License-Identifier: LGPL-2.1+ */
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>
23 #include <stdio_ext.h>
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
39 static bool startswith_comma(const char *s, const char *prefix) {
40 s = startswith(s, prefix);
41 if (!s)
42 return false;
43
44 return IN_SET(*s, ',', '\0');
45 }
46
47 static const char* strnulldash(const char *s) {
48 return isempty(s) || streq(s, "-") ? NULL : s;
49 }
50
51 static 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
61 static 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
71 static 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
78 static 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
83 static 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
90 void context_free(Context *c) {
91 context_free_locale(c);
92 context_free_x11(c);
93 context_free_vconsole(c);
94 };
95
96 void 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
104 static 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
148 static 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
164 static 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
182 if (IN_SET(l[0], 0, '#'))
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) {
205 free_and_replace(*p, a[2]);
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
226 int 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
236 int 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
281 *settings = TAKE_PTR(l);
282 return 0;
283 }
284
285 int 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
339 int 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
361 (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
362 (void) fchmod(fileno(f), 0644);
363
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);
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
383 fputs("EndSection\n", f);
384
385 r = fflush_sync_and_check(f);
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
396 fail:
397 if (temp_path)
398 (void) unlink(temp_path);
399
400 return r;
401 }
402
403 static 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);
428 if (IN_SET(l[0], 0, '#'))
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
448 int vconsole_convert_to_x11(Context *c) {
449 const char *map;
450 int modified = -1;
451
452 map = systemd_kbd_model_map();
453
454 if (isempty(c->vc_keymap)) {
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
466 f = fopen(map, "re");
467 if (!f)
468 return -errno;
469
470 for (;;) {
471 _cleanup_strv_free_ char **a = NULL;
472 int r;
473
474 r = read_next_mapping(map, 5, UINT_MAX, f, &n, &a);
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
501 if (modified > 0)
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));
507 else if (modified < 0)
508 log_notice("X11 keyboard layout was not modified: no conversion found for \"%s\".",
509 c->vc_keymap);
510 else
511 log_debug("X11 keyboard layout did not need to be modified.");
512
513 return modified > 0;
514 }
515
516 int 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)
521 n = strjoin(x11_layout, "-", x11_variant);
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
531 p = strjoin(dir, "xkb/", n, ".map");
532 pz = strjoin(dir, "xkb/", n, ".map.gz");
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
541 *new_keymap = TAKE_PTR(n);
542 return 1;
543 }
544 }
545
546 return 0;
547 }
548
549 int find_legacy_keymap(Context *c, char **new_keymap) {
550 const char *map;
551 _cleanup_fclose_ FILE *f = NULL;
552 unsigned n = 0;
553 unsigned best_matching = 0;
554 int r;
555
556 assert(!isempty(c->x11_layout));
557
558 map = systemd_kbd_model_map();
559
560 f = fopen(map, "re");
561 if (!f)
562 return -errno;
563
564 for (;;) {
565 _cleanup_strv_free_ char **a = NULL;
566 unsigned matching = 0;
567
568 r = read_next_mapping(map, 5, UINT_MAX, f, &n, &a);
569 if (r < 0)
570 return r;
571 if (r == 0)
572 break;
573
574 /* Determine how well matching this entry is */
575 if (streq(c->x11_layout, a[1]))
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
642 return (bool) *new_keymap;
643 }
644
645 int find_language_fallback(const char *lang, char **language) {
646 const char *map;
647 _cleanup_fclose_ FILE *f = NULL;
648 unsigned n = 0;
649
650 assert(lang);
651 assert(language);
652
653 map = systemd_language_fallback_map();
654
655 f = fopen(map, "re");
656 if (!f)
657 return -errno;
658
659 for (;;) {
660 _cleanup_strv_free_ char **a = NULL;
661 int r;
662
663 r = read_next_mapping(map, 2, 2, f, &n, &a);
664 if (r <= 0)
665 return r;
666
667 if (streq(lang, a[0])) {
668 assert(strv_length(a) == 2);
669 *language = a[1];
670 a[1] = NULL;
671 return 1;
672 }
673 }
674
675 assert_not_reached("should not be here");
676 }
677
678 int x11_convert_to_vconsole(Context *c) {
679 bool modified = false;
680
681 if (isempty(c->x11_layout)) {
682 modified =
683 !isempty(c->vc_keymap) ||
684 !isempty(c->vc_keymap_toggle);
685
686 context_free_vconsole(c);
687 } else {
688 char *new_keymap = NULL;
689 int r;
690
691 r = find_converted_keymap(c->x11_layout, c->x11_variant, &new_keymap);
692 if (r < 0)
693 return r;
694 else if (r == 0) {
695 r = find_legacy_keymap(c, &new_keymap);
696 if (r < 0)
697 return r;
698 }
699 if (r == 0)
700 /* We search for layout-variant match first, but then we also look
701 * for anything which matches just the layout. So it's accurate to say
702 * that we couldn't find anything which matches the layout. */
703 log_notice("No conversion to virtual console map found for \"%s\".",
704 c->x11_layout);
705
706 if (!streq_ptr(c->vc_keymap, new_keymap)) {
707 free(c->vc_keymap);
708 c->vc_keymap = new_keymap;
709 c->vc_keymap_toggle = mfree(c->vc_keymap_toggle);
710 modified = true;
711 } else
712 free(new_keymap);
713 }
714
715 if (modified)
716 log_info("Changing virtual console keymap to '%s' toggle '%s'",
717 strempty(c->vc_keymap), strempty(c->vc_keymap_toggle));
718 else
719 log_debug("Virtual console keymap was not modified.");
720
721 return modified;
722 }