ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
mdl_export_meta.c
Go to the documentation of this file.
1
13
14#include <ctype.h>
15#include <errno.h>
16#include <limits.h>
17#include <math.h>
18#include <stdio.h>
19#include <stdlib.h>
20#include <string.h>
21
22#include "mdl_export.h"
23#include "mdl_export_internal.h"
24#include "mdl_sanitize.h"
25#include "mdl_url_guard.h"
26#include "ra8_attributes.h"
27
35
55{
56 const size_t len = strnlen(url, (size_t)k_mdl_meta_url_max);
57 if (len == (size_t)k_mdl_meta_url_max) {
59 }
60 if (len == 0U) {
61 return k_ra8_ok;
62 }
63 char host[k_mdl_meta_url_max];
64 if (!mdl_url_scheme_allowed(url) || !mdl_url_host(url, host, sizeof(host))) {
66 }
67 for (size_t i = 0U; i < len; ++i) {
68 if (isspace((unsigned char)url[i]) || iscntrl((unsigned char)url[i])) {
70 }
71 }
72 return k_ra8_ok;
73}
74
90RA8_PRIV bool priv_mdl_export_snprintf_fit(int written, size_t cap)
91{
92 return (written >= 0) && ((size_t)written < cap);
93}
94
96{
97 if (meta == nullptr) {
98 return;
99 }
100 memset(meta, 0, sizeof(*meta));
101 meta->cover_index = -1;
103 (void)snprintf(meta->language, sizeof(meta->language), "en");
104 meta->modified[0] = '\0';
105}
106
124RA8_INTERNAL static bool internal_str_copy_trimmed(char* dst, size_t cap, const char* src)
125{
126 while ((*src != '\0') && isspace((unsigned char)*src)) {
127 src++;
128 }
129 size_t len = strlen(src);
130 while ((len > 0U) && isspace((unsigned char)src[len - 1U])) {
131 len--;
132 }
133 if ((cap == 0U) || (len >= cap)) {
134 return false;
135 }
136 memcpy(dst, src, len);
137 dst[len] = '\0';
138 return true;
139}
140
157RA8_INTERNAL static bool internal_parse_double_strict(const char* text, double* out)
158{
159 errno = 0;
160 char* end = nullptr;
161 const double value = strtod(text, &end);
162 while ((end != nullptr) && isspace((unsigned char)*end)) {
163 ++end;
164 }
165 if ((end == text) || (end == nullptr) || (*end != '\0') || (errno == ERANGE) ||
166 !isfinite(value) || (value < 0.0)) {
167 return false;
168 }
169 *out = value;
170 return true;
171}
172
189RA8_INTERNAL static bool internal_parse_int_strict(const char* text, int* out)
190{
191 errno = 0;
192 char* end = nullptr;
193 const long value = strtol(text, &end, k_decimal_radix);
194 while ((end != nullptr) && isspace((unsigned char)*end)) {
195 ++end;
196 }
197 if ((end == text) || (end == nullptr) || (*end != '\0') || (errno == ERANGE) || (value < -1L) ||
198 (value > INT_MAX)) {
199 return false;
200 }
201 *out = (int)value;
202 return true;
203}
204
221RA8_INTERNAL static bool internal_xml_unescape(const char* raw, char* out, size_t cap)
222{
223 size_t r = 0U;
224 size_t w = 0U;
225 while ((raw[r] != '\0') && (w + 1U < cap)) {
226 const char* entity = nullptr;
227 char decoded = '\0';
228 size_t consumed = 0U;
229 if (raw[r] == '&') {
230 if (strncmp(raw + r, "&amp;", k_xml_amp_entity_len) == 0) {
231 entity = "&amp;";
232 decoded = '&';
233 } else if (strncmp(raw + r, "&lt;", 4U) == 0) {
234 entity = "&lt;";
235 decoded = '<';
236 } else if (strncmp(raw + r, "&gt;", 4U) == 0) {
237 entity = "&gt;";
238 decoded = '>';
239 } else if (strncmp(raw + r, "&quot;", 6U) == 0) {
240 entity = "&quot;";
241 decoded = '"';
242 } else if (strncmp(raw + r, "&apos;", 6U) == 0) {
243 entity = "&apos;";
244 decoded = '\'';
245 }
246 }
247 consumed = (entity == nullptr) ? 1U : strlen(entity);
248 out[w++] = (char)((entity == nullptr) ? raw[r] : decoded);
249 r += consumed;
250 }
251 out[w] = '\0';
252 return raw[r] == '\0';
253}
254
272RA8_INTERNAL static bool
273internal_parse_xml_tag(const char* xml, const char* tag, char* out, size_t cap)
274{
275 char open_tag[64];
276 char close_tag[64];
277 (void)snprintf(open_tag, sizeof(open_tag), "<%s>", tag);
278 (void)snprintf(close_tag, sizeof(close_tag), "</%s>", tag);
279 const char* start = strstr(xml, open_tag);
280 if (start == nullptr) {
281 (void)snprintf(open_tag, sizeof(open_tag), "<%s ", tag);
282 start = strstr(xml, open_tag);
283 start = (start == nullptr) ? nullptr : strchr(start, '>');
284 start = (start == nullptr) ? nullptr : start + 1;
285 } else {
286 start += strlen(open_tag);
287 }
288 const char* end = (start == nullptr) ? nullptr : strstr(start, close_tag);
289 if (end == nullptr) {
290 return true;
291 }
292 const size_t len = (size_t)(end - start);
293 char raw[k_mdl_meta_path_max + 1U];
294 if (len >= sizeof(raw)) {
295 return false;
296 }
297 memcpy(raw, start, len);
298 raw[len] = '\0';
299 return internal_xml_unescape(raw, out, cap);
300}
301
303typedef struct {
304 const char* tag;
305 char* dst;
306 size_t cap;
308
325 const mdl_meta_xml_field_t* field)
326{
327 char value[k_mdl_meta_path_max + 1U] = {};
328 if (!internal_parse_xml_tag(text, field->tag, value, sizeof(value))) {
330 }
331 if ((value[0] != '\0') && !internal_str_copy_trimmed(field->dst, field->cap, value)) {
333 }
334 return k_ra8_ok;
335}
336
354 const char* text)
355{
356 char value[k_mdl_meta_path_max + 1U] = {};
357 if (!internal_parse_xml_tag(text, "Web", value, sizeof(value))) {
359 }
360 if ((value[0] != '\0') &&
361 !internal_str_copy_trimmed(meta->source_url, sizeof(meta->source_url), value)) {
363 }
364 if (value[0] != '\0') {
366 if (rc != k_ra8_ok) {
367 meta->source_url[0] = '\0';
368 return rc;
369 }
370 }
371 value[0] = '\0';
372 if (!internal_parse_xml_tag(text, "Number", value, sizeof(value))) {
374 }
375 if ((value[0] != '\0') && !internal_parse_double_strict(value, &meta->chapter_number)) {
377 }
378 value[0] = '\0';
379 if (!internal_parse_xml_tag(text, "Manga", value, sizeof(value))) {
381 }
382 if (strcmp(value, "YesAndRightToLeft") == 0) {
384 } else if (strcmp(value, "No") == 0) {
386 }
387 return k_ra8_ok;
388}
389
408{
409 mdl_meta_xml_field_t fields[] = {{"Title", meta->chapter_title, sizeof(meta->chapter_title)},
410 {"Series", meta->series_title, sizeof(meta->series_title)},
411 {"Summary", meta->summary, sizeof(meta->summary)},
412 {"Writer", meta->writer, sizeof(meta->writer)},
413 {"Artist", meta->artist, sizeof(meta->artist)},
414 {"LanguageISO", meta->language, sizeof(meta->language)},
415 {"CoverImage", meta->cover_path, sizeof(meta->cover_path)}};
416 for (size_t i = 0U; i < (sizeof(fields) / sizeof(fields[0])); ++i) {
417 const ra8_err_t rc = internal_parse_xml_field(text, &fields[i]);
418 if (rc != k_ra8_ok) {
419 return rc;
420 }
421 }
422 return internal_parse_xml_semantics(meta, text);
423}
424
444RA8_INTERNAL static bool internal_key_is(const char* key,
445 const char* a,
446 const char* b,
447 const char* c,
448 const char* d,
449 const char* e)
450{
451 return (strcmp(key, a) == 0) || ((b != nullptr) && (strcmp(key, b) == 0)) ||
452 ((c != nullptr) && (strcmp(key, c) == 0)) || ((d != nullptr) && (strcmp(key, d) == 0)) ||
453 ((e != nullptr) && (strcmp(key, e) == 0));
454}
455
473RA8_INTERNAL static bool
474internal_find_descriptive_key(mdl_export_meta_t* meta, const char* key, char** dst, size_t* cap)
475{
476 if (internal_key_is(key, "series", "series_title", "seriestitle", "series title", nullptr)) {
477 *dst = meta->series_title;
478 *cap = sizeof(meta->series_title);
479 } else if (internal_key_is(key, "summary", "description", "abstract", nullptr, nullptr)) {
480 *dst = meta->summary;
481 *cap = sizeof(meta->summary);
482 } else if (internal_key_is(key,
483 "chapter_title",
484 "chaptertitle",
485 "chapter title",
486 "title",
487 nullptr)) {
488 *dst = meta->chapter_title;
489 *cap = sizeof(meta->chapter_title);
490 } else {
491 return false;
492 }
493 return true;
494}
495
513RA8_INTERNAL static bool
514internal_find_publication_key(mdl_export_meta_t* meta, const char* key, char** dst, size_t* cap)
515{
516 if (internal_key_is(key, "writer", "author", "creator", nullptr, nullptr)) {
517 *dst = meta->writer;
518 *cap = sizeof(meta->writer);
519 } else if (internal_key_is(key, "artist", "penciller", "illustrator", nullptr, nullptr)) {
520 *dst = meta->artist;
521 *cap = sizeof(meta->artist);
522 } else if (internal_key_is(key,
523 "cover",
524 "cover_path",
525 "coverpath",
526 "cover path",
527 "cover_image")) {
528 *dst = meta->cover_path;
529 *cap = sizeof(meta->cover_path);
530 } else if (internal_key_is(key, "language", "language_iso", "lang", nullptr, nullptr)) {
531 *dst = meta->language;
532 *cap = sizeof(meta->language);
533 } else if (internal_key_is(key, "identifier", "uuid", nullptr, nullptr, nullptr)) {
534 *dst = meta->identifier;
535 *cap = sizeof(meta->identifier);
536 } else if (internal_key_is(key, "modified", "date_modified", nullptr, nullptr, nullptr)) {
537 *dst = meta->modified;
538 *cap = sizeof(meta->modified);
539 } else {
540 return false;
541 }
542 return true;
543}
544
564internal_apply_text_key(mdl_export_meta_t* meta, const char* key, const char* value)
565{
566 char* dst = nullptr;
567 size_t cap = 0U;
568 if (!internal_find_descriptive_key(meta, key, &dst, &cap) &&
569 !internal_find_publication_key(meta, key, &dst, &cap)) {
570 return k_ra8_err_not_found;
571 }
573}
574
593internal_apply_semantic_key(mdl_export_meta_t* meta, const char* key, const char* value)
594{
595 char parsed[64];
596 if (internal_key_is(key, "source_url", "source", "web", nullptr, nullptr)) {
597 if (!internal_str_copy_trimmed(meta->source_url, sizeof(meta->source_url), value)) {
599 }
601 if (rc != k_ra8_ok) {
602 meta->source_url[0] = '\0';
603 }
604 return rc;
605 }
606 if (internal_key_is(key, "number", "chapter_number", "chapternumber", "chapter number", "num")) {
607 if (!internal_str_copy_trimmed(parsed, sizeof(parsed), value)) {
609 }
612 }
613 if (internal_key_is(key, "cover_index", "coverindex", "cover index", "cover_idx", nullptr)) {
614 if (!internal_str_copy_trimmed(parsed, sizeof(parsed), value)) {
616 }
618 }
619 if (internal_key_is(key,
620 "reading_direction",
621 "direction",
622 "page_progression",
623 nullptr,
624 nullptr)) {
625 if (!internal_str_copy_trimmed(parsed, sizeof(parsed), value)) {
627 }
628 if (strcmp(parsed, "rtl") == 0) {
630 } else if (strcmp(parsed, "ltr") == 0) {
632 } else {
634 }
635 }
636 return k_ra8_ok;
637}
638
656{
657 char* start = line;
658 while ((*start != '\0') && isspace((unsigned char)*start)) {
659 ++start;
660 }
661 if ((*start == '\0') || (*start == '#') || (*start == ';')) {
662 return k_ra8_ok;
663 }
664 char* separator = strchr(start, '=');
665 separator = (separator == nullptr) ? strchr(start, ':') : separator;
666 if (separator == nullptr) {
667 return k_ra8_ok;
668 }
669 *separator = '\0';
670 char key[128];
671 if (!internal_str_copy_trimmed(key, sizeof(key), start)) {
673 }
674 for (size_t i = 0U; key[i] != '\0'; ++i) {
675 key[i] = (char)tolower((unsigned char)key[i]);
676 }
677 ra8_err_t rc = internal_apply_text_key(meta, key, separator + 1);
678 if (rc == k_ra8_err_not_found) {
679 rc = internal_apply_semantic_key(meta, key, separator + 1);
680 }
681 return rc;
682}
683
685{
686 if ((meta == nullptr) || (text == nullptr)) {
688 }
689 if ((strstr(text, "<ComicInfo") != nullptr) || (strstr(text, "<Title>") != nullptr) ||
690 (strstr(text, "<Series>") != nullptr) || (strstr(text, "<Web>") != nullptr)) {
691 return internal_parse_xml(meta, text);
692 }
693 const char* cursor = text;
694 while (*cursor != '\0') {
695 const char* end = strchr(cursor, '\n');
696 const size_t length = (end == nullptr) ? strlen(cursor) : (size_t)(end - cursor);
698 if (length >= sizeof(line)) {
700 }
701 memcpy(line, cursor, length);
702 line[length] = '\0';
703 const ra8_err_t rc = internal_parse_kv_line(meta, line);
704 if (rc != k_ra8_ok) {
705 return rc;
706 }
707 cursor += length;
708 if (*cursor == '\n') {
709 ++cursor;
710 }
711 }
712 return k_ra8_ok;
713}
714
735 const char* candidate,
736 bool parent,
737 char* out,
738 size_t capacity)
739{
740 if (!parent) {
741 return priv_mdl_export_path_join(out, capacity, directory, candidate);
742 }
743 char parent_path[k_fw_fs_path_cap];
744 size_t length = strnlen(directory, sizeof(parent_path));
745 if ((length == 0U) || (length >= sizeof(parent_path))) {
747 }
748 memcpy(parent_path, directory, length + 1U);
749 while ((length > 1U) && (parent_path[length - 1U] != '/')) {
750 --length;
751 }
752 if (length > 1U) {
753 parent_path[length - 1U] = '\0';
754 } else {
755 parent_path[1] = '\0';
756 }
757 return priv_mdl_export_path_join(out, capacity, parent_path, candidate);
758}
759
779 const char* dir,
780 const char* candidate,
781 bool is_fallback,
782 mdl_export_meta_t* meta)
783{
784 char path[k_fw_fs_path_cap];
785 ra8_err_t err = internal_meta_candidate_path(dir, candidate, is_fallback, path, sizeof(path));
786 if (err != k_ra8_ok) {
787 return err;
788 }
789 fw_fs_stat_t stat = {};
790 err = fw_fs_stat(&storage->fs->names, path, &stat);
791 if (err != k_ra8_ok) {
792 return err;
793 }
794 if (!stat.exists) {
795 return k_ra8_ok;
796 }
797 if (stat.type != k_fw_fs_node_file) {
799 }
800 if (stat.size_bytes == 0U) {
801 return k_ra8_ok;
802 }
803 char buf[4096];
804 if (stat.size_bytes >= sizeof(buf)) {
806 }
807 size_t got = 0U;
808 err = priv_mdl_export_source_slurp(storage, path, (uint8_t*)buf, sizeof(buf) - 1U, &got);
809 if (err != k_ra8_ok) {
810 return err;
811 }
812 buf[got] = '\0';
813 return mdl_meta_parse(meta, buf);
814}
815
817{
818 if ((storage == nullptr) || (storage->fs == nullptr) || (meta == nullptr) || (dir == nullptr)) {
820 }
821 mdl_meta_init(meta);
822
823 static const char* const candidate_files[] = {"metadata.txt",
824 "ComicInfo.xml",
825 ".mdl_meta",
826 "metadata.conf",
827 "metadata.txt"};
828
829 for (size_t i = 0U; i < (sizeof(candidate_files) / sizeof(candidate_files[0])); ++i) {
830 const ra8_err_t err =
831 internal_meta_load_candidate(storage, dir, candidate_files[i], i == 4U, meta);
832 if (err != k_ra8_ok) {
833 return err;
834 }
835 }
836 return k_ra8_ok;
837}
838
849
866 mdl_comicinfo_text_t* escaped)
867{
868 const char* title_fallback = (meta->series_title[0] != '\0') ? meta->series_title : "Chapter";
869 const char* title = (meta->chapter_title[0] != '\0') ? meta->chapter_title : title_fallback;
870 const char* series = (meta->series_title[0] != '\0') ? meta->series_title : "Series";
871 if (!mdl_xml_escape(title, escaped->title, sizeof(escaped->title))) {
872 (void)snprintf(escaped->title, sizeof(escaped->title), "Chapter");
873 }
874 if (!mdl_xml_escape(series, escaped->series, sizeof(escaped->series))) {
875 (void)snprintf(escaped->series, sizeof(escaped->series), "Series");
876 }
877 if (!mdl_xml_escape(meta->summary, escaped->summary, sizeof(escaped->summary))) {
878 escaped->summary[0] = '\0';
879 }
880 if (!mdl_xml_escape(meta->writer, escaped->writer, sizeof(escaped->writer))) {
881 escaped->writer[0] = '\0';
882 }
883 if (!mdl_xml_escape(meta->artist, escaped->artist, sizeof(escaped->artist))) {
884 escaped->artist[0] = '\0';
885 }
886 return (mdl_xml_escape(meta->language, escaped->language, sizeof(escaped->language)) &&
887 mdl_xml_escape(meta->source_url, escaped->source, sizeof(escaped->source)))
888 ? k_ra8_ok
890}
891
914 const mdl_comicinfo_text_t* escaped,
915 size_t page_count,
916 const char* web,
917 const char* number,
918 const char* pages,
919 char* buf,
920 size_t cap)
921{
922 const char* manga = (meta->reading_direction == k_mdl_read_rtl) ? "YesAndRightToLeft" : "No";
923 const int written = snprintf(buf,
924 cap,
925 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
926 "<ComicInfo xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
927 "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n"
928 " <Title>%s</Title>\n <Series>%s</Series>\n <Number>%s</Number>\n"
929 " <Summary>%s</Summary>\n <Writer>%s</Writer>\n "
930 "<Artist>%s</Artist>\n%s"
931 " <PageCount>%zu</PageCount>\n <LanguageISO>%s</LanguageISO>\n"
932 " <Manga>%s</Manga>\n%s</ComicInfo>",
933 escaped->title,
934 escaped->series,
935 number,
936 escaped->summary,
937 escaped->writer,
938 escaped->artist,
939 web,
940 page_count,
941 escaped->language,
942 manga,
943 pages);
945}
946
948 size_t page_count,
949 char* buf,
950 size_t cap)
951{
952 if ((buf == nullptr) || (cap == 0U)) {
954 }
955 mdl_export_meta_t resolved;
956 if (meta != nullptr) {
957 resolved = *meta;
958 } else {
959 mdl_meta_init(&resolved);
960 }
962 mdl_comicinfo_text_t escaped = {};
963 if (rc == k_ra8_ok) {
964 rc = internal_escape_comicinfo(&resolved, &escaped);
965 }
966 if (rc != k_ra8_ok) {
967 return rc;
968 }
969 char web[k_mdl_meta_url_max * 6U + k_meta_fragment_slack] = {};
970 if (escaped.source[0] != '\0') {
971 const int n = snprintf(web, sizeof(web), " <Web>%s</Web>\n", escaped.source);
972 if (!priv_mdl_export_snprintf_fit(n, sizeof(web))) {
974 }
975 }
976 char number[32];
977 if (resolved.chapter_number <= 0.0) {
978 (void)snprintf(number, sizeof(number), "1");
979 } else if (resolved.chapter_number == (double)(long)resolved.chapter_number) {
980 (void)snprintf(number, sizeof(number), "%ld", (long)resolved.chapter_number);
981 } else {
982 (void)snprintf(number, sizeof(number), "%.1f", resolved.chapter_number);
983 }
984 char pages[160] = {};
985 if ((resolved.cover_index >= 0) && ((size_t)resolved.cover_index < page_count)) {
986 const int n = snprintf(pages,
987 sizeof(pages),
988 " <Pages><Page Image=\"%d\" Type=\"FrontCover\"/></Pages>\n",
989 resolved.cover_index);
990 if (!priv_mdl_export_snprintf_fit(n, sizeof(pages))) {
992 }
993 }
994 return internal_render_comicinfo(&resolved, &escaped, page_count, web, number, pages, buf, cap);
995}
996
997ra8_err_t mdl_export_build_comicinfo(const mdl_export_meta_t* meta, char* buf, size_t cap)
998{
999 return mdl_export_build_comicinfo_pages(meta, 0U, buf, cap);
1000}
ra8_err_t fw_fs_stat(const fw_fs_namespace_t *names, const char *path, fw_fs_stat_t *out)
Query a path; a miss is success with out->exists == false.
Definition fw_if_fs.c:480
@ k_fw_fs_node_file
Regular byte stream.
@ k_fw_fs_path_cap
Largest portable path including its NUL.
Package a downloaded chapter folder into a reader-openable container.
@ k_mdl_meta_title_max
Title (series or chapter) buffer max bytes.
Definition mdl_export.h:76
@ k_mdl_meta_name_max
Person name (writer/artist) buffer max bytes.
Definition mdl_export.h:78
@ k_mdl_meta_summary_max
Summary description buffer max bytes.
Definition mdl_export.h:77
@ k_mdl_meta_lang_max
BCP-47 language tag buffer max bytes.
Definition mdl_export.h:81
@ k_mdl_meta_path_max
Bounded host cover path, including NUL.
Definition mdl_export.h:80
@ k_mdl_meta_url_max
Absolute source URL max bytes, including NUL.
Definition mdl_export.h:79
@ k_mdl_read_rtl
Right-to-left page progression.
Definition mdl_export.h:89
@ k_mdl_read_ltr
Left-to-right page progression.
Definition mdl_export.h:88
Module-private contracts shared by chapter export writers.
ra8_err_t priv_mdl_export_path_join(char *out, size_t capacity, const char *directory, const char *leaf)
Join a canonical directory and leaf without truncation.
ra8_err_t priv_mdl_export_source_slurp(mdl_storage_t *storage, const char *path, uint8_t *destination, size_t capacity, size_t *out_length)
Read one complete bounded portable source into caller storage.
static bool internal_key_is(const char *key, const char *a, const char *b, const char *c, const char *d, const char *e)
Compare one normalized metadata key with up to five aliases.
static bool internal_find_publication_key(mdl_export_meta_t *meta, const char *key, char **dst, size_t *cap)
Resolve attribution and publication text aliases.
static ra8_err_t internal_parse_xml_field(const char *text, const mdl_meta_xml_field_t *field)
Copy one optional XML field into fixed metadata storage.
ra8_err_t mdl_export_build_comicinfo(const mdl_export_meta_t *meta, char *buf, size_t cap)
Generate ComicInfo.xml content from metadata.
static ra8_err_t internal_parse_kv_line(mdl_export_meta_t *meta, char *line)
Parse and apply one metadata descriptor line.
static bool internal_parse_xml_tag(const char *xml, const char *tag, char *out, size_t cap)
Extract and unescape one bounded simple XML element.
ra8_err_t priv_mdl_export_validate_source_url(const char *url)
Validate an optional bounded source-attribution URL.
static ra8_err_t internal_apply_text_key(mdl_export_meta_t *meta, const char *key, const char *value)
Apply one text-valued metadata key.
static bool internal_xml_unescape(const char *raw, char *out, size_t cap)
Decode the five XML entities accepted by metadata input.
ra8_err_t mdl_export_build_comicinfo_pages(const mdl_export_meta_t *meta, size_t page_count, char *buf, size_t cap)
Generate ComicInfo.xml including page count and direction semantics.
static bool internal_find_descriptive_key(mdl_export_meta_t *meta, const char *key, char **dst, size_t *cap)
Resolve title and descriptive text aliases.
static bool internal_str_copy_trimmed(char *dst, size_t cap, const char *src)
Copy trimmed text without truncating fixed metadata storage.
static ra8_err_t internal_escape_comicinfo(const mdl_export_meta_t *meta, mdl_comicinfo_text_t *escaped)
Escape metadata fields with the original fallback policy.
static ra8_err_t internal_parse_xml(mdl_export_meta_t *meta, const char *text)
Parse ComicInfo-style XML into bounded metadata.
static ra8_err_t internal_parse_xml_semantics(mdl_export_meta_t *meta, const char *text)
Apply XML fields that require semantic validation.
bool priv_mdl_export_snprintf_fit(int written, size_t cap)
Test whether an snprintf result fully fit its buffer.
static ra8_err_t internal_meta_load_candidate(mdl_storage_t *storage, const char *dir, const char *candidate, bool is_fallback, mdl_export_meta_t *meta)
Load and parse one metadata candidate file, if present.
ra8_err_t mdl_meta_parse(mdl_export_meta_t *meta, const char *text)
Parse metadata key-value lines or XML text into a metadata struct.
static ra8_err_t internal_render_comicinfo(const mdl_export_meta_t *meta, const mdl_comicinfo_text_t *escaped, size_t page_count, const char *web, const char *number, const char *pages, char *buf, size_t cap)
Render already-escaped ComicInfo fields into caller storage.
ra8_err_t mdl_meta_load_dir(mdl_storage_t *storage, mdl_export_meta_t *meta, const char *dir)
Load metadata from a directory by looking for metadata files.
static ra8_err_t internal_apply_semantic_key(mdl_export_meta_t *meta, const char *key, const char *value)
Apply one semantic metadata key.
static bool internal_parse_int_strict(const char *text, int *out)
Parse one bounded decimal cover index strictly.
mdl_meta_text_bounds_t
Radices and bounded metadata text expansion sizes.
@ k_meta_fragment_slack
Fixed bytes around an XML fragment.
@ k_meta_line_slack
Key/delimiter space beyond a path.
@ k_decimal_radix
Decimal integer parsing radix.
@ k_xml_amp_entity_len
Bytes in the XML entity "&".
static bool internal_parse_double_strict(const char *text, double *out)
Parse one finite nonnegative decimal value strictly.
void mdl_meta_init(mdl_export_meta_t *meta)
Initialise a metadata struct to empty/default values.
static ra8_err_t internal_meta_candidate_path(const char *directory, const char *candidate, bool parent, char *out, size_t capacity)
Compose one metadata candidate without a traversal component.
Neutralise untrusted names before they reach a filesystem or XML sink.
bool mdl_xml_escape(const char *src, char *out, size_t cap)
XML-escape src into out, failing rather than truncating.
Pure URL / address safety predicates for the libcurl backend.
bool mdl_url_host(const char *url, char *out, size_t cap)
Extract the authority (host and optional port) from an http(s) URL.
bool mdl_url_scheme_allowed(const char *url)
True when url uses an allowlisted scheme (http:// or https://).
Annotation-attribute framework macros for ra8-firmware.
#define RA8_PRIV
Module-private helper: shared across TUs but only inside one library.
#define RA8_INTERNAL
Marker that a function is intended to be static (file-local).
@ k_ra8_err_invalid_arg
Invalid function argument.
Definition ra8_err.h:152
@ k_ra8_ok
Success – operation completed with all postconditions satisfied.
Definition ra8_err.h:119
@ k_ra8_err_not_found
Requested item not found (lookup / search missed).
Definition ra8_err.h:173
@ k_ra8_err_invalid_size
Invalid size parameter (too large, too small, or misaligned).
Definition ra8_err.h:167
ra8_err_codes_t ra8_err_t
Canonical error-return type used by every ra8-firmware API.
Definition ra8_err.h:546
int strncmp(const char *s1, const char *s2, size_t n)
Compare two strings up to a specified length.
int strcmp(const char *s1, const char *s2)
Compare two null-terminated strings.
void * memset(void *dst, int value, size_t n)
Fill memory with a constant byte value.
size_t strlen(const char *s)
Calculate string length.
void * memcpy(void *dst, const void *src, size_t n)
Copy memory area between non-overlapping regions.
char * strstr(const char *haystack, const char *needle)
Locate substring in string.
char * strchr(const char *s, int c)
Locate first occurrence of character in string.
size_t strnlen(const char *s, size_t maxlen)
Calculate bounded string length.
Result of a portable metadata query.
uint64_t size_bytes
File length; zero for a directory.
bool exists
False means a clean lookup miss.
fw_fs_node_type_t type
Kind of node at the path.
fw_fs_namespace_t names
Namespace operations.
Escaped text storage used while rendering ComicInfo.
char artist[k_mdl_meta_name_max *6U]
Escaped artist.
char source[k_mdl_meta_url_max *6U]
Escaped source URL.
char summary[k_mdl_meta_summary_max *6U]
Escaped summary.
char language[k_mdl_meta_lang_max *6U]
Escaped language.
char writer[k_mdl_meta_name_max *6U]
Escaped writer.
char series[k_mdl_meta_title_max *6U]
Escaped series title.
char title[k_mdl_meta_title_max *6U]
Escaped chapter title.
Rich series and chapter metadata for export containers (ComicInfo.xml, EPUB OPF).
Definition mdl_export.h:96
int cover_index
Cover page index (0-based, -1 if unset).
Definition mdl_export.h:105
char chapter_title[k_mdl_meta_title_max]
Chapter title.
Definition mdl_export.h:101
mdl_reading_direction_t reading_direction
Fixed-layout page progression.
Definition mdl_export.h:107
double chapter_number
Chapter number (0.0 if unnumbered).
Definition mdl_export.h:102
char identifier[k_mdl_meta_id_max]
Stable identifier; derived when empty.
Definition mdl_export.h:108
char language[k_mdl_meta_lang_max]
BCP-47 language tag (default "en").
Definition mdl_export.h:106
char cover_path[k_mdl_meta_path_max]
Trusted verified local cover path/name.
Definition mdl_export.h:104
char writer[k_mdl_meta_name_max]
Writer / Author.
Definition mdl_export.h:99
char artist[k_mdl_meta_name_max]
Artist / Illustrator.
Definition mdl_export.h:100
char modified[k_mdl_meta_date_max]
Deterministic ISO-8601 modified time.
Definition mdl_export.h:109
char source_url[k_mdl_meta_url_max]
Validated absolute HTTP(S) source URL.
Definition mdl_export.h:103
char summary[k_mdl_meta_summary_max]
Summary / description.
Definition mdl_export.h:98
char series_title[k_mdl_meta_title_max]
Series title.
Definition mdl_export.h:97
Destination descriptor for a simple metadata XML element.
char * dst
Fixed metadata destination.
size_t cap
Destination capacity.
const char * tag
XML element name.
One non-reentrant downloader filesystem dependency bundle.
Definition mdl_storage.h:40
const fw_fs_t * fs
Injected portable filesystem.
Definition mdl_storage.h:41