29typedef enum : uint16_t {
67 for (
size_t i = 0U; i < len; ++i) {
68 if (isspace((
unsigned char)url[i]) || iscntrl((
unsigned char)url[i])) {
92 return (written >= 0) && ((size_t)written < cap);
97 if (meta ==
nullptr) {
100 memset(meta, 0,
sizeof(*meta));
126 while ((*src !=
'\0') && isspace((
unsigned char)*src)) {
130 while ((len > 0U) && isspace((
unsigned char)src[len - 1U])) {
133 if ((cap == 0U) || (len >= cap)) {
161 const double value = strtod(text, &end);
162 while ((end !=
nullptr) && isspace((
unsigned char)*end)) {
165 if ((end == text) || (end ==
nullptr) || (*end !=
'\0') || (errno == ERANGE) ||
166 !isfinite(value) || (value < 0.0)) {
194 while ((end !=
nullptr) && isspace((
unsigned char)*end)) {
197 if ((end == text) || (end ==
nullptr) || (*end !=
'\0') || (errno == ERANGE) || (value < -1L) ||
225 while ((raw[r] !=
'\0') && (w + 1U < cap)) {
226 const char* entity =
nullptr;
228 size_t consumed = 0U;
233 }
else if (
strncmp(raw + r,
"<", 4U) == 0) {
236 }
else if (
strncmp(raw + r,
">", 4U) == 0) {
239 }
else if (
strncmp(raw + r,
""", 6U) == 0) {
242 }
else if (
strncmp(raw + r,
"'", 6U) == 0) {
247 consumed = (entity ==
nullptr) ? 1U :
strlen(entity);
248 out[w++] = (char)((entity ==
nullptr) ? raw[r] : decoded);
252 return raw[r] ==
'\0';
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;
286 start +=
strlen(open_tag);
288 const char* end = (start ==
nullptr) ?
nullptr :
strstr(start, close_tag);
289 if (end ==
nullptr) {
292 const size_t len = (size_t)(end - start);
294 if (len >=
sizeof(raw)) {
360 if ((value[0] !=
'\0') &&
364 if (value[0] !=
'\0') {
382 if (
strcmp(value,
"YesAndRightToLeft") == 0) {
384 }
else if (
strcmp(value,
"No") == 0) {
416 for (
size_t i = 0U; i < (
sizeof(fields) /
sizeof(fields[0])); ++i) {
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));
476 if (
internal_key_is(key,
"series",
"series_title",
"seriestitle",
"series title",
nullptr)) {
479 }
else if (
internal_key_is(key,
"summary",
"description",
"abstract",
nullptr,
nullptr)) {
516 if (
internal_key_is(key,
"writer",
"author",
"creator",
nullptr,
nullptr)) {
518 *cap =
sizeof(meta->
writer);
519 }
else if (
internal_key_is(key,
"artist",
"penciller",
"illustrator",
nullptr,
nullptr)) {
521 *cap =
sizeof(meta->
artist);
530 }
else if (
internal_key_is(key,
"language",
"language_iso",
"lang",
nullptr,
nullptr)) {
533 }
else if (
internal_key_is(key,
"identifier",
"uuid",
nullptr,
nullptr,
nullptr)) {
536 }
else if (
internal_key_is(key,
"modified",
"date_modified",
nullptr,
nullptr,
nullptr)) {
596 if (
internal_key_is(key,
"source_url",
"source",
"web",
nullptr,
nullptr)) {
606 if (
internal_key_is(key,
"number",
"chapter_number",
"chapternumber",
"chapter number",
"num")) {
613 if (
internal_key_is(key,
"cover_index",
"coverindex",
"cover index",
"cover_idx",
nullptr)) {
628 if (
strcmp(parsed,
"rtl") == 0) {
630 }
else if (
strcmp(parsed,
"ltr") == 0) {
658 while ((*start !=
'\0') && isspace((
unsigned char)*start)) {
661 if ((*start ==
'\0') || (*start ==
'#') || (*start ==
';')) {
664 char* separator =
strchr(start,
'=');
665 separator = (separator ==
nullptr) ?
strchr(start,
':') : separator;
666 if (separator ==
nullptr) {
674 for (
size_t i = 0U; key[i] !=
'\0'; ++i) {
675 key[i] = (char)tolower((
unsigned char)key[i]);
686 if ((meta ==
nullptr) || (text ==
nullptr)) {
689 if ((
strstr(text,
"<ComicInfo") !=
nullptr) || (
strstr(text,
"<Title>") !=
nullptr) ||
690 (
strstr(text,
"<Series>") !=
nullptr) || (
strstr(text,
"<Web>") !=
nullptr)) {
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)) {
701 memcpy(line, cursor, length);
708 if (*cursor ==
'\n') {
735 const char* candidate,
744 size_t length =
strnlen(directory,
sizeof(parent_path));
745 if ((length == 0U) || (length >=
sizeof(parent_path))) {
748 memcpy(parent_path, directory, length + 1U);
749 while ((length > 1U) && (parent_path[length - 1U] !=
'/')) {
753 parent_path[length - 1U] =
'\0';
755 parent_path[1] =
'\0';
780 const char* candidate,
818 if ((storage ==
nullptr) || (storage->
fs ==
nullptr) || (meta ==
nullptr) || (dir ==
nullptr)) {
823 static const char*
const candidate_files[] = {
"metadata.txt",
829 for (
size_t i = 0U; i < (
sizeof(candidate_files) /
sizeof(candidate_files[0])); ++i) {
872 (void)snprintf(escaped->
title,
sizeof(escaped->
title),
"Chapter");
875 (void)snprintf(escaped->
series,
sizeof(escaped->
series),
"Series");
881 escaped->
writer[0] =
'\0';
884 escaped->
artist[0] =
'\0';
923 const int written = snprintf(buf,
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>",
952 if ((buf ==
nullptr) || (cap == 0U)) {
956 if (meta !=
nullptr) {
970 if (escaped.
source[0] !=
'\0') {
971 const int n = snprintf(web,
sizeof(web),
" <Web>%s</Web>\n", escaped.
source);
978 (void)snprintf(number,
sizeof(number),
"1");
980 (void)snprintf(number,
sizeof(number),
"%ld", (
long)resolved.
chapter_number);
982 (void)snprintf(number,
sizeof(number),
"%.1f", resolved.
chapter_number);
984 char pages[160] = {};
986 const int n = snprintf(pages,
988 " <Pages><Page Image=\"%d\" Type=\"FrontCover\"/></Pages>\n",
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.
@ 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.
@ k_mdl_meta_name_max
Person name (writer/artist) buffer max bytes.
@ k_mdl_meta_summary_max
Summary description buffer max bytes.
@ k_mdl_meta_lang_max
BCP-47 language tag buffer max bytes.
@ k_mdl_meta_path_max
Bounded host cover path, including NUL.
@ k_mdl_meta_url_max
Absolute source URL max bytes, including NUL.
@ k_mdl_read_rtl
Right-to-left page progression.
@ k_mdl_read_ltr
Left-to-right page progression.
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.
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.
@ k_ra8_ok
Success – operation completed with all postconditions satisfied.
@ k_ra8_err_not_found
Requested item not found (lookup / search missed).
@ k_ra8_err_invalid_size
Invalid size parameter (too large, too small, or misaligned).
ra8_err_codes_t ra8_err_t
Canonical error-return type used by every ra8-firmware API.
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.
One non-reentrant downloader filesystem dependency bundle.
const fw_fs_t * fs
Injected portable filesystem.