From 29e2381c5a8e3c443383ba57c71f1f60acae3517 Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Tue, 4 Aug 2026 20:21:49 +0200 Subject: [PATCH 1/4] fix: handle large MP4 atoms safely --- include/parser.hpp | 11 +- src/parser.cpp | 278 +++++++++++++++++++++++--------------- tests/parser_fallback.cpp | 4 +- tests/parser_safety.cpp | 99 +++++++++++++- 4 files changed, 271 insertions(+), 121 deletions(-) diff --git a/include/parser.hpp b/include/parser.hpp index ed64b67..99ceee5 100644 --- a/include/parser.hpp +++ b/include/parser.hpp @@ -16,9 +16,10 @@ #include struct Mp4AtomInfo { - uint32_t type; - uint64_t size; // total atom size. - uint64_t offset; // offset in file. + uint32_t type = 0; + uint64_t size = 0; // Total atom size, including its header. + uint64_t offset = 0; // Offset of the atom header in the file or enclosing stream. + uint64_t header_size = 8; // 8 normally, 16 when a 64-bit extended size is present. }; namespace parser_detail { @@ -35,12 +36,13 @@ struct TrackParseResult { std::vector stsc; std::vector stsz; std::vector stco; + std::vector co64; }; } // namespace parser_detail // Minimal parsed MP4 data for our authoring needs. struct ParsedMp4 { - bool used_fallback_stbl = false; // true if stbl atoms were recovered via flat scan. + bool used_fallback_stbl = false; // Reserved for reporting parser recovery paths. // All parsed tracks (audio/text/video). std::vector tracks; @@ -60,6 +62,7 @@ struct ParsedMp4 { std::vector stsc; std::vector stsz; std::vector stco; + std::vector co64; }; // Utility: read big-endian 32-bit value. diff --git a/src/parser.cpp b/src/parser.cpp index baf154e..e805a3f 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -8,9 +8,11 @@ #include "parser.hpp" +#include #include #include #include +#include #include #include #include @@ -26,7 +28,7 @@ constexpr uint64_t kAtomHeaderSize = 8; constexpr uint64_t kFullBoxBaseHeader = 12; // size+type+version/flags constexpr uint64_t kMetaReservedBytes = 4; constexpr uint64_t kHdlrMinPayload = 20; -constexpr uint64_t kMaxAtomPayload = 512 * 1024 * 1024; // 512 MB safety bound +constexpr uint64_t kMaxBufferedPayload = 512 * 1024 * 1024; } // namespace @@ -47,23 +49,39 @@ uint64_t read_u64(std::istream &in) { (uint64_t(b[6]) << 8) | (uint64_t(b[7])); } -// Read atom header: size + type. -static Mp4AtomInfo read_atom_header(std::istream &in) { +// Read and validate an atom header against its enclosing byte range. +static Mp4AtomInfo read_atom_header(std::istream &in, uint64_t enclosing_end) { Mp4AtomInfo info; - info.offset = in.tellg(); - info.size = read_u32(in); + const std::streampos pos = in.tellg(); + if (pos < 0) { + return info; + } + info.offset = static_cast(pos); + if (info.offset > enclosing_end || enclosing_end - info.offset < kAtomHeaderSize) { + return info; + } + + const uint32_t size32 = read_u32(in); info.type = read_u32(in); + if (!in) { + info.size = 0; + return info; + } - if (info.size == 1) { - // 64-bit extended size. + if (size32 == 1) { + if (enclosing_end - info.offset < 16) { + return info; + } + info.header_size = 16; info.size = read_u64(in); + } else if (size32 == 0) { + // A zero size extends to the end of the enclosing box (or file at top level). + info.size = enclosing_end - info.offset; + } else { + info.size = size32; } - // Hard sanity: reject absurd payloads early (protect against corrupted headers). - if (info.size >= kAtomHeaderSize && - (info.size - kAtomHeaderSize) > kMaxAtomPayload) { - CH_LOG("warn", "atom " << fourcc_to_string(info.type) - << " claims payload " << (info.size - kAtomHeaderSize) - << " bytes; exceeds safety bound, skipping"); + + if (!in || info.size < info.header_size || info.size > enclosing_end - info.offset) { info.size = 0; } return info; @@ -71,65 +89,89 @@ static Mp4AtomInfo read_atom_header(std::istream &in) { // Utility: read an atom payload into a byte buffer. static std::vector read_bytes(std::istream &in, uint64_t size) { - std::vector buf(size); - in.read(reinterpret_cast(buf.data()), size); - return buf; -} - - -static bool grab_atom_from_buffer(const std::vector &buf, const char *fourcc, - std::vector &dst) { - if (!dst.empty()) { - return false; + if (size > kMaxBufferedPayload || size > std::numeric_limits::max() || + size > static_cast(std::numeric_limits::max())) { + CH_LOG("error", "refusing to buffer atom payload of " << size << " bytes"); + skip(in, size); + return {}; } - for (size_t i = 0; i + 8 <= buf.size(); ++i) { - if (buf[i + 4] == static_cast(fourcc[0]) && - buf[i + 5] == static_cast(fourcc[1]) && - buf[i + 6] == static_cast(fourcc[2]) && - buf[i + 7] == static_cast(fourcc[3])) { - uint32_t sz = (buf[i] << 24) | (buf[i + 1] << 16) | (buf[i + 2] << 8) | buf[i + 3]; - uint64_t end = static_cast(i) + static_cast(sz); - if (sz >= 8 && end <= buf.size() && end >= i + 8) { - dst.assign(buf.begin() + i + 8, buf.begin() + end); - CH_LOG("debug", "grabbed " << fourcc << " via raw scan, bytes=" << dst.size()); - return true; - } - } + std::vector buf(static_cast(size)); + in.read(reinterpret_cast(buf.data()), static_cast(size)); + if (in.gcount() != static_cast(size)) { + return {}; } - return false; + return buf; } -// Naive scan for ilst payload (fallback when structured parse misses it). -static std::vector scan_ilst_payload(const std::string &path) { +// Streaming scan for ilst payload inside a validated moov range. +static std::vector scan_ilst_payload(const std::string &path, uint64_t range_offset, + uint64_t range_size) { std::ifstream f(path, std::ios::binary); if (!f.is_open()) { return {}; } f.seekg(0, std::ios::end); - std::streamoff len = f.tellg(); + const std::streamoff len = f.tellg(); if (len <= 0) { return {}; } - size_t sz = static_cast(len); - f.seekg(0, std::ios::beg); - std::vector data(sz); - f.read(reinterpret_cast(data.data()), sz); - for (size_t i = 0; i + 4 <= data.size(); ++i) { - if (data[i] == 'i' && data[i + 1] == 'l' && data[i + 2] == 's' && data[i + 3] == 't') { - if (i < 4) { + const uint64_t file_size = static_cast(len); + if (range_offset > file_size || range_size > file_size - range_offset) { + return {}; + } + const uint64_t range_end = range_offset + range_size; + f.seekg(static_cast(range_offset), std::ios::beg); + + constexpr size_t kScanChunkSize = 1024 * 1024; + constexpr size_t kHeaderOverlap = 7; + std::vector window(kScanChunkSize + kHeaderOverlap); + size_t carry = 0; + uint64_t consumed = range_offset; + + while (consumed < range_end) { + const size_t wanted = + static_cast(std::min(kScanChunkSize, range_end - consumed)); + f.read(reinterpret_cast(window.data() + carry), + static_cast(wanted)); + const size_t got = static_cast(f.gcount()); + if (got == 0) { + break; + } + + const size_t available = carry + got; + const uint64_t window_offset = consumed - carry; + for (size_t i = 0; i + kAtomHeaderSize <= available; ++i) { + if (window[i + 4] != 'i' || window[i + 5] != 'l' || window[i + 6] != 's' || + window[i + 7] != 't') { continue; } - uint32_t size = - (data[i - 4] << 24) | (data[i - 3] << 16) | (data[i - 2] << 8) | (data[i - 1]); - if (size < 8) { + + const uint32_t atom_size = (window[i] << 24) | (window[i + 1] << 16) | + (window[i + 2] << 8) | window[i + 3]; + if (atom_size < kAtomHeaderSize) { continue; } - size_t payload_size = size - 8; - if (i + 4 + payload_size <= data.size()) { - return std::vector(data.begin() + i + 4, - data.begin() + i + 4 + payload_size); + const uint64_t atom_offset = window_offset + i; + const uint64_t payload_size = atom_size - kAtomHeaderSize; + if (payload_size > kMaxBufferedPayload || atom_offset < range_offset || + atom_offset > range_end || atom_size > range_end - atom_offset) { + continue; + } + + std::ifstream payload_in(path, std::ios::binary); + payload_in.seekg(static_cast(atom_offset + kAtomHeaderSize), + std::ios::beg); + std::vector payload(static_cast(payload_size)); + payload_in.read(reinterpret_cast(payload.data()), + static_cast(payload_size)); + if (payload_in.gcount() == static_cast(payload_size)) { + return payload; } } + + consumed += got; + carry = std::min(kHeaderOverlap, available); + std::memmove(window.data(), window.data() + available - carry, carry); } return {}; } @@ -161,6 +203,11 @@ static void parse_mdhd(std::istream &in, uint64_t size, uint32_t ×cale, uin // Extract ilst from meta payload (payload only, after size/type). static void parse_meta_payload(std::istream &in, uint64_t size, ParsedMp4 &out) { const uint64_t start = (uint64_t)in.tellg(); + const uint64_t end = start + size; + if (size < 4) { + in.seekg(end); + return; + } // meta full box header. uint8_t version = in.get(); @@ -192,13 +239,13 @@ static void parse_meta_payload(std::istream &in, uint64_t size, ParsedMp4 &out) remain -= kMetaReservedBytes; } while (remain > kAtomHeaderSize) { - auto child = read_atom_header(in); + auto child = read_atom_header(in, end); if (!in || child.size == 0 || child.size < kAtomHeaderSize || child.size > remain) { CH_LOG("debug", "meta child invalid size=" << child.size << " remain=" << remain); break; } - uint64_t payload_size = child.size - kAtomHeaderSize; - if (payload_size > remain - kAtomHeaderSize) { + uint64_t payload_size = child.size - child.header_size; + if (child.header_size > remain || payload_size > remain - child.header_size) { CH_LOG("debug", "meta child payload exceeds remain; breaking"); break; } @@ -226,7 +273,6 @@ static void parse_meta_payload(std::istream &in, uint64_t size, ParsedMp4 &out) } // Seek to end of meta box. - uint64_t end = start + size; in.seekg(end); } @@ -252,6 +298,10 @@ static void parse_hdlr(std::istream &in, uint64_t payload_size, TrackParseResult std::string name; if (payload_size > consumed) { uint64_t name_len = payload_size - consumed; + if (name_len > 1024 * 1024) { + skip(in, name_len); + return; + } name.resize(static_cast(name_len)); in.read(name.data(), (std::streamsize)name_len); // Trim any trailing nulls. @@ -294,14 +344,15 @@ static void parse_tkhd(std::istream &in, uint64_t payload_size, TrackParseResult static void parse_stbl(std::istream &in, uint64_t size, TrackParseResult &track) { CH_LOG("debug", "parse_stbl size=" << size); uint64_t start = (uint64_t)in.tellg(); + uint64_t end = start + size; uint64_t remain = size; while (remain >= 8) { - auto info = read_atom_header(in); + auto info = read_atom_header(in, end); if (!in || info.size == 0 || info.size < 8) { break; } - uint64_t payload_size = info.size - 8; + uint64_t payload_size = info.size - info.header_size; switch (info.type) { case ('s' << 24 | 't' << 16 | 's' << 8 | 'd'): // stsd @@ -339,6 +390,13 @@ static void parse_stbl(std::istream &in, uint64_t size, TrackParseResult &track) skip(in, payload_size); } break; + case ('c' << 24 | 'o' << 16 | '6' << 8 | '4'): // co64 + if (track.co64.empty()) { + track.co64 = read_bytes(in, payload_size); + } else { + skip(in, payload_size); + } + break; default: skip(in, payload_size); } @@ -346,13 +404,13 @@ static void parse_stbl(std::istream &in, uint64_t size, TrackParseResult &track) remain -= info.size; } - uint64_t end = start + size; in.seekg(end); CH_LOG("debug", "stbl parsed sizes stsd=" << track.stsd.size() << " stts=" << track.stts.size() << " stsc=" << track.stsc.size() << " stsz=" << track.stsz.size() - << " stco=" << track.stco.size()); + << " stco=" << track.stco.size() + << " co64=" << track.co64.size()); } // Parse mdia box of a track. @@ -363,12 +421,12 @@ static bool parse_mdia(std::istream &in, uint64_t mpay, uint64_t mdia_end, while (mdia_remain >= 8) { CH_LOG("debug", " mdia pos=" << (uint64_t)in.tellg() << " remain=" << mdia_remain); - auto m = read_atom_header(in); + auto m = read_atom_header(in, mdia_end); if (!in || m.size == 0 || m.size < 8) { CH_LOG("debug", "mdia break: stream bad or size<8"); break; } - uint64_t mpay_child = m.size - 8; + uint64_t mpay_child = m.size - m.header_size; if (m.size > mdia_remain) { CH_LOG("debug", " mdia child type=" << fourcc_to_string(m.type) << " claims size=" << m.size << " but remain=" << mdia_remain @@ -394,7 +452,7 @@ static bool parse_mdia(std::istream &in, uint64_t mpay, uint64_t mdia_end, } CH_LOG("debug", " mdia child overflow; clamping size " << m.size << " -> " << allowed); m.size = allowed; - mpay_child = m.size - 8; + mpay_child = m.size - m.header_size; } if (m.type == ('m' << 24 | 'd' << 16 | 'h' << 8 | 'd')) { @@ -414,12 +472,12 @@ static bool parse_mdia(std::istream &in, uint64_t mpay, uint64_t mdia_end, CH_LOG("debug", " enter minf end=" << minf_end); while (minf_rem >= 8) { - auto mi = read_atom_header(in); + auto mi = read_atom_header(in, minf_end); if (!in || mi.size < 8) { CH_LOG("debug", " minf break: stream bad or size<8"); break; } - uint64_t mi_pay = mi.size - 8; + uint64_t mi_pay = mi.size - mi.header_size; CH_LOG("debug", " minf child=" << std::hex << mi.type << std::dec << " size=" << mi.size); @@ -478,17 +536,22 @@ static bool parse_mdia(std::istream &in, uint64_t mpay, uint64_t mdia_end, static std::optional parse_trak(std::istream &in, uint64_t c_payload, uint64_t file_size, bool &force_fallback) { (void)file_size; + if (c_payload < kAtomHeaderSize) { + skip(in, c_payload); + force_fallback = true; + return std::nullopt; + } uint64_t trak_end = (uint64_t)in.tellg() + c_payload; uint64_t trak_remain = c_payload; TrackParseResult track; CH_LOG("debug", "trak start end=" << trak_end); while (trak_remain >= 8) { - auto tchild = read_atom_header(in); + auto tchild = read_atom_header(in, trak_end); if (!in || tchild.size == 0 || tchild.size < 8) { break; } - uint64_t tpay = tchild.size - 8; + uint64_t tpay = tchild.size - tchild.header_size; CH_LOG("debug", " trak child=" << fourcc_to_string(tchild.type) << " size=" << tchild.size); if (tchild.type == ('t' << 24 | 'k' << 16 | 'h' << 8 | 'd')) { @@ -533,7 +596,7 @@ static void parse_moov(std::istream &in, const Mp4AtomInfo &atom, uint64_t file_ CH_LOG("debug", "enter moov @0x" << std::hex << atom.offset << std::dec << " end=" << end); while (((uint64_t)in.tellg()) + 8 <= end) { - auto child = read_atom_header(in); + auto child = read_atom_header(in, end); if (!in || child.size == 0 || child.size < 8) { break; } @@ -548,7 +611,7 @@ static void parse_moov(std::istream &in, const Mp4AtomInfo &atom, uint64_t file_ } } - uint64_t c_payload = child.size - 8; + uint64_t c_payload = child.size - child.header_size; CH_LOG("debug", "moov child=" << fourcc_to_string(child.type) << " size=" << child.size << " offset=0x" << std::hex << child.offset << std::dec); @@ -556,15 +619,18 @@ static void parse_moov(std::istream &in, const Mp4AtomInfo &atom, uint64_t file_ case ('u' << 24 | 'd' << 16 | 't' << 8 | 'a'): { uint64_t udta_end = (uint64_t)in.tellg() + c_payload; while (((uint64_t)in.tellg()) + 8 <= udta_end) { - auto u = read_atom_header(in); + auto u = read_atom_header(in, udta_end); if (!in || u.size < 8) { break; } - uint64_t upay = u.size - 8; + uint64_t upay = u.size - u.header_size; if (u.type == ('m' << 24 | 'e' << 16 | 't' << 8 | 'a')) { CH_LOG("debug", "found meta inside udta"); auto meta_buf = read_bytes(in, upay); + if (meta_buf.empty() && upay != 0) { + break; + } if (out.meta_payload.empty()) { out.meta_payload = meta_buf; } @@ -580,6 +646,9 @@ static void parse_moov(std::istream &in, const Mp4AtomInfo &atom, uint64_t file_ case ('m' << 24 | 'e' << 16 | 't' << 8 | 'a'): { CH_LOG("debug", "found meta under moov"); auto meta_buf = read_bytes(in, c_payload); + if (meta_buf.empty() && c_payload != 0) { + break; + } if (out.meta_payload.empty()) { out.meta_payload = meta_buf; } @@ -605,6 +674,7 @@ static void parse_moov(std::istream &in, const Mp4AtomInfo &atom, uint64_t file_ out.stsc = track.stsc; out.stsz = track.stsz; out.stco = track.stco; + out.co64 = track.co64; } } out.tracks.push_back(track); @@ -626,6 +696,7 @@ std::optional parse_mp4(const std::string &path) { ParsedMp4 out; uint32_t best_audio_samples = 0; bool force_fallback = false; + std::vector moov_atoms; CH_LOG("debug", "parse_mp4 enter path=" << path); std::ifstream in(path, std::ios::binary); @@ -635,7 +706,12 @@ std::optional parse_mp4(const std::string &path) { } in.seekg(0, std::ios::end); - const uint64_t file_size = static_cast(in.tellg()); + const std::streampos file_end = in.tellg(); + if (file_end < 0) { + CH_LOG("error", "parse_mp4: cannot determine file size for " << path); + return std::nullopt; + } + const uint64_t file_size = static_cast(file_end); in.seekg(0, std::ios::beg); CH_LOG("debug", "parse_mp4: size=" << file_size << " path=" << path); @@ -643,9 +719,9 @@ std::optional parse_mp4(const std::string &path) { if (force_fallback) { break; } - Mp4AtomInfo atom = read_atom_header(in); + Mp4AtomInfo atom = read_atom_header(in, file_size); if (atom.size == 0) { - CH_LOG("warn", "parse_mp4: atom with zero/invalid size encountered, bailing"); + CH_LOG("error", "parse_mp4: invalid atom size encountered, bailing"); break; } if (atom.size < 8 || atom.offset + atom.size > file_size) { @@ -654,7 +730,7 @@ std::optional parse_mp4(const std::string &path) { << " file=" << file_size); break; } - uint64_t payload_size = atom.size - 8; + uint64_t payload_size = atom.size - atom.header_size; switch (atom.type) { case ('m' << 24 | 'd' << 16 | 'a' << 8 | 't'): { // mdat @@ -663,6 +739,7 @@ std::optional parse_mp4(const std::string &path) { break; } case ('m' << 24 | 'o' << 16 | 'o' << 8 | 'v'): { // moov + moov_atoms.push_back(atom); parse_moov(in, atom, file_size, out, best_audio_samples, force_fallback); break; } @@ -673,43 +750,23 @@ std::optional parse_mp4(const std::string &path) { const auto t_struct_done = std::chrono::steady_clock::now(); - // Fallback: flat scan for sample-table atoms if they were not captured. - if (out.stsz.empty() || out.stco.empty() || out.stsc.empty() || out.stsd.empty()) { - CH_LOG("debug", "fallback flat scan for stbl atoms"); - out.used_fallback_stbl = true; - // load whole file into memory and search for atoms by signature. - in.clear(); - in.seekg(0, std::ios::end); - std::streamoff len = in.tellg(); - if (len <= 0) { - return out; - } - in.seekg(0, std::ios::beg); - std::vector buf(static_cast(len)); - in.read(reinterpret_cast(buf.data()), len); - - grab_atom_from_buffer(buf, "stsd", out.stsd); - grab_atom_from_buffer(buf, "stts", out.stts); - grab_atom_from_buffer(buf, "stsc", out.stsc); - grab_atom_from_buffer(buf, "stsz", out.stsz); - grab_atom_from_buffer(buf, "stco", out.stco); - if (out.ilst_payload.empty()) { - grab_atom_from_buffer(buf, "ilst", out.ilst_payload); - } - } - - // Fallback scan for ilst if still missing. + // Metadata fallback stays within validated moov ranges and never scans media payloads. if (out.ilst_payload.empty()) { - auto ilst = scan_ilst_payload(path); - if (!ilst.empty()) { - out.ilst_payload = std::move(ilst); - CH_LOG("debug", "ilst found via naive scan, bytes=" << out.ilst_payload.size()); + for (const auto &moov : moov_atoms) { + auto ilst = scan_ilst_payload(path, moov.offset, moov.size); + if (!ilst.empty()) { + out.ilst_payload = std::move(ilst); + CH_LOG("debug", "ilst found via bounded moov scan, bytes=" + << out.ilst_payload.size()); + break; + } } } const auto t_fallback_done = std::chrono::steady_clock::now(); - if (out.stco.empty() || out.stsc.empty() || out.stsz.empty() || out.stsd.empty()) { - CH_LOG("error", "parse_mp4: missing stbl atoms stco/stsc/stsz/stsd"); + if ((out.stco.empty() && out.co64.empty()) || out.stsc.empty() || out.stsz.empty() || + out.stsd.empty()) { + CH_LOG("error", "parse_mp4: missing stbl atoms stco/co64/stsc/stsz/stsd"); } const auto t_done = std::chrono::steady_clock::now(); const auto ms_struct = @@ -720,7 +777,8 @@ std::optional parse_mp4(const std::string &path) { const auto ms_total = std::chrono::duration_cast(t_done - t_start).count(); - CH_LOG("debug", "parse_mp4 done stco=" << out.stco.size() << " stsc=" << out.stsc.size() + CH_LOG("debug", "parse_mp4 done stco=" << out.stco.size() << " co64=" << out.co64.size() + << " stsc=" << out.stsc.size() << " stsz=" << out.stsz.size() << " stsd=" << out.stsd.size() << " ilst=" << out.ilst_payload.size() diff --git a/tests/parser_fallback.cpp b/tests/parser_fallback.cpp index 9a14634..f7bd102 100644 --- a/tests/parser_fallback.cpp +++ b/tests/parser_fallback.cpp @@ -23,8 +23,8 @@ int main() { std::cerr << "parse_mp4 returned nullopt for " << path << "\n"; return 1; } - if (parsed->stco.empty() || parsed->stsc.empty() || parsed->stsz.empty() || - parsed->stsd.empty()) { + if ((parsed->stco.empty() && parsed->co64.empty()) || parsed->stsc.empty() || + parsed->stsz.empty() || parsed->stsd.empty()) { std::cerr << "stbl atoms missing for " << path << "\n"; return 2; } diff --git a/tests/parser_safety.cpp b/tests/parser_safety.cpp index 7071de8..4961b83 100644 --- a/tests/parser_safety.cpp +++ b/tests/parser_safety.cpp @@ -1,10 +1,13 @@ #include "parser.hpp" +#include "parser_test_utils.hpp" #include #include #include #include +using namespace parser_test_utils; + namespace { void write_u32(std::ofstream &out, uint32_t v) { @@ -13,20 +16,79 @@ void write_u32(std::ofstream &out, uint32_t v) { out.write(reinterpret_cast(b), 4); } -// Build a file whose first atom claims an absurd payload (> safety bound). The parser should -// refuse that size and still return without crashing. +void write_u64(std::ofstream &out, uint64_t v) { + write_u32(out, static_cast(v >> 32)); + write_u32(out, static_cast(v & 0xFFFFFFFF)); +} +// Build a truncated file whose first atom claims a payload beyond the end of the file. The parser +// should reject it and still return without crashing. bool make_huge_atom_file(const std::filesystem::path &p) { std::ofstream out(p, std::ios::binary | std::ios::trunc); if (!out.is_open()) { return false; } - const uint32_t huge_size = 0x20000010; // payload >512MB; triggers size guard + const uint32_t huge_size = 0x20000010; write_u32(out, huge_size); out.write("mdat", 4); - // No payload is written; size guard in parser will zero the atom and bail. + // No payload is written; the top-level file bound must reject the atom. return true; } +// Build a valid sparse file with an mdat payload larger than 512 MB followed by a small moov. +// ChapterForge only seeks across mdat, so large lossless media must not trip the buffered-atom +// safety limit. +bool make_large_mdat_file(const std::filesystem::path &p) { + std::ofstream out(p, std::ios::binary | std::ios::trunc); + if (!out.is_open()) { + return false; + } + + constexpr uint64_t payload_size = 512ull * 1024 * 1024 + 4096; + write_u32(out, 1); // extended-size atom + out.write("mdat", 4); + write_u64(out, payload_size + 16); + out.seekp(static_cast(payload_size - 1), std::ios::cur); + out.put(0); + + std::vector stbl; + append_atom(stbl, 'stsd', make_stsd()); + append_atom(stbl, 'stts', make_stts(1, 4096)); + append_atom(stbl, 'stsc', make_stsc_empty()); + append_atom(stbl, 'stsz', make_stsz(1, 1)); + append_atom(stbl, 'stco', make_stco_empty()); + const auto mdia = make_mdia(44100, 4096, stbl, 'soun'); + auto moov_payload = make_moov(make_trak(mdia)); + std::vector meta(4, 0); // version/flags + append_atom(meta, 'ilst', std::vector{0}); + std::vector udta; + append_atom(udta, 'meta', meta); + append_atom(moov_payload, 'udta', udta); + std::vector moov; + append_atom(moov, 'moov', moov_payload); + out.write(reinterpret_cast(moov.data()), + static_cast(moov.size())); + return static_cast(out); +} + +bool make_zero_sized_moov_file(const std::filesystem::path &p) { + std::ofstream out(p, std::ios::binary | std::ios::trunc); + if (!out.is_open()) { + return false; + } + std::vector stbl; + append_atom(stbl, 'stsd', make_stsd()); + append_atom(stbl, 'stts', make_stts(1, 1024)); + append_atom(stbl, 'stsc', make_stsc_empty()); + append_atom(stbl, 'stsz', make_stsz(1, 1)); + append_atom(stbl, 'stco', make_stco_empty()); + const auto payload = make_moov(make_trak(make_mdia(44100, 1024, stbl, 'soun'))); + write_u32(out, 0); // extends to end of file + out.write("moov", 4); + out.write(reinterpret_cast(payload.data()), + static_cast(payload.size())); + return static_cast(out); +} + // Build a moov/trak hierarchy where the child overflows its parent. Parser should clamp and exit. bool make_overflow_child_file(const std::filesystem::path &p) { std::ofstream out(p, std::ios::binary | std::ios::trunc); @@ -50,6 +112,7 @@ int run_case(const std::filesystem::path &p, bool (*builder)(const std::filesyst return 2; } auto parsed = parse_mp4(p.string()); + std::filesystem::remove(p); if (!parsed) { std::cerr << "[parser_safety] parse_mp4 returned nullopt for " << p << "\n"; return 3; @@ -64,11 +127,37 @@ int main() { const auto tmp = std::filesystem::temp_directory_path(); const auto huge_atom = tmp / "parser_huge_atom.mp4"; const auto overflow = tmp / "parser_overflow_child.mp4"; + const auto large_mdat = tmp / "parser_large_mdat.mp4"; + const auto zero_sized_moov = tmp / "parser_zero_sized_moov.mp4"; int rc = run_case(huge_atom, make_huge_atom_file); if (rc != 0) { return rc; } rc = run_case(overflow, make_overflow_child_file); - return rc; + if (rc != 0) { + return rc; + } + + if (!make_large_mdat_file(large_mdat)) { + std::cerr << "[parser_safety] failed to build large-mdat fixture\n"; + return 4; + } + auto parsed = parse_mp4(large_mdat.string()); + std::filesystem::remove(large_mdat); + if (!parsed || parsed->audio_timescale != 44100 || parsed->stsd.empty()) { + std::cerr << "[parser_safety] failed to parse moov after large mdat\n"; + return 5; + } + if (!make_zero_sized_moov_file(zero_sized_moov)) { + std::cerr << "[parser_safety] failed to build zero-sized moov fixture\n"; + return 6; + } + parsed = parse_mp4(zero_sized_moov.string()); + std::filesystem::remove(zero_sized_moov); + if (!parsed || parsed->audio_timescale != 44100 || parsed->stsd.empty()) { + std::cerr << "[parser_safety] failed to parse zero-sized moov\n"; + return 7; + } + return 0; } From 7f78048cb6c08ab43715a6af893d9dc78fe52934 Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Tue, 4 Aug 2026 20:21:56 +0200 Subject: [PATCH 2/4] fix: preserve lossless MP4 timing and offsets --- CMakeLists.txt | 8 +- include/aac_extractor.hpp | 6 +- include/mdat_writer.hpp | 24 +++-- include/stbl_audio_builder.hpp | 5 +- src/aac_extractor.cpp | 178 ++++++++++++++++++++++---------- src/chapterforge.cpp | 22 ++-- src/mdat_writer.cpp | 182 +++++++++++++++++++++++++-------- src/mp4_muxer.cpp | 57 +++++++++-- src/stbl_audio_builder.cpp | 15 ++- tests/aac_extractor_unit.cpp | 107 +++++++++++++++++++ tests/writer_unit.cpp | 40 +++++++- 11 files changed, 512 insertions(+), 132 deletions(-) create mode 100644 tests/aac_extractor_unit.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3df8d30..7a54df0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -240,13 +240,19 @@ add_test(NAME parser_fallback_check COMMAND parser_fallback_check) set_tests_properties(parser_fallback_check PROPERTIES FIXTURES_REQUIRED assets) set_tests_properties(parser_fallback_check PROPERTIES LABELS "core") -# Safety regression: refuse absurd atom sizes / overflows without crashing. +# Safety regressions: reject malformed sizes and accept large media payloads without crashing. add_executable(parser_safety_check tests/parser_safety.cpp) target_link_libraries(parser_safety_check PRIVATE chapterforge) target_include_directories(parser_safety_check PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) add_test(NAME parser_safety_check COMMAND parser_safety_check) set_tests_properties(parser_safety_check PROPERTIES LABELS "core") +add_executable(aac_extractor_unit tests/aac_extractor_unit.cpp) +target_link_libraries(aac_extractor_unit PRIVATE chapterforge) +target_include_directories(aac_extractor_unit PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) +add_test(NAME aac_extractor_unit COMMAND aac_extractor_unit) +set_tests_properties(aac_extractor_unit PROPERTIES LABELS "unit") + if(nlohmann_json_FOUND) add_executable(image_fixtures tests/image_fixtures.cpp) target_link_libraries(image_fixtures PRIVATE chapterforge nlohmann_json::nlohmann_json) diff --git a/include/aac_extractor.hpp b/include/aac_extractor.hpp index b4f111e..68c949f 100644 --- a/include/aac_extractor.hpp +++ b/include/aac_extractor.hpp @@ -15,6 +15,7 @@ struct AacExtractResult { std::vector> frames; // raw AAC frames (ADTS header stripped) std::vector sizes; // raw frame sizes + std::vector chunk_sizes; // samples per source chunk uint32_t sample_rate = 0; uint8_t sampling_index = 0; @@ -27,6 +28,7 @@ struct AacExtractResult { std::vector stsc_payload; std::vector stsz_payload; std::vector stco_payload; + std::vector co64_payload; // Optional: original meta/ilst payloads (when source is MP4/M4A) std::vector meta_payload; @@ -41,7 +43,7 @@ AacExtractResult extract_adts_frames(const std::vector &data); /** * @brief Extract AAC frames and related tables from an MP4/M4A source. * - * Preferred when the input is already an MP4 container so we can reuse stsd/stts/stsc/stsz/stco and - * any meta/ilst payloads. + * Preferred when the input is already an MP4 container so we can reuse its audio sample tables + * (including stco or co64) and any meta/ilst payloads. */ std::optional extract_from_mp4(const std::string &path); diff --git a/include/mdat_writer.hpp b/include/mdat_writer.hpp index de63bf7..657fff4 100644 --- a/include/mdat_writer.hpp +++ b/include/mdat_writer.hpp @@ -13,11 +13,11 @@ #include "mp4_atoms.hpp" -// Stores final chunk offsets per track, used for STCO patching. +// Stores final chunk offsets per track, used for stco/co64 patching. struct MdatOffsets { - std::vector audio_offsets; - std::vector> text_offsets; // one entry per text track - std::vector image_offsets; + std::vector audio_offsets; + std::vector> text_offsets; // one entry per text track + std::vector image_offsets; uint64_t payload_start = 0; // absolute file offset where mdat payload begins }; @@ -30,14 +30,20 @@ MdatOffsets write_mdat(std::ofstream &out, const std::vector> &text_chunk_sizes, const std::vector &image_chunk_sizes); -// Patch a single stco atom. -void patch_stco_table(Atom *stco, const std::vector &offsets, - uint64_t mdat_payload_start); +// Promote all 32-bit chunk-offset tables before layout when output offsets may exceed 32 bits. +void promote_stco_to_co64(Atom *root); -// Patch stco boxes in moov (audio, text tracks, image). If patch_audio is false, -// audio stco (first one) is left untouched. +// Patch stco/co64 boxes in moov (audio, text tracks, image). If patch_audio is false, +// the first audio offset table is left untouched. void patch_all_stco(Atom *moov, const MdatOffsets &offs, bool patch_audio = true); +uint64_t media_payload_size( + const std::vector> &audio_samples, + const std::vector>> &text_tracks_samples, + const std::vector> &image_samples); + +uint64_t mdat_header_size(uint64_t payload_size); + // Compute chunk offsets without writing, given starting payload offset. MdatOffsets compute_mdat_offsets(uint64_t payload_start, const std::vector> &audio_samples, diff --git a/include/stbl_audio_builder.hpp b/include/stbl_audio_builder.hpp index a138b67..00f6ebe 100644 --- a/include/stbl_audio_builder.hpp +++ b/include/stbl_audio_builder.hpp @@ -19,9 +19,10 @@ std::unique_ptr build_audio_stbl(const Mp4aConfig &cfg, uint32_t num_samples, const std::vector *raw_stsd = nullptr); -// Build stbl from pre-existing box payloads (stsd/stts/stsc/stsz/stco) +// Build stbl from pre-existing box payloads (stsd/stts/stsc/stsz and stco or co64). std::unique_ptr build_audio_stbl_raw(const std::vector &stsd_payload, const std::vector &stts_payload, const std::vector &stsc_payload, const std::vector &stsz_payload, - const std::vector &stco_payload); + const std::vector &stco_payload, + const std::vector &co64_payload = {}); diff --git a/src/aac_extractor.cpp b/src/aac_extractor.cpp index 37ce8dc..1314d6e 100644 --- a/src/aac_extractor.cpp +++ b/src/aac_extractor.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -84,7 +85,6 @@ AacExtractResult extract_adts_frames(const std::vector &data) { return out; } - // Helpers for MP4 extraction (from container) static std::optional> parse_stsz_sizes( const std::vector &stsz_payload) { @@ -161,7 +161,7 @@ static void parse_esds_audio_cfg(const std::vector &stsd_payload, Mp4aC } static std::vector derive_chunk_plan(const std::vector &stsc_payload, - uint32_t sample_count) { + uint32_t chunk_count) { std::vector plan; if (stsc_payload.size() < kStscHeaderSize + kStscEntrySize) { return plan; @@ -169,7 +169,6 @@ static std::vector derive_chunk_plan(const std::vector &stsc_ uint32_t entry_count = (stsc_payload[4] << 24) | (stsc_payload[5] << 16) | (stsc_payload[6] << 8) | stsc_payload[7]; size_t pos = kStscHeaderSize; - uint32_t consumed = 0; for (uint32_t i = 0; i < entry_count; ++i) { if (pos + kStscEntrySize > stsc_payload.size()) { break; @@ -183,21 +182,57 @@ static std::vector derive_chunk_plan(const std::vector &stsc_ next_first = (stsc_payload[pos + 12] << 24) | (stsc_payload[pos + 13] << 16) | (stsc_payload[pos + 14] << 8) | (stsc_payload[pos + 15]); } - uint32_t chunk_count = (next_first > 0) ? (next_first - first_chunk) : 0; - if (chunk_count == 0) { - while (consumed < sample_count) { - plan.push_back(samples_per_chunk); - consumed += samples_per_chunk; - } - } else { - for (uint32_t c = 0; c < chunk_count && consumed < sample_count; ++c) { - plan.push_back(samples_per_chunk); - consumed += samples_per_chunk; - } + if (first_chunk == 0 || first_chunk > chunk_count || samples_per_chunk == 0) { + return {}; + } + const uint32_t run_end = next_first > 0 ? next_first - 1 : chunk_count; + if (run_end < first_chunk || run_end > chunk_count) { + return {}; + } + for (uint32_t chunk = first_chunk; chunk <= run_end; ++chunk) { + plan.push_back(samples_per_chunk); } pos += 12; } - return plan; + return plan.size() == chunk_count ? plan : std::vector{}; +} + +static uint32_t stts_sample_count(const std::vector &stts_payload) { + if (stts_payload.size() < 8) { + return 0; + } + const uint32_t entry_count = (stts_payload[4] << 24) | (stts_payload[5] << 16) | + (stts_payload[6] << 8) | stts_payload[7]; + if (entry_count > (stts_payload.size() - 8) / 8) { + return 0; + } + uint64_t count = 0; + for (uint32_t i = 0; i < entry_count; ++i) { + const size_t pos = 8 + static_cast(i) * 8; + const uint32_t run_count = (stts_payload[pos] << 24) | (stts_payload[pos + 1] << 16) | + (stts_payload[pos + 2] << 8) | stts_payload[pos + 3]; + count += run_count; + if (count > std::numeric_limits::max()) { + return 0; + } + } + return static_cast(count); +} + +static bool normalize_stsz(std::vector &payload, uint32_t sample_count) { + if (payload.size() < kStszHeaderSize) { + return false; + } + payload[8] = static_cast((sample_count >> 24) & 0xFF); + payload[9] = static_cast((sample_count >> 16) & 0xFF); + payload[10] = static_cast((sample_count >> 8) & 0xFF); + payload[11] = static_cast(sample_count & 0xFF); + const uint32_t fixed_size = (payload[4] << 24) | (payload[5] << 16) | (payload[6] << 8) | + payload[7]; + if (fixed_size == 0) { + payload.resize(kStszHeaderSize + static_cast(sample_count) * 4); + } + return true; } std::optional extract_from_mp4(const std::string &path) { @@ -227,19 +262,22 @@ std::optional extract_from_mp4(const std::string &path) { } CH_LOG("debug", "mp4 parsed optional has value for " << path); ParsedMp4 &parsed = *parsed_opt; - CH_LOG("debug", "mp4 parsed: stco=" << parsed.stco.size() << " stsc=" << parsed.stsc.size() + CH_LOG("debug", "mp4 parsed: stco=" << parsed.stco.size() << " co64=" << parsed.co64.size() + << " stsc=" << parsed.stsc.size() << " stsz=" << parsed.stsz.size() << " stsd=" << parsed.stsd.size()); - if (parsed.stco.empty() || parsed.stsc.empty() || parsed.stsz.empty() || parsed.stsd.empty()) { - CH_LOG("error", "Missing required stbl atoms (stco/stsc/stsz/stsd) in " << path); + if ((parsed.stco.empty() && parsed.co64.empty()) || parsed.stsc.empty() || + parsed.stsz.empty() || parsed.stsd.empty() || parsed.stts.empty()) { + CH_LOG("error", "Missing required stbl atoms (stco/co64/stsc/stsz/stsd/stts) in " + << path); return std::nullopt; } auto sizes_opt = parse_stsz_sizes(parsed.stsz); - if (!sizes_opt || parsed.stco.empty() || parsed.stsc.empty()) { + if (!sizes_opt) { return std::nullopt; } - const auto &sizes = *sizes_opt; + auto sizes = std::move(*sizes_opt); if (sizes.empty()) { return std::nullopt; } @@ -250,33 +288,67 @@ std::optional extract_from_mp4(const std::string &path) { return std::nullopt; } - CH_LOG("debug", "mp4 reuse: sizes=" << sizes.size() << " stco_bytes=" << parsed.stco.size() - << " stsc_bytes=" << parsed.stsc.size() - << " file_size=" << file_size); - - std::vector chunk_plan = - derive_chunk_plan(parsed.stsc, static_cast(sizes.size())); - if (chunk_plan.empty()) { + const auto read_be32 = [](const std::vector &payload, size_t pos) { + return (static_cast(payload[pos]) << 24) | + (static_cast(payload[pos + 1]) << 16) | + (static_cast(payload[pos + 2]) << 8) | payload[pos + 3]; + }; + const auto read_be64 = [&](const std::vector &payload, size_t pos) { + return (static_cast(read_be32(payload, pos)) << 32) | + read_be32(payload, pos + 4); + }; + + const bool uses_co64 = !parsed.co64.empty(); + const auto &offset_payload = uses_co64 ? parsed.co64 : parsed.stco; + if (offset_payload.size() < 8) { return std::nullopt; } - if (chunk_plan.size() > sizes.size() * 4 || chunk_plan.size() > 1000000) { - CH_LOG("error", "Unreasonable chunk plan size=" << chunk_plan.size() - << " samples=" << sizes.size()); + const uint32_t chunk_count = read_be32(offset_payload, 4); + const uint64_t offset_width = uses_co64 ? 8 : 4; + const uint64_t offsets_expected = 8ull + offset_width * chunk_count; + if (offsets_expected > offset_payload.size()) { + CH_LOG("error", (uses_co64 ? "co64" : "stco") + << " table truncated: size=" << offset_payload.size() + << " expected>=" << offsets_expected); return std::nullopt; } + std::vector chunk_offsets; + chunk_offsets.reserve(chunk_count); + for (uint32_t i = 0; i < chunk_count; ++i) { + const size_t pos = 8 + static_cast(i) * offset_width; + chunk_offsets.push_back(uses_co64 ? read_be64(offset_payload, pos) + : read_be32(offset_payload, pos)); + } - std::vector> frames; - frames.reserve(sizes.size()); - size_t sample_idx = 0; - const uint8_t *pco = parsed.stco.data(); - uint32_t stco_count = (pco[4] << 24) | (pco[5] << 16) | (pco[6] << 8) | pco[7]; - // Validate stco table size matches count and fits in file. - uint64_t stco_expected = 8ull + 4ull * stco_count; - if (parsed.stco.size() < stco_expected || stco_expected > file_size) { - CH_LOG("error", "stco table truncated: size=" << parsed.stco.size() - << " expected>=" << stco_expected); + std::vector chunk_plan = derive_chunk_plan(parsed.stsc, chunk_count); + if (chunk_plan.empty()) { return std::nullopt; } + uint64_t mapped_samples = 0; + for (const uint32_t samples_per_chunk : chunk_plan) { + mapped_samples += samples_per_chunk; + } + const uint32_t timed_samples = stts_sample_count(parsed.stts); + if (mapped_samples == 0 || mapped_samples > std::numeric_limits::max() || + sizes.size() < mapped_samples || timed_samples != mapped_samples) { + CH_LOG("error", "inconsistent audio sample tables: stsz=" << sizes.size() + << " mapped=" << mapped_samples + << " timed=" << timed_samples); + return std::nullopt; + } + if (sizes.size() > mapped_samples) { + CH_LOG("debug", "normalizing " << (sizes.size() - mapped_samples) + << " unaddressed stsz entries"); + sizes.resize(static_cast(mapped_samples)); + if (!normalize_stsz(parsed.stsz, static_cast(mapped_samples))) { + return std::nullopt; + } + } + + CH_LOG("debug", "mp4 reuse: sizes=" << sizes.size() << " chunks=" << chunk_count + << " offsets=" << (uses_co64 ? "co64" : "stco") + << " stsc_bytes=" << parsed.stsc.size() + << " file_size=" << file_size); // Validate stsc table size matches entry count (12 bytes each after header). if (parsed.stsc.size() < 8) { @@ -290,24 +362,19 @@ std::optional extract_from_mp4(const std::string &path) { << " expected>=" << stsc_expected); return std::nullopt; } - CH_LOG("debug", "mp4 reuse: stco_count=" << stco_count << " stsc_entries=" << stsc_entries + CH_LOG("debug", "mp4 reuse: chunk_count=" << chunk_count << " stsc_entries=" << stsc_entries << " chunk_plan=" << chunk_plan.size()); const auto t_parse = std::chrono::steady_clock::now(); - size_t stco_pos = 8; - for (uint32_t chunk_idx = 0; chunk_idx < stco_count && chunk_idx < chunk_plan.size() && - sample_idx < sizes.size(); - ++chunk_idx) { - if (stco_pos + 4 > parsed.stco.size()) { - break; - } - uint32_t chunk_offset = (pco[stco_pos] << 24) | (pco[stco_pos + 1] << 16) | - (pco[stco_pos + 2] << 8) | pco[stco_pos + 3]; - stco_pos += 4; + std::vector> frames; + frames.reserve(sizes.size()); + size_t sample_idx = 0; + for (uint32_t chunk_idx = 0; chunk_idx < chunk_count; ++chunk_idx) { + const uint64_t chunk_offset = chunk_offsets[chunk_idx]; uint32_t samples_in_chunk = chunk_plan[chunk_idx]; uint64_t chunk_size = 0; - for (uint32_t i = 0; i < samples_in_chunk && sample_idx + i < sizes.size(); ++i) { + for (uint32_t i = 0; i < samples_in_chunk; ++i) { chunk_size += sizes[sample_idx + i]; } if (chunk_size == 0) { @@ -315,14 +382,14 @@ std::optional extract_from_mp4(const std::string &path) { } // Bounds guard: chunk must fit in file. - if (static_cast(chunk_offset) + chunk_size > file_size) { + if (chunk_offset > file_size || chunk_size > file_size - chunk_offset) { CH_LOG("error", "Chunk exceeds file size: offset=" << chunk_offset << " size=" << chunk_size << " file_size=" << file_size); break; } - std::vector chunk(chunk_size); + std::vector chunk(static_cast(chunk_size)); f.seekg(static_cast(chunk_offset), std::ios::beg); f.read(reinterpret_cast(chunk.data()), static_cast(chunk_size)); if (f.gcount() != static_cast(chunk_size)) { @@ -341,6 +408,7 @@ std::optional extract_from_mp4(const std::string &path) { offset += s; } } + const auto t_samples = std::chrono::steady_clock::now(); if (frames.size() != sizes.size()) { @@ -350,6 +418,7 @@ std::optional extract_from_mp4(const std::string &path) { AacExtractResult out; out.frames = std::move(frames); out.sizes = sizes; + out.chunk_sizes = chunk_plan; out.sample_rate = parsed.audio_timescale; Mp4aConfig cfg; cfg.sample_rate = parsed.audio_timescale; @@ -365,6 +434,7 @@ std::optional extract_from_mp4(const std::string &path) { out.stsc_payload = parsed.stsc; out.stsz_payload = parsed.stsz; out.stco_payload = parsed.stco; + out.co64_payload = parsed.co64; out.meta_payload = parsed.meta_payload; out.ilst_payload = parsed.ilst_payload; const auto t_done = std::chrono::steady_clock::now(); diff --git a/src/chapterforge.cpp b/src/chapterforge.cpp index a5bb5be..875f215 100644 --- a/src/chapterforge.cpp +++ b/src/chapterforge.cpp @@ -405,6 +405,10 @@ uint32_t read_u32_be(const std::vector &buf, size_t off) { (static_cast(buf[off + 2]) << 8) | (static_cast(buf[off + 3])); } +uint64_t read_u64_be(const std::vector &buf, size_t off) { + return (static_cast(read_u32_be(buf, off)) << 32) | read_u32_be(buf, off + 4); +} + uint16_t read_u16_be(const std::vector &buf, size_t off) { return static_cast((static_cast(buf[off]) << 8) | static_cast(buf[off + 1])); @@ -424,7 +428,7 @@ std::optional build_sample_plan(const parser_detail::TrackParseResul SamplePlan plan; // stsz: fixed or per-sample sizes const auto &stsz = trk.stsz; - if (stsz.size() < 20) { + if (stsz.size() < 12) { return std::nullopt; } // stsz layout (payload only, size/type stripped): @@ -446,19 +450,23 @@ std::optional build_sample_plan(const parser_detail::TrackParseResul plan.sizes.assign(sample_count, sample_size); } - // stco: chunk offsets - const auto &stco = trk.stco; - if (stco.size() < 16) { + // stco/co64: chunk offsets + const bool uses_co64 = !trk.co64.empty(); + const auto &chunk_table = uses_co64 ? trk.co64 : trk.stco; + if (chunk_table.size() < 8) { return std::nullopt; } - uint32_t chunk_count = read_u32_be(stco, 4); - if (stco.size() < 8 + chunk_count * 4) { + uint32_t chunk_count = read_u32_be(chunk_table, 4); + const size_t offset_width = uses_co64 ? 8 : 4; + if (chunk_count > (chunk_table.size() - 8) / offset_width) { return std::nullopt; } std::vector chunk_offsets; chunk_offsets.reserve(chunk_count); for (uint32_t i = 0; i < chunk_count; ++i) { - chunk_offsets.push_back(read_u32_be(stco, 8 + i * 4)); + const size_t offset = 8 + static_cast(i) * offset_width; + chunk_offsets.push_back(uses_co64 ? read_u64_be(chunk_table, offset) + : read_u32_be(chunk_table, offset)); } // stsc: samples per chunk mapping diff --git a/src/mdat_writer.cpp b/src/mdat_writer.cpp index fddeae5..3fa4a32 100644 --- a/src/mdat_writer.cpp +++ b/src/mdat_writer.cpp @@ -8,8 +8,34 @@ #include "mdat_writer.hpp" +#include #include +uint64_t media_payload_size( + const std::vector> &audio_samples, + const std::vector>> &text_tracks_samples, + const std::vector> &image_samples) { + uint64_t size = 0; + auto add_samples = [&](const std::vector> &samples) { + for (const auto &sample : samples) { + if (sample.size() > std::numeric_limits::max() - size) { + throw std::runtime_error("media payload size overflow"); + } + size += sample.size(); + } + }; + add_samples(audio_samples); + for (const auto &track : text_tracks_samples) { + add_samples(track); + } + add_samples(image_samples); + return size; +} + +uint64_t mdat_header_size(uint64_t payload_size) { + return payload_size <= std::numeric_limits::max() - 8 ? 8 : 16; +} + // Write the mdat box and collect relative offsets for each track. MdatOffsets write_mdat( std::ofstream &out, const std::vector> &audio_samples, @@ -23,9 +49,16 @@ MdatOffsets write_mdat( // Start of mdat box. uint64_t mdat_header_pos = out.tellp(); - // Size placeholder (4 bytes) + 'mdat' - uint8_t header[8] = {0, 0, 0, 0, 'm', 'd', 'a', 't'}; - out.write(reinterpret_cast(header), 8); + const uint64_t payload_size = + media_payload_size(audio_samples, text_tracks_samples, image_samples); + const uint64_t header_size = mdat_header_size(payload_size); + if (header_size == 8) { + uint8_t header[8] = {0, 0, 0, 0, 'm', 'd', 'a', 't'}; + out.write(reinterpret_cast(header), 8); + } else { + uint8_t header[16] = {0, 0, 0, 1, 'm', 'd', 'a', 't', 0, 0, 0, 0, 0, 0, 0, 0}; + out.write(reinterpret_cast(header), 16); + } // Payload begins right after 'mdat' uint64_t payload_start = out.tellp(); @@ -33,7 +66,7 @@ MdatOffsets write_mdat( auto write_track = [&](const std::vector> &samples, const std::vector &chunk_sizes, - std::vector &offsets) { + std::vector &offsets) { if (samples.empty()) { return; } @@ -48,7 +81,7 @@ MdatOffsets write_mdat( break; } uint64_t pos = out.tellp(); - uint32_t rel = static_cast(pos - payload_start); + uint64_t rel = static_cast(pos) - payload_start; offsets.push_back(rel); for (uint32_t i = 0; i < chunk_size && sample_index < samples.size(); ++i) { @@ -60,7 +93,7 @@ MdatOffsets write_mdat( // Write any stragglers if plan was shorter than sample count. if (sample_index < samples.size()) { uint64_t pos = out.tellp(); - uint32_t rel = static_cast(pos - payload_start); + uint64_t rel = static_cast(pos) - payload_start; offsets.push_back(rel); for (; sample_index < samples.size(); ++sample_index) { const auto &sample = samples[sample_index]; @@ -72,7 +105,7 @@ MdatOffsets write_mdat( // Apple convention: audio first, then text tracks, then image. write_track(audio_samples, audio_chunk_sizes, result.audio_offsets); for (size_t i = 0; i < text_tracks_samples.size(); ++i) { - std::vector offsets; + std::vector offsets; const auto &samples = text_tracks_samples[i]; const auto &plan = (i < text_chunk_sizes.size()) ? text_chunk_sizes[i] : std::vector(); @@ -85,65 +118,126 @@ MdatOffsets write_mdat( uint64_t end_pos = out.tellp(); uint64_t box_size = end_pos - mdat_header_pos; - if (box_size > 0xFFFFFFFFULL) { - throw std::runtime_error("mdat too large ( > 4 GB )"); - } - - uint32_t size32 = static_cast(box_size); - uint8_t size_bytes[4] = { - static_cast((size32 >> 24) & 0xFF), static_cast((size32 >> 16) & 0xFF), - static_cast((size32 >> 8) & 0xFF), static_cast((size32) & 0xFF)}; - out.seekp(mdat_header_pos); - out.write(reinterpret_cast(size_bytes), 4); + if (header_size == 8) { + uint32_t size32 = static_cast(box_size); + uint8_t size_bytes[4] = { + static_cast((size32 >> 24) & 0xFF), + static_cast((size32 >> 16) & 0xFF), + static_cast((size32 >> 8) & 0xFF), static_cast(size32 & 0xFF)}; + out.write(reinterpret_cast(size_bytes), 4); + } else { + uint8_t size_bytes[8] = { + static_cast((box_size >> 56) & 0xFF), + static_cast((box_size >> 48) & 0xFF), + static_cast((box_size >> 40) & 0xFF), + static_cast((box_size >> 32) & 0xFF), + static_cast((box_size >> 24) & 0xFF), + static_cast((box_size >> 16) & 0xFF), + static_cast((box_size >> 8) & 0xFF), static_cast(box_size & 0xFF)}; + out.seekp(mdat_header_pos + std::streamoff(8)); + out.write(reinterpret_cast(size_bytes), 8); + } out.seekp(end_pos); return result; } -// Update a single stco table with absolute offsets based on the mdat payload start. -void patch_stco_table(Atom *stco, const std::vector &offsets, - uint64_t mdat_payload_start) { - if (!stco) { +namespace { + +void collect_chunk_offset_atoms(Atom *atom, std::vector &out) { + if (!atom) { return; } - auto &p = stco->payload; + if (atom->type == fourcc("stco") || atom->type == fourcc("co64")) { + out.push_back(atom); + } + for (auto &child : atom->children) { + collect_chunk_offset_atoms(child.get(), out); + } +} - if (p.size() < 8) { +uint32_t payload_u32(const std::vector &payload, size_t pos) { + return (static_cast(payload[pos]) << 24) | + (static_cast(payload[pos + 1]) << 16) | + (static_cast(payload[pos + 2]) << 8) | payload[pos + 3]; +} + +void patch_chunk_offset_table(Atom *atom, const std::vector &offsets, + uint64_t mdat_payload_start) { + if (!atom) { return; } + auto &p = atom->payload; - uint32_t entry_count = (p[4] << 24) | (p[5] << 16) | (p[6] << 8) | (p[7]); + if (p.size() < 8) { + return; + } - entry_count = std::min(entry_count, offsets.size()); + const bool is_co64 = atom->type == fourcc("co64"); + const size_t width = is_co64 ? 8 : 4; + const uint32_t declared_count = payload_u32(p, 4); + if (declared_count > (p.size() - 8) / width || offsets.size() < declared_count) { + throw std::runtime_error("chunk offset table/count mismatch"); + } size_t pos = 8; - for (uint32_t i = 0; i < entry_count; ++i) { - uint32_t abs_offset = offsets[i] + static_cast(mdat_payload_start); + for (uint32_t i = 0; i < declared_count; ++i) { + if (offsets[i] > std::numeric_limits::max() - mdat_payload_start) { + throw std::runtime_error("chunk offset overflow"); + } + const uint64_t absolute = offsets[i] + mdat_payload_start; + if (!is_co64 && absolute > std::numeric_limits::max()) { + throw std::runtime_error("stco offset exceeds 32 bits"); + } + for (size_t byte = 0; byte < width; ++byte) { + const unsigned shift = static_cast((width - byte - 1) * 8); + p[pos + byte] = static_cast((absolute >> shift) & 0xFF); + } + pos += width; + } +} - p[pos + 0] = (abs_offset >> 24) & 0xFF; - p[pos + 1] = (abs_offset >> 16) & 0xFF; - p[pos + 2] = (abs_offset >> 8) & 0xFF; - p[pos + 3] = (abs_offset) & 0xFF; +} // namespace - pos += 4; +void promote_stco_to_co64(Atom *root) { + std::vector atoms; + collect_chunk_offset_atoms(root, atoms); + for (Atom *atom : atoms) { + if (atom->type != fourcc("stco") || atom->payload.size() < 8) { + continue; + } + const uint32_t count = payload_u32(atom->payload, 4); + if (count > (atom->payload.size() - 8) / 4) { + throw std::runtime_error("invalid stco table"); + } + std::vector promoted; + promoted.reserve(8 + static_cast(count) * 8); + promoted.insert(promoted.end(), atom->payload.begin(), atom->payload.begin() + 8); + for (uint32_t i = 0; i < count; ++i) { + const uint32_t value = payload_u32(atom->payload, 8 + static_cast(i) * 4); + write_u64(promoted, value); + } + atom->type = fourcc("co64"); + atom->payload = std::move(promoted); } } -// Patch all stco tables (audio, text tracks, images) found under moov. +// Patch all stco/co64 tables (audio, text tracks, images) found under moov. void patch_all_stco(Atom *moov, const MdatOffsets &offs, bool patch_audio) { if (!moov) { return; } - auto stcos = moov->find("stco"); + std::vector offset_atoms; + collect_chunk_offset_atoms(moov, offset_atoms); size_t idx = 0; - if (patch_audio && stcos.size() > idx) { - patch_stco_table(stcos[idx], offs.audio_offsets, offs.payload_start); + if (patch_audio && offset_atoms.size() > idx) { + patch_chunk_offset_table(offset_atoms[idx], offs.audio_offsets, offs.payload_start); } idx += 1; - for (size_t t = 0; t < offs.text_offsets.size() && idx < stcos.size(); ++t, ++idx) { - patch_stco_table(stcos[idx], offs.text_offsets[t], offs.payload_start); + for (size_t t = 0; t < offs.text_offsets.size() && idx < offset_atoms.size(); ++t, ++idx) { + patch_chunk_offset_table(offset_atoms[idx], offs.text_offsets[t], offs.payload_start); } - if (idx < stcos.size() && !offs.image_offsets.empty()) { - patch_stco_table(stcos[idx], offs.image_offsets, offs.payload_start); + if (idx < offset_atoms.size() && !offs.image_offsets.empty()) { + patch_chunk_offset_table(offset_atoms[idx], offs.image_offsets, offs.payload_start); } } @@ -161,26 +255,26 @@ MdatOffsets compute_mdat_offsets( uint64_t payload_start, auto compute_track = [&](const std::vector> &samples, const std::vector &chunk_sizes, - std::vector &offsets) { + std::vector &offsets) { if (samples.empty()) { return; } std::vector plan = chunk_sizes.empty() ? std::vector(samples.size(), 1) : chunk_sizes; size_t sample_index = 0; for (uint32_t chunk_size : plan) { if (sample_index >= samples.size()) { break; } - offsets.push_back(static_cast(cursor - payload_start)); + offsets.push_back(cursor - payload_start); for (uint32_t i = 0; i < chunk_size && sample_index < samples.size(); ++i, ++sample_index) { cursor += samples[sample_index].size(); } } if (sample_index < samples.size()) { - offsets.push_back(static_cast(cursor - payload_start)); + offsets.push_back(cursor - payload_start); for (; sample_index < samples.size(); ++sample_index) { cursor += samples[sample_index].size(); } } }; compute_track(audio_samples, audio_chunk_sizes, result.audio_offsets); for (size_t i = 0; i < text_tracks_samples.size(); ++i) { - std::vector offsets; + std::vector offsets; const auto &samples = text_tracks_samples[i]; const auto &plan = (i < text_chunk_sizes.size()) ? text_chunk_sizes[i] : std::vector(); compute_track(samples, plan, offsets); diff --git a/src/mp4_muxer.cpp b/src/mp4_muxer.cpp index 97a26bd..f0011c5 100644 --- a/src/mp4_muxer.cpp +++ b/src/mp4_muxer.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -70,7 +71,6 @@ static std::vector build_audio_chunk_plan(uint32_t sample_count) { } return chunks; } - // Derive samples-per-chunk plan from stsc payload. static std::vector derive_chunk_plan(const std::vector &stsc_payload, uint32_t sample_count) { @@ -124,6 +124,34 @@ static std::vector derive_chunk_plan(const std::vector &stsc_ return plan; } +static std::optional duration_from_stts(const std::vector &stts_payload) { + if (stts_payload.size() < 8) { + return std::nullopt; + } + const uint32_t entry_count = (stts_payload[4] << 24) | (stts_payload[5] << 16) | + (stts_payload[6] << 8) | stts_payload[7]; + if (entry_count > (stts_payload.size() - 8) / 8) { + return std::nullopt; + } + + uint64_t duration = 0; + size_t pos = 8; + for (uint32_t i = 0; i < entry_count; ++i, pos += 8) { + const uint32_t sample_count = (stts_payload[pos] << 24) | + (stts_payload[pos + 1] << 16) | + (stts_payload[pos + 2] << 8) | stts_payload[pos + 3]; + const uint32_t sample_delta = (stts_payload[pos + 4] << 24) | + (stts_payload[pos + 5] << 16) | + (stts_payload[pos + 6] << 8) | stts_payload[pos + 7]; + if (sample_delta != 0 && + sample_count > (std::numeric_limits::max() - duration) / sample_delta) { + return std::nullopt; + } + duration += static_cast(sample_count) * sample_delta; + } + return duration == 0 ? std::nullopt : std::optional(duration); +} + struct DurationInfo { uint32_t audio_timescale = 0; uint64_t audio_duration_ts = 0; @@ -245,7 +273,10 @@ static DurationInfo compute_durations(const AacExtractResult &aac, Mp4aConfig &a aac.audio_object_type ? aac.audio_object_type : audio_cfg.audio_object_type; info.audio_timescale = audio_cfg.sample_rate; - info.audio_duration_ts = static_cast(aac.frames.size()) * kAacSamplesPerFrame; + // MP4 inputs carry their exact packet timing in stts. This matters for codecs such as ALAC, + // whose packets commonly represent 4096 PCM samples rather than AAC-LC's 1024. + info.audio_duration_ts = duration_from_stts(aac.stts_payload).value_or( + static_cast(aac.frames.size()) * kAacSamplesPerFrame); info.audio_duration_ms = static_cast((info.audio_duration_ts * 1000 + info.audio_timescale - 1) / info.audio_timescale); @@ -309,9 +340,11 @@ static ChunkPlans build_chunk_plans(const AacExtractResult &aac, uint32_t audio_ const PreparedTextTracks &texts, const std::vector> &image_samples) { ChunkPlans plans; - plans.audio = - aac.stsc_payload.empty() ? build_audio_chunk_plan(audio_sample_count) - : derive_chunk_plan(aac.stsc_payload, audio_sample_count); + plans.audio = !aac.chunk_sizes.empty() + ? aac.chunk_sizes + : (aac.stsc_payload.empty() ? build_audio_chunk_plan(audio_sample_count) + : derive_chunk_plan(aac.stsc_payload, + audio_sample_count)); plans.text.push_back(std::vector(texts.primary.size(), 1)); for (const auto &samples : texts.extras) { plans.text.emplace_back(samples.size(), 1); @@ -512,13 +545,13 @@ bool write_mp4(const std::string &output_path, const AacExtractResult &aac, // Pre-build stbls (needed for both fast-start and normal paths) std::unique_ptr stbl_audio; if (!aac.stsd_payload.empty() && !aac.stts_payload.empty() && !aac.stsc_payload.empty() && - !aac.stsz_payload.empty() && !aac.stco_payload.empty()) { + !aac.stsz_payload.empty() && (!aac.stco_payload.empty() || !aac.co64_payload.empty())) { // Golden-aligned path: reuse the source audio stbl verbatim. Rebuilding stbl for audio // triggered decoding issues in Apple players even when the fields were “correct” per spec, // so we preserve the original structure whenever we can. CH_LOG("debug", "Reusing source audio stbl"); stbl_audio = build_audio_stbl_raw(aac.stsd_payload, aac.stts_payload, aac.stsc_payload, - aac.stsz_payload, aac.stco_payload); + aac.stsz_payload, aac.stco_payload, aac.co64_payload); } else { CH_LOG("debug", "Building new audio stbl"); stbl_audio = @@ -613,6 +646,14 @@ bool write_mp4(const std::string &output_path, const AacExtractResult &aac, auto moov = build_moov(mvhd_timescale, mvhd_duration, std::move(trak_audio), std::move(text_traks), std::move(trak_image), std::move(udta)); moov->fix_size_recursive(); + const uint64_t media_bytes = + media_payload_size(audio_samples, all_text_samples, image_samples); + const uint64_t media_header_bytes = mdat_header_size(media_bytes); + if (static_cast(ftyp_size) + moov->size() + media_header_bytes + media_bytes > + std::numeric_limits::max()) { + promote_stco_to_co64(moov.get()); + moov->fix_size_recursive(); + } CH_LOG("debug", "moov size=" << moov->size() << " mvhd_duration=" << mvhd_duration); auto t_moov_end = now(); @@ -625,7 +666,7 @@ bool write_mp4(const std::string &output_path, const AacExtractResult &aac, // other valid layouts have shown sporadic playback regressions despite being // spec-compliant. uint64_t payload_start = - static_cast(ftyp_size) + moov->size() + 8; // +8 for mdat header + static_cast(ftyp_size) + moov->size() + media_header_bytes; MdatOffsets mdat_offs = compute_mdat_offsets(payload_start, audio_samples, all_text_samples, image_samples, chunk_plans.audio, all_text_chunk_plans, chunk_plans.image); diff --git a/src/stbl_audio_builder.cpp b/src/stbl_audio_builder.cpp index c080072..95d2f25 100644 --- a/src/stbl_audio_builder.cpp +++ b/src/stbl_audio_builder.cpp @@ -123,7 +123,8 @@ std::unique_ptr build_audio_stbl_raw(const std::vector &stsd_payl const std::vector &stts_payload, const std::vector &stsc_payload, const std::vector &stsz_payload, - const std::vector &stco_payload) { + const std::vector &stco_payload, + const std::vector &co64_payload) { auto stbl = Atom::create("stbl"); auto stsd = Atom::create("stsd"); @@ -142,9 +143,15 @@ std::unique_ptr build_audio_stbl_raw(const std::vector &stsd_payl stsz->payload = stsz_payload; stbl->add(std::move(stsz)); - auto stco = Atom::create("stco"); - stco->payload = stco_payload; - stbl->add(std::move(stco)); + if (!co64_payload.empty()) { + auto co64 = Atom::create("co64"); + co64->payload = co64_payload; + stbl->add(std::move(co64)); + } else { + auto stco = Atom::create("stco"); + stco->payload = stco_payload; + stbl->add(std::move(stco)); + } return stbl; } diff --git a/tests/aac_extractor_unit.cpp b/tests/aac_extractor_unit.cpp new file mode 100644 index 0000000..fabc166 --- /dev/null +++ b/tests/aac_extractor_unit.cpp @@ -0,0 +1,107 @@ +#include +#include +#include +#include + +#include "aac_extractor.hpp" +#include "parser_test_utils.hpp" + +using namespace parser_test_utils; + +namespace { + +std::vector make_alac_stsd() { + std::vector entry; + write_u32_be(entry, 16); + write_u32_be(entry, 'alac'); + write_u32_be(entry, 0); + write_u32_be(entry, 0); + + std::vector payload(4, 0); // version/flags + write_u32_be(payload, 1); // entry_count + payload.insert(payload.end(), entry.begin(), entry.end()); + return payload; +} + +std::vector make_stsc(uint32_t samples_per_chunk) { + std::vector payload(4, 0); // version/flags + write_u32_be(payload, 1); // entry_count + write_u32_be(payload, 1); // first_chunk + write_u32_be(payload, samples_per_chunk); + write_u32_be(payload, 1); // sample_description_index + return payload; +} + +std::vector make_stsz(const std::vector &sizes) { + std::vector payload(4, 0); // version/flags + write_u32_be(payload, 0); // variable sample sizes + write_u32_be(payload, static_cast(sizes.size())); + for (const uint32_t size : sizes) { + write_u32_be(payload, size); + } + return payload; +} + +std::vector make_stco(uint32_t offset) { + std::vector payload(4, 0); // version/flags + write_u32_be(payload, 1); // entry_count + write_u32_be(payload, offset); + return payload; +} + +std::vector make_co64(uint64_t offset) { + std::vector payload(4, 0); // version/flags + write_u32_be(payload, 1); // entry_count + write_u64_be(payload, offset); + return payload; +} + +std::vector make_file(uint64_t chunk_offset, bool use_co64) { + std::vector stbl; + append_atom(stbl, 'stsd', make_alac_stsd()); + append_atom(stbl, 'stts', make_stts(2, 4096)); + append_atom(stbl, 'stsc', make_stsc(2)); + append_atom(stbl, 'stsz', make_stsz({3, 4, 8})); + if (use_co64) { + append_atom(stbl, 'co64', make_co64(chunk_offset)); + } else { + append_atom(stbl, 'stco', make_stco(static_cast(chunk_offset))); + } + + const auto mdia = make_mdia(44100, 8192, stbl, 'soun'); + std::vector file; + append_atom(file, 'moov', make_moov(make_trak(mdia))); + const std::vector media = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + append_atom(file, 'mdat', media); + return file; +} + +} // namespace + +int main() { + for (const bool use_co64 : {false, true}) { + auto provisional = make_file(0, use_co64); + const uint64_t mdat_payload_offset = provisional.size() - 15; + const auto fixture = write_temp_file(make_file(mdat_payload_offset, use_co64), + use_co64 ? "alac_extractor_co64_unit.m4a" + : "alac_extractor_stco_unit.m4a"); + + const auto extracted = extract_from_mp4(fixture.string()); + std::filesystem::remove(fixture); + if (!extracted) { + std::cerr << "[aac_extractor_unit] extraction failed\n"; + return 1; + } + if (extracted->frames.size() != 2 || extracted->sizes != std::vector({3, 4}) || + extracted->stsz_payload.size() != 20) { + std::cerr << "[aac_extractor_unit] unaddressed stsz entry was not normalized\n"; + return 2; + } + if (extracted->frames[0] != std::vector({1, 2, 3}) || + extracted->frames[1] != std::vector({4, 5, 6, 7})) { + std::cerr << "[aac_extractor_unit] addressable sample data mismatch\n"; + return 3; + } + } + return 0; +} diff --git a/tests/writer_unit.cpp b/tests/writer_unit.cpp index 02a4466..a1af5e4 100644 --- a/tests/writer_unit.cpp +++ b/tests/writer_unit.cpp @@ -1,6 +1,7 @@ // Unit coverage for writer/builder helpers in mp4_muxer.cpp. #include #include +#include #include #include #include @@ -8,6 +9,7 @@ #include "aac_extractor.hpp" #include "chapter_image_sample.hpp" #include "chapter_text_sample.hpp" +#include "mdat_writer.hpp" #include "mp4a_builder.hpp" #include "mp4_muxer.hpp" @@ -26,7 +28,6 @@ bool check(bool cond, const std::string &msg) { } return cond; } - std::vector make_stsc_payload(const std::vector> &entries) { // version+flags (4 bytes) + entry_count (4 bytes) + N entries of 12 bytes. std::vector out(8, 0); @@ -128,6 +129,42 @@ bool test_compute_durations() { "text durations derived from starts"); ok &= check(info.image_ms == std::vector({10240}), "image duration fills remainder"); + + // MP4 packet timing is codec-specific. ALAC commonly uses 4096 samples per packet, so stts + // must take precedence over the AAC-LC fallback of 1024. + aac.stts_payload = {0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 10, 0, 0, 16, 0}; + info = compute_durations_for_test(aac, cfg, {t0, t1}, {img0}); + ok &= check(info.audio_duration_ts == 40960, "stts duration used for ALAC packets"); + ok &= check(info.audio_duration_ms == 40960, "ALAC stts duration converted to ms"); + return ok; +} + +bool test_64_bit_chunk_offsets() { + auto moov = Atom::create("moov"); + auto stco = Atom::create("stco"); + stco->payload = {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0}; + moov->add(std::move(stco)); + + promote_stco_to_co64(moov.get()); + auto co64 = moov->find("co64"); + bool ok = check(co64.size() == 1 && co64[0]->payload.size() == 16, + "stco promoted to co64"); + + MdatOffsets offsets; + offsets.payload_start = static_cast(std::numeric_limits::max()) + 100; + offsets.audio_offsets = {5}; + patch_all_stco(moov.get(), offsets); + const auto &p = co64[0]->payload; + uint64_t patched = 0; + for (size_t i = 8; i < 16; ++i) { + patched = (patched << 8) | p[i]; + } + ok &= check(patched == offsets.payload_start + 5, "64-bit chunk offset patched"); + ok &= check(mdat_header_size(std::numeric_limits::max() - 8) == 8, + "32-bit mdat boundary uses compact header"); + ok &= check(mdat_header_size(std::numeric_limits::max() - 7) == 16, + "large mdat uses extended header"); return ok; } @@ -140,5 +177,6 @@ int main() { ok &= test_encode_tx3g(); ok &= test_encode_track_counts(); ok &= test_compute_durations(); + ok &= test_64_bit_chunk_offsets(); return ok ? 0 : 1; } From b6228240df65e7ba47e016aa609f16e41e682a34 Mon Sep 17 00:00:00 2001 From: Till Toenshoff Date: Tue, 4 Aug 2026 20:48:32 +0200 Subject: [PATCH 3/4] feat: adds logo --- README.md | 2 ++ site/images/chapterforge_logo.png | Bin 0 -> 101681 bytes 2 files changed, 2 insertions(+) create mode 100644 site/images/chapterforge_logo.png diff --git a/README.md b/README.md index be024e9..a4765b4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ [![Docs](https://img.shields.io/badge/docs-Doxygen-blueviolet.svg)](https://tillt.github.io/ChapterForge/) [![Homebrew](https://img.shields.io/badge/homebrew-tap-181717.svg)](#macos-homebrew) +

+ ChapterForge is a library and CLI to mux chapters (text and optional images) into AAC/M4A files while preserving metadata and handling Apple-compatible chapter tracks. diff --git a/site/images/chapterforge_logo.png b/site/images/chapterforge_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bde471a0ac6a01b884e233307a64605880a44731 GIT binary patch literal 101681 zcmeFY4`Qm&*)ch7Wn)z4s&QS!?ZJd07b@Oma*B0D$xD>lZ};0I>uBKzK$+h5yGDjr$sa z004aZBC6~%ckt+%s-vbsiCJQrUL%h4ZU8F7i?bMBlqvbyyxd6LX&r(0@!cxj+c$5o zv8cbXZZu%r8Y6N#`S?fqbG&);#uwY)-=BR^3f0Qb{94bl2BNN*_b30S(K>3qP!&sG ze7>Y&lc~scr2Un!`*FoJ-!@BMb-!1<*Rh<6byG*uNX4|yh=$L{*Ldj2h`#^-pZ|}x zpv|3Vz8Vq}{{KD(cKWYC-;XCpokG-2k!d#zhFb1VOSCmwwKn=Me1wGJAZn6Lk(^-w z(AFzm9d&iS4u66gX-odwP6D|`tEKDHNbgiu?Rx(nYVXt+l+Bt#mmAwvmd1aV`1O<# z58aBi+HyX`-gFodP;Iq*H43R&ys80sJO*Hq002U9q6{egA=CD_g@uJa7`4U*1OUOi zyZw^q^GJ^k+UN6_^amqcfJDSA1vWYc#{H@yQIfvzPiifzVTtlbbO6`Ejsuh#ei;MG`iPVKqZH`0O(xW_$E zgTFG<%YUKY$Rb~kf&AT>@;t;{*ln2_@q{y@#n`|ANnAogO1!F2p{lwCT$Fjwmq9j& zTvX7q{f1TloX-v7=zY_BI;B!|qgQz`3d`OmSZN`8m99-NpDOc}-VHPT#^(_?QFc#IJIn5G_A0p>0H5!lOvp4os>66muk;wUQtRqOCah_nZTU5kLSN)K(@yF~Us(;AAY) zOZf+u@5fS;u^5@xc!ewn^EHtXlmaFN1JTjZ-spfwuNFPc^H)u2jM}J@I7|sBbP8=5 z-&^&3@)0Y4BHjcPC}O$}%ZIcca68A@eP46UC#nn#UM$r|Iuq;}A0Ov^ImUam-15wO zF8KWZy=>}D<-yQ?H=d8cyiR3hr9Y=i(F=g?-FU)V(5L@~=t;HSZa-(WoXksrWqR_% z&UoDBZsw}O&UjW_KjIYI7a{lcbUp+ek`N+up})0;kT;2 zen&Q0Dv6!n<^D89%WNcqi^E8oP>Ya|&?B00q>jWUKlh)&z`%^y_LGxR&oJB<`n3tO z%R%C9q*RpcCp%H7KHKTuI>5A#uYK6zIS;V*BO+!Q9IEID^I`&Q=tcdiSCfrV;fd2V zQ=bi43`P&Z{w$RiS-{})nFUP$?xP-;2Y))U4|Q5n z(k;(yM}c3%NXVn6MM9g?Gd#=?{9TFAk*#@$2iZ;}j-UAveIl2h9rvwT9`iRS@{w&r6rQ7$Ip(8b~PukG35v4wLa4*JXp;|xV8_VaTWxFL=GX1x3 zx@ex(jd;W(tW2rtKF+D>dJ;A1j9`$`hmaE?w@9Nl+90GNDwTF(R3NKiu3* zmm>Y&Ua*9JQBq{NudC^cD0+RK|5brdUyKQ20rDXaXXJKAG0z5PYtRytZy@S-s0~K@ z)S`dUGud!0JIKn)N=!^_-PzgU_IU=pexB(OzCwB-e6u3{;h?}ZaXaD_-&vZmQBu;v zAp{lIBxUY#WKQ^h)#qe5wNcWuND)&#UgZGH?wM*JqvY)zN~MOC!f2#zGU6%Id)1Ap zSs}P}92ibXMb!tF1e={ zg7*!Njo#?%jVk65`wiEloIEj*)Bm*&WK*_9tIBX`dY-(b?P&`w)SqI1iY<#Jb=|jl zT#J$h9vfZo3k3tG>v560l!wX5U$Ha}?u(0y+w2$BthEPV-K>$c+`Is2dBM-|S=dJt zuu?YS6@eh%*SJS}y(k5+*=Tm3az7+57VVw2O{Pz}u&~kRDN*c$hZ%zo3L?T%9`5<6 zaz~tbG9%`wV1H#dXxP4Z`57Kg&hJAhyz7_Kx;@8FN5@`l zKGW3jH6OPe?+f#v&X|hiP%96ug=KVL6iJ5oQsSsd|x zIE90aDfSYmkC4L-p3vo_(!~jr|cG7iSNoclJorcPJ#eIXmyl0*0V?%p}KWNaBxon^pW69%Ortu=1f~sl8oH`ssOR8$0%{~2i zh@}x>&7u=Ue0==L-0gG=TI+zsQNy)F#Dep&8e*%GoKRN~E~T3qAO0(WJIfV5am@N3 z$S@5*(9|E2t}C_Is;^bo;+XKz|G2;l&9T-axjo*Gek2-l7|?_eLmRyF1&>$?l1_4- zE;DNzaDGpQA?4Cp?cIU~93_Ih4FZDf89YQ9^frHus-PHzCU<2drXdmh}g4iT8V-WdVns(UAqu>qTm} zYr^9D-kQ%78-a>u8}Y#jrq`Y%3P`DLQ>`KlZo?`bAyY+2CGdNs<| z3U0)_$d#F!QXYFXQC+`0wIToU%g&|Sk{g9HA^@bJapwfkt-U{L@ZLLF?a*z4Ip35S z_Iz-{?yRODCx!ekaaSnKX2z{zR0eOBEyRA&bQAi&VcL6SanU8^mBJp8m^9XxLGw7R zaWVTor4O4~Ds)n}o6dphp_WI6y%N!6Oq>Z0#=ygmPe^!{1GG5boUEqT-KTZvhVl$G zekVVe_|4IbGNFA4uhiywCU6k@cQpX(nh`KD@$WY=WE0JS0pJf!>B-&JMd04!D^(2S(f@&dtt2>#-~K^{Zg8sf_7;eS;413*|ttTJe)P01N)8hbd1u_Ry7b_k4bOba=S2 zWt~-Fyl4gBPbW_I^U!eJ`vUY#OvGiW|JP{TOAUKiLv|^(2MXw56SYX(L=_z0M8ha? zmI)??s?AxIhS9s3fG`j-G^DH1YVrG$Oa={2p>7KmEiLV+hBv`~u+CJI`}I;2G3K;s z`sG3ShXvsyVDN4jf}We3hV0w_dz8)H1v3dsaLA6v6o6oz^YPMha#~vRv&rJM=;|I}QFeOC`ba@l^;6w2Q28H(_=k6WoWGpskE<#V zF=&ZZf{Bfp8%ncSJ5uga?zJgl*)XGx@*moG#M=x($Q`Z8&vA;N8Y@bm?SIkq$*@mmVrHv+_<>O&enEH{H+Gu7vZgU0 zIRwq4<3T=D(~PUH?%{@9tq98#S}+UI7#@wwFd;H|*}CAsRumnL$WfS@v3!gK02Wg8 z1maM@F|h}mkM4XGczWCUVD7fZY7HUm3YGfZ#H*N>v7l%!iT_P<-M{Jb-tmm|T$E+T zI{;8ix13gF4|@bP8@PW~)lp#o#3Z(`SF*yu3`o^8FhHa=%nAZLoJD90q^G5(UWCjD z9MyhCXzh66jU;q3>bFN}paTvo&@2pGYH5cf@bAt@Zhr@(s$_Y2<#a-a?H=sC#jPAx z8D(;FDU#xw$LRT1SvvwjuZnuJ^|8M+Nu~avMUvhO3xZpp`iUC9XhcN9K-&FnadvF z_y@V}QRry4Kafm%POyroTHi2H?JLun7TO-Mq1ax+ZOeF1{HA`NR%B$P;RIBMl!77! z^@JM0Ec9@~3!jq0)1M}@T>DfA0QJYs_nd%PO%GBdt7P?e6epg#Sci7mLBEz7G($lD zOC-~f>FS6lR%VkzrWPy#i=3rjTltAg<&b<4Y%&}5$y}tJS0OF%Fz!3j6(@ zc^H{FP5aCYsa3-vu<>rMzy*$>nqRbbp;5V=paCpBF2*G8BZaO&Iy$7?DVS7x zn>*5EKCVUmfZsIHq3DD8#11PQ>GGq4VpSx;uHoPCwbZ|q9jUAy5L-iQfpZHp{OY{O zc%*<1=9~ziaMH7cL=Xojr@xeSWAH!%oA(+L^-b&hzc|mkw!Hjo9sY_lD1h4SH22A9 z+Mv%Ed*S@3l?}QDj{O-tetkEWbB{W2Eh$A9rV8Olw*_YjRt}G6L)ddz=V!Xe2UrLV zv8|+~=D5SDIg|C-)F@)YC&rmd za{^YK#vg26r=jTpo2TacqkGfq1^bahIl)tZuZPq32!KeDTVyNtb4dnfm(R zKZ;`I*H4@;v!B%rHB#65EetYe%cBBWcv=Xio|jZpp&NkWOXc1j#_U$)T%9e~3LsHS zAjc(niET9NyPn#t(sXwAvF| z9yVJLS2p3HyZt`$72gAbQfqTnR{`ThbqFL;;YDZuN&OJdhv|Q#^nbu71DjfeSiO_E z8~GQs*{Nd2t2@w~3Mswb%s?riCd>kA$z5b@oTnZ+pM8>p+GLq{vw63~S;2DF1X&?$ zfS3K5#$Dozdrz%Kms0@*kKNwrj_0-BRrlt-qE~~;Wo=y}u4(S&cr@d7TmJEqt1v+N$p=Le->mspa*DL*mcimGSJZ7UY#s3Ebs#E;tL-d@jye5@^mF z0KU|U@=<+RGSScp`M>-f&NlD5_^qu}*bXfcO~q&r((L5nBlK@BWiSDnbWC-5SBFm0 zz|e_HV|j2&HJN)r*1U)Padg-?IQsB<`8s?+DxsOOwT@N-_jv&hzK$9dmYScuuB#fL z88CYOQ4^DF{r>?@8+&8|(&g!yUCN0ZCk!rhNKv~e+ONJu0VSI7fq3CPjIfPq}azAfx>Ek9Rc8Y;4|4KK5{6d6$jk zOgUFq|G1>4aSh2z*4JBH3GfLg?uLi?KR)k7mZO+48;rw~qRn0K#+_oqClCd!#Otk; z;58V^(`^JSE7b15L>)WpCY@4##Y$k=M();AuQ74AO*nYa9ZmsHyzgEu&pil(RJHq~ zB}el$*3UkE#F$tr*WPM@3aozr*tfs5k&mc{tQMW#YTPr)nu{#{pc6$8>fI(1UFwb=9)`-0x%y zn6N-M>hHMm){pGR-|c@aL;TQ4_1n{E&Px^@;o%~gB*T|u4j znt5L~mTz9l-ST4imy+Ef$$!YA@6ODHt%k_Hl|2OlV$>E!hEJttdMse28;twH#P^nk zV|(Hbb3F@4isXxlfxvE!>~oW&}z<69Z0PRI(s5IK2KZ&=rhfqMN$>BIEQd;8k4qyCUy3&SeoKTxU?rP+WWK zOq{4-H9MUC+(0+{Md84~Tzkuddr_~l(X1MHmdFeuBvjhr5kzjFCM)k!W^(1|nnpVk z%3TR(Obpk%!%4}>@92HxWMyQ8>KyN@MSjlUdQL=ba_+wAC>Fg^p#T2u?@9cJW);}` zfM-wnRpvaXIbSB7EC#LfqD(uUt;?<0$us2-PT3j<@g+HD2alZE_PsPAq;Qvcpgj5f zW36>NY86PqDsdi88%Rq_>$$kxSOK1=VTbc+@OHA#8*o}$>Rmr;mLrc$KDe<2L+9dZuPU2wqF!ZZcp)Fkom+!RFPA$Z{f+?Q`r<| zVXBL%@3h_2#!J7m!^^XyK*+$B$E43jERrsvVFxJsOB<^iv?>dlf^?k8MYc4eDp;@b zhY$C=yT#c#fnl4Qn+@>^B~N?t5Y2SJiRbok>LUwIGcrJO`!!-MD1`BdMBO#x=9>ZL zpPx?H$8S8Q)I6u)2qzea0%qKGn##cHB&|yMA({Ky_UYkP*hkLmb_$EjZWsSQqZml# zv^($9FiX_Kr*C4~sEQNM98T;9rIFuWDE8?(-BzLw+9@o8ouV#Epw|InJQcI5`B()X*oz6_L)XjNtw zNr4pUFXEyj4@*ITOjlrWayV(+1E=fb6N-f$9B=zS!#htz$48YcVq9bz2&Daf>AD#V zDD_$o{A6$GFcTZ+SLl^TX<{#Jtm}38-SWeKSmx6HwKye?qCsp%`k@Sq0A9$5sGnS{ ziZn6>H)j>2f*$nS)_oMK+sGg+W;-o$i-pO5Q=E-c|3^Ak6)PQdbcZ5BtP~ zF#Z8ahWJRHq!1nV{So@GUDUuIXh_7_u~3{?sJ0lk%@IrFr^~wf2Y_+TexH@ZdZ-4Y zVW^N-(6CW!-weSdy4HOXZ(m&y0?{kVV%XHAMH^j|}{R631U zJ9vJ*v#lY_r|ahFPFBUwewBdF?QmP;>70Wo{KkE!oqRf7sk$zl0;D!g;Dk3{2AyPI zm&6M_ag6C}hZM$^i()#O)on?@sr;m^`Y2zX9(zk_Sy|Z-C(wWRRz0HA(&`@p4CM}E zu_}_N=w5ARITcd%N_@vyIkin|5i2ra``Ovz)ZLrX#0ZY;1KAFNmx0hx#gJYK)L?hV zO_VDh>{3{ECCy2TV||AbuJ?RE z(2^5QON|k%4fC~<&t`<>Ym$<}_JAzGk7sKNuq$cuSTb0H%eOu|Fd#8Kodi|&zr?U# z15Ty$H1B&k9v*YYQ+=G!y4Xim&FCZp=adI|r2%3uVKG5#ZSmUhMciM+3tI)T5xm{4 zK(Uj+F!0WM3j3cYh{P|ynRMMTTw9y8aq{^wO4iRs)-jxAHMh;V6u@V6^mDjWrmE0L zeEw_D)g0JLWcU@+^&5G8K-Bc6&e`{_@FWX`GS&`L(r-QQx%m$lm9aHt3T)9yOG|P5 z5bFEUq_4fc@y#t41x~Tp74WL>Gj6iO%Al+@8I`!0`Dig7DEYiLzV|5h5Q<8GF+nRS zz1vW~#A1sYiN%5eSNwiflF)m#{o?}kFPM^igR8p%TEnkDf@RXSs))gZ)k|#mv;k$r z{~3x}FTuJUvDu_p{gnJ$+APM>Epy6$pbmv^u;x2+oo*XNMRM|1nZibl-%qwC^7$(U z%-lJg=7pEl=1rC);=(-C;*H1Fg!(_2R$Z{vZJ|X_RvCsl7}u$aZ2hIi|IJ;X%h3u; zGBx$FcSRvqcfU1%3H2~eK}iJBKOuTlw(`03bx8BK;k)=KEDg2EDf^gz+r>@Ui4SkXg?`u=_W;G>fW=HEm4cQBsooBbM$-Jf7NA16i{0uuCf zJ0h;+8|vmhy`-E`9pMwr`;%@taqVge?I?sbTJo7Of}@2U)J4`lS*)^}GV3$(d_;F_ zDI5Jk_?glI?APP2!CLBkti$-Mwt1yI1$3H$D8k~hruXaS)wJL__>Wt2^eLi>qhy=T zSOGKo8)!dlHD$_H_sP6gnXK17wX*Hi4-&~vi7*JzNm=)q%Cr;un()$*YY24oxWxuY-b~&VCQOv)r&!Tt*RY&dP1@iQLCh) zXSZtho-9LJqA;w+*Xpse2k3bFs0~DVKtsx92cBNVhq2zd5F9i#2AH~-a7!Ow=04|( z?{J;sabjYkrf!5%*dr0fevS$7%yBWce`q+uJ*s<|Ch}jlqf}CvIpZp+Gmv4obcSOv zU9%NPlxk)^XYrG@lNq|ea5=eMe_Epb30qJCzxgkN<9#HrMn-H4{}F^kP-00Z zU=fy^RDIonGtNJr_)DdKjggsseny*jC4<4WAovmoYf*3ntQf?p&!dqMR37ph#2TU{ zmOm9Z5Z(hZNb4vKkbl-ULdKBFVbp3HqwD!YWKt)SrE!w%3!0EhVD-%W5!3Y^1|fa! z6uPE*!XG|0loP()U;^B%wD}n|+C>UKVwWJy`b;&|kNd_}bYGUj;Jo5LWN3y0 znEwvPy>kP^7B`soE7?@r@!4K`ezq==wIwI7jUkr1Gi|^BDX^rX%cife8?&(#gfXoK z#xXz%7}V#h;wdRG|gAxEU z`aO#b>x&(Lv_|Iq&jA1 zSef(pdG=Y=hA1;K*&WCv6N0(1je3t%%jzI92Cs}&8!YDum4tp6oAvih30oJYzP#`D zq(D6vt?HVCQ#ImNWL;-xoN%hP#n8ZDO#qil@Wz_wCjFJ?9bB<#ujAz6@)L5us@+fM z0of)jItj&gSxEo4B}_+y)#*MkS}AS5>*o*!rTL-S)1%w-+4h z@I0%DHDkU)QLb$g`yXM4+IE%qZBCfy+fn!=#3e*sC7b`gVMi+EL2EuaxADV-(Sr%t z!xZ9bh$F?p0v=S)91?vqP~IJws~n?|uU`avy}jer;km{tHM0Yy!{;Qb6-V$n#R zp({}7iCdsU`A>DrFJtE&88v>)v=1B>)q1Cy&73(nQpb9$5R`|N1XJ#azZG#n{YoSY zH4C5pG*!RCgLYbYN>`$6uMQz=>s6nQFDt3aUidvXrj>wgxTQ;lM91UBprUi)JD{Z* z3ED~)pL!^68$5etvp(nbvlO)Xfg36TFwzJ6>uctQYc6Hg1?c0MH_F`KpJ( zN3?&HdIja6U0s$VTE=ah4u(rv8PD1xZ>(kh>U zuaKdSX@RvMTGBz^e*R!U@xUc)^DqQ5U!63fZYw0K{j8Lb4O9#Yr{Fa(GD3dzdVWS* zInZ`qysSXOeKix3!uS#v(-^hCsc|@Q<@xqMQV+jAeAkaJu3NH_YvA7$#Z^^kD5FpY z8U-QDYz8i<*ps6o)Z%E3xv2GI#s4TkW|AnNpi^@dlW2dgWcZ{44u^izGbwiH%m&u> z5i=L-zT1C_3ONg}L+gB5@q5S;xG9AclIf`{erzUz% z*nN_{*#hlC_CX&%gs!gp-ID5KwL7Sv5;v> zbVoDO-wlNSexF*OHBHyPbbs-xu|R3n6~6O4p3j<2B=`9xPkQTTkmY6~)8SOZoYG8C zx#EW=YN$*Aib6~?m(eGsdX?$2*B8UTjrJfNaLeVK2Z z2K?&I%Wv=nSNkyx&Vo-W2GTF}RMjO@K2=TczvG!=AnWVwM22gxQQNn0`psjxS*WGn z{rafE_Pn(8fUB6X&U)bTC!^{=#?7qzsp;NoaC^b=wZ-ox<=p09H}IEy-F$JcyFpwB zpnDIgh>xi%{yC8~bS1a*Dtc~mGx96aOXGY!6r>@u@vTtF5_PUcIe>A#?cOvK>{B`{ z_E)@2@25jg$73xOrki=_B@WVDb`n!8lmC#(DgQV3bh%EwVQ?{OUK;`q$d=?$!*&1E zFj11tIE<{0HI)Tcq8m9jrm(>W7fRm4MT*Crjg5_tqixGs#ji%kwd=To7bkufPQ)c- z3dVJB`^QHK^FZj_Bl)RbZbtWoX?7dr=vF&%4`yB9r3h)AOGt}4D zH)1(oeV77RS*WuI{IzCKrMqn?cyL6j8F22A`4x5CetUXDnk2rln~Bf=-Tr2n*hV(Kk(6@ zZF>_bZOb(bCxQp_ZrQY*F=TV1Q%^gM`O9B%3GUFDDo|@V>W^d6{aYPOD`C%st!!xm z&cmABXf01~*0hu$IV*v8P2Yb+ZM$-Yefn+iFD;L87f@EbdrfNfZ$tnO_kWkir_$Oz z9vNbkeG%&+jh4*F-_g<_CK*O1Q&vqykca+?p)DofVhp_N) zeFq0ZwPLM0Fr3O87%Kpr>{xq-P`j)k2%fYe{fTYQ5x!(- zetxsH~O4_L<%tJb)#rh6-*g1LWro_a}_& z4QCb*Fu$F->j0 zlxSG1lYA!MGq2IUU1<5i9yI?EaDDTNLNlnO_R_QV2b|0(RdL3B_s=ZQr({l-OmRM6n*HbIU6yy3JVz4d?<0Sh!fhr$-ob7 zOahA~JIcynue&)LR7;4IIG&2wTHSZ(VB+PgHqWtW6f-rckj@x^dTQjTjT@`j{N-(% z8?RV0}f)tPxGKj==P_;>BAXoey2HDiMh>BgRO=oS{kpiaJy*e z%>rFPsg1Y^JlAWM+?bYJ)^Qpi&IY_GTK8WFU5tt}!t+e7VSD9cm z)ewdmwd{|QZJdn8rp?A0GHv>#D!FHT5vCyxycIV6z*rLhFvY>NzDsjUMw`J(<2cha zULZHZT(NF$D3*0MD{XEpd@3G~Ca=sf-6CE8~!2vJ7%`IyRpUm?t_UQwObhU zLC4T9CTf-EzG3-z-tY~!WG+!@U2$2KzG8w}Dq67e)-@1Aw6_-Oq0`|>EkM=Ky$|f_ z4^*1qSe_F_#moCs48#yuoAnv^TE87JkgB$vl~rh|#T8~WgNm;Y=l@f9t-Ik4+>^5b zwr6dC^^*7VqsRk1d@bwK!0Kt+2gRQ)DeCr%tK#8qWVqZ{O1w(w^a||1*MGy^6%MS( zY~GaLGnt8t0~yDu|V1ar`|R2MhQQt(RBf`S;&bNkY?KUxW+z+dc5o}g{(Wq|9XeA;nH4bkL60N5(-Z&2b z$A|_d91a(E#o^DF6fc*~fE7&y>7>$g;^jOaSui#g-YA=Re4-_@P%1EbtMY+xW?C(| z>3iH}cVKF@xlRB!+2!6rR>cCA{W;Py14~TeJ~o2RjSX_wO})Bx>uhWn=EUgCv{g_ngE^CVIxewgRE*tZ0a+h_E|({(w9jXwnV+di3tgs`Y!l~3iiIfgF({e-l~1e>_D9UqIrqx3 zb(ma-p)@t49vUofFrBi;e^{7p^ii1h8}u84csycmzW4QuE=b_kwO2pMb4L1+W$Z5$Clv7TbZNyH(K`5jO_{qNptN8yR9a#k=pXD7#;u9o zQry;HX#uAYR=_O2hMy^R&u!4jkdKY#GdUh~RgFd;JCb@kKUebDPgRu!5YMyMTz2%1 z`4aTwdL1%nIWG5FDYL7*$y@CNe*NG`A5z?sl*hsKDz7agE`UAbzRr;wZV#)yglT3H zo@}L%Jk$8FQSv+NEdTwWW$CI5$qr~6TS7}#L-eVuoO~n9rkWJw7CTI4y$Fi$XELsYV`mbtGy!!)G95Yi`{WrU@ zjWRgH`A#Gm=pjeSL4ix9*rV?EM@&0XIXBpms?vN&+ljzRaXt`VUCcr|7#(gY_`9sJ z{matE#)oad!!TS>_PT0qYpAY%_ak6CTp-FWf&~$Cwza(-k&T%d{r=x10K!U~6E=qq z_wFntD&MlZ^%QzU8LpMYpvV<|K*x&Q2_OF^XORE0c~8k(nb@G7r|@;YAQE7zf@-9PfB}W)dDIbG!zO=zgHzVI9#^T5OhG1N1hrW zzepvbJ*rnGg*axvW1YGDu)mo48kWsr6J^pm>BD?ij~ZUo(1d-i%k!u1{=?G2<}q7` z8SVnh2uJrKvz#Yo)ix(!sP}8to>S5Mm(s&D3R;hTwUf{X1*g27a}Vtm6a&=?IH-vk(|C0G^}EBj0_TX{5s3Q zsS(95p)f5~66vW{nILqRMzX{bAUGvA11Z&aPI5buq)(62=?#2L+$Jd+v{aqSg90H zey15tRK?I=nDHT|3+?($?(hc>gA^FlSJY@{Kae~7iSOjzk>W|~&|alocY;zb@S1a$tE+3&KtJRzi>#07iy;rvt|1lrLa#0E2#R_E0cdi z=0eKe6=pzzE$rQ~|2Y3Xh8N^E7CSse7_8A@IHCnC@-RH55*xdoh9(i*S3AAR)Sgat z^DguqD)i3p5O#<$+pm-eg~Gga{o5+5?egiR88~~_E5Cdy8YeD+YRa0&8tyu3|2!EV z(!HFnn?^o%>v#f0gM0@9KW3wo` z+pNA03hzLhQpOppahxS6-}%fW{ug&pW{+3t@v=E-PAS&>TkS4I9|K+wwUmOk(fSuf z_lLYv67JJom8pC$gw>`h^ugxRUCWols^A51zV{2AXhkLWe z1*r5b#m}j#-KQ9n!LL^Ry;&i7JpY@%KSt?R{s3h7hVhwQLm}m?d&+M5EupgOcsJ(z z>2k)+6+Ei-%(rh<2eXqY4zGYZeKR`*ZUP;x(Vtdm@# zr9TKnrxZ>Xe@vdqx|ce!_q}vDG9+D*GzDIBvJzj}7uc%_G!J9?kFDZg6yAQ?y;GoK zgGnU&hBaPRYT%s0WhA(iR47wA;PVsJu81Wxdi?S>Ag){_h}k?=G@P9sGK?flOY{|0 z!)g*8dKcbe77}e?7@I7pR05VD6`(81B%GRA%B@oV-TO6w3@^H-&7hnLC=XeRa27{a$0WKBiUiZc4dai;<5tcG;%q7*j-z!Pu1K6S2s{ES4~aL-!Fl zuw2KMl4BXa?uwm6(5voM-8YihftHVOflN<^N=S=SA7wr(dfKWyQ!IA55mD`!j75Wm zOSSw>apdmncvTonE4yNL_mtT?@I;p=iFdvp#K1CzMso35AYd#5^(?@G(`iS7Jpdm1833?;IBe3`w zVkE$rlY~|@F{Pb!rpz%{FGQ2Rn0?A-ys9E_G_cM!>7zEM_SOJY09xPAt5}H5n`|x5 zdoxZP|C-IX@anELfJXZDd1Yy_7yp7t#MYFi>X6Ps>21SoMAcT~N47%`w9F`3^Jut5 z01o$IG>7C4-?=%oIAYG%4lQ%ueAdqFnM+KO2or@XLil%6E z&ypq*fK$(zaMsqQx%L$Zk4ulGS#12(15Eainb&^xMZge#D z7TijfH>_yd51Uon-C{xYIh?PVfcM~(=S?fP`w>2IYfdsf1dWt;gBT~`w05`1!(fh` zWIhT~CY;hrJOMwFm;X#>AH5~2(;m%RYd?&zF2A+l>g9zg9o$&I6q! zPj}r)dIUU8_V(hj>UvF__u>rQXLa#16*S56ssE|g&Rj13ZK_mERED6Wxvp5C8K{42 zp?zFO5VTjC-up3kgtS&;jB%`gDxQSGcDmSHLuSw>-M7FOs#iJ1CiH}V(;~cMtND!dAa33l3WdfJ7XLM=e6Jd6r&BLxT}tM7 zR@%qdMP~^f%Uf@e%2)0W1?ZN;^1!nviBQ((!q;PFp8l8G|s;K}e`x?^8wQPXv6%(>E+ zi6LZ=lDo#5R|!Xlp8+;^Sto|LySsl_;37crI4I9G=0~y-Jaw{ocV{Y#3S1o@`IOSK zMv()U%ZnZ_ON7~&8mh?1h{W3A&TYWT`FpvvhiN}5x%At?ZC0WCgHvmSlb~tq1)r|X zcR^Ds3Yts_Z4M3ni}HDilraq~cCdI`+1Xm2ZxV3Oni*LZ!JX0-!aY6IHq1;YURt$2 zVDUjlAZ2~3q2IsAY)8~nQ@OdtE}Uiz7NX6sMhK6 z*PsE(q+@sNs5TEStuWgZWhf?m#7<66pFTf7|KH?JIxi_hK0+5ryGcJPW1mNH=xTs% z8iUJGCrGKD%PJCsU*y83W1v3mI-@znfU^!T<&k$13vdtC3y= zLa{0cl@#yC2}Pn9sWjA@ycrxa)5S^XQoMk5(yoQT&0Sw1RK|+2P#f9+tJ3B(oExbM z7b=wWgiuq!!VNp>Pe-(lZLcVvN8T@cTqR{KW3R};3-T)bw(_v80;j9-&jj~WGE`Z= z`FGT!&aR|C$2LsolzVZII<7l-diY*wy{&$YmB0nOJi6lv;*6_C zD5`<*kt!pzBF8IC9n=wahN!RG=y9KHHPT}quW99R4-4BLq)o>%Y!s>ymHg)_(F)X1 zr!Fr#3laiU5$qz(gQhHH3~pbVp*7sq1u{r!TUPz}TBOR9#M$Z0lOl6q1v>Db*uJv< z<6CYfS!Ryu9~VbtxztDoXFI$$#VM_GWQU(#`pg3!My7S#anwq5E<^ZZ%$HRwvr+L^ zPsh3|t9PLdzz#{UfS;i#Q z0xE(%#a#NfM<^E9mzZ^u&@XiVaB%A!tj@yy)VcoFP)0fr^#{Aag0SwB4hBj;1t*~$ z9_NQHnGz3O8e0Yj>fGgNXh~LmxmmLzbCB1jg;`M%!Xj|~e;yq%I>Lbmw<5u>i@JBPmB!;6fKvI5B-FbrqmKCPnAou1&)>`)`2!LeyTxVg z2f2LTrmL~It^s=R+rh!C!!+5F5Do#8X5tYgNf5##2R!5fBXazdQfX3=(k9Fof*&;3lgPrqvRW;7F1 zo?QXQXeVH{mODu@tB*^kW0l^D^@Ecfqm_^p%$d>*Sx;F4za=M+?g$|{i*+{In%%J& z1~(Ql3r!O5nDful5qS3R(L=W;pF-7J0&1G8jT&`I)b0mWcE+E_DV-NiXC{aH+&3cL z{N)^KF+P%U_Upyu8pu$Tv95tQabpbi%F2r<#d=D z`w^9NitmGptWASK7ycXk<=#wDg%5q)G{y8>Qf?#I!gIupBR3=QPqG{qr_LkbNlm}?u-Dfz1Jl_bYOp0N% z#lQE>3BqRK>n-h}IbGv%PbitPXg~CL8OC%xW}{#`P3^%FU@rTQ2dCUc^5-@&5VGO7Ha6~or%{2SSs>1khb1br+@-f3^ADdfD@=uu%z zcY74JGiw#y?sG6S>xg|s(9m|scGOGd2zaFA_B-%fh-9X523wKDt=Z5|gGD}lAP5MO zP>S)?#gj^B-y`2fV2IZ304(WDFV!`qvtCwu%yd*!-o$DxnSnfi1Qk!NOnp4FKebD^ zO8NyMUm0O-&~{vq=!LRllzaC=lV=r{7SwV()m^-*%3JSlyr}lpz+p?nZ2o7rTfPP? z{!ROM-}yGVL(_>sWORinXr%*~D_;j-OIYS9y4CsLT|YCp(Bd|g9K|F#1+x)V+JjPI z1yfOXk4`onc$oF+8E`zV)}u5~FX(y&2rHZ}{z8;vsL5s|EB`bo zjM=t+QRnHlkET_6+G5W~s)S#on}zQg~|owYI_ zrrBLzFRrWET&cC^w{OBqH^G)JOZnLYR{HK5E>EhFSY)gkfyq=pi3B8i1D%KC)>rs^ zmgFs?h3dh0_2rE8)5BqcGC>!LQ4PbI;hFxMud!6!*puQbcu_Xx{c@}lo{AY3h1loPB^|skso;bxwt{;qOQ}WA1y5{ zH7VKx(J}qcIBym(SDlT=# zB>z%O9q+9K{t?orMW*BuIc2!7IB1(PKV`)5OS-aU54cdxG&(cf!(x(*11=mvLV<^E zXV)`}VKEDv0n}-t|G4dpD4sA*^r}j?OVUWcJF00S3U>fvw+Iqhhv{3 zCQe0_uyypsilDMf8lwe?6~jdQB%ZT%T|My0N#dsYNp2mcr=_RD5reC8ZyDsaUD3Sd zp-%7^<7sAY>0R6&9C!J2(Xd+Sj*-su(~Lc7oKHU)-_YoUh)m)Yya;QS7noz}{Q z20Zp5n>=o6593mI1FAJh(lSw{vqTBg!W!Q+mht2m^ef@L9MUg?-)u5Ng|~93$g>^3 z|7#q6TnbgC+RZIjO0y*zEvjuNuAGs0Yhb14K2Ys)bOXQm?z-Gu(1Y8NxJWbl^cC|a z^-aLCt+l%(7wWAr20f5oF;!Gt_&k`Dbc86~x)TZFTJ}Hh?+^lnbpLMxhy0Ex%=C#M zq7{1&a$4z>V6niycVCr`8T9I8ReohD2(ed~+x?+1rKRCvYWHh%Kadwa-Rp=oa&Jv)0-?maRCvjR^sg+plp`N}K0zS)qN-GJIm|Z!~Yo(P$dV zI1Q)EWH3w|@ZzSqFo$P9+2i^<@ zKI;4c>;|`A--Z#U&`$?kcLn3_Y>%1$xMT!&hZp4kdN*wk)E-E>j33Unc+|l^>!ehHP}9JBktBXtLK(EIWOS zR1uwJHRsu}jCL&kv*j)+=oTApBUmA)ZNG@~Y{$uvztVoFWbjE#1s77;__={(#gK~z ze!XBP!u{1pEii2b2iD}LgA{LM`qthT%XPxHW#s~m0^~uxD$1sSXY;BEkwXHHx7XXT`tY+&=w;irIMaRT@rDvsLMz zdL!OT4!t=* zRv4hS6_(ly{@HPSZ+dfu-59oa4ec=BrSHrAkrqzS(>L5xHuN^2GB%-y@$W+n_Isny zv0K8xKKO47qtyu*dX}h@$+AZi)uVf8gO>Ako(SyMWSMkxin`%I@Pg1&bi1?P`rCs1bX8oJwM?6#k*fj=zbXpJ(>u zaQy2lVYd$_IsYQ%`Vj>%?EGjd8mxCiTUM4Gz0tV>XE@pprbf&El~^!9jeZxyj@aCR zE1m6uz{e-i^~6{IEl?Am1?%^Qo3Ax1H@uC^Q&S77-v;&_ypd_7VE7h{&FJN>`H+S*6MnKSal?iX<(9t%QD8|hn(6PM9>Cr_|Y`~uaBz} z^Y#hu3{HD9Rm_<_yTuh4N6Ijxwg@N+hHaywYp*9fa{O;HpcwKBW%~9?eYE)>E2G8I zAGES2*#V|7+2YP6!wk=VoGst&BeXdC>xW-)iUVl^VjjzRKBCn^*^#{ytqhg$z0&RmCwj7yKhOB=l zyt&}+GiKzvf1h~x`9DKZmw}|6u7Gm$91c(u6bBdO@9lI?jFKB1(L?;bL%ZsV(=R5z zDWx14J`d+M`H?7!E!sak6rZOQP+2NJ!A~dy`w%>%KEtl z0<$~c`2zO!`GvRL(pK9Ko_$0YrhK63xyl*SWYF^Kb6d1G8q#lY&;&W1?&(T)3)9!g zH_?2#{C6b&`&d`&3r%EX`f(_zp@03fv$c(5qypR`c^A4H20$(hcx_mJfY@9rn^ z{-T2lLD>-Wm_P6xs&QhBo zwoY26#@eVN6(uu^ZFKmf6z}cuxGE@$+p=lA(d7j;Yuun4+`|=CUeNM)U`i%OujyK$ z%h{oS|L&mri$v{))WX5{;tWySvymFBZ$BdeMI8jDskMz6E3#aQ{{HvbO!A^G2qv7E zyjlD8t3F^Ei!PH3e9{S&sy^QBwgZJZgyDz(76GwA_BW}d&t)v2pC7tEsz)e47bS*{ zZvT*-JiRcShN#f2|8&5DE9o-c=)aXM)_Nf%*mLB_q9vC9hrPpG$x{-bm|gpWL_tIC zfMRCaTB5r*zr6q1Qib$THO&loM`50lDviGqX~hfLo2?1MX_EW0OyQap-o#t$&C!<( zJ!>-+dZ(E91X}Hii)W8|s4ci;vA7j*wjRv_A4*=}k>Rl8ZyB8Lne2;>a+0DoFG6oM zcF$~!lmW(Bg_HJ2oqNDw0&DQlvgHc`BT_E zWvsqHFv-TnJ>Br}fnF@s0vG#N?Pj@eG+kDTsK;K911RujSAi~x%lPz1ja$kFF>hH& zmX%S%FZvf&hUUB+(0&73VKn7jyaBjbQWFQ^gPpcCxIho^u6n*cUl_BOq6(cDqHlZz zsp=j*Pif}k$UdFM8(k^!^0cyUW#W*@-$;lc?Monnd0UL;|B`Z!V!e)jn`KpM!%?>? zJ7|Z)lptBUNQUq`yxtKO5A=zCj(WUL$ZhUCXlRENaw~d>lbtPTU$+1!F}tUT0C2DS zD#|u+eY-~UZsBg$hrzo4@X5yjZ zopEwJ(w5ogGB9kSq>PP<+Pu}w0mJ#?EgxIB=hsbei0fA7T_LC*!)p`Z_f*?&N95PK zyH_7Ykb_u%41tRX#RT5t@gstbj#&f-$2?$D?JzT z(K#@dKq%lIqDyE={%=5WejG$Ok?|-RJDq7*lm@`#nTg8Yk=;_}(8B*X)Y~`prFmM7 zB~V9j3~>_e2B@meObPDth8 z$Go_|df6)ikJw_X1vY!I?@z~B7hIaa(-~b5;KWUU-jK9Oobe*^7vg+jngMVBL~21 zGa>5v(cj;8%Q1}@Pxs!F+lnr$em!MFjrO5^Czy(0bX2(b>Mhml@UEEisD5N`PGjRE zc$!l3*&gKL8e!`)F|X6~{U&xlT-$OqEXulI>p0Qn~%q(A^`cH0-Eaji1vkED(c^(#{gqn?lx1mt3Ootpfc*gW&-7z`KV;|r<&L?h}^1pJLIETqR0 zY|pu{+Wu>BXxZe?PYQ7$;;~Fk0aV`6fa4u-(k>FM#Ab*V(q^R2-SYG-LafoJ_9+X0 z?hlY9lGht{;DX70SUG1kbsfxRT&rG}$epDJ=MkPYe-cqMx->6q^>_Z=cj)_%nIA`# zTK^i5^I{`#_8x3CV#j0=EDu?SReTV@7N!~J#G~?1{gpaT^l~uNXJW$FCJJhFF;n%t z6*5Qz8eMMl6+CD?M%sC7PR>r$xB_t!_S9fyZLI}foUPA1MQ8k+PFg2iqq3X^Z=wDZ z$upem1N?s0v~7)Kd`&+~aTcWeb8bF@#7`dVGx%@OBEP0^o2kVfj#3ii^X^~%;G}I< z$X5U>r*1?dYqt^bE^~uYbAzNUtmnbZ%>J_=zO{cl6JlUZeixn^4=a(^>X}xss^*9R zY&AZ$s!ysLj^9$JbQBAh4vn!~qyPL-y@%}qcO*b1A5~nuuY9kPry53&3~_-#S%yM! z0J$Jx9mWjozd@erVs>#3{JpjOV2k`(>+C*D)MTpkNtxg-_$3tu8!xPHYijpU!y=-J zJ$muW%Ou>WDkVcPhk2dtpN)qjL)2TqhCuU!3p^*_^JackXQ`6^Iu+dN*%U!5zm3&8*i;NS^^VCZWd@4LpUI}y; zV8{qKEQJ65J@yN9dsS~gAMKE~aLXYDiF$pX67Fmn<{g}p4JEUbd|BxG&w7^$HLP6i zJ4Rt{^y{RkqY&oEe$Dm`wppu{Xsf56GqkXPYbNkoy=1!+=E)%E7!+&4p7b(CAX`|* zsrGz40Ff#R-gSS`_QC1zx;&_JZ-@n_({st2In#T0XIb3ca-r_1d)ljnw)Wv@K$GOgOIDQ(R;b4lzLRBtMU;H)J_V22qSI*}YTnJof zqiQ!bd!9ZO9eVzff&8PVr@V#>l-N^`97{G9Mj{4)XlB4|6CSI}?%O7gTL*?-w+pTS z6gC(X@TZ%K%z0yOs%U9WV86+^sKj>Dl@4%|q1MJ~%vzAHt;4pQn(-kM2R!Kd$*tr! zlj5|$&QjI{|D}3tuoqM+WgJ#Pr5nZCjJVvWM)C0Qd;o>rq>KNAX>NuZg7BewlvXj+ zAWhPPEO6`6+kh@P`v2iZG&H~rBsBm;;mOl|{kul}o>by0z>!qBKyaT2sSLHiI>9R( zOBeq7HlSO%rYbPM7&`XT<2ywy2|_oKahdLtc^Vw>vPOefnz6y$pZSB5UGi&-XT5v8 z5V-k^FCqr}`p@Bh#)dd-VDX)5zk{vD+X7WQ-qS_$@M=#E20l^O$Awl~=fKN>6+{g> zm~mki2QKKwscC6fq#(LG1d$&oiIj_MsR<%h;@jG|)?PVVY`@#cLCyf_aAU}mE;x$r z6D_4=fRZb{h7riJ6|^7qPw6G~Y3C=nKR@4fAYad);fYwSJYqxMtUSQ!6z=r4cS7-l z+=F$fXF(((3z(jdo;i=OjI;=n>Wyu?Ji`y7=>;><$P2oq_K0s>Dr)zsrDzBU@J1+6 zF>?#>`aS970!&mVvw+^5JT6rM6~Xi2gvyNU1H0|Xl%l^!gr?pw zK}T_Hnf;t6Nqdo45tCq|+T{A@74OyV1{;?%@AHM0;jOy0x_BV}6kc4+BGTftDF`aV zJ$nA(!v`(E3NFCprz<+F(__GBcI89T$6qHL%0FySdx30@WRB|~tdiCgBB2XlLa^M? zyY+aIgu`hCo4~K8Q9dgn!+J{aj?%QYCFlp#moBEj%}QsEC4M=rYF-hM2!O2z+(7oQ zj*J@|mwnvO)8%)*k=~002Ii z{PuH@=H(_7f3FHDJL_9vg1cMvYwKE-rFdc0&E@y*M(187_mr8r(rP5sGl3rp#y0%ElB6kgP{<;zi-`0ePld@o!2j@(u zGc?b_yC4bApM7nwdSsxZGfIPVg4i7f`zpm}6TdJLYGG&n?60ziTs_n{(8I3ys(v+| zxqGQ8q^eVc10y7K6)!s_(+~Bqu)t?vFb8r^h+6;xd3}Gc?Fb7Gpza*W5_XkURBZ@$v3H-l{~y_t9tX^wd#4phO5 zGJ!hgrkY}F?JSymZK1#-0d)#qcS%Zt07BnRA4Vj`xz*N4*1av$-sotJng^{G+ z*z1;67th8Fw>>+s^k7#Ex8*~6;;_fm*z3}4=7(|DCr0onh3!A}RysV8$*ZH&p0@;BA~~M7j=4)Iz4IOd|76*n zvoiNra$LtN`6A^)jkh~sJNA0-Xzu+eXe$A~XdyCj=2h! z_gR}v@MF0(Lv{3|raDD4Ngm~=?ygThTsF9vd96HTq@=W1M|Gfp%~kj}ou)ZC*)}ay zWPSK@JfE~qa>^UGEhNjX7mN^o>Z)Kn&U9jv%H(+EJCkwfvrWqGMk-pEgSDi>MtwEw zuapSsfB!N!2hGkPLsKDI!m`Nev0t|1^?d#{$ue~dVX~Qc<|}gUD~>C$TepgWDR4al z_-lNCq*M}TB?TB(V8;UO{VC!UhrrQr|1+G^9&^wy&U-?dg5>4I4i$e?SWvf5AHBGz zK5_aocYb>NyO}>n9OaTm|IU#ot%G*0X_tXu(9iJwJn7~7{3Gqjn{+qHvl#~hS!A<< z&*N*JdG@}&f2B3oRiQPzd~-mk?$qKw{8<1w`WV!H|5#pYQG$CORKV3W zS2^g<(Y{%av81JYan^9g=g`jlM?9{_?_Gfdv%YkR8y57L9!DCrhgRECZ+7NBbK8b} zlbP=vXA>Aa=E*fOePwc;w6FN?!q+?FD~EFCma1=ij>&#@Vd!_r+fa8ckdsfqMzT9ArLUu4%wE!yu!(sYMc|GB}R@Ue?l&zFQdn9q(Enfs?Sjt8ZOKI#&euARn zvw+~pQ_#bFi$jN0x%US_+;y(FO?NFbK3k+4iiWJf)qfTxK(wGz{lxr}*+|H#8BKEy z_M;&r@|Gz`6(o5A^a%ijKeKFk3;cP>%#J#f@R{ED&A60-cggf!yGeIYV*x4fjaS^7 z?6&ZPjt$EkgF`zj&Gea=7v_f4lbERn2HY5C9VKYr)u>~P=**h7DALCE zZ*t__;0vjH_evT+M&fdN>OR{m0|LmszpvqVfIA+U#uOaj_@eJfZDeldv><0NO1e(Q zq}baniAA6h|K{@|gzItE=MyL;J_X%Ga=jS%ST*hQ$R|G;Imn}T8bVa$%9dNyC6-TI z(wg&Q-!fQ%guHD-aTB!MZQC)gG7(5c9M74Mr4#Uv#trXYURx}8OHS}FQxzP2tB1#a zGZx#;NQ*qcPtO#IPjL4S$Pq>VZ?=NcSV#pD>JD}X%`)1R9!WctL6W&>(R1!6S)2@O z658ZH&vGm9sK91}djUzlUCI};Kg@B7Ka{&LXsR~A_yok3_IHt@hZ?{gHvQBO{&^%? zpi))vaG_wcQNVa8IninK_4pa@=GIo0h|j-m6y|yMH!`kOKr?0SGG?t{3v~fK((wfg z3sbb=3N2+c%U>nC&^pCLu@C;(^6lsBOP;Mfk0Q`gzZ{*6e4`jDl~Q7ZTGT-qRfHK; zG?e&SvUio%B{NRy0EKn*GmMN?TN)OCW|J^fL~SA%3(`X?SU!>8<{KOb^5Q_phV1NY zVzS}JLNvz=Ux%E#LI!*7Q7gM1$Lg}Sc zwj1t?z7@&A$Y^Z}@s)aeecny6R`szA&n254$LWP?lM4QLdMBgH&)ti(8WM%(*W5$5Ti5D_jp*a0Y+zR z^(|D4t1Y;$-bNkf_*$W5q#GAZy`uZ!6)f5FXu+UH+os~5I4v!$Z01%XNj*IKBasDL z$0)S|q>i3{Q{yRVYuWAuYSSQRJhn@GNf-t9btO<^EJd_)m9ss65sxzvp=uCHT$D7M z10HeJA4+6*u5bP%k+kJST8H{J8|4*%_zecM?%vUlek*F#j!qfi(V991Tuc@0Ko&8=aP5>u56MTAjA>$n$x9pj`-C^L3jed33;?-id#6dM}Z# zvt~-~eDU4iO~;7rGX6M08@bU90R(TBxJ!g~Iha3Ur`IpPah8L_5GM#C`~j!07zhbK zGXemLrsCdW*8vb!HfyWeHm&KCJfhL7rr+=l+l@WN`mR9V1B*@0hJTKH3y!vyWjh5o zY?V(pvnnvym9(@nCBO6-9)9ncLGS>Dx4p#IQ%$tZphmLJmfL z^H&b}C?o$x{Y}7l$A}v=@7S?eh8h8RJ?Q*SSFu*^+ITZ-YCr6;*?&5P{JSfK^OIrz zb*pIw)=_FYeXu!zbQE&mB?d$dx2yFW`dn?^7vsx19RdD4jeN!Gkgm7{>==qjo9W;$ zc1P>$0z)HNHhSsC7?mmq6GlxHzFh&@UBWpT$Ft&_8cykIFO*cey!*R^iH#)Hw!m@{ zVd+rf^kw&T(FJsg+)D{vk^7e> zrmZ1Zvk#h-f=4Ryw`Nu^0mW7(ATUDl__3}QVbAQgsq3~_LvZ9V85gJ%Xr-e5|1+9< zKAzP6k3Q(`HlNF1UZ9tt2U^h%6XpL@1kB3@d60QU>)!}F{?^iD?W^MySF5{CpuNXI zb8GE+XR|vx0-=J=TM|RAlhjcOs~>HQ@Oxg`nXio<`u{mI#yP6qBPpAXm4WkbIIKg{ zpkEXIejhQ)g`0NvpqrPe+64<1EAm}sYWtYFwONAP38y!pO)kpL6k9+~9MHJ{@OlB^ z(lk5J)a27dp)wW#!Cp=?vH}e@B>1!G=?|dA`hFIurpIr)n0R=d%Uy4!}%lDGsH&zEX=yzF9&SU zRJIeGt)jIXbbdPT$b5X2;TcdKOQ7;bKq}fU2$?P%=a`lH%39V=-(#7yrN19%Q+edi z`a0p0SUtVwGK^6`?1o)Mt;1MQSZHHu8S2}%1e~3Tu7`K|)oo$&F+oIJ&GZBM>kzqLUWet&a@iz!>aY;+KKewHLk!L7)8I zMSxl9ny+tMfwoR{Iv;Zl6)*C%2Hu}$r!QYHiD|FQgk8XGP0;Q&WH6)Z1mqyD?`>)Q zi^)A*bdyqXeUZg0ad-6)=;YWZ5Q29-I#VDX_n(H=@oN2E8Ojgm^^_}r6BBls@f?kd zb>SJ<#p8Z^d-$M{0#B#w#MIP5!cNSmn5DUK^DO1)t?bmpV$@GESU|&t1;cOGjOuY0 zMl7Dd9VWU>jcTLFCW5wH^}(_D^Ucs|p}-Cqq{BUA;l3Y`nl-HX983ha`>po0yw2$} zFU277FMJtSS64j&-lg5p@@&Mavdl?Q)eXtx%hVi~`SFSHhk3JAG%P25`FC~szYSZY z7<4(n)n&y;!y5tR*3m>cism{Lz2hToJ#884GXGABn0_7;tHESPMn^*bf7}BLjz?H zda#fki)`)k(hwzZtaqPLc8g(rjwUFU;Gu&DV54EXXlU~-)ILc8dJ`F^&71iMrrdcR zEa+ah=APf(v$ji+@cHGt1Pf~F2Zyv4Z^aV`PPD4CX_GpZ(+Ey-LZsvTUewx@_{mYI ztE)TO{5n{=xzaOsHCeu$V+({csdT&t#U5lKb@0JQ%tF*gap#<@M4#H zaWX)_uqyg7`yR8B1^jLsU#jpGYpnz|t#9ajdy0{j7l>({(&93o4DT6pRZWb?vJ=oB z1bk{i2&wo_uYIeU1r;Y?h7;rb_W}M9(tL}v<3&bB1{`ScRSA?-fC2t;Q#BvSToelM zE71Dx(exW|Thu_SoiZ|n2U-d|O94{^{)_o0SLWeN33!X=K}1TqXEz&H+Ys<&Tovv8 zH~UL%XNjD*U>h(+H5doTFFbH152D<86;Hb5trAu?z#?;4Bh$Vc6ZU!AU=*Yu@E ze8ddO?aSOax-jpH{sWW|NkG*1+`j#Jxhi@OpzqP{`Jc5Y?7f;13D>viaDih zX|tW(c_vc^v?$9Ri|`GN-S!KU2RfqjHftS_dn>8b9!oqJWs`gAS=tgWuVk+BY<7T@;=SMOD9+&1H{e8_Y2Zxi>HgM z+Y&lj{l?`o(Ej%uS9?Fo)M=!PAQNzDyc7%ve~DwZ%HH=xP&T3e2xlSC?w08qjO&LhC9o>L+2y!wUH_*6$hfoIGL*>p5)Fl{^8 z9;zPV3$0+L7nPB1D=tMnDQE4RU(0uR+%rAikli#mm6_Uw9}LB}>sUUkt6+yDBxW!}AetUHD-c`W!Yir%|0@7e3b2k&HtK3?#Z@|Me` zjG**fQ;N$$PMKRxYW*PYs_IYbo1qnT)9+HUE0VvJUOov*CvSd_!CPL9Xkp=bPBP3j z=T|hY#|JX2mp;zxf}O;!ww|~C5T@|izeclxZpVP1`h158=k)X}FdtK@qV=E49q{i) z1IGEpq3`mQ59C=i=1)!MLIjJt2Ct(Y@W`r04rH7eKav|8(FybX4d_F=+!fEq&r-uv zG-tCFN7J7^<>2Hb#1N{ftUR(>SJb6BJy^U-!ng(+TyYb#q;Va79O=cwt0xfBNFuOK za(91=VRqh$D{+L2aeXoG&|*&xveHdV==&m!8ICT`oD3f%H@;3U4@k6o|0!#A3M(ipu0{{g zgsfz+>+NhL8C!x5v5s&pzAbQHYlE@`LVg27#Aw8}(`~F)584AAoe2K9j2z&kp#}uP zhgX0sGb-!g>?+Xf=JEurWLl=mW-<9b7TiDtjR|_H!I4>EjJFaP*Jk#o6idL)2Ksko zKeFITFwRM{r#|sY@H=%6<#D{)DEDoN`?B$&FE#V$IiC3Qy?cKHDd%9a~1l3^KTlA$U%;FEgE$X@<79cGrm1yF-Dy_s_WokQ(MjdVA}_74i(%X+NEABr&cv5+V1&{C;wA z@*BcS7;l!&6k@Eqdk!wbwVfm>c(4vUBL)c292z}W-8+BuEp4=Q$*oA=m#Glr^bYw`vvZlIUkPX7s&Cy%GmsVU?Bjo$bZddXxqD4--FQrH z2cadwLesNV_@9^6DxWApU31bDguLGp28|D#-tFlTMX1HeJ3Jn~{VaQuf zmP;jKGqUEFrq4%01-~tB+N!4rs8_>6yBIuDi5vgou{&^XY$Yu3_z;A8AHwe>`owxV6XnGoKLGQ9vHk_2M_})~DJ$nQoDX*%3Dc zzJw4Gl0r9%P?cXmt)+NjnSuv^(bI0~j3&fMlatLco(zmN`V9Hor&5ymQi?8{Z#4*K zEu2Z$Xg^5ZXn;2u(qZyVnJHz^J4@k-z<=+>`HGvf(vDh^miS4aG(WvRxXt&eA&qUI zF%Nc0x)Ip$m*%z}%<0AX>(P-oUtu`{f2?p40huzWtn2|efMi@o4}#<}{ZD5*IINoP zm+0^V2Qy-GMJaaof_)~^C35wEFFcNeM@(d!j(yi8Zxg?!#MVR`&BT7k?s2oJ!W3wv zIKwdcy_b?6Ug>li+3*HH=R@et;-cSS+y3~f^El>e@%LrRJj(DcnUM3;^Or@)Rkw}K z^LI`_Z!DsF+jXEJ(B;GU#67yoeQnH~+kG$e>NRyZa`qIq}+tK)opW>UZ) z09BiJNcoGqfJ76l(HP;Xi7Yf5a`eGxo1-5+wZmKwzN@t}5n{{3 zM^*O7QV}=NQjet|L}12SSZ;xzNYd*DvgCS_%>z1akpz&zp zPGs8L1*CeSrs?arO;i#852C|Rvb>Y`=ho2Qm)fA~8g>R^C0)389D2dS9^l6(_tuST z#P#9!K8qV_(xf$K6xbOeUYwh{#5DFoibmt`hIL(Z-cAXq3L z|4co2AV+9Vn@Fb@B!|p+;lOL)d8Zvqz-Zv^1u8C7M{P) z0ywZXvV&B8e3lfUBq$LQtM|k*H!M*04|xprEb1+(5*yoxr@c6?Jf1pJ?@h}iV7L`d z;O3E;;rjA{Zwkgeq8&*&hb-jW{4Zq5IzYmgBNtPNiR#2B^)#t zVG%hQmrDZk07nvNu6T2js|K=Xx1YRX=h$&*lOtnE`Gx}ln1`3>q`6EKExcqMZ{?!D)QTQN9m%r-+9WOoK-%Gs~h7RT}z#1n9=DuAu1O_}U z13o|S)NLARj1;@1)gvA|7(}y4%ZKsuy*4Q#zAoUJ4=EeOt9e{Nzu(sMqBH_8V0l*# zE$Fgu&rlD0oH8KADygoGy%9w4u_Q^CRLQ2pYx=dDmF$)_xY9aoXQ13Ds?nrNqlvgU z?S*hKKGS5uli6uiO0RF@X%3cR*GIqF08@^ts6^u)F)R?PlZYS+-~8shS#RA^X!t;R z@Tz%Wb(G21k1f9GOyi*RuVWA7rT$JP1*n-A2IKG7<4RK0*YTTrJlf;t?+vs2M+BW+tP%jk}R0~O*2AUB!B>W*qs^Vkj7Z|HcV zMth$`mW3r^sDtWZRktQazm=8pG0aeu7$oj`{h;9NU^W0^b5Y3I;k|PUgdIEr0$LM9 zsnSuvI`fE2bA8ooS3o}29gP$f#GPXf)!Zn7;U4TFg12X?Jagu|GTgBn_{AB6ct!$v(vkBB@QGR2mJAx5#WCtm}K6 z!nMZw-E}Gj>gMdg+%Zq$fG1pFa%FvIg*N(W64GQSM<~-71<|(GuFO?c6lU*ypK<@p}MIg$B1kW-P z4^OQ_R4o5^azi+dvFu|QW!cV?gxz~-RQbOqgo#2h*^~8 zL9NG(HA&4u$+i9BX4is*HgTbD&Lc%p8AHvURCElcjryeWM+fzc6sT*v3Xi%Of31f{ z?)W4VMAFDv|0pkW;wz5t-~q|+F86Vz)sl3zcy)5NFNGCVLiz>A)vOD zJv*z*Vt{>*CwWakH5RTZm^ajtq7p1IHKp0CH&>ZE!yRDyOQmb5^P4)pZrQ4(rGwi7 zxycjsDq+hxR7M7C&>+PRg-KG4o;}q=_Z~)Z;ie;9O8AhEWZ6&BT?(p~*(53;ze_My zV&!Ogu=mzi^9S#6I{kqHWcT*A^}deFNw!P{_3W$s_(CV48daJJ#5A-sBm4T`16w}Ky2kOII-Gd!UFF7aPQKk*dLk(@PDKj6a^giG=EuHQR)2lj2?U?X2SF>_Kebm&F+6AU;4CuLc zG&;!DaUKk6w{mkMUt2?Gq~IFTmg)+74eoh&0q&Y{pCn5xWWBc0TP^o8wO96osPEd@ zFMUjGB0La3b+8gW#juAXG#>vHzW@mm`TcS$u*f094c4~O%j|b7DMK9`{1iD*l#&h$ zwCI!tvaG70b6^_NB?MhOz%@O(`O5C6`M}=2d8uhMICLVHFpg2s8Pa87CL|`HF}&D= z!l%-$kKnh_oPYnRvbOO{DVsLXiSb$=)b0t`FXfxi_hz6n?=L$7KnuK`_kxa3wYLFF zD$oi1m~wE1un=EdAS4ZJe6pi8ah@+mV+7eW2D_gx4p=;Ik0dQGdfJ$#HE9teN6)Vv zyOe7-zdR3X7Pc88#e6#UCc>XUcaJ+^h~@mk(4cnwdJoV$0__RIIVolU74`J^IEmd{ z?NdN-I@)YDxA-F5tY#B}k=AgK8&9H<9W^W___@);jeAyVhGS%?d|IRTk`>USZFW)m z8V?b2on}s!eN}TO<@x6xomDE2$K?`NLrRL=O+3z=1DdmLrTR*En=*WA337h<-sWu4 z=##+o{P`TE5_9wDkDn@7={R#lA8g^(n;8$`)nZHUcw}v+nh%M!q2fvn&{rxP$^1O95SS z_coE)u?Ygay_oEPq>o44fKi=-!9~!@&!oQ2;!APX)%YXM4KiZ>`cB0|GW>eTS$gwq z+y`{cuyt&N zckcKiQgwr;y%?kJkA1#NW$~=%hyM2@v9l>9;C-GN4g)kN2ev1yI@7bQ3g68rV@r0T z8R*3h$mAnqqQwbT4u^&^uj5RJfQ?R-IkxvEM#||z+035%)R;#x7=EbjO9FB_TIbVGrHqr1IRh<%^#0#B ztTHy<*KAx5m1;gJ3df0mOab>(iy%z#*RZo_zb|fi1*;1kse-qnD<4fv#ECf2y)-Ac zC+CMVuo~<4X*4dtV4gLB&$d%ZjeLV~J*lnhRy4n-?=J~v^`}1vwN*MLwh(HW!`+7w z2!QGol_x`Q0!e^C9tZ?>{t1Zk3v-$7TWn7S1O7w+(Fj^0!?^whROobLY!>UN_-dH< z4*_TJwUJFPF+i*GBIB#bZc{rqKpZea`ZE#MF*0%<&XU%tp^^St5gaZl-&F!h%|+7M zB`r*9r$_ZFBD|xjI(e9c%J1?iTPEYPGtDdw(OH_j15lu%mnrUhFm`iW)4Y5KBS24^ z+F9hKyFlFjh)45Wv1q}3>@9I+c|J7HFw`b@?~05R4_v(9FN@*nu-_}TZ(XlNK?ftT ziBma#ry|G5OH-?F`LJXyr-$rP>uqPMpjNZL`rBrjDCT9WP6%dEN0*4$ci^*24gPThr)fP^6?AwQ)l-AE%qnniYQa zf1R4CgOXWVSyhEHVu^!*KDPfy(^-Z^^+kJl=oXcf6i}o~hAt(Plp4Cbdr0Y!M!Fk@ z?hXOzZiepek_Nqp|GoDE&jSx1;Ov=mX79Du?|myGqayXKtV;E!YA5^%x7@Zr4%Sn# z?SB4Pnlu=^)h0M<&?mV}AnMVl2{u#EG4$k#71^#jxeb2N-D+Z-m%=}_4x4BmN(}Xr z9r5|u0FMiYg?_7db1ca(qBC7$xGIpE_B3y6a|}!f|HEJwy^=aZhaCRDPm=e zgDqVbhQky)3~rE8FLB@YocAnPPm#jee1quqv!W3o!GJB$Bu zzB{LRev`Pv6e@qT^lRwR7Be)QdO(G~zPRkLr7Txqerd<-PfF5momS&>p2pX!U3l^I z=HCG@uh`qyM}A#pP-mb7=odY#GzDg4-sf}BKXE$>)Rl{0N+DV3K=JtB81fjd^jUv= zC*G{H%);o*vh8+4eApGnXyY&)Y+X?A*u1_=K%Em9h;k|lb3W-ekS(k7+@nCNyImC7 zwTNV{@mw4=zvSzxy?*d)z1vCcsYm+D>w^nSsd@FCZ}u4mGG_35$Vsm=0FNA>o6iDP z{eJ?5{8xYp)>3xA&Ge|;r5$}gM zzO}fD(h3w#eT*dywWMQ*gDs{XTmsOKx7D%a40N2;22DuJ<(At<#9R%GZ7eD;E+#i8 zgTcZ)e{U&o|6%{S?O}aV0DRg|PrgswPi1$YmiH(i1IZ?rVjIp3uXP_6< zfnBtvcK=Knx`6Y^^L=NPG=~lX;&GHC!^5pC4J^;+x0~-dDooVXu)y5$jI5P&A#~Bb zYJ$JAsPG>UdSg=jmu9_7?`Td@#jjp$&f(DYxX*7ReE~a_ufVO17=pL+{{wMja%O2 zmI2gZS2_R}C}@omm{!~Di_%SKNdUGxzHc3JIk;tV>A~UAslkSv+6;7xUA?(+qwsgD z#EzcaKz+Cy6DHwEklx=^{`gvrh* zMi$++$5>zwJ(&s2On1Q|@J5weU%aDEo>5Ds4wj}|u1sBV@c-RWNY^N06EE$IgF-kx16*XR?w4 zGb6z{+6`1RhgCmdAefW{=%TE$Q|^q~S>Q{o=%;<+pH8}Lv=^{K7pyjic~s*by6LGv z_bzv$k5C7Xy&S`9wVq-Q`y1|x-n`Ry=iMoN!KWowujLtY)z7BXh8jtH2?aa?FTo7(|fzMyhCdU6@0n|lo zUnjRN)$;9eAZ6UFQEqXtI%Z{>nlG32PKl)4(A_(!{HBAsQ^xqd=Q3rY zbp^-{QUV*R$#3oFy`5U!+d{%XA+!g7*n(eYC+xPEMl|Liq+Kno` zatY{s@+WR|hN2J=5uN|NX{Q9dA$KgGe~()Ov5K{r&m$WPoE2WVKAG=CLrPD|11*%i z+`{Q4ollwrxn{y*eufkg*)P;2aM)*v{3`02L4m=4w+c6&sp&KCWU+olpOrk@+fJ%S@;R=H@3lX8uVlC#C~;$>BL zZPE*e_#-dFFKzIBUqdCOv|F4BfTYcH=QFscr>7lzO*ezb z{xh}aa|ISzyP)i87ZdeoXXm)noqx`h{}pM=HK#Mn<}Npj(I#!^ll6kDZ77gn7Ta zC<*uxC%!((sVJ)T&KN+FtUz~x2f6po`cd2F+P{;2dvT@#Pb;^`~l}dA=GD5b> zd-%0V^C|sTFQYDv#{0K{*v-jr=%O4Mp5)8m@Zq;ckspv3gQ~`hZS>4&9d!tHC7~G) zHl*>kcC|jte{u+h1{qdf264P+BxWjF6QH^_W{~JAQ|}Ag7lG^Hosgj>7*-<$U41#( zKt}{!7VQ23GVG!1s_*?j%NVG+d5-66Vk*!lW_4z|17x%3l>@Xv1i%TQ4X>j;Gc~n^ zgPZ#U_dA5{U4So>CCRv>IR~pK0du^66jgE`W=Y;(|JUo>t7aHT|0$cuIyK55$u)M8 z-Fbq9AwMflqmH+fR5SIW?&yGQ0&=R6de2kTy@L?a!%LH-9K0QXHD&`pMe!9EC200%ui2Kj2*Fgul@7}lUN;Lw%D;pM} z9uMTE4a+w{OexpRh_dv5S_bA2?xq}%?zgz|!vbeV)1WiKkU=|@a-FWbS)D%XhGkUH z>I*$HufN0X_tOPJQ1KJ7XGk=nR{Qt2?p>2<;o7AS+JwU~tNjWjZK?atq9J<%w{q#N zMJcIb3C40JynSV!8mXiGk~0hfkodvkjVdSpLSoA8B(<4>%{zAvoI-~BIR5n0JY2}4 z7=&FnowI-NW(eEUZC;&5JYU+sxQS|sX%5g@M*^;ITUMYTdIAX54Xuo7g`hIXIXbPD zLX~DE)%%{A`Vp;1FI0Wszk&3z`!JCrW@RaGVx>H~aYZ6DC3CTQy4FqD?ZPq3cqpBE znqtbe-ur!31kHlzm0kW?bFT^gb`KawqsH7(ZLJ<(-;fbin* zI6Jb44!+VXQGP{fp#yfHE8Ots?2tzSv)4;{7%(+zhA(!rDZ`;+0YLOg@BG5}`)wCx z@5aVPCem5vSQ>AQ!_M%Vrt@c_G`fmrZBUWAekbnx=FFDr=AzHFI#%)=uNbbHiIvEt zUtxyXaMRNtoqelPvhML}!uGd-^Q6%BjGf zS?L`S9!e1;XsN1}uU}#9Mr_YyqWnx3d|2{!-HQIA0-SJyHi%bgZ)|IL~jT6uQr^Ar7g-VFtF zF`kH1C(VlbN^~sz#k0@nMGKY0=t$@r{sFD9H-AErXBlfo-{P(O?VKUwng8>L6Bx?9 z08F6Av*p?kxNaG%zw9?9T1Kx;mfmg**3R4hG86A=P+?HE22}VK`8T>WIs7}g^G;f;0~AGi zDDfPud-(}aS^h-<0Y1Z}OV!Z~4+_xPSr_gUO_V?yr_&_cwVcOoRJ?!70x2fd-sc|`EscllHRY9m*<9IjJDe}ecKCJ{ z>1mhSXI)x(rA^#v)G%rMcHC3RqSt3pu%Kn{5RK;xRPFA%+Tf*@_~CN}1<*%?SDjyt zApvfsc01?wG@%x|7n%)AzPR4EUGqdYZ6SDv62` zoywzNI>jwWu7>m}234g_Z1~E%wa~&klp<)wkPESQ2-Uu4Ui^{;YYJxA(^TG%f3=T` zuAaJ+h$4}^s=}&ubny_y*-hb{g`hddeS-Cxl(5*h7j=T`ym3S&AK^q5l>ct~_BSd2 zx$a1z0;vynUS7AG$;ruy{QUfH*toccR#wBY-}7Ke%jSD~(*kdtn`FoxuXC4QXi;Ej zEV6~d)P3jHN`%0=-*qJdW38GzrQ63W+&&(BqOy$f+(IHOEtaKyP1{fKNIk!qo~$1^ zwZ*5@vA`JG`|-!3TsB|gk&752-te`Qq(P{sMA=2~@32{HvE;7G>UcCd6aX$o0K^;7 ze0V|LN8RV9rvgAkw*3)q>ov%a?tK~k&Fw)JQ}w^wt$Y>w7UmzZ->)d1XH5Nj|4&vV z_p8J^{qFfOE+XXgvk@oT`l7hZ!b}E(j7EDLMo=2xd{K5#D&`p17hhUJlIW8=V*jPO z%|~ViEdtQrtq;PUf>!Q=*;`_4W3$ad8pK>RT|782(ND+7Fh= z4i1nb(ZAv2|COq7iZr9yVTbZB;=JJd`66%}GQ2a2uJl2U3w{*eyc3YJ3HzkpXu~Cr0NbTz zPpoupo<7qt!Kf&~$Cpn#G?{~A8J==BJpWO?Nq@I8v2D|l)VL_Th_aK1+e=5rMHZBy!WLSaL=0cq% zF~!Mqv6ccz7B?uK3bxoqlJCTCJJb9}$7K&^OeNKXh}id~eol9@zO<0Z{|sgq&=-|0 zJe1kjHl){HaiG_&A$~YXLi$v?*A_Xtv#|DiDNfX+fiafcuqbEPF)t~s^4KjHTa$?JqlyH~cv&7{H`JvmwCnBW1u61)^SR^(m z!6!1rUBcjVHq3Jz4~*o~(<9Er!VC!i9vwk{R{#g)18vvkCBArM2ep@q0732CzQ<-9_Uyl>q?fng|k~lIsaHyfkOwb^d=(| z&jQ5Rf8gv@Wl*K#t{fi_Il-mI!MrNS7Au6YL$M|C3(#uHGa1vmxm4Q>pVUU$IEJBJ zRv;}(L^wHGPt)#@jF!b9q`nwoeq+M`7X-H@{x_0i=DB0u3$mUH7i*xO6 z$z1PT9;`kU*4=e-$Cq0)nCjVs+_OO<+LFiOJ}EDnXz^DP%0*)XTP=!hlC&hAxuAtf zr>gBKrzk~$OAmE_Tp!OrBp?$2O@GnN7MIh%^F+Ge&l7b$j=@0RXF!uBR>L@rR#7Kk zR#HQoSfXWw0PH11!Zd2Ij!RFegJMI59%SV$#sYlyiL9pt?%F7&Iat}nhKgUcYAB#s zaE`dXgAedmq^`tTb%(i<^&!frtZP5Ifn2=*7MZr`13h1W?2iEKrE5D{HbBSb5JkeTBjdJFfedSS+<+5V`qTbpOM^ydOP{ zuyJI`=7<^lF&Em*7P4L2(7PruuTX$cKE1FmHgUkKFm`Lp1&64<@zpsVTqAWjo5v)Z z4U677F_>&83cS>;dDGxY<% zsCr|ZI;rdTPjU7k5h)9EBMrx3?O4~8wpnAhc*S%hB%qW==R|52gT`r{O1o@oA*iLY zaK!4enyhsVFz`1Y?=DXrg`Wr0ON3S^T-Ofa-@bkO_gGTVcCEYm{ZiIysd0jF82gY= zTENm##B9=xo7}c&vuSVp_1aWY zG`N{3ZeUQ?uzju<)u+fWTenkjA*LXAfrlWnV|^Oe4J=El8KqNK#i#d7DL$S$ys}abuV0qf*Ntn` zsE?`P=^o8Fyt&<7Ku(S9_lqon#X}TkL%-iF2E}c z3<;71pb~`#Q-4^N@Kt+XpiHks;i1YVb21Vl{gh+FG}?*EoTzS(R~Zb(G|qm}_QQg^ zr2aF_^q>O067zwgv8Pzq+>G*XVoR3a(2wiIe-^fT&5!Ci%PZ;Wda+Zd*;t!`eOjlP zqaq`7DlX9)tQPOTAY@fI9vqT^s-GTiCcAKjjfAhN`Y1NPuHP4cT0bS<@5rWd>ASi% z-db(vG0eQ9QZG`<>lLe<%PMfi�{HuKXIE8&}Mk_Q@h`9=B+dN9L#+M_Prbq*VG# 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 Date: Tue, 4 Aug 2026 21:01:27 +0200 Subject: [PATCH 4/4] fix: slimmed and corrected --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index a4765b4..a8f9fd2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

-ChapterForge is a library and CLI to mux chapters (text and optional images) into AAC/M4A files while preserving metadata and handling Apple-compatible chapter tracks. +ChapterForge is a library and CLI to mux chapters (text and optional images) into MP4 files while preserving metadata and handling Apple-compatible chapter tracks. ## Table of Contents @@ -219,12 +219,6 @@ Notes: - If top-level metadata fields are omitted and the input file already contains metadata (`ilst`), that metadata is preserved automatically. - Paths for `cover` and per-chapter `image` are resolved relative to the JSON file location. -> **First chapter behavior (Apple/VLC)** -> The chapter tracks are duration-based (`stts`), but most players force the first sample to start -> at t=0. A non-zero first `start_ms` will be snapped to 0 in QuickTime, Music.app, AVFoundation, -> and VLC. If you need silence/blank time before your “real” first chapter, add a leading placeholder -> chapter that covers 0..gap_ms and then start your first “real” chapter after that. - ## Output