1 /* SPDX-License-Identifier: LGPL-2.1+ */
3 This file is part of systemd.
5 Copyright 2012 Lennart Poettering
7 systemd is free software; you can redistribute it and/or modify it
8 under the terms of the GNU Lesser General Public License as published by
9 the Free Software Foundation; either version 2.1 of the License, or
10 (at your option) any later version.
12 systemd is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public License
18 along with systemd; If not, see <http://www.gnu.org/licenses/>.
31 #include "alloc-util.h"
33 #include "conf-files.h"
39 #include "path-util.h"
40 #include "siphash24.h"
41 #include "sparse-endian.h"
43 #include "string-util.h"
47 const char * const catalog_file_dirs
[] = {
48 "/usr/local/lib/systemd/catalog/",
49 "/usr/lib/systemd/catalog/",
53 #define CATALOG_SIGNATURE (uint8_t[]) { 'R', 'H', 'H', 'H', 'K', 'S', 'L', 'P' }
55 typedef struct CatalogHeader
{
56 uint8_t signature
[8]; /* "RHHHKSLP" */
57 le32_t compatible_flags
;
58 le32_t incompatible_flags
;
61 le64_t catalog_item_size
;
64 typedef struct CatalogItem
{
70 static void catalog_hash_func(const void *p
, struct siphash
*state
) {
71 const CatalogItem
*i
= p
;
73 siphash24_compress(&i
->id
, sizeof(i
->id
), state
);
74 siphash24_compress(i
->language
, strlen(i
->language
), state
);
77 static int catalog_compare_func(const void *a
, const void *b
) {
78 const CatalogItem
*i
= a
, *j
= b
;
81 for (k
= 0; k
< ELEMENTSOF(j
->id
.bytes
); k
++) {
82 if (i
->id
.bytes
[k
] < j
->id
.bytes
[k
])
84 if (i
->id
.bytes
[k
] > j
->id
.bytes
[k
])
88 return strcmp(i
->language
, j
->language
);
91 const struct hash_ops catalog_hash_ops
= {
92 .hash
= catalog_hash_func
,
93 .compare
= catalog_compare_func
96 static bool next_header(const char **s
) {
113 static const char *skip_header(const char *s
) {
114 while (next_header(&s
))
119 static char *combine_entries(const char *one
, const char *two
) {
124 /* Find split point of headers to body */
125 b1
= skip_header(one
);
126 b2
= skip_header(two
);
130 dest
= new(char, l1
+ l2
+ 1);
138 /* Headers from @one */
140 p
= mempcpy(p
, one
, n
);
142 /* Headers from @two, these will only be found if not present above */
144 p
= mempcpy(p
, two
, n
);
159 assert(p
- dest
<= (ptrdiff_t)(l1
+ l2
));
164 static int finish_item(
167 const char *language
,
168 char *payload
, size_t payload_size
) {
170 _cleanup_free_ CatalogItem
*i
= NULL
;
171 _cleanup_free_
char *prev
= NULL
, *combined
= NULL
;
175 assert(payload_size
> 0);
177 i
= new0(CatalogItem
, 1);
183 assert(strlen(language
) > 1 && strlen(language
) < 32);
184 strcpy(i
->language
, language
);
187 prev
= hashmap_get(h
, i
);
189 /* Already have such an item, combine them */
190 combined
= combine_entries(payload
, prev
);
194 if (hashmap_update(h
, i
, combined
) < 0)
199 combined
= memdup(payload
, payload_size
+ 1);
203 if (hashmap_put(h
, i
, combined
) < 0)
212 int catalog_file_lang(const char* filename
, char **lang
) {
213 char *beg
, *end
, *_lang
;
215 end
= endswith(filename
, ".catalog");
220 while (beg
> filename
&& !IN_SET(*beg
, '.', '/') && end
- beg
< 32)
223 if (*beg
!= '.' || end
<= beg
+ 1)
226 _lang
= strndup(beg
+ 1, end
- beg
- 1);
234 static int catalog_entry_lang(const char* filename
, int line
,
235 const char* t
, const char* deflang
, char **lang
) {
240 log_error("[%s:%u] Language too short.", filename
, line
);
244 log_error("[%s:%u] language too long.", filename
, line
);
249 if (streq(t
, deflang
)) {
250 log_warning("[%s:%u] language specified unnecessarily",
254 log_warning("[%s:%u] language differs from default for file",
265 int catalog_import_file(Hashmap
*h
, const char *path
) {
266 _cleanup_fclose_
FILE *f
= NULL
;
267 _cleanup_free_
char *payload
= NULL
;
268 size_t payload_size
= 0, payload_allocated
= 0;
271 _cleanup_free_
char *deflang
= NULL
, *lang
= NULL
;
272 bool got_id
= false, empty_line
= true;
278 f
= fopen(path
, "re");
280 return log_error_errno(errno
, "Failed to open file %s: %m", path
);
282 r
= catalog_file_lang(path
, &deflang
);
284 log_error_errno(r
, "Failed to determine language for file %s: %m", path
);
286 log_debug("File %s has language %s.", path
, deflang
);
292 if (!fgets(line
, sizeof(line
), f
)) {
296 return log_error_errno(errno
, "Failed to read file %s: %m", path
);
308 if (strchr(COMMENTS
"\n", line
[0]))
312 strlen(line
) >= 2+1+32 &&
316 IN_SET(line
[2+1+32], ' ', '\0')) {
323 with_language
= line
[2+1+32] != '\0';
326 if (sd_id128_from_string(line
+ 2 + 1, &jd
) >= 0) {
329 if (payload_size
== 0) {
330 log_error("[%s:%u] No payload text.", path
, n
);
334 r
= finish_item(h
, id
, lang
?: deflang
, payload
, payload_size
);
345 t
= strstrip(line
+ 2 + 1 + 32 + 1);
346 r
= catalog_entry_lang(path
, n
, t
, deflang
, &lang
);
361 log_error("[%s:%u] Got payload before ID.", path
, n
);
365 line_len
= strlen(line
);
366 if (!GREEDY_REALLOC(payload
, payload_allocated
,
367 payload_size
+ (empty_line
? 1 : 0) + line_len
+ 1 + 1))
371 payload
[payload_size
++] = '\n';
372 memcpy(payload
+ payload_size
, line
, line_len
);
373 payload_size
+= line_len
;
374 payload
[payload_size
++] = '\n';
375 payload
[payload_size
] = '\0';
381 if (payload_size
== 0) {
382 log_error("[%s:%u] No payload text.", path
, n
);
386 r
= finish_item(h
, id
, lang
?: deflang
, payload
, payload_size
);
394 static int64_t write_catalog(const char *database
, struct strbuf
*sb
,
395 CatalogItem
*items
, size_t n
) {
396 CatalogHeader header
;
397 _cleanup_fclose_
FILE *w
= NULL
;
399 _cleanup_free_
char *d
, *p
= NULL
;
402 d
= dirname_malloc(database
);
406 r
= mkdir_p(d
, 0775);
408 return log_error_errno(r
, "Recursive mkdir %s: %m", d
);
410 r
= fopen_temporary(database
, &w
, &p
);
412 return log_error_errno(r
, "Failed to open database for writing: %s: %m",
416 memcpy(header
.signature
, CATALOG_SIGNATURE
, sizeof(header
.signature
));
417 header
.header_size
= htole64(ALIGN_TO(sizeof(CatalogHeader
), 8));
418 header
.catalog_item_size
= htole64(sizeof(CatalogItem
));
419 header
.n_items
= htole64(n
);
423 k
= fwrite(&header
, 1, sizeof(header
), w
);
424 if (k
!= sizeof(header
)) {
425 log_error("%s: failed to write header.", p
);
429 k
= fwrite(items
, 1, n
* sizeof(CatalogItem
), w
);
430 if (k
!= n
* sizeof(CatalogItem
)) {
431 log_error("%s: failed to write database.", p
);
435 k
= fwrite(sb
->buf
, 1, sb
->len
, w
);
437 log_error("%s: failed to write strings.", p
);
441 r
= fflush_and_check(w
);
443 log_error_errno(r
, "%s: failed to write database: %m", p
);
447 fchmod(fileno(w
), 0644);
449 if (rename(p
, database
) < 0) {
450 r
= log_error_errno(errno
, "rename (%s -> %s) failed: %m", p
, database
);
461 int catalog_update(const char* database
, const char* root
, const char* const* dirs
) {
462 _cleanup_strv_free_
char **files
= NULL
;
464 struct strbuf
*sb
= NULL
;
465 _cleanup_hashmap_free_free_free_ Hashmap
*h
= NULL
;
466 _cleanup_free_ CatalogItem
*items
= NULL
;
475 h
= hashmap_new(&catalog_hash_ops
);
483 r
= conf_files_list_strv(&files
, ".catalog", root
, 0, dirs
);
485 log_error_errno(r
, "Failed to get catalog files: %m");
489 STRV_FOREACH(f
, files
) {
490 log_debug("Reading file '%s'", *f
);
491 r
= catalog_import_file(h
, *f
);
493 log_error_errno(r
, "Failed to import file '%s': %m", *f
);
498 if (hashmap_size(h
) <= 0) {
499 log_info("No items in catalog.");
502 log_debug("Found %u items in catalog.", hashmap_size(h
));
504 items
= new(CatalogItem
, hashmap_size(h
));
511 HASHMAP_FOREACH_KEY(payload
, i
, h
, j
) {
512 log_debug("Found " SD_ID128_FORMAT_STR
", language %s",
513 SD_ID128_FORMAT_VAL(i
->id
),
514 isempty(i
->language
) ? "C" : i
->language
);
516 offset
= strbuf_add_string(sb
, payload
, strlen(payload
));
521 i
->offset
= htole64((uint64_t) offset
);
525 assert(n
== hashmap_size(h
));
526 qsort_safe(items
, n
, sizeof(CatalogItem
), catalog_compare_func
);
530 sz
= write_catalog(database
, sb
, items
, n
);
532 r
= log_error_errno(sz
, "Failed to write %s: %m", database
);
535 log_debug("%s: wrote %u items, with %zu bytes of strings, %"PRIi64
" total size.",
536 database
, n
, sb
->len
, sz
);
545 static int open_mmap(const char *database
, int *_fd
, struct stat
*_st
, void **_p
) {
546 const CatalogHeader
*h
;
555 fd
= open(database
, O_RDONLY
|O_CLOEXEC
);
559 if (fstat(fd
, &st
) < 0) {
564 if (st
.st_size
< (off_t
) sizeof(CatalogHeader
)) {
569 p
= mmap(NULL
, PAGE_ALIGN(st
.st_size
), PROT_READ
, MAP_SHARED
, fd
, 0);
570 if (p
== MAP_FAILED
) {
576 if (memcmp(h
->signature
, CATALOG_SIGNATURE
, sizeof(h
->signature
)) != 0 ||
577 le64toh(h
->header_size
) < sizeof(CatalogHeader
) ||
578 le64toh(h
->catalog_item_size
) < sizeof(CatalogItem
) ||
579 h
->incompatible_flags
!= 0 ||
580 le64toh(h
->n_items
) <= 0 ||
581 st
.st_size
< (off_t
) (le64toh(h
->header_size
) + le64toh(h
->catalog_item_size
) * le64toh(h
->n_items
))) {
583 munmap(p
, st
.st_size
);
594 static const char *find_id(void *p
, sd_id128_t id
) {
595 CatalogItem key
, *f
= NULL
;
596 const CatalogHeader
*h
= p
;
602 loc
= setlocale(LC_MESSAGES
, NULL
);
603 if (loc
&& loc
[0] && !streq(loc
, "C") && !streq(loc
, "POSIX")) {
604 strncpy(key
.language
, loc
, sizeof(key
.language
));
605 key
.language
[strcspn(key
.language
, ".@")] = 0;
607 f
= bsearch(&key
, (const uint8_t*) p
+ le64toh(h
->header_size
), le64toh(h
->n_items
), le64toh(h
->catalog_item_size
), catalog_compare_func
);
611 e
= strchr(key
.language
, '_');
614 f
= bsearch(&key
, (const uint8_t*) p
+ le64toh(h
->header_size
), le64toh(h
->n_items
), le64toh(h
->catalog_item_size
), catalog_compare_func
);
621 f
= bsearch(&key
, (const uint8_t*) p
+ le64toh(h
->header_size
), le64toh(h
->n_items
), le64toh(h
->catalog_item_size
), catalog_compare_func
);
627 return (const char*) p
+
628 le64toh(h
->header_size
) +
629 le64toh(h
->n_items
) * le64toh(h
->catalog_item_size
) +
633 int catalog_get(const char* database
, sd_id128_t id
, char **_text
) {
634 _cleanup_close_
int fd
= -1;
643 r
= open_mmap(database
, &fd
, &st
, &p
);
664 munmap(p
, st
.st_size
);
669 static char *find_header(const char *s
, const char *header
) {
674 v
= startswith(s
, header
);
676 v
+= strspn(v
, WHITESPACE
);
677 return strndup(v
, strcspn(v
, NEWLINE
));
680 if (!next_header(&s
))
685 static void dump_catalog_entry(FILE *f
, sd_id128_t id
, const char *s
, bool oneline
) {
687 _cleanup_free_
char *subject
= NULL
, *defined_by
= NULL
;
689 subject
= find_header(s
, "Subject:");
690 defined_by
= find_header(s
, "Defined-By:");
692 fprintf(f
, SD_ID128_FORMAT_STR
" %s: %s\n",
693 SD_ID128_FORMAT_VAL(id
),
694 strna(defined_by
), strna(subject
));
696 fprintf(f
, "-- " SD_ID128_FORMAT_STR
"\n%s\n",
697 SD_ID128_FORMAT_VAL(id
), s
);
701 int catalog_list(FILE *f
, const char *database
, bool oneline
) {
702 _cleanup_close_
int fd
= -1;
705 const CatalogHeader
*h
;
706 const CatalogItem
*items
;
710 bool last_id_set
= false;
712 r
= open_mmap(database
, &fd
, &st
, &p
);
717 items
= (const CatalogItem
*) ((const uint8_t*) p
+ le64toh(h
->header_size
));
719 for (n
= 0; n
< le64toh(h
->n_items
); n
++) {
722 if (last_id_set
&& sd_id128_equal(last_id
, items
[n
].id
))
725 assert_se(s
= find_id(p
, items
[n
].id
));
727 dump_catalog_entry(f
, items
[n
].id
, s
, oneline
);
730 last_id
= items
[n
].id
;
733 munmap(p
, st
.st_size
);
738 int catalog_list_items(FILE *f
, const char *database
, bool oneline
, char **items
) {
742 STRV_FOREACH(item
, items
) {
745 _cleanup_free_
char *msg
= NULL
;
747 k
= sd_id128_from_string(*item
, &id
);
749 log_error_errno(k
, "Failed to parse id128 '%s': %m", *item
);
755 k
= catalog_get(database
, id
, &msg
);
757 log_full_errno(k
== -ENOENT
? LOG_NOTICE
: LOG_ERR
, k
,
758 "Failed to retrieve catalog entry for '%s': %m", *item
);
764 dump_catalog_entry(f
, id
, msg
, oneline
);