4"""Generate epub_stress_fixture.h, a synthetic large-STRUCTURE EPUB3.
6Reproduces the bounded ZIP and XML structure pressure of a big real-world book
7(#144 bug 1) without shipping a copyrighted 7 MB novel.
9The insight the fixture is built on: miniz arena pressure is proportional to
10archive FILE COUNT, while XML consumer bounds are proportional to the number
11of manifest/spine/nav items. Neither comes from total byte size. So this packs many
12tiny files instead of a few large ones -- 60 chapters (the spine, deliberately
13under the k_epub_max_chapters=64 cap), 60 extra manifest-only resources, an
14NCX with 60 navPoints, and a cover. That is ~120 archive entries and a ~20 KB
15OPF, comparable to the 108-file, 41-chapter real book, in tens of KB total. It
16therefore bakes into MRAM and opens in memory like epub_parse.
18Raise the byte size and the fixture gets no more stressful; raise the entry
21Output is pure 7-bit ASCII, 16 bytes per row inside a clang-format-off guard.
24 python3 examples/ek_ra8d2/hw_pending/epub_stress/scripts/make_stress_fixture.py
27from __future__
import annotations
31from pathlib
import Path
37 '<?xml version="1.0"?>\n'
38 '<container version="1.0" '
39 'xmlns="urn:oasis:names:tc:opendocument:xmlns:container">\n'
41 ' <rootfile full-path="OEBPS/content.opf" '
42 'media-type="application/oebps-package+xml"/>\n'
48def build_opf() -> str:
49 """Build the OPF package document used to stress XML consumer bounds.
51 Emits every chapter as both a manifest item and a spine itemref, plus the
52 manifest-only CSS resources that inflate the file count without adding
53 spine entries. The resulting element count drives the bounded event and
54 semantic-consumer paths.
56 Declares EPUB3 `properties="cover-image"` and the legacy
57 `<meta name="cover">` both, so the fixture exercises either cover-resolution
58 path a reader might take.
61 The complete OPF as ASCII-safe text.
64 ' <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>',
65 ' <item id="cover" href="cover.png" media-type="image/png" properties="cover-image"/>',
68 for i
in range(N_CHAPTERS):
70 f
' <item id="ch{i}" href="ch{i}.xhtml" media-type="application/xhtml+xml"/>'
72 spine.append(f
' <itemref idref="ch{i}"/>')
74 f
' <item id="res{i}" href="res{i}.css" media-type="text/css"/>'
75 for i
in range(N_RESOURCES)
78 '<?xml version="1.0" encoding="UTF-8"?>\n'
79 '<package xmlns="http://www.idpf.org/2007/opf" version="3.0" '
80 'unique-identifier="id">\n'
81 ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">\n'
82 " <dc:title>Stress Structure Book</dc:title>\n"
83 " <dc:creator>Brighton Sikarskie</dc:creator>\n"
84 " <dc:language>en</dc:language>\n"
85 ' <dc:identifier id="id">urn:test:stress</dc:identifier>\n'
86 ' <meta name="cover" content="cover"/>\n'
88 " <manifest>\n" +
"\n".join(items) +
"\n </manifest>\n"
89 ' <spine toc="ncx">\n' +
"\n".join(spine) +
"\n </spine>\n"
94def build_ncx() -> str:
95 """Build the EPUB2 NCX table of contents, one navPoint per chapter.
97 An EPUB3 fixture does not need an NCX, and that is exactly why it is here:
98 real books ship one for backward compatibility, and it adds another
99 60-element XML document the parser must handle. `playOrder` is 1-based
100 while the chapter hrefs are 0-based, matching how real books number them.
103 The complete NCX as ASCII-safe text.
106 f
' <navPoint id="np{i}" playOrder="{i + 1}">\n'
107 f
" <navLabel><text>Chapter {i + 1}</text></navLabel>\n"
108 f
' <content src="ch{i}.xhtml"/>\n'
110 for i
in range(N_CHAPTERS)
113 '<?xml version="1.0" encoding="UTF-8"?>\n'
114 '<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">\n'
115 ' <head><meta name="dtb:uid" content="urn:test:stress"/></head>\n'
116 " <docTitle><text>Stress Structure Book</text></docTitle>\n"
117 " <navMap>\n" +
"\n".join(points) +
"\n </navMap>\n"
128 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
129 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
130 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
131 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00,
132 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, 0xB0, 0x00, 0x00,
133 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
139def build_epub() -> bytes:
140 """Assemble the whole EPUB archive in memory and return its bytes.
142 Byte-for-byte reproducible: every entry is written with a fixed 2026-01-01
143 timestamp and fixed permissions, so re-running the generator on a clean tree
144 produces an identical header and an empty diff. Without the pinned
145 timestamps the baked fixture would churn on every regeneration.
147 Two entries are ZIP_STORED rather than deflated. `mimetype` must be stored
148 first and uncompressed per the EPUB spec, and the 1x1 cover PNG is stored
149 because deflating an already-compressed PNG would only grow it.
152 The complete .epub archive as bytes.
155 fixed = (2026, 1, 1, 0, 0, 0)
157 def add(zf: zipfile.ZipFile, name: str, data: bytes | str, store: bool =
False) ->
None:
158 info = zipfile.ZipInfo(name, date_time=fixed)
159 info.compress_type = zipfile.ZIP_STORED
if store
else zipfile.ZIP_DEFLATED
160 info.external_attr = 0o600 << 16
161 zf.writestr(info, data)
163 with zipfile.ZipFile(out,
"w")
as zf:
164 add(zf,
"mimetype", b
"application/epub+zip", store=
True)
165 add(zf,
"META-INF/container.xml", CONTAINER_XML.encode(
"ascii"))
166 add(zf,
"OEBPS/content.opf", build_opf().encode(
"ascii"))
167 add(zf,
"OEBPS/toc.ncx", build_ncx().encode(
"ascii"))
168 add(zf,
"OEBPS/cover.png", COVER_PNG, store=
True)
169 for i
in range(N_CHAPTERS):
171 f
'<?xml version="1.0"?><html><body><h1>Chapter {i + 1}</h1>'
172 f
"<p>Body text for chapter {i + 1}.</p></body></html>"
174 add(zf, f
"OEBPS/ch{i}.xhtml", body.encode(
"ascii"))
175 for i
in range(N_RESOURCES):
176 add(zf, f
"OEBPS/res{i}.css", f
".c{i} {{ margin: 0; }}\n".encode(
"ascii"))
177 return out.getvalue()
180def bake_header(epub: bytes) -> str:
181 """Render the EPUB bytes as a C header with a `static const uint8_t` table.
183 The array is sized by a generated `enum : size_t` rather than a `[]`, so the
184 firmware gets the length as a compile-time constant and the two can never
187 The byte table is wrapped in `clang-format off`/`on`. That is load-bearing,
188 not cosmetic: the formatter would otherwise reflow tens of thousands of
189 bytes into its own line width and the generator would no longer own the
190 file's shape, making every regeneration a large spurious diff.
193 epub: The archive bytes to bake, emitted 16 per row as `0xNN`.
196 The complete header source, pure 7-bit ASCII.
199 for i
in range(0, len(epub), 16):
200 chunk = epub[i : i + 16]
201 rows.append(
" " +
", ".join(f
"0x{b:02X}" for b
in chunk) +
",")
202 body =
"\n".join(rows)
205 " * @file epub_stress_fixture.h\n"
206 " * @brief Baked synthetic large-structure EPUB3 for the #144 pool-stress gate.\n"
208 f
" * @details {N_CHAPTERS} chapters + {N_RESOURCES} manifest resources + an NCX\n"
209 f
" * with {N_CHAPTERS} navPoints + a cover -- ~{N_CHAPTERS + N_RESOURCES + 5} archive\n"
210 " * entries / a large OPF, exercising bounded ZIP/XML resources. Pure ASCII.\n"
212 " * @generated by examples/ek_ra8d2/hw_pending/epub_stress/scripts/"
213 "make_stress_fixture.py -- do not edit by hand.\n"
214 " * The generator owns\n"
215 " * this file's length (a baked byte table).\n"
217 " * @copyright Copyright (c) 2026 Brighton Sikarskie\n"
218 " * SPDX-License-Identifier: MIT\n"
222 "#include <stddef.h>\n"
223 "#include <stdint.h>\n"
225 "/** @brief Length of the baked stress EPUB blob, bytes. */\n"
226 f
"enum : size_t {{ k_epub_stress_fixture_len = {len(epub)}U "
227 "/**< EPUB stress fixture length. */ };\n"
229 "/** @brief Baked synthetic large-structure EPUB3 byte stream. */\n"
230 "/* Suppression rationale: generated bytes stay at 16 per row for stable diffs. */\n"
231 "/* clang-format off */\n"
232 "static const uint8_t k_epub_stress_fixture[k_epub_stress_fixture_len] = {\n"
235 "/* clang-format on */\n"
240 """Regenerate the owning component's inc/epub_stress_fixture.h.
242 Writes with `encoding="ascii"`, so any non-ASCII that crept into the
243 generated text raises here rather than producing a file the encoding gate
247 output = Path(__file__).resolve().parent.parent /
"inc" /
"epub_stress_fixture.h"
248 with output.open(
"w", encoding=
"ascii")
as f:
249 f.write(bake_header(epub))
250 print(f
"wrote {output} ({len(epub)} epub bytes)")
253if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.