1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
12 #include "alloc-util.h"
14 #include "conf-files.h"
15 #include "errno-util.h"
21 #include "memory-util.h"
23 #include "siphash24.h"
24 #include "sort-util.h"
25 #include "sparse-endian.h"
27 #include "string-util.h"
29 #include "tmpfile-util.h"
31 const char * const catalog_file_dirs
[] = {
32 "/usr/local/lib/systemd/catalog/",
33 "/usr/lib/systemd/catalog/",
37 #define CATALOG_SIGNATURE { 'R', 'H', 'H', 'H', 'K', 'S', 'L', 'P' }
39 typedef struct CatalogHeader
{
40 uint8_t signature
[8]; /* "RHHHKSLP" */
41 le32_t compatible_flags
;
42 le32_t incompatible_flags
;
45 le64_t catalog_item_size
;
48 typedef struct CatalogItem
{
50 char language
[32]; /* One byte is used for termination, so the maximum allowed
51 * length of the string is actually 31 bytes. */
55 static void catalog_hash_func(const CatalogItem
*i
, struct siphash
*state
) {
59 siphash24_compress_typesafe(i
->id
, state
);
60 siphash24_compress_string(i
->language
, state
);
63 static int catalog_compare_func(const CatalogItem
*a
, const CatalogItem
*b
) {
69 for (size_t k
= 0; k
< ELEMENTSOF(b
->id
.bytes
); k
++) {
70 r
= CMP(a
->id
.bytes
[k
], b
->id
.bytes
[k
]);
75 return strcmp(a
->language
, b
->language
);
78 DEFINE_PRIVATE_HASH_OPS_FULL(
80 CatalogItem
, catalog_hash_func
, catalog_compare_func
, free
,
83 static bool next_header(const char **s
) {
103 static const char* skip_header(const char *s
) {
106 while (next_header(&s
))
111 static char* combine_entries(const char *one
, const char *two
) {
119 /* Find split point of headers to body */
120 b1
= skip_header(one
);
121 b2
= skip_header(two
);
125 dest
= new(char, l1
+ l2
+ 1);
133 /* Headers from @one */
135 p
= mempcpy(p
, one
, n
);
137 /* Headers from @two, these will only be found if not present above */
139 p
= mempcpy(p
, two
, n
);
144 p
= mempcpy(p
, b1
, n
);
148 p
= mempcpy(p
, b2
, n
);
151 assert(p
- dest
<= (ptrdiff_t)(l1
+ l2
));
156 static int finish_item(
159 const char *language
,
161 size_t payload_size
) {
163 _cleanup_free_ CatalogItem
*i
= NULL
;
164 _cleanup_free_
char *combined
= NULL
;
170 assert(payload_size
> 0);
172 i
= new0(CatalogItem
, 1);
178 assert(strlen(language
) > 1 && strlen(language
) < 32);
179 strcpy(i
->language
, language
);
182 prev
= ordered_hashmap_get(*h
, i
);
184 /* Already have such an item, combine them */
185 combined
= combine_entries(payload
, prev
);
189 r
= ordered_hashmap_update(*h
, i
, combined
);
191 return log_error_errno(r
, "Failed to update catalog item: %m");
197 combined
= memdup(payload
, payload_size
+ 1);
201 r
= ordered_hashmap_ensure_put(h
, &catalog_hash_ops
, i
, combined
);
203 return log_error_errno(r
, "Failed to insert catalog item: %m");
212 int catalog_file_lang(const char *filename
, char **ret
) {
213 char *beg
, *end
, *lang
;
218 end
= endswith(filename
, ".catalog");
223 while (beg
> filename
&& !IN_SET(*beg
, '.', '/') && end
- beg
< 32)
226 if (*beg
!= '.' || end
<= beg
+ 1)
229 lang
= strndup(beg
+ 1, end
- beg
- 1);
237 static int catalog_entry_lang(
238 const char *filename
,
246 size_t c
= strlen(t
);
248 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
249 "[%s:%u] Language too short.", filename
, line
);
251 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
252 "[%s:%u] language too long.", filename
, line
);
255 if (streq(t
, deflang
)) {
256 log_warning("[%s:%u] language specified unnecessarily", filename
, line
);
260 log_warning("[%s:%u] language differs from default for file", filename
, line
);
263 return strdup_to(ret
, t
);
266 int catalog_import_file(OrderedHashmap
**h
, int fd
, const char *path
) {
267 _cleanup_fclose_
FILE *f
= NULL
;
268 _cleanup_free_
char *payload
= NULL
;
269 size_t payload_size
= 0;
272 _cleanup_free_
char *deflang
= NULL
, *lang
= NULL
;
273 bool got_id
= false, empty_line
= true;
280 f
= fopen(FORMAT_PROC_FD_PATH(fd
), "re");
282 return log_error_errno(errno
, "Failed to open file %s: %m", path
);
284 r
= catalog_file_lang(path
, &deflang
);
286 log_error_errno(r
, "Failed to determine language for file %s: %m", path
);
288 log_debug("File %s has language %s.", path
, deflang
);
291 _cleanup_free_
char *line
= NULL
;
294 r
= read_line(f
, LONG_LINE_MAX
, &line
);
296 return log_error_errno(r
, "Failed to read file %s: %m", path
);
307 if (strchr(COMMENTS
, line
[0]))
311 strlen(line
) >= 2+1+32 &&
315 IN_SET(line
[2+1+32], ' ', '\0')) {
322 with_language
= line
[2+1+32] != '\0';
325 if (sd_id128_from_string(line
+ 2 + 1, &jd
) >= 0) {
328 if (payload_size
== 0)
329 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
330 "[%s:%u] No payload text.",
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 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
362 "[%s:%u] Got payload before ID.",
365 line_len
= strlen(line
);
366 if (!GREEDY_REALLOC(payload
, payload_size
+ (empty_line
? 1 : 0) + line_len
+ 1 + 1))
370 payload
[payload_size
++] = '\n';
371 memcpy(payload
+ payload_size
, line
, line_len
);
372 payload_size
+= line_len
;
373 payload
[payload_size
++] = '\n';
374 payload
[payload_size
] = '\0';
380 if (payload_size
== 0)
381 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
382 "[%s:%u] No payload text.",
385 r
= finish_item(h
, id
, lang
?: deflang
, payload
, payload_size
);
393 static int64_t write_catalog(
394 const char *database
,
395 const struct strbuf
*sb
,
396 const CatalogItem
*items
,
399 _cleanup_(unlink_and_freep
) char *p
= NULL
;
400 _cleanup_fclose_
FILE *w
= NULL
;
408 r
= mkdir_parents(database
, 0755);
410 return log_error_errno(r
, "Failed to create parent directories of %s: %m", database
);
412 r
= fopen_temporary(database
, &w
, &p
);
414 return log_error_errno(r
, "Failed to open database for writing: %s: %m", database
);
416 CatalogHeader header
= {
417 .signature
= CATALOG_SIGNATURE
,
418 .header_size
= htole64(CONST_ALIGN_TO(sizeof(CatalogHeader
), 8)),
419 .catalog_item_size
= htole64(sizeof(CatalogItem
)),
420 .n_items
= htole64(n_items
),
423 if (fwrite(&header
, sizeof(header
), 1, w
) != 1)
424 return log_error_errno(SYNTHETIC_ERRNO(EIO
), "%s: failed to write header.", p
);
426 if (fwrite(items
, sizeof(CatalogItem
), n_items
, w
) != n_items
)
427 return log_error_errno(SYNTHETIC_ERRNO(EIO
), "%s: failed to write database.", p
);
429 if (fwrite(sb
->buf
, sb
->len
, 1, w
) != 1)
430 return log_error_errno(SYNTHETIC_ERRNO(EIO
), "%s: failed to write strings.", p
);
432 r
= fflush_and_check(w
);
434 return log_error_errno(r
, "%s: failed to write database: %m", p
);
436 (void) fchmod(fileno(w
), 0644);
438 if (rename(p
, database
) < 0)
439 return log_error_errno(errno
, "rename (%s -> %s) failed: %m", p
, database
);
441 p
= mfree(p
); /* free without unlinking */
445 int catalog_update(const char *database
, const char *root
, const char* const *dirs
) {
451 dirs
= catalog_file_dirs
;
453 ConfFile
**files
= NULL
;
456 CLEANUP_ARRAY(files
, n_files
, conf_file_free_many
);
458 r
= conf_files_list_strv_full(".catalog", root
, CONF_FILES_REGULAR
| CONF_FILES_FILTER_MASKED
, dirs
, &files
, &n_files
);
460 return log_error_errno(r
, "Failed to get catalog files: %m");
462 _cleanup_ordered_hashmap_free_ OrderedHashmap
*h
= NULL
;
463 FOREACH_ARRAY(i
, files
, n_files
) {
466 log_debug("Reading file: '%s' -> '%s'", c
->original_path
, c
->resolved_path
);
467 r
= catalog_import_file(&h
, c
->fd
, c
->original_path
);
469 return log_error_errno(r
, "Failed to import file '%s': %m", c
->original_path
);
472 if (ordered_hashmap_isempty(h
)) {
473 log_info("No items in catalog.");
477 log_debug("Found %u items in catalog.", ordered_hashmap_size(h
));
479 _cleanup_free_ CatalogItem
*items
= new(CatalogItem
, ordered_hashmap_size(h
));
483 _cleanup_(strbuf_freep
) struct strbuf
*sb
= strbuf_new();
490 ORDERED_HASHMAP_FOREACH_KEY(payload
, i
, h
) {
491 log_trace("Found " SD_ID128_FORMAT_STR
", language %s",
492 SD_ID128_FORMAT_VAL(i
->id
),
493 isempty(i
->language
) ? "C" : i
->language
);
495 ssize_t offset
= strbuf_add_string(sb
, payload
);
499 i
->offset
= htole64((uint64_t) offset
);
503 assert(n
== ordered_hashmap_size(h
));
504 typesafe_qsort(items
, n
, catalog_compare_func
);
508 int64_t sz
= write_catalog(database
, sb
, items
, n
);
510 return log_error_errno(sz
, "Failed to write %s: %m", database
);
512 log_debug("%s: wrote %u items, with %zu bytes of strings, %"PRIi64
" total size.",
513 database
, n
, sb
->len
, sz
);
517 static int open_mmap(const char *database
, int *ret_fd
, struct stat
*ret_st
, void **ret_map
) {
523 _cleanup_close_
int fd
= open(database
, O_RDONLY
|O_CLOEXEC
);
528 if (fstat(fd
, &st
) < 0)
531 if (st
.st_size
< (off_t
) sizeof(CatalogHeader
) || file_offset_beyond_memory_size(st
.st_size
))
534 void *p
= mmap(NULL
, st
.st_size
, PROT_READ
, MAP_SHARED
, fd
, 0);
538 const CatalogHeader
*h
= p
;
539 if (memcmp(h
->signature
, (const uint8_t[]) CATALOG_SIGNATURE
, sizeof(h
->signature
)) != 0 ||
540 le64toh(h
->header_size
) < sizeof(CatalogHeader
) ||
541 le64toh(h
->catalog_item_size
) < sizeof(CatalogItem
) ||
542 h
->incompatible_flags
!= 0 ||
543 le64toh(h
->n_items
) <= 0 ||
544 st
.st_size
< (off_t
) (le64toh(h
->header_size
) + le64toh(h
->catalog_item_size
) * le64toh(h
->n_items
))) {
545 munmap(p
, st
.st_size
);
549 *ret_fd
= TAKE_FD(fd
);
555 static const char* find_id(const void *p
, sd_id128_t id
) {
556 CatalogItem
*f
= NULL
, key
= { .id
= id
};
557 const CatalogHeader
*h
= ASSERT_PTR(p
);
560 loc
= setlocale(LC_MESSAGES
, NULL
);
561 if (!isempty(loc
) && !STR_IN_SET(loc
, "C", "POSIX")) {
564 len
= strcspn(loc
, ".@");
565 if (len
> sizeof(key
.language
) - 1)
566 log_debug("LC_MESSAGES value too long, ignoring: \"%.*s\"", (int) len
, loc
);
568 strncpy(key
.language
, loc
, len
);
569 key
.language
[len
] = '\0';
572 (const uint8_t*) p
+ le64toh(h
->header_size
),
574 le64toh(h
->catalog_item_size
),
575 (comparison_fn_t
) catalog_compare_func
);
579 e
= strchr(key
.language
, '_');
583 (const uint8_t*) p
+ le64toh(h
->header_size
),
585 le64toh(h
->catalog_item_size
),
586 (comparison_fn_t
) catalog_compare_func
);
595 (const uint8_t*) p
+ le64toh(h
->header_size
),
597 le64toh(h
->catalog_item_size
),
598 (comparison_fn_t
) catalog_compare_func
);
604 return (const char*) p
+
605 le64toh(h
->header_size
) +
606 le64toh(h
->n_items
) * le64toh(h
->catalog_item_size
) +
610 int catalog_get(const char *database
, sd_id128_t id
, char **ret_text
) {
616 _cleanup_close_
int fd
= -EBADF
;
619 r
= open_mmap(database
, &fd
, &st
, &p
);
623 const char *s
= find_id(p
, id
);
627 r
= strdup_to(ret_text
, s
);
629 (void) munmap(p
, st
.st_size
);
633 static char* find_header(const char *s
, const char *header
) {
641 v
= startswith(s
, header
);
643 v
+= strspn(v
, WHITESPACE
);
644 return strndup(v
, strcspn(v
, NEWLINE
));
647 if (!next_header(&s
))
652 static void dump_catalog_entry(FILE *f
, sd_id128_t id
, const char *s
, bool oneline
) {
659 _cleanup_free_
char *subject
= NULL
, *defined_by
= NULL
;
661 subject
= find_header(s
, "Subject:");
662 defined_by
= find_header(s
, "Defined-By:");
664 fprintf(f
, SD_ID128_FORMAT_STR
" %s: %s\n",
665 SD_ID128_FORMAT_VAL(id
),
666 strna(defined_by
), strna(subject
));
668 fprintf(f
, "-- " SD_ID128_FORMAT_STR
"\n%s\n",
669 SD_ID128_FORMAT_VAL(id
), s
);
672 int catalog_list(FILE *f
, const char *database
, bool oneline
) {
677 _cleanup_close_
int fd
= -EBADF
;
680 r
= open_mmap(database
, &fd
, &st
, &p
);
684 const CatalogHeader
*h
= p
;
685 const CatalogItem
*items
= (const CatalogItem
*) ((const uint8_t*) p
+ le64toh(h
->header_size
));
688 bool last_id_set
= false;
689 for (size_t n
= 0; n
< le64toh(h
->n_items
); n
++) {
692 if (last_id_set
&& sd_id128_equal(last_id
, items
[n
].id
))
695 assert_se(s
= find_id(p
, items
[n
].id
));
697 dump_catalog_entry(f
, items
[n
].id
, s
, oneline
);
700 last_id
= items
[n
].id
;
703 (void) munmap(p
, st
.st_size
);
707 int catalog_list_items(FILE *f
, const char *database
, bool oneline
, char **items
) {
712 STRV_FOREACH(item
, items
) {
714 r
= sd_id128_from_string(*item
, &id
);
716 RET_GATHER(ret
, log_error_errno(r
, "Failed to parse id128 '%s': %m", *item
));
720 _cleanup_free_
char *msg
= NULL
;
721 r
= catalog_get(database
, id
, &msg
);
723 RET_GATHER(ret
, log_full_errno(r
== -ENOENT
? LOG_NOTICE
: LOG_ERR
, r
,
724 "Failed to retrieve catalog entry for '%s': %m", *item
));
728 dump_catalog_entry(f
, id
, msg
, oneline
);