Coverage Report

Created: 2025-12-11 06:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/systemd/src/shared/vconsole-util.c
Line
Count
Source
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3
#include <stdlib.h>
4
#include <unistd.h>
5
6
#include "alloc-util.h"
7
#include "env-util.h"
8
#include "extract-word.h"
9
#include "fd-util.h"
10
#include "fileio.h"
11
#include "kbd-util.h"
12
#include "log.h"
13
#include "string-util.h"
14
#include "strv.h"
15
#include "vconsole-util.h"
16
17
0
static bool startswith_comma(const char *s, const char *prefix) {
18
0
        assert(s);
19
0
        assert(prefix);
20
21
0
        s = startswith(s, prefix);
22
0
        if (!s)
23
0
                return false;
24
25
0
        return IN_SET(*s, ',', '\0');
26
0
}
27
28
0
static const char* systemd_kbd_model_map(void) {
29
0
        const char* s;
30
31
0
        s = getenv("SYSTEMD_KBD_MODEL_MAP");
32
0
        if (s)
33
0
                return s;
34
35
0
        return SYSTEMD_KBD_MODEL_MAP;
36
0
}
37
38
0
static const char* systemd_language_fallback_map(void) {
39
0
        const char* s;
40
41
0
        s = getenv("SYSTEMD_LANGUAGE_FALLBACK_MAP");
42
0
        if (s)
43
0
                return s;
44
45
0
        return SYSTEMD_LANGUAGE_FALLBACK_MAP;
46
0
}
47
48
0
void x11_context_clear(X11Context *xc) {
49
0
        assert(xc);
50
51
0
        xc->layout  = mfree(xc->layout);
52
0
        xc->options = mfree(xc->options);
53
0
        xc->model   = mfree(xc->model);
54
0
        xc->variant = mfree(xc->variant);
55
0
}
56
57
0
void x11_context_replace(X11Context *dest, X11Context *src) {
58
0
        assert(dest);
59
0
        assert(src);
60
61
0
        x11_context_clear(dest);
62
0
        *dest = TAKE_STRUCT(*src);
63
0
}
64
65
0
bool x11_context_isempty(const X11Context *xc) {
66
0
        assert(xc);
67
68
0
        return
69
0
                isempty(xc->layout)  &&
70
0
                isempty(xc->model)   &&
71
0
                isempty(xc->variant) &&
72
0
                isempty(xc->options);
73
0
}
74
75
0
void x11_context_empty_to_null(X11Context *xc) {
76
0
        assert(xc);
77
78
        /* Do not call x11_context_clear() for the passed object. */
79
80
0
        xc->layout  = empty_to_null(xc->layout);
81
0
        xc->model   = empty_to_null(xc->model);
82
0
        xc->variant = empty_to_null(xc->variant);
83
0
        xc->options = empty_to_null(xc->options);
84
0
}
85
86
0
bool x11_context_is_safe(const X11Context *xc) {
87
0
        assert(xc);
88
89
0
        return
90
0
                (!xc->layout  || string_is_safe(xc->layout))  &&
91
0
                (!xc->model   || string_is_safe(xc->model))   &&
92
0
                (!xc->variant || string_is_safe(xc->variant)) &&
93
0
                (!xc->options || string_is_safe(xc->options));
94
0
}
95
96
0
bool x11_context_equal(const X11Context *a, const X11Context *b) {
97
0
        assert(a);
98
0
        assert(b);
99
100
0
        return
101
0
                streq_ptr(a->layout,  b->layout)  &&
102
0
                streq_ptr(a->model,   b->model)   &&
103
0
                streq_ptr(a->variant, b->variant) &&
104
0
                streq_ptr(a->options, b->options);
105
0
}
106
107
0
int x11_context_copy(X11Context *dest, const X11Context *src) {
108
0
        bool modified;
109
0
        int r;
110
111
0
        assert(dest);
112
113
0
        if (dest == src)
114
0
                return 0;
115
116
0
        if (!src) {
117
0
                modified = !x11_context_isempty(dest);
118
0
                x11_context_clear(dest);
119
0
                return modified;
120
0
        }
121
122
0
        r = free_and_strdup(&dest->layout, src->layout);
123
0
        if (r < 0)
124
0
                return r;
125
0
        modified = r > 0;
126
127
0
        r = free_and_strdup(&dest->model, src->model);
128
0
        if (r < 0)
129
0
                return r;
130
0
        modified = modified || r > 0;
131
132
0
        r = free_and_strdup(&dest->variant, src->variant);
133
0
        if (r < 0)
134
0
                return r;
135
0
        modified = modified || r > 0;
136
137
0
        r = free_and_strdup(&dest->options, src->options);
138
0
        if (r < 0)
139
0
                return r;
140
0
        modified = modified || r > 0;
141
142
0
        return modified;
143
0
}
144
145
0
void vc_context_clear(VCContext *vc) {
146
0
        assert(vc);
147
148
0
        vc->keymap = mfree(vc->keymap);
149
0
        vc->toggle = mfree(vc->toggle);
150
0
}
151
152
0
void vc_context_replace(VCContext *dest, VCContext *src) {
153
0
        assert(dest);
154
0
        assert(src);
155
156
0
        vc_context_clear(dest);
157
0
        *dest = TAKE_STRUCT(*src);
158
0
}
159
160
0
bool vc_context_isempty(const VCContext *vc) {
161
0
        assert(vc);
162
163
0
        return
164
0
                isempty(vc->keymap) &&
165
0
                isempty(vc->toggle);
166
0
}
167
168
0
void vc_context_empty_to_null(VCContext *vc) {
169
0
        assert(vc);
170
171
        /* Do not call vc_context_clear() for the passed object. */
172
173
0
        vc->keymap = empty_to_null(vc->keymap);
174
0
        vc->toggle = empty_to_null(vc->toggle);
175
0
}
176
177
0
bool vc_context_equal(const VCContext *a, const VCContext *b) {
178
0
        assert(a);
179
0
        assert(b);
180
181
0
        return
182
0
                streq_ptr(a->keymap, b->keymap) &&
183
0
                streq_ptr(a->toggle, b->toggle);
184
0
}
185
186
0
int vc_context_copy(VCContext *dest, const VCContext *src) {
187
0
        bool modified;
188
0
        int r;
189
190
0
        assert(dest);
191
192
0
        if (dest == src)
193
0
                return 0;
194
195
0
        if (!src) {
196
0
                modified = !vc_context_isempty(dest);
197
0
                vc_context_clear(dest);
198
0
                return modified;
199
0
        }
200
201
0
        r = free_and_strdup(&dest->keymap, src->keymap);
202
0
        if (r < 0)
203
0
                return r;
204
0
        modified = r > 0;
205
206
0
        r = free_and_strdup(&dest->toggle, src->toggle);
207
0
        if (r < 0)
208
0
                return r;
209
0
        modified = modified || r > 0;
210
211
0
        return modified;
212
0
}
213
214
static int read_next_mapping(
215
                const char *filename,
216
                unsigned min_fields,
217
                unsigned max_fields,
218
                FILE *f,
219
                unsigned *n,
220
0
                char ***ret) {
221
222
0
        assert(f);
223
0
        assert(n);
224
0
        assert(ret);
225
226
0
        for (;;) {
227
0
                _cleanup_strv_free_ char **b = NULL;
228
0
                _cleanup_free_ char *line = NULL;
229
0
                size_t length;
230
0
                int r;
231
232
0
                r = read_stripped_line(f, LONG_LINE_MAX, &line);
233
0
                if (r < 0)
234
0
                        return r;
235
0
                if (r == 0)
236
0
                        break;
237
238
0
                (*n)++;
239
240
0
                if (IN_SET(line[0], 0, '#'))
241
0
                        continue;
242
243
0
                r = strv_split_full(&b, line, WHITESPACE, EXTRACT_UNQUOTE);
244
0
                if (r < 0)
245
0
                        return r;
246
247
0
                length = strv_length(b);
248
0
                if (length < min_fields || length > max_fields) {
249
0
                        log_debug("Invalid line %s:%u, ignoring.", strna(filename), *n);
250
0
                        continue;
251
252
0
                }
253
254
0
                *ret = TAKE_PTR(b);
255
0
                return 1;
256
0
        }
257
258
0
        *ret = NULL;
259
0
        return 0;
260
0
}
261
262
0
int vconsole_convert_to_x11(const VCContext *vc, X11VerifyCallback verify, X11Context *ret) {
263
0
        _cleanup_fclose_ FILE *f = NULL;
264
0
        const char *map;
265
0
        X11Context xc;
266
0
        int r;
267
268
0
        assert(vc);
269
0
        assert(ret);
270
271
0
        if (isempty(vc->keymap)) {
272
0
                *ret = (X11Context) {};
273
0
                return 0;
274
0
        }
275
276
0
        map = systemd_kbd_model_map();
277
0
        f = fopen(map, "re");
278
0
        if (!f)
279
0
                return -errno;
280
281
0
        for (unsigned n = 0;;) {
282
0
                _cleanup_strv_free_ char **a = NULL;
283
284
0
                r = read_next_mapping(map, 5, UINT_MAX, f, &n, &a);
285
0
                if (r < 0)
286
0
                        return r;
287
0
                if (r == 0)
288
0
                        break;
289
290
0
                if (!streq(vc->keymap, a[0]))
291
0
                        continue;
292
293
0
                xc = (X11Context) {
294
0
                        .layout  = empty_or_dash_to_null(a[1]),
295
0
                        .model   = empty_or_dash_to_null(a[2]),
296
0
                        .variant = empty_or_dash_to_null(a[3]),
297
0
                        .options = empty_or_dash_to_null(a[4]),
298
0
                };
299
300
0
                if (verify && verify(&xc) < 0)
301
0
                        continue;
302
303
0
                return x11_context_copy(ret, &xc);
304
0
        }
305
306
        /* No custom mapping has been found, see if the keymap is a converted one. In such case deducing the
307
         * corresponding x11 layout is easy. */
308
0
        _cleanup_free_ char *xlayout = NULL, *converted = NULL;
309
0
        char *xvariant;
310
311
0
        xlayout = strdup(vc->keymap);
312
0
        if (!xlayout)
313
0
                return -ENOMEM;
314
0
        xvariant = strchr(xlayout, '-');
315
0
        if (xvariant) {
316
0
                xvariant[0] = '\0';
317
0
                xvariant++;
318
0
        }
319
320
        /* Note: by default we use keyboard model "microsoftpro" which should be equivalent to "pc105" but
321
         * with the internet/media key mapping added. */
322
0
        xc = (X11Context) {
323
0
                .layout  = xlayout,
324
0
                .model   = (char*) "microsoftpro",
325
0
                .variant = xvariant,
326
0
                .options = (char*) "terminate:ctrl_alt_bksp",
327
0
        };
328
329
        /* This sanity check seems redundant with the verification of the X11 layout done on the next
330
         * step. However xkbcommon is an optional dependency hence the verification might be a NOP.  */
331
0
        r = find_converted_keymap(&xc, &converted);
332
0
        if (r == 0 && xc.variant) {
333
                /* If we still haven't find a match, try with no variant, it's still better than nothing.  */
334
0
                xc.variant = NULL;
335
0
                r = find_converted_keymap(&xc, &converted);
336
0
        }
337
0
        if (r < 0)
338
0
                return r;
339
340
0
        if (r == 0 || (verify && verify(&xc) < 0)) {
341
0
                *ret = (X11Context) {};
342
0
                return 0;
343
0
        }
344
345
0
        return x11_context_copy(ret, &xc);
346
0
}
347
348
0
int find_converted_keymap(const X11Context *xc, char **ret) {
349
0
        _cleanup_free_ char *n = NULL, *p = NULL, *pz = NULL;
350
0
        _cleanup_strv_free_ char **keymap_dirs = NULL;
351
0
        int r;
352
353
0
        assert(xc);
354
0
        assert(!isempty(xc->layout));
355
0
        assert(ret);
356
357
0
        if (xc->variant)
358
0
                n = strjoin(xc->layout, "-", xc->variant);
359
0
        else
360
0
                n = strdup(xc->layout);
361
0
        if (!n)
362
0
                return -ENOMEM;
363
364
0
        p = strjoin("xkb/", n, ".map");
365
0
        pz = strjoin("xkb/", n, ".map.gz");
366
0
        if (!p || !pz)
367
0
                return -ENOMEM;
368
369
0
        r = keymap_directories(&keymap_dirs);
370
0
        if (r < 0)
371
0
                return r;
372
373
0
        STRV_FOREACH(dir, keymap_dirs) {
374
0
                _cleanup_close_ int dir_fd = -EBADF;
375
0
                bool uncompressed;
376
377
0
                dir_fd = open(*dir, O_CLOEXEC | O_DIRECTORY | O_PATH);
378
0
                if (dir_fd < 0) {
379
0
                        if (errno != ENOENT)
380
0
                                log_debug_errno(errno, "Failed to open %s, ignoring: %m", *dir);
381
0
                        continue;
382
0
                }
383
384
0
                uncompressed = faccessat(dir_fd, p, F_OK, 0) >= 0;
385
0
                if (uncompressed || faccessat(dir_fd, pz, F_OK, 0) >= 0) {
386
0
                        log_debug("Found converted keymap %s at %s/%s", n, *dir, uncompressed ? p : pz);
387
0
                        *ret = TAKE_PTR(n);
388
0
                        return 1;
389
0
                }
390
0
        }
391
392
0
        *ret = NULL;
393
0
        return 0;
394
0
}
395
396
0
int find_legacy_keymap(const X11Context *xc, char **ret) {
397
0
        const char *map;
398
0
        _cleanup_fclose_ FILE *f = NULL;
399
0
        _cleanup_free_ char *new_keymap = NULL;
400
0
        unsigned best_matching = 0;
401
0
        int r;
402
403
0
        assert(xc);
404
0
        assert(!isempty(xc->layout));
405
406
0
        map = systemd_kbd_model_map();
407
0
        f = fopen(map, "re");
408
0
        if (!f)
409
0
                return -errno;
410
411
0
        for (unsigned n = 0;;) {
412
0
                _cleanup_strv_free_ char **a = NULL;
413
0
                unsigned matching = 0;
414
415
0
                r = read_next_mapping(map, 5, UINT_MAX, f, &n, &a);
416
0
                if (r < 0)
417
0
                        return r;
418
0
                if (r == 0)
419
0
                        break;
420
421
                /* Determine how well matching this entry is */
422
0
                if (streq(xc->layout, a[1]))
423
                        /* If we got an exact match, this is the best */
424
0
                        matching = 10;
425
0
                else {
426
                        /* see if we get an exact match with the order reversed */
427
0
                        _cleanup_strv_free_ char **b = NULL;
428
0
                        _cleanup_free_ char *c = NULL;
429
0
                        r = strv_split_full(&b, a[1], ",", 0);
430
0
                        if (r < 0)
431
0
                                return r;
432
0
                        strv_reverse(b);
433
0
                        c = strv_join(b, ",");
434
0
                        if (!c)
435
0
                                return log_oom();
436
0
                        if (streq(xc->layout, c))
437
0
                                matching = 9;
438
0
                        else {
439
                                /* We have multiple X layouts, look for an
440
                                 * entry that matches our key with everything
441
                                 * but the first layout stripped off. */
442
0
                                if (startswith_comma(xc->layout, a[1]))
443
0
                                        matching = 5;
444
0
                                else {
445
0
                                        _cleanup_free_ char *x = NULL;
446
447
                                        /* If that didn't work, strip off the
448
                                         * other layouts from the entry, too */
449
0
                                        x = strdupcspn(a[1], ",");
450
0
                                        if (!x)
451
0
                                                return -ENOMEM;
452
0
                                        if (startswith_comma(xc->layout, x))
453
0
                                                matching = 1;
454
0
                                }
455
0
                        }
456
0
                }
457
458
0
                if (matching > 0) {
459
0
                        if (isempty(xc->model) || streq_ptr(xc->model, a[2])) {
460
0
                                matching++;
461
462
0
                                if (streq_ptr(xc->variant, a[3]) || ((isempty(xc->variant) || streq_skip_trailing_chars(xc->variant, "", ",")) && streq(a[3], "-"))) {
463
0
                                        matching++;
464
465
0
                                        if (streq_ptr(xc->options, a[4]))
466
0
                                                matching++;
467
0
                                }
468
0
                        }
469
0
                }
470
471
                /* The best matching entry so far, then let's save that */
472
0
                if (matching >= MAX(best_matching, 1u)) {
473
0
                        log_debug("Found legacy keymap %s with score %u", a[0], matching);
474
475
0
                        if (matching > best_matching) {
476
0
                                best_matching = matching;
477
478
0
                                r = free_and_strdup(&new_keymap, a[0]);
479
0
                                if (r < 0)
480
0
                                        return r;
481
0
                        }
482
0
                }
483
0
        }
484
485
0
        if (best_matching < 9 && !isempty(xc->layout)) {
486
0
                _cleanup_free_ char *l = NULL, *v = NULL, *converted = NULL;
487
488
                /* The best match is only the first part of the X11
489
                 * keymap. Check if we have a converted map which
490
                 * matches just the first layout.
491
                 */
492
493
0
                l = strdupcspn(xc->layout, ",");
494
0
                if (!l)
495
0
                        return -ENOMEM;
496
497
0
                if (!isempty(xc->variant)) {
498
0
                        v = strdupcspn(xc->variant, ",");
499
0
                        if (!v)
500
0
                                return -ENOMEM;
501
0
                }
502
503
0
                r = find_converted_keymap(
504
0
                                &(X11Context) {
505
0
                                        .layout = l,
506
0
                                        .variant = v,
507
0
                                },
508
0
                                &converted);
509
0
                if (r < 0)
510
0
                        return r;
511
0
                if (r > 0)
512
0
                        free_and_replace(new_keymap, converted);
513
0
        }
514
515
0
        *ret = TAKE_PTR(new_keymap);
516
0
        return !!*ret;
517
0
}
518
519
0
int x11_convert_to_vconsole(const X11Context *xc, VCContext *ret) {
520
0
        _cleanup_free_ char *keymap = NULL;
521
0
        int r;
522
523
0
        assert(xc);
524
0
        assert(ret);
525
526
0
        if (isempty(xc->layout)) {
527
0
                *ret = (VCContext) {};
528
0
                return 0;
529
0
        }
530
531
0
        r = find_converted_keymap(xc, &keymap);
532
0
        if (r == 0) {
533
0
                r = find_legacy_keymap(xc, &keymap);
534
0
                if (r == 0 && xc->variant)
535
                        /* If we still haven't find a match, try with no variant, it's still better than
536
                         * nothing.  */
537
0
                        r = find_converted_keymap(
538
0
                                        &(X11Context) {
539
0
                                                .layout = xc->layout,
540
0
                                        },
541
0
                                        &keymap);
542
0
        }
543
0
        if (r < 0)
544
0
                return r;
545
546
0
        *ret = (VCContext) {
547
0
                .keymap = TAKE_PTR(keymap),
548
0
        };
549
0
        return 0;
550
0
}
551
552
0
int find_language_fallback(const char *lang, char **ret) {
553
0
        const char *map;
554
0
        _cleanup_fclose_ FILE *f = NULL;
555
0
        unsigned n = 0;
556
0
        int r;
557
558
0
        assert(lang);
559
0
        assert(ret);
560
561
0
        map = systemd_language_fallback_map();
562
0
        f = fopen(map, "re");
563
0
        if (!f)
564
0
                return -errno;
565
566
0
        for (;;) {
567
0
                _cleanup_strv_free_ char **a = NULL;
568
569
0
                r = read_next_mapping(map, 2, 2, f, &n, &a);
570
0
                if (r <= 0)
571
0
                        return r;
572
573
0
                if (streq(lang, a[0])) {
574
0
                        assert(strv_length(a) == 2);
575
0
                        *ret = TAKE_PTR(a[1]);
576
0
                        return 1;
577
0
                }
578
0
        }
579
0
}
580
581
0
int vconsole_serialize(const VCContext *vc, const X11Context *xc, char ***env) {
582
0
        int r;
583
584
        /* This function modifies the passed strv in place. */
585
586
0
        assert(vc);
587
0
        assert(xc);
588
0
        assert(env);
589
590
0
        r = strv_env_assign(env, "KEYMAP", empty_to_null(vc->keymap));
591
0
        if (r < 0)
592
0
                return r;
593
594
0
        r = strv_env_assign(env, "KEYMAP_TOGGLE", empty_to_null(vc->toggle));
595
0
        if (r < 0)
596
0
                return r;
597
598
0
        r = strv_env_assign(env, "XKBLAYOUT", empty_to_null(xc->layout));
599
0
        if (r < 0)
600
0
                return r;
601
602
0
        r = strv_env_assign(env, "XKBMODEL", empty_to_null(xc->model));
603
0
        if (r < 0)
604
0
                return r;
605
606
0
        r = strv_env_assign(env, "XKBVARIANT", empty_to_null(xc->variant));
607
0
        if (r < 0)
608
0
                return r;
609
610
0
        r = strv_env_assign(env, "XKBOPTIONS", empty_to_null(xc->options));
611
0
        if (r < 0)
612
0
                return r;
613
614
0
        return 0;
615
0
}