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
*f
= NULL
, key
= { .id
= id
};
596 const CatalogHeader
*h
= p
;
599 loc
= setlocale(LC_MESSAGES
, NULL
);
600 if (loc
&& loc
[0] && !streq(loc
, "C") && !streq(loc
, "POSIX")) {
601 strncpy(key
.language
, loc
, sizeof(key
.language
));
602 key
.language
[strcspn(key
.language
, ".@")] = 0;
604 f
= bsearch(&key
, (const uint8_t*) p
+ le64toh(h
->header_size
), le64toh(h
->n_items
), le64toh(h
->catalog_item_size
), catalog_compare_func
);
608 e
= strchr(key
.language
, '_');
611 f
= bsearch(&key
, (const uint8_t*) p
+ le64toh(h
->header_size
), le64toh(h
->n_items
), le64toh(h
->catalog_item_size
), catalog_compare_func
);
618 f
= bsearch(&key
, (const uint8_t*) p
+ le64toh(h
->header_size
), le64toh(h
->n_items
), le64toh(h
->catalog_item_size
), catalog_compare_func
);
624 return (const char*) p
+
625 le64toh(h
->header_size
) +
626 le64toh(h
->n_items
) * le64toh(h
->catalog_item_size
) +
630 int catalog_get(const char* database
, sd_id128_t id
, char **_text
) {
631 _cleanup_close_
int fd
= -1;
640 r
= open_mmap(database
, &fd
, &st
, &p
);
661 munmap(p
, st
.st_size
);
666 static char *find_header(const char *s
, const char *header
) {
671 v
= startswith(s
, header
);
673 v
+= strspn(v
, WHITESPACE
);
674 return strndup(v
, strcspn(v
, NEWLINE
));
677 if (!next_header(&s
))
682 static void dump_catalog_entry(FILE *f
, sd_id128_t id
, const char *s
, bool oneline
) {
684 _cleanup_free_
char *subject
= NULL
, *defined_by
= NULL
;
686 subject
= find_header(s
, "Subject:");
687 defined_by
= find_header(s
, "Defined-By:");
689 fprintf(f
, SD_ID128_FORMAT_STR
" %s: %s\n",
690 SD_ID128_FORMAT_VAL(id
),
691 strna(defined_by
), strna(subject
));
693 fprintf(f
, "-- " SD_ID128_FORMAT_STR
"\n%s\n",
694 SD_ID128_FORMAT_VAL(id
), s
);
698 int catalog_list(FILE *f
, const char *database
, bool oneline
) {
699 _cleanup_close_
int fd
= -1;
702 const CatalogHeader
*h
;
703 const CatalogItem
*items
;
707 bool last_id_set
= false;
709 r
= open_mmap(database
, &fd
, &st
, &p
);
714 items
= (const CatalogItem
*) ((const uint8_t*) p
+ le64toh(h
->header_size
));
716 for (n
= 0; n
< le64toh(h
->n_items
); n
++) {
719 if (last_id_set
&& sd_id128_equal(last_id
, items
[n
].id
))
722 assert_se(s
= find_id(p
, items
[n
].id
));
724 dump_catalog_entry(f
, items
[n
].id
, s
, oneline
);
727 last_id
= items
[n
].id
;
730 munmap(p
, st
.st_size
);
735 int catalog_list_items(FILE *f
, const char *database
, bool oneline
, char **items
) {
739 STRV_FOREACH(item
, items
) {
742 _cleanup_free_
char *msg
= NULL
;
744 k
= sd_id128_from_string(*item
, &id
);
746 log_error_errno(k
, "Failed to parse id128 '%s': %m", *item
);
752 k
= catalog_get(database
, id
, &msg
);
754 log_full_errno(k
== -ENOENT
? LOG_NOTICE
: LOG_ERR
, k
,
755 "Failed to retrieve catalog entry for '%s': %m", *item
);
761 dump_catalog_entry(f
, id
, msg
, oneline
);