ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
mdl_export_epub.c
Go to the documentation of this file.
1
14
15#include <limits.h>
16#include <stdio.h>
17#include <string.h>
18
19#include "mdl_export.h"
21#include "mdl_export_internal.h"
22#include "mdl_sanitize.h"
23#include "mdl_urlname.h"
24#include "miniz.h"
25#include "ra8_attributes.h"
26
27/* --- EPUB (self-contained: a valid EPUB3 of the page images via miniz) ---- */
28
30static const char* const s_epub_container_xml =
31 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
32 "<container version=\"1.0\" "
33 "xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\"><rootfiles>"
34 "<rootfile full-path=\"OEBPS/content.opf\" "
35 "media-type=\"application/oebps-package+xml\"/></rootfiles></container>";
36
60 const char* dir,
61 const char* name,
62 const char** out)
63{
64 char src[k_fw_fs_path_cap];
65 if (priv_mdl_export_path_join(src, sizeof(src), dir, name) != k_ra8_ok) {
67 }
68 char mime[64];
69 const ra8_err_t sniff =
70 mdl_urlname_sniff_file(storage, src, nullptr, nullptr, 0U, mime, sizeof(mime));
71 if (sniff != k_ra8_ok) {
72 return (sniff == k_ra8_err_validation_failed) ? k_ra8_err_not_found : sniff;
73 }
74 if (strcmp(mime, "image/png") == 0) {
75 *out = "image/png";
76 return k_ra8_ok;
77 }
78 if (strcmp(mime, "image/gif") == 0) {
79 *out = "image/gif";
80 return k_ra8_ok;
81 }
82 if (strcmp(mime, "image/webp") == 0) {
83 *out = "image/webp";
84 return k_ra8_ok;
85 }
86 if (strcmp(mime, "image/jpeg") == 0) {
87 *out = "image/jpeg";
88 return k_ra8_ok;
89 }
91}
92
107RA8_INTERNAL static void internal_epub_media_type_from_suffix(const char* name, const char** out)
108{
109 const char* dot = strrchr(name, '.');
110 if (dot != nullptr) {
111 if ((strcmp(dot, ".png") == 0) || (strcmp(dot, ".PNG") == 0)) {
112 *out = "image/png";
113 return;
114 }
115 if ((strcmp(dot, ".gif") == 0) || (strcmp(dot, ".GIF") == 0)) {
116 *out = "image/gif";
117 return;
118 }
119 if ((strcmp(dot, ".webp") == 0) || (strcmp(dot, ".WEBP") == 0)) {
120 *out = "image/webp";
121 return;
122 }
123 if ((strcmp(dot, ".bmp") == 0) || (strcmp(dot, ".BMP") == 0)) {
124 *out = "image/bmp";
125 return;
126 }
127 }
128 *out = "image/jpeg";
129}
130
151 const char* dir,
152 const char* name,
153 const char** out)
154{
155 if (dir != nullptr) {
156 const ra8_err_t sniffed = internal_epub_media_type_from_sniff(storage, dir, name, out);
157 if (sniffed == k_ra8_ok) {
158 return k_ra8_ok;
159 }
160 if (sniffed != k_ra8_err_not_found) {
161 return sniffed;
162 }
163 }
165 return k_ra8_ok;
166}
167
168RA8_PRIV bool priv_mdl_epub_str_cat(char* dst, size_t cap, const char* text)
169{
170 const size_t cur = strlen(dst);
171 const size_t add = strlen(text);
172 if (cur + add + 1U > cap) {
173 return false;
174 }
175 memcpy(dst + cur, text, add + 1U);
176 return true;
177}
178
179RA8_PRIV bool priv_mdl_epub_add_str(mz_zip_archive* zip, const char* name, const char* body)
180{
182 name,
183 (const uint8_t*)body,
184 strlen(body),
185 MZ_NO_COMPRESSION) == k_ra8_ok;
186}
187
208 mdl_storage_t* storage,
209 const mdl_external_cover_t* cover,
210 char* mani,
211 size_t cap)
212{
213 if (!cover->external) {
214 return k_ra8_ok;
215 }
216 char archive_entry[k_epub_entry_max];
217 const int entry_len = snprintf(archive_entry, sizeof(archive_entry), "OEBPS/%s", cover->entry);
218 if (!priv_mdl_export_snprintf_fit(entry_len, sizeof(archive_entry)) ||
220 storage,
221 archive_entry,
222 cover->source,
223 MZ_NO_COMPRESSION) != k_ra8_ok)) {
224 return k_ra8_fail;
225 }
226 char frag[k_epub_frag_max];
227 const int frag_len = snprintf(frag,
228 sizeof(frag),
229 "<item id=\"cover-image\" href=\"%s\" media-type=\"%s\" "
230 "properties=\"cover-image\"/>",
231 cover->entry,
232 cover->mime);
233 return priv_mdl_export_snprintf_fit(frag_len, sizeof(frag)) &&
234 priv_mdl_epub_str_cat(mani, cap, frag)
235 ? k_ra8_ok
236 : k_ra8_fail;
237}
238
263 char* spine,
264 char* nav,
265 size_t cap,
266 const char* esc_name,
267 const char* media,
268 size_t idx,
269 unsigned n,
270 bool is_cover)
271{
272 char frag[k_epub_frag_max];
273 const char* prop_attr = is_cover ? " properties=\"cover-image\"" : "";
274 const int fn = snprintf(frag,
275 sizeof(frag),
276 "<item id=\"pg%zu\" href=\"page_%03u.xhtml\" "
277 "media-type=\"application/xhtml+xml\"/>"
278 "<item id=\"img%zu\" href=\"images/%s\" media-type=\"%s\"%s/>",
279 idx,
280 n,
281 idx,
282 esc_name,
283 media,
284 prop_attr);
285 if (!priv_mdl_export_snprintf_fit(fn, sizeof(frag))) {
286 return k_ra8_fail;
287 }
288 if (!priv_mdl_epub_str_cat(mani, cap, frag)) {
289 return k_ra8_fail;
290 }
291 (void)snprintf(frag, sizeof(frag), "<itemref idref=\"pg%zu\"/>", idx);
292 if (!priv_mdl_epub_str_cat(spine, cap, frag)) {
293 return k_ra8_fail;
294 }
295 (void)snprintf(frag, sizeof(frag), "<li><a href=\"page_%03u.xhtml\">Page %u</a></li>", n, n);
296 if (!priv_mdl_epub_str_cat(nav, cap, frag)) {
297 return k_ra8_fail;
298 }
299 return k_ra8_ok;
300}
301
320RA8_INTERNAL static bool
321internal_epub_page_is_cover(const mdl_export_meta_t* meta, const char* name, size_t idx)
322{
323 if (meta == nullptr) {
324 return false;
325 }
326 if ((meta->cover_index >= 0) && ((size_t)meta->cover_index == idx)) {
327 return true;
328 }
329 return (meta->cover_path[0] != '\0') && (strcmp(name, meta->cover_path) == 0);
330}
331
352 const char* name,
353 unsigned n,
354 char* esc,
355 size_t esc_cap)
356{
357 if (!mdl_xml_escape(name, esc, esc_cap)) {
358 return k_ra8_fail; /* untrusted filename must not break the container XML */
359 }
360 char xhtml[k_epub_xhtml_max];
361 const int xn = snprintf(xhtml,
362 sizeof(xhtml),
363 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
364 "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>Page %u"
365 "</title></head><body><img src=\"images/%s\" alt=\"Page %u\"/>"
366 "</body></html>",
367 n,
368 esc,
369 n);
370 if (!priv_mdl_export_snprintf_fit(xn, sizeof(xhtml))) {
371 return k_ra8_fail;
372 }
373 char entry[k_epub_entry_max];
374 (void)snprintf(entry, sizeof(entry), "OEBPS/page_%03u.xhtml", n);
375 if (!priv_mdl_epub_add_str(zip, entry, xhtml)) {
376 return k_ra8_fail;
377 }
378 return k_ra8_ok;
379}
380
406 mz_zip_archive* zip,
407 const char* dir,
408 const char* name,
409 size_t idx,
410 char* mani,
411 char* spine,
412 char* nav,
413 size_t cap,
414 const mdl_export_meta_t* meta)
415{
416 const unsigned n = (unsigned)(idx + 1U);
417 char esc[k_epub_name_esc_max];
418 const ra8_err_t xhtml_err = internal_epub_write_page_xhtml(zip, name, n, esc, sizeof(esc));
419 if (xhtml_err != k_ra8_ok) {
420 return xhtml_err;
421 }
422 char src[k_fw_fs_path_cap];
423 if (priv_mdl_export_path_join(src, sizeof(src), dir, name) != k_ra8_ok) {
425 }
426 char entry[k_epub_entry_max];
427 const int en = snprintf(entry, sizeof(entry), "OEBPS/images/%s", name);
428 if (!priv_mdl_export_snprintf_fit(en, sizeof(entry))) {
429 return k_ra8_fail;
430 }
431 const ra8_err_t source_err =
432 priv_mdl_export_zip_add_file(zip, storage, entry, src, MZ_NO_COMPRESSION);
433 if (source_err != k_ra8_ok) {
434 return source_err;
435 }
436 const bool is_cover = internal_epub_page_is_cover(meta, name, idx);
437 const char* media_type = nullptr;
438 const ra8_err_t media_err = internal_epub_media_type(storage, dir, name, &media_type);
439 if (media_err != k_ra8_ok) {
440 return media_err;
441 }
442 return internal_epub_append_frags(mani, spine, nav, cap, esc, media_type, idx, n, is_cover);
443}
444
464 const mdl_export_meta_t* meta,
465 mdl_export_meta_t* page_meta)
466{
467 if (!cover->external) {
468 return meta;
469 }
470 *page_meta = *meta;
471 page_meta->cover_index = -1;
472 page_meta->cover_path[0] = '\0';
473 return page_meta;
474}
475
499 size_t cap,
500 char** mani,
501 char** spine,
502 char** nav,
503 char** opf,
504 char** navdoc)
505{
506 *mani = (char*)mdl_export_workspace_take(ws, cap, 1U);
507 *spine = (char*)mdl_export_workspace_take(ws, cap, 1U);
508 *nav = (char*)mdl_export_workspace_take(ws, cap, 1U);
509 *opf = (char*)mdl_export_workspace_take(ws, cap, 1U);
510 *navdoc = (char*)mdl_export_workspace_take(ws, cap, 1U);
511 if ((*mani == nullptr) || (*spine == nullptr) || (*nav == nullptr) || (*opf == nullptr) ||
512 (*navdoc == nullptr)) {
514 }
515 (*mani)[0] = '\0';
516 (*spine)[0] = '\0';
517 (*nav)[0] = '\0';
518 return k_ra8_ok;
519}
520
543 bool zip_open,
544 mdl_zip_allocator_t* zip_alloc,
545 ra8_err_t rc,
546 const mdl_export_output_t* output)
547{
548 if (zip_open) {
549 (void)mz_zip_writer_end(zip); /* alloc-allow: releases caller-arena blocks */
550 }
551 if ((rc == k_ra8_fail) && zip_alloc->exhausted) {
553 }
554 if ((rc == k_ra8_fail) && (output->error != k_ra8_ok)) {
555 rc = output->error;
556 }
558 return rc;
559}
560
585 const char* dir,
586 char names[][k_name_max],
587 size_t count,
588 mdl_export_output_t* output,
589 const mdl_export_meta_t* meta,
591{
593 ra8_err_t rc = priv_mdl_export_prepare_cover(storage, meta, names, count, &cover);
594 if (rc != k_ra8_ok) {
595 return rc;
596 }
597 const size_t cap = (size_t)k_epub_base_bytes + (count * (size_t)k_epub_per_page_bytes);
598 char* mani;
599 char* spine;
600 char* nav;
601 char* opf;
602 char* navdoc;
603 rc = internal_epub_carve_workspace(ws, cap, &mani, &spine, &nav, &opf, &navdoc);
604 if (rc != k_ra8_ok) {
605 return rc;
606 }
607 mz_zip_archive zip;
608 mdl_zip_allocator_t zip_alloc;
609 priv_mdl_zip_workspace_bind(&zip, &zip_alloc, ws);
610 zip.m_pWrite = priv_mdl_export_zip_write;
611 zip.m_pIO_opaque = output;
612 const bool zip_open = (mz_zip_writer_init(&zip, 0) != MZ_FALSE);
613 rc = zip_open ? k_ra8_ok : priv_mdl_zip_workspace_error(&zip_alloc);
614 if ((rc == k_ra8_ok) &&
615 (!priv_mdl_epub_add_str(&zip, "mimetype", "application/epub+zip") ||
616 !priv_mdl_epub_add_str(&zip, "META-INF/container.xml", s_epub_container_xml))) {
617 rc = k_ra8_fail;
618 }
619 if (rc == k_ra8_ok) {
620 rc = internal_epub_add_external_cover(&zip, storage, &cover, mani, cap);
621 }
622 mdl_export_meta_t page_meta;
623 const mdl_export_meta_t* page_meta_ptr = internal_epub_page_meta(&cover, meta, &page_meta);
624 for (size_t i = 0U; (rc == k_ra8_ok) && (i < count); ++i) {
625 rc =
626 internal_epub_add_page(storage, &zip, dir, names[i], i, mani, spine, nav, cap, page_meta_ptr);
627 }
628 if (rc == k_ra8_ok) {
629 rc = priv_mdl_epub_add_meta(&zip, mani, spine, nav, count, meta, opf, cap, navdoc, cap);
630 }
631 /* The coordinator aborts this stage unless writer completion and verification pass. */
632 return internal_epub_finish_writer(&zip, zip_open, &zip_alloc, rc, output);
633}
@ k_fw_fs_path_cap
Largest portable path including its NUL.
Package a downloaded chapter folder into a reader-openable container.
void * mdl_export_workspace_take(mdl_export_workspace_t *ws, size_t bytes, size_t alignment)
Reserve aligned bytes from an exporter arena.
struct mdl_export_workspace mdl_export_workspace_t
Caller-owned bounded arena for all exporter scratch state.
static ra8_err_t internal_epub_add_external_cover(mz_zip_archive *zip, mdl_storage_t *storage, const mdl_external_cover_t *cover, char *mani, size_t cap)
Store and declare a validated external EPUB cover.
static ra8_err_t internal_epub_finish_writer(mz_zip_archive *zip, bool zip_open, mdl_zip_allocator_t *zip_alloc, ra8_err_t rc, const mdl_export_output_t *output)
End the ZIP writer and translate exhaustion/output faults to status.
bool priv_mdl_epub_add_str(mz_zip_archive *zip, const char *name, const char *body)
Add an in-memory string as a stored ZIP entry.
ra8_err_t priv_mdl_export_epub(mdl_storage_t *storage, const char *dir, char names[][k_name_max], size_t count, mdl_export_output_t *output, const mdl_export_meta_t *meta, mdl_export_workspace_t *ws)
Package chapter pages into a valid fixed-layout EPUB3.
static ra8_err_t internal_epub_add_page(mdl_storage_t *storage, mz_zip_archive *zip, const char *dir, const char *name, size_t idx, char *mani, char *spine, char *nav, size_t cap, const mdl_export_meta_t *meta)
Add one fixed-layout EPUB page and its image.
static ra8_err_t internal_epub_append_frags(char *mani, char *spine, char *nav, size_t cap, const char *esc_name, const char *media, size_t idx, unsigned n, bool is_cover)
Append one page's manifest, spine, and navigation fragments.
static ra8_err_t internal_epub_media_type_from_sniff(mdl_storage_t *storage, const char *dir, const char *name, const char **out)
Classify one page's image media type from sniffed portable bytes.
static ra8_err_t internal_epub_carve_workspace(mdl_export_workspace_t *ws, size_t cap, char **mani, char **spine, char **nav, char **opf, char **navdoc)
Carve the five bounded XML/ZIP accumulators from workspace arena.
static ra8_err_t internal_epub_media_type(mdl_storage_t *storage, const char *dir, const char *name, const char **out)
Resolve one page's image media type from portable bytes then suffix.
bool priv_mdl_epub_str_cat(char *dst, size_t cap, const char *text)
Append text to NUL-terminated dst; report whether it fully fit.
static bool internal_epub_page_is_cover(const mdl_export_meta_t *meta, const char *name, size_t idx)
Is one exported image the publication's declared cover?
static ra8_err_t internal_epub_write_page_xhtml(mz_zip_archive *zip, const char *name, unsigned n, char *esc, size_t esc_cap)
Escape a page name and write its XHTML wrapper into the ZIP.
static const mdl_export_meta_t * internal_epub_page_meta(const mdl_external_cover_t *cover, const mdl_export_meta_t *meta, mdl_export_meta_t *page_meta)
Select page metadata after accounting for an external cover.
static void internal_epub_media_type_from_suffix(const char *name, const char **out)
Classify one page's image media type from its filename suffix.
static const char *const s_epub_container_xml
OCF container pointing at the OPF package (fixed).
Private bounded EPUB3 writer contracts for the media downloader.
ra8_err_t priv_mdl_epub_add_meta(mz_zip_archive *zip, const char *mani, const char *spine, const char *nav, size_t page_count, const mdl_export_meta_t *meta, char *opf, size_t opf_buf_cap, char *navdoc, size_t nav_buf_cap)
Build EPUB package metadata and finalize the archive.
@ k_epub_xhtml_max
One page's xhtml document (embeds the name).
@ k_epub_name_esc_max
XML-escaped page name (k_name_max * 6).
@ k_epub_base_bytes
Fixed opf/nav overhead.
@ k_epub_entry_max
A zip entry path ("OEBPS/images/" + name).
@ k_epub_per_page_bytes
Per-page opf/nav accumulator growth.
@ k_epub_frag_max
One manifest fragment (fixed + escaped name).
Module-private contracts shared by chapter export writers.
ra8_err_t priv_mdl_zip_workspace_error(const mdl_zip_allocator_t *alloc)
Classify a miniz writer failure.
void priv_mdl_zip_workspace_bind(mz_zip_archive *zip, mdl_zip_allocator_t *alloc, mdl_export_workspace_t *ws)
Bind a miniz archive to caller-owned bounded storage.
@ k_name_max
Maximum entry-name bytes.
void priv_mdl_zip_workspace_release(mdl_zip_allocator_t *alloc)
Restore a workspace after a miniz writer ends.
bool priv_mdl_export_snprintf_fit(int written, size_t cap)
Report whether an snprintf result fit its destination.
ra8_err_t priv_mdl_export_prepare_cover(mdl_storage_t *storage, const mdl_export_meta_t *meta, const char names[][k_name_max], size_t count, mdl_external_cover_t *cover)
Validate and canonicalize an external cover.
ra8_err_t priv_mdl_export_zip_add_file(mz_zip_archive *zip, mdl_storage_t *storage, const char *member, const char *path, mz_uint flags)
Add one portable source file through miniz callbacks.
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.
size_t priv_mdl_export_zip_write(void *opaque, mz_uint64 file_offset, const void *bytes, size_t length)
Adapt random-offset miniz output to the active export stage.
ra8_err_t priv_mdl_export_zip_add_memory(mz_zip_archive *zip, const char *member, const uint8_t *bytes, size_t length, mz_uint flags)
Add caller-owned memory through the deterministic ZIP read seam.
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.
Bounded URL naming and portable image-classification helpers.
ra8_err_t mdl_urlname_sniff_file(mdl_storage_t *storage, const char *file_path, const char *content_type, char *out_ext, size_t ext_cap, char *out_mime, size_t mime_cap)
Sniff true image extension and MIME type from portable storage.
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_fail
Generic unspecified failure.
Definition ra8_err.h:133
@ k_ra8_err_validation_failed
Validation rule failed (caller-supplied invariant not satisfied).
Definition ra8_err.h:459
@ 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 strcmp(const char *s1, const char *s2)
Compare two null-terminated strings.
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 * strrchr(const char *s, int c)
Locate last occurrence of character in string.
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 cover_path[k_mdl_meta_path_max]
Trusted verified local cover path/name.
Definition mdl_export.h:104
One validated staged publication, including random ZIP backfill.
ra8_err_t error
First callback error, if any.
Validated external cover details retained without owning storage.
const char * source
Trusted caller-owned source path.
char entry[k_cover_entry_bytes]
Canonical relative member path.
char mime[k_cover_mime_bytes]
MIME derived from magic bytes.
bool external
True when a separate cover exists.
One non-reentrant downloader filesystem dependency bundle.
Definition mdl_storage.h:40
Per-writer adapter from miniz callbacks to one caller workspace.
bool exhausted
A callback exceeded bounded space.